Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Sources/AWSLambdaRuntime/Lambda+Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
import NIOCore
import NIOFoundationCompat

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import struct Foundation.Data
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
#endif

// MARK: - SimpleLambdaHandler Codable support

Expand Down Expand Up @@ -138,3 +142,6 @@ extension Lambda {
extension JSONDecoder: LambdaCodableDecoder {}

extension JSONEncoder: LambdaCodableEncoder {}

extension JSONDecoder: AWSLambdaRuntimeCore.LambdaEventDecoder {}
extension JSONEncoder: AWSLambdaRuntimeCore.LambdaOutputEncoder {}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@

import NIOCore

package protocol LambdaResponseStreamWriter {
package protocol LambdaRuntimeClientResponseStreamWriter: LambdaResponseStreamWriter {
mutating func write(_ buffer: ByteBuffer) async throws
func finish() async throws
func writeAndFinish(_ buffer: ByteBuffer) async throws
func reportError(_ error: any Error) async throws
}

package protocol LambdaRuntimeClientProtocol {
associatedtype Writer: LambdaResponseStreamWriter
associatedtype Writer: LambdaRuntimeClientResponseStreamWriter

func nextInvocation() async throws -> (Invocation, Writer)
}
Expand Down
151 changes: 151 additions & 0 deletions Sources/AWSLambdaRuntimeCore/NewLambda+JSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIOCore

/// The protocol a decoder must conform to so that it can be used with ``LambdaCodableAdapter`` to decode incoming
/// ``ByteBuffer`` events.
package protocol LambdaEventDecoder {
/// Decode the ``ByteBuffer`` representing the received event into the generic ``Event`` type
/// the handler will receive.
/// - Parameters:
/// - type: The type of the object to decode the buffer into.
/// - buffer: The buffer to be decoded.
/// - Returns: An object containing the decoded data.
func decode<Event: Decodable>(_ type: Event.Type, from buffer: ByteBuffer) throws -> Event
}

/// The protocol an encoder must conform to so that it can be used with ``LambdaCodableAdapter`` to encode the generic
/// ``Output`` object into a ``ByteBuffer``.
package protocol LambdaOutputEncoder {
/// Encode the generic type `Output` the handler has returned into a ``ByteBuffer``.
/// - Parameters:
/// - value: The object to encode into a ``ByteBuffer``.
/// - buffer: The ``ByteBuffer`` where the encoded value will be written to.
func encode<Output: Encodable>(_ value: Output, into buffer: inout ByteBuffer) throws
}

package struct VoidEncoder: LambdaOutputEncoder {
package func encode<Output>(_ value: Output, into buffer: inout NIOCore.ByteBuffer) throws where Output: Encodable {
fatalError("LambdaOutputEncoder must never be called on a void output")
}
}

/// Adapts a ``NewLambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``.
package struct LambdaHandlerAdapter<
Event: Decodable,
Output,
Handler: NewLambdaHandler
>: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output {
let handler: Handler

/// Initializes an instance given a concrete handler.
/// - Parameter handler: The ``NewLambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``.
package init(handler: Handler) {
self.handler = handler
}

/// Passes the generic ``Event`` object to the ``NewLambdaHandler/handle(_:context:)`` function, and
/// the resulting output is then written to ``LambdaWithBackgroundProcessingHandler``'s `outputWriter`.
/// - Parameters:
/// - event: The received event.
/// - outputWriter: The writer to write the computed response to.
/// - context: The ``NewLambdaContext`` containing the invocation's metadata.
package func handle(
_ event: Event,
outputWriter: consuming some LambdaResponseWriter<Output>,
context: NewLambdaContext
) async throws {
let response = try await self.handler.handle(event, context: context)
try await outputWriter.write(response: response)
}
}

/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``.
package struct LambdaCodableAdapter<
Handler: LambdaWithBackgroundProcessingHandler,
Event: Decodable,
Output,
Decoder: LambdaEventDecoder,
Encoder: LambdaOutputEncoder
>: StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output {
let handler: Handler
let encoder: Encoder
let decoder: Decoder
private var byteBuffer: ByteBuffer = .init()

/// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output.
/// - Parameters:
/// - encoder: The encoder object that will be used to encode the generic ``Output`` obtained from the `handler`'s `outputWriter` into a ``ByteBuffer``.
/// - decoder: The decoder object that will be used to decode the received ``ByteBuffer`` event into the generic ``Event`` type served to the `handler`.
/// - handler: The handler object.
package init(encoder: Encoder, decoder: Decoder, handler: Handler) where Output: Encodable {
self.encoder = encoder
self.decoder = decoder
self.handler = handler
}

/// Initializes an instance given a decoder, and a handler with a `Void` output.
/// - Parameters:
/// - decoder: The decoder object that will be used to decode the received ``ByteBuffer`` event into the generic ``Event`` type served to the `handler`.
/// - handler: The handler object.
package init(decoder: Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder {
self.encoder = VoidEncoder()
self.decoder = decoder
self.handler = handler
}

/// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper.
/// - Parameters:
/// - event: The received event.
/// - outputWriter: The writer to write the computed response to.
/// - context: The ``NewLambdaContext`` containing the invocation's metadata.
package mutating func handle(
_ request: ByteBuffer,
responseWriter: some LambdaResponseStreamWriter,
context: NewLambdaContext
) async throws {
let event = try self.decoder.decode(Event.self, from: request)

let writer = ResponseWriter<Output>(encoder: self.encoder, streamWriter: responseWriter)
try await self.handler.handle(event, outputWriter: writer, context: context)
}
}

/// A ``LambdaResponseStreamWriter`` wrapper that conforms to ``LambdaResponseWriter``.
package struct ResponseWriter<Output>: LambdaResponseWriter {
let underlyingStreamWriter: LambdaResponseStreamWriter
let encoder: LambdaOutputEncoder
var byteBuffer = ByteBuffer()

/// Initializes an instance given an encoder and an underlying ``LambdaResponseStreamWriter``.
/// - Parameters:
/// - encoder: The encoder object that will be used to encode the generic ``Output`` into a ``ByteBuffer``, which will then be passed to `streamWriter`.
/// - streamWriter: The underlying ``LambdaResponseStreamWriter`` that will be wrapped.
package init(encoder: LambdaOutputEncoder, streamWriter: LambdaResponseStreamWriter) {
self.encoder = encoder
self.underlyingStreamWriter = streamWriter
}

/// Passes the `response` argument to ``LambdaResponseStreamWriter/writeAndFinish(_:)``.
/// - Parameter response: The generic ``Output`` object that will be passed to ``LambdaResponseStreamWriter/writeAndFinish(_:)``.
package mutating func write(response: Output) async throws {
if Output.self == Void.self {
try await self.underlyingStreamWriter.finish()
} else if let response = response as? Encodable {
try self.encoder.encode(response, into: &self.byteBuffer)
try await self.underlyingStreamWriter.writeAndFinish(self.byteBuffer)
}
}
}
9 changes: 0 additions & 9 deletions Sources/AWSLambdaRuntimeCore/NewLambda.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,6 @@

import Dispatch
import Logging
import NIOCore

package protocol StreamingLambdaHandler {
mutating func handle(
_ event: ByteBuffer,
responseWriter: some LambdaResponseStreamWriter,
context: NewLambdaContext
) async throws
}

extension Lambda {
package static func runLoop<RuntimeClient: LambdaRuntimeClientProtocol, Handler>(
Expand Down
173 changes: 173 additions & 0 deletions Sources/AWSLambdaRuntimeCore/NewLambdaHandlers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIOCore

/// The base handler protocol that receives a ``ByteBuffer`` representing the incoming event and returns the response as a ``ByteBuffer`` too.
/// This handler protocol supports response streaming. Bytes can be streamed outwards through the ``LambdaResponseStreamWriter``
/// passed as an argument in the ``handle(_:responseWriter:context:)`` function.
/// Background work can also be executed after returning the response. After closing the response stream by calling
/// ``LambdaResponseStreamWriter/finish()`` or ``LambdaResponseStreamWriter/writeAndFinish(_:)``,
/// the ``handle(_:responseWriter:context:)`` function is free to execute any background work.
package protocol StreamingLambdaHandler {
/// The handler function -- implement the business logic of the Lambda function here.
/// - Parameters:
/// - event: The invocation's input data.
/// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to.
/// If no response or error is written to `responseWriter` an error will be reported to the invoker.
/// - context: The ``NewLambdaContext`` containing the invocation's metadata.
/// - Throws:
/// How the thrown error will be handled by the runtime:
/// - An invocation error will be reported if the error is thrown before the first call to
/// ``LambdaResponseStreamWriter/write(_:)``.
/// - If the error is thrown after call(s) to ``LambdaResponseStreamWriter/write(_:)`` but before
/// a call to ``LambdaResponseStreamWriter/finish()``, the response stream will be closed and trailing
/// headers will be sent.
/// - If ``LambdaResponseStreamWriter/finish()`` has already been called before the error is thrown, the
/// error will be logged.
mutating func handle(
_ event: ByteBuffer,
responseWriter: some LambdaResponseStreamWriter,
context: NewLambdaContext
) async throws
}

/// A writer object to write the Lambda response stream into. The HTTP response is started lazily.
/// before the first call to ``write(_:)`` or ``writeAndFinish(_:)``.
package protocol LambdaResponseStreamWriter {
/// Write a response part into the stream. Bytes written are streamed continually.
/// - Parameter buffer: The buffer to write.
mutating func write(_ buffer: ByteBuffer) async throws

/// End the response stream and the underlying HTTP response.
func finish() async throws

/// Write a response part into the stream and then end the stream as well as the underlying HTTP response.
/// - Parameter buffer: The buffer to write.
func writeAndFinish(_ buffer: ByteBuffer) async throws
}

/// This handler protocol is intended to serve the most common use-cases.
/// This protocol is completely agnostic to any encoding/decoding -- decoding the received event invocation into an ``Event`` object and encoding the returned ``Output`` object is handled by the library.
/// The``handle(_:context:)`` function simply receives the generic ``Event`` object as input and returns the generic ``Output`` object.
///
/// - note: This handler protocol does not support response streaming because the output has to be encoded prior to it being sent, e.g. it is not possible to encode a partial/incomplete JSON string.
/// This protocol also does not support the execution of background work after the response has been returned -- the ``LambdaWithBackgroundProcessingHandler`` protocol caters for such use-cases.
package protocol NewLambdaHandler {
/// Generic input type.
/// The body of the request sent to Lambda will be decoded into this type for the handler to consume.
associatedtype Event: Decodable
/// Generic output type.
/// This is the return type of the ``handle(_:context:)`` function.
associatedtype Output

/// Implement the business logic of the Lambda function here.
/// - Parameters:
/// - event: The generic ``Event`` object representing the invocation's input data.
/// - context: The ``NewLambdaContext`` containing the invocation's metadata.
/// - Returns: A generic ``Output`` object representing the computed result.
func handle(_ event: Event, context: NewLambdaContext) async throws -> Output
}

/// This protocol is exactly like ``NewLambdaHandler``, with the only difference being the added support for executing background
/// work after the result has been sent to the AWS Lambda control plane.
/// This is achieved by not having a return type in the `handle` function. The output is instead written into a
/// ``LambdaResponseWriter``that is passed in as an argument, meaning that the ``handle(_:)`` function is then free to implement
/// any background work after the result has been sent to the AWS Lambda control plane.
package protocol LambdaWithBackgroundProcessingHandler {
/// Generic input type.
/// The body of the request sent to Lambda will be decoded into this type for the handler to consume.
associatedtype Event: Decodable
/// Generic output type.
/// This is the type that the `handle` function will send through the ``LambdaResponseWriter``.
associatedtype Output

/// Implement the business logic of the Lambda function here.
/// - Parameters:
/// - event: The generic ``Event`` object representing the invocation's input data.
/// - outputWriter: The writer to send the computed response to. A call to `outputWriter.write(_:)` will return the response to the AWS Lambda response endpoint.
/// Any background work can then be executed before returning.
/// - context: The ``NewLambdaContext`` containing the invocation's metadata.
func handle(
_ event: Event,
outputWriter: some LambdaResponseWriter<Output>,
context: NewLambdaContext
) async throws
}

/// Used with ``LambdaWithBackgroundProcessingHandler``.
/// A mechanism to "return" an output from ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` without the function needing to
/// have a return type and exit at that point. This allows for background work to be executed _after_ a response has been sent to the AWS Lambda response endpoint.
package protocol LambdaResponseWriter<Output>: ~Copyable {
associatedtype Output
/// Sends the generic ``Output`` object (representing the computed result of the handler)
/// to the AWS Lambda response endpoint.
/// This function simply serves as a mechanism to return the computed result from a handler function
/// without an explicit `return`.
mutating func write(response: Output) async throws
}

/// A ``StreamingLambdaHandler`` conforming handler object that can be constructed with a closure.
/// Allows for a handler to be defined in a clean manner, leveraging Swift's trailing closure syntax.
package struct StreamingClosureHandler: StreamingLambdaHandler {
let body: @Sendable (ByteBuffer, LambdaResponseStreamWriter, NewLambdaContext) async throws -> Void

/// Initialize an instance from a handler function in the form of a closure.
/// - Parameter body: The handler function written as a closure.
package init(
body: @Sendable @escaping (ByteBuffer, LambdaResponseStreamWriter, NewLambdaContext) async throws -> Void
) {
self.body = body
}

/// Calls the provided `self.body` closure with the ``ByteBuffer`` invocation event, the ``LambdaResponseStreamWriter``, and the ``NewLambdaContext``
/// - Parameters:
/// - event: The invocation's input data.
/// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to.
/// If no response or error is written to `responseWriter` an error will be reported to the invoker.
/// - context: The ``NewLambdaContext`` containing the invocation's metadata.
package func handle(
_ request: ByteBuffer,
responseWriter: some LambdaResponseStreamWriter,
context: NewLambdaContext
) async throws {
try await self.body(request, responseWriter, context)
}
}

/// A ``NewLambdaHandler`` conforming handler object that can be constructed with a closure.
/// Allows for a handler to be defined in a clean manner, leveraging Swift's trailing closure syntax.
package struct ClosureHandler<Event: Decodable, Output>: NewLambdaHandler {
let body: (Event, NewLambdaContext) async throws -> Output

/// Initialize with a closure handler over generic `Input` and `Output` types.
/// - Parameter body: The handler function written as a closure.
package init(body: @escaping (Event, NewLambdaContext) async throws -> Output) where Output: Encodable {
self.body = body
}

/// Initialize with a closure handler over a generic `Input` type, and a `Void` `Output`.
/// - Parameter body: The handler function written as a closure.
package init(body: @escaping (Event, NewLambdaContext) async throws -> Void) where Output == Void {
self.body = body
}

/// Calls the provided `self.body` closure with the generic ``Event`` object representing the incoming event, and the ``NewLambdaContext``
/// - Parameters:
/// - event: The generic ``Event`` object representing the invocation's input data.
/// - context: The ``NewLambdaContext`` containing the invocation's metadata.
package func handle(_ event: Event, context: NewLambdaContext) async throws -> Output {
try await self.body(event, context)
}
}
2 changes: 1 addition & 1 deletion Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Foundation
import Logging
import NIOCore

struct LambdaMockWriter: LambdaResponseStreamWriter {
struct LambdaMockWriter: LambdaRuntimeClientResponseStreamWriter {
var underlying: LambdaMockClient

init(underlying: LambdaMockClient) {
Expand Down
Loading