-
Notifications
You must be signed in to change notification settings - Fork 113
Add new handler protocols + Codable support #351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
804bed7
Add NewLambdaHandler + LambdaWithBackgroundProcessingHandler protocol…
aryan-25 b4a6a4f
Add documentation, add new internal `LambdaRuntimeClientResponseStrea…
aryan-25 6bf879c
Refactor
aryan-25 3bcc17a
Refactor
aryan-25 5eebc52
Remove mutating keyword from write(_:)
aryan-25 bf7497a
Fix formatting
aryan-25 6f384ae
Add LambdaJSONOutputEncoder
aryan-25 f386e76
Apply formatter
aryan-25 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
aryan-25 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let underlyingStreamWriter: LambdaResponseStreamWriter | ||
let encoder: LambdaOutputEncoder | ||
var byteBuffer = ByteBuffer() | ||
fabianfett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// 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) { | ||
fabianfett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 { | ||
fabianfett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
} | ||
fabianfett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
fabianfett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// 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, | ||
fabianfett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
fabianfett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// 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 { | ||
fabianfett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 { | ||
fabianfett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 { | ||
fabianfett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.