Skip to content
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

Add new handler protocols + Codable support #351

Merged
merged 8 commits into from
Sep 4, 2024
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")
aryan-25 marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// 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
}
}
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 {
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)
}
}
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