From 4b45451dc1e9d3884488c305b1dda46afb588abd Mon Sep 17 00:00:00 2001 From: aryan-25 Date: Wed, 4 Sep 2024 12:46:00 +0100 Subject: [PATCH] Add new handler protocols + Codable support (#351) --- Sources/AWSLambdaRuntime/Lambda+Codable.swift | 46 +++++ .../LambdaRuntimeClientProtocol.swift | 6 +- .../AWSLambdaRuntimeCore/NewLambda+JSON.swift | 161 ++++++++++++++++ Sources/AWSLambdaRuntimeCore/NewLambda.swift | 9 - .../NewLambdaContext.swift | 3 +- .../NewLambdaHandlers.swift | 173 ++++++++++++++++++ .../LambdaMockClient.swift | 4 +- .../NewLambda+CodableTests.swift | 100 ++++++++++ 8 files changed, 486 insertions(+), 16 deletions(-) create mode 100644 Sources/AWSLambdaRuntimeCore/NewLambda+JSON.swift create mode 100644 Sources/AWSLambdaRuntimeCore/NewLambdaHandlers.swift create mode 100644 Tests/AWSLambdaRuntimeTests/NewLambda+CodableTests.swift diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index 3f80ee17..fedd85c9 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -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 @@ -138,3 +142,45 @@ extension Lambda { extension JSONDecoder: LambdaCodableDecoder {} extension JSONEncoder: LambdaCodableEncoder {} + +extension JSONDecoder: AWSLambdaRuntimeCore.LambdaEventDecoder {} + +@usableFromInline +package struct LambdaJSONOutputEncoder: LambdaOutputEncoder { + @usableFromInline let jsonEncoder: JSONEncoder + + @inlinable + package init(_ jsonEncoder: JSONEncoder) { + self.jsonEncoder = jsonEncoder + } + + @inlinable + package func encode(_ value: Output, into buffer: inout ByteBuffer) throws { + try self.jsonEncoder.encode(value, into: &buffer) + } +} + +extension LambdaCodableAdapter { + /// 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: JSONEncoder, + decoder: JSONDecoder, + handler: Handler + ) + where + Output: Encodable, + Output == Handler.Output, + Encoder == LambdaJSONOutputEncoder, + Decoder == JSONDecoder + { + self.init( + encoder: LambdaJSONOutputEncoder(encoder), + decoder: decoder, + handler: handler + ) + } +} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClientProtocol.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClientProtocol.swift index f85b1b4f..801bdf82 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClientProtocol.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClientProtocol.swift @@ -14,15 +14,15 @@ import NIOCore -package protocol LambdaResponseStreamWriter { - mutating func write(_ buffer: ByteBuffer) async throws +package protocol LambdaRuntimeClientResponseStreamWriter: LambdaResponseStreamWriter { + 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) } diff --git a/Sources/AWSLambdaRuntimeCore/NewLambda+JSON.swift b/Sources/AWSLambdaRuntimeCore/NewLambda+JSON.swift new file mode 100644 index 00000000..4475a81f --- /dev/null +++ b/Sources/AWSLambdaRuntimeCore/NewLambda+JSON.swift @@ -0,0 +1,161 @@ +//===----------------------------------------------------------------------===// +// +// 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(_ 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 { + associatedtype Output + + /// 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(_ value: Output, into buffer: inout ByteBuffer) throws +} + +package struct VoidEncoder: LambdaOutputEncoder { + package typealias Output = Void + + @inlinable + package func encode(_ value: Void, into buffer: inout NIOCore.ByteBuffer) throws {} +} + +/// 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 { + @usableFromInline let handler: Handler + + /// Initializes an instance given a concrete handler. + /// - Parameter handler: The ``NewLambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. + @inlinable + 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. + @inlinable + package func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: NewLambdaContext + ) async throws { + let output = try await self.handler.handle(event, context: context) + try await outputWriter.write(output) + } +} + +/// 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, Encoder.Output == Output { + @usableFromInline let handler: Handler + @usableFromInline let encoder: Encoder + @usableFromInline let decoder: Decoder + // + @usableFromInline 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. + @inlinable + 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. + @inlinable + 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. + @inlinable + package mutating func handle( + _ request: ByteBuffer, + responseWriter: Writer, + context: NewLambdaContext + ) async throws { + let event = try self.decoder.decode(Event.self, from: request) + + let writer = LambdaCodableResponseWriter( + encoder: self.encoder, + streamWriter: responseWriter + ) + try await self.handler.handle(event, outputWriter: writer, context: context) + } +} + +/// A ``LambdaResponseStreamWriter`` wrapper that conforms to ``LambdaResponseWriter``. +package struct LambdaCodableResponseWriter: + LambdaResponseWriter +where Output == Encoder.Output { + @usableFromInline let underlyingStreamWriter: Base + @usableFromInline let encoder: Encoder + + /// 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. + @inlinable + package init(encoder: Encoder, streamWriter: Base) { + self.encoder = encoder + self.underlyingStreamWriter = streamWriter + } + + @inlinable + package func write(_ output: Output) async throws { + var outputBuffer = ByteBuffer() + try self.encoder.encode(output, into: &outputBuffer) + try await self.underlyingStreamWriter.writeAndFinish(outputBuffer) + } +} diff --git a/Sources/AWSLambdaRuntimeCore/NewLambda.swift b/Sources/AWSLambdaRuntimeCore/NewLambda.swift index 5adb8f57..28eb7df9 100644 --- a/Sources/AWSLambdaRuntimeCore/NewLambda.swift +++ b/Sources/AWSLambdaRuntimeCore/NewLambda.swift @@ -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( diff --git a/Sources/AWSLambdaRuntimeCore/NewLambdaContext.swift b/Sources/AWSLambdaRuntimeCore/NewLambdaContext.swift index 89414728..abd2e61f 100644 --- a/Sources/AWSLambdaRuntimeCore/NewLambdaContext.swift +++ b/Sources/AWSLambdaRuntimeCore/NewLambdaContext.swift @@ -126,8 +126,7 @@ package struct NewLambdaContext: CustomDebugStringConvertible, Sendable { traceID: String, invokedFunctionARN: String, timeout: DispatchTimeInterval, - logger: Logger, - eventLoop: EventLoop + logger: Logger ) -> NewLambdaContext { NewLambdaContext( requestID: requestID, diff --git a/Sources/AWSLambdaRuntimeCore/NewLambdaHandlers.swift b/Sources/AWSLambdaRuntimeCore/NewLambdaHandlers.swift new file mode 100644 index 00000000..0f9e7412 --- /dev/null +++ b/Sources/AWSLambdaRuntimeCore/NewLambdaHandlers.swift @@ -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. + 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, + 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 { + 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`. + func write(_ output: 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: 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) + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift index 4023dc76..15a30223 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift @@ -17,14 +17,14 @@ import Foundation import Logging import NIOCore -struct LambdaMockWriter: LambdaResponseStreamWriter { +struct LambdaMockWriter: LambdaRuntimeClientResponseStreamWriter { var underlying: LambdaMockClient init(underlying: LambdaMockClient) { self.underlying = underlying } - mutating func write(_ buffer: ByteBuffer) async throws { + func write(_ buffer: ByteBuffer) async throws { try await self.underlying.write(buffer) } diff --git a/Tests/AWSLambdaRuntimeTests/NewLambda+CodableTests.swift b/Tests/AWSLambdaRuntimeTests/NewLambda+CodableTests.swift new file mode 100644 index 00000000..08e55f36 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/NewLambda+CodableTests.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaRuntime +import Logging +import NIOCore +import Testing + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite +struct JSONTests { + + let logger = Logger(label: "JSONTests") + + struct Foo: Codable { + var bar: String + } + + @Test + func testEncodingConformance() { + let encoder = LambdaJSONOutputEncoder(JSONEncoder()) + let foo = Foo(bar: "baz") + var byteBuffer = ByteBuffer() + + #expect(throws: Never.self) { + try encoder.encode(foo, into: &byteBuffer) + } + + #expect(byteBuffer == ByteBuffer(string: #"{"bar":"baz"}"#)) + } + + @Test + func testJSONHandlerWithOutput() async { + let jsonEncoder = JSONEncoder() + let jsonDecoder = JSONDecoder() + + let closureHandler = ClosureHandler { (foo: Foo, context) in + foo + } + + var handler = LambdaCodableAdapter( + encoder: jsonEncoder, + decoder: jsonDecoder, + handler: LambdaHandlerAdapter(handler: closureHandler) + ) + + let event = ByteBuffer(string: #"{"bar":"baz"}"#) + let writer = MockLambdaWriter() + let context = NewLambdaContext.__forTestsOnly( + requestID: UUID().uuidString, + traceID: UUID().uuidString, + invokedFunctionARN: "arn:", + timeout: .milliseconds(6000), + logger: self.logger + ) + + await #expect(throws: Never.self) { + try await handler.handle(event, responseWriter: writer, context: context) + } + + let result = await writer.output + #expect(result == ByteBuffer(string: #"{"bar":"baz"}"#)) + } +} + +final actor MockLambdaWriter: LambdaResponseStreamWriter { + private var _buffer: ByteBuffer? + + var output: ByteBuffer? { + self._buffer + } + + func writeAndFinish(_ buffer: ByteBuffer) async throws { + self._buffer = buffer + } + + func write(_ buffer: ByteBuffer) async throws { + fatalError("Unexpected call") + } + + func finish() async throws { + fatalError("Unexpected call") + } +}