diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4eefa3f1..f668b9d5 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -36,7 +36,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HummingbirdLambda', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'StreamingFromEvent', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" + examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HummingbirdLambda', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" archive_plugin_enabled: true diff --git a/Examples/README.md b/Examples/README.md index 2f3fa92f..71c68353 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -38,7 +38,7 @@ This directory contains example code for Lambda functions. - **[Streaming](Streaming/README.md)**: create a Lambda function exposed as an URL. The Lambda function streams its response over time. (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). -- **[StreamingFromEvent](StreamingFromEvent/README.md)**: a Lambda function that combines JSON input decoding with response streaming capabilities, demonstrating the new streaming codable interface (requires [AWS SAM](https://aws.amazon.com/serverless/sam/) or the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). +- **[Streaming+Codable](Streaming+Codable/README.md)**: a Lambda function that combines JSON input decoding with response streaming capabilities, demonstrating a streaming codable interface (requires [AWS SAM](https://aws.amazon.com/serverless/sam/) or the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). - **[Testing](Testing/README.md)**: a test suite for Lambda functions. diff --git a/Examples/StreamingFromEvent/Package.swift b/Examples/Streaming+Codable/Package.swift similarity index 72% rename from Examples/StreamingFromEvent/Package.swift rename to Examples/Streaming+Codable/Package.swift index cc04e4a4..7bed318c 100644 --- a/Examples/StreamingFromEvent/Package.swift +++ b/Examples/Streaming+Codable/Package.swift @@ -6,19 +6,28 @@ import PackageDescription import struct Foundation.URL let package = Package( - name: "StreamingFromEvent", + name: "StreamingCodable", platforms: [.macOS(.v15)], dependencies: [ // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0-beta.1") + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0-beta.1"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.2.0"), ], targets: [ .executableTarget( - name: "StreamingFromEvent", + name: "StreamingCodable", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), ] - ) + ), + .testTarget( + name: "Streaming+CodableTests", + dependencies: [ + "StreamingCodable", + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ] + ), ] ) diff --git a/Examples/StreamingFromEvent/README.md b/Examples/Streaming+Codable/README.md similarity index 88% rename from Examples/StreamingFromEvent/README.md rename to Examples/Streaming+Codable/README.md index 8d33f145..c1784a48 100644 --- a/Examples/StreamingFromEvent/README.md +++ b/Examples/Streaming+Codable/README.md @@ -1,12 +1,33 @@ # Streaming Codable Lambda function -This example demonstrates how to use the `StreamingLambdaHandlerWithEvent` protocol to create Lambda functions that: +This example demonstrates how to use a `StreamingLambdaHandlerWithEvent` protocol to create Lambda functions, exposed through a FunctionUrl, that: 1. **Receive JSON input**: Automatically decode JSON events into Swift structs 2. **Stream responses**: Send data incrementally as it becomes available 3. **Execute background work**: Perform additional processing after the response is sent -The example uses the streaming codable interface that combines the benefits of: +## When to Use This Approach + +**⚠️ Important Limitations:** + +1. **Function URL Only**: This streaming codable approach only works with Lambda functions exposed through [Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html) +2. **Limited Request Access**: This approach hides the details of the `FunctionURLRequest` (like HTTP headers, query parameters, etc.) from developers + +**Decision Rule:** + +- **Use this streaming codable approach when:** + - Your function is exposed through a Lambda Function URL + - You have a JSON payload that you want automatically decoded + - You don't need to inspect HTTP headers, query parameters, or other request details + - You prioritize convenience over flexibility + +- **Use the ByteBuffer `StreamingLambdaHandler` approach when:** + - You need full control over the `FunctionURLRequest` details + - You're invoking the Lambda through other means (API Gateway, direct invocation, etc.) + - You need access to HTTP headers, query parameters, or request metadata + - You require maximum flexibility (requires writing more code) + +This example balances convenience and flexibility. The streaming codable interface combines the benefits of: - Type-safe JSON input decoding (like regular `LambdaHandler`) - Response streaming capabilities (like `StreamingLambdaHandler`) - Background work execution after response completion diff --git a/Sources/AWSLambdaRuntime/LambdaStreaming+Codable.swift b/Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift similarity index 79% rename from Sources/AWSLambdaRuntime/LambdaStreaming+Codable.swift rename to Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift index a0b9a750..4b447fcb 100644 --- a/Sources/AWSLambdaRuntime/LambdaStreaming+Codable.swift +++ b/Examples/Streaming+Codable/Sources/LambdaStreaming+Codable.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import AWSLambdaEvents +import AWSLambdaRuntime import Logging import NIOCore @@ -71,6 +73,9 @@ public struct StreamingLambdaCodableAdapter< } /// Handles the raw ByteBuffer by decoding it and passing to the underlying handler. + /// This function attempts to decode the event as a `FunctionURLRequest` first, which allows for + /// handling Function URL requests that may have a base64-encoded body. + /// If the decoding fails, it falls back to decoding the event "as-is" with the provided JSON type. /// - Parameters: /// - event: The raw ByteBuffer event to decode. /// - responseWriter: The response writer to pass to the underlying handler. @@ -82,43 +87,20 @@ public struct StreamingLambdaCodableAdapter< context: LambdaContext ) async throws { - // try to decode the event as a FunctionURLRequest and extract its body - let urlRequestBody = bodyFromFunctionURLRequest(event) + var decodedBody: Handler.Event! - // decode the body or the event as user-provided JSON - let decodedEvent = try self.decoder.decode(Handler.Event.self, from: urlRequestBody ?? event) + // try to decode the event as a FunctionURLRequest, then fetch its body attribute + if let request = try? self.decoder.decode(FunctionURLRequest.self, from: event) { + // decode the body as user-provided JSON type + // this function handles the base64 decoding when needed + decodedBody = try request.decodeBody(Handler.Event.self) + } else { + // try to decode the event "as-is" with the provided JSON type + decodedBody = try self.decoder.decode(Handler.Event.self, from: event) + } // and pass it to the handler - try await self.handler.handle(decodedEvent, responseWriter: responseWriter, context: context) - } - - /// Extract the body payload from a FunctionURLRequest event. - /// This function checks if the event is a valid `FunctionURLRequest` and decodes the body if it is base64 encoded. - /// If the event is not a valid `FunctionURLRequest`, it returns nil. - /// - Parameter event: The raw ByteBuffer event to check. - /// - Returns: The base64 decoded body of the FunctionURLRequest if it is a valid FunctionURLRequest, otherwise nil. - @inlinable - package func bodyFromFunctionURLRequest(_ event: ByteBuffer) -> ByteBuffer? { - do { - // try to decode as a FunctionURLRequest - let request = try self.decoder.decode(FunctionURLRequest.self, from: event) - - // if the body is encoded in base64, decode it - if request.isBase64Encoded, - let base64EncodedString = request.body, - // this is the minimal way to base64 decode without importing new dependencies - let decodedData = Data(base64Encoded: base64EncodedString), - let decodedString = String(data: decodedData, encoding: .utf8) - { - - return ByteBuffer(string: decodedString) - } else { - return ByteBuffer(string: request.body ?? "") - } - } catch { - // not a FunctionURLRequest, return nil - return nil - } + try await self.handler.handle(decodedBody, responseWriter: responseWriter, context: context) } } @@ -149,8 +131,6 @@ public struct StreamingFromEventClosureHandler: StreamingLambd } } -#if FoundationJSONSupport - extension StreamingLambdaCodableAdapter { /// Initialize with a JSON decoder and handler. /// - Parameters: @@ -203,4 +183,3 @@ extension LambdaRuntime { self.init(handler: adapter, logger: logger) } } -#endif // FoundationJSONSupport diff --git a/Examples/StreamingFromEvent/Sources/main.swift b/Examples/Streaming+Codable/Sources/main.swift similarity index 100% rename from Examples/StreamingFromEvent/Sources/main.swift rename to Examples/Streaming+Codable/Sources/main.swift diff --git a/Tests/AWSLambdaRuntimeTests/LambdaStreamingCodableTests.swift b/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift similarity index 99% rename from Tests/AWSLambdaRuntimeTests/LambdaStreamingCodableTests.swift rename to Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift index 3e761ae4..b95388aa 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaStreamingCodableTests.swift +++ b/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift @@ -18,6 +18,7 @@ import Synchronization import Testing @testable import AWSLambdaRuntime +@testable import StreamingCodable #if canImport(FoundationEssentials) import FoundationEssentials diff --git a/Examples/StreamingFromEvent/events/sample-request.json b/Examples/Streaming+Codable/events/sample-request.json similarity index 100% rename from Examples/StreamingFromEvent/events/sample-request.json rename to Examples/Streaming+Codable/events/sample-request.json diff --git a/Examples/StreamingFromEvent/template.yaml b/Examples/Streaming+Codable/template.yaml similarity index 82% rename from Examples/StreamingFromEvent/template.yaml rename to Examples/Streaming+Codable/template.yaml index 6ebb5d61..0632a50e 100644 --- a/Examples/StreamingFromEvent/template.yaml +++ b/Examples/Streaming+Codable/template.yaml @@ -4,10 +4,10 @@ Description: SAM Template for StreamingfromEvent Example Resources: # Lambda function - StreamingFromEvent: + StreamingCodable: Type: AWS::Serverless::Function Properties: - CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingFromEvent/StreamingFromEvent.zip + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingCodable/StreamingCodable.zip Timeout: 15 Handler: swift.bootstrap # ignored by the Swift runtime Runtime: provided.al2 @@ -22,4 +22,4 @@ Outputs: # print Lambda function URL LambdaURL: Description: Lambda URL - Value: !GetAtt StreamingFromEventUrl.FunctionUrl + Value: !GetAtt StreamingCodableUrl.FunctionUrl diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/FunctionURL-HTTPType.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/FunctionURL-HTTPType.swift deleted file mode 100644 index 67f853e7..00000000 --- a/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/FunctionURL-HTTPType.swift +++ /dev/null @@ -1,110 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2022 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 -// -//===----------------------------------------------------------------------===// - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -// https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html - -/// This is a simplified version of the FunctionURLRequest structure, with no dependencies on the HTTPType module. -/// This file is copied from AWS Lambda Event project at https://github.com/swift-server/swift-aws-lambda-events - -/// FunctionURLRequest contains data coming from a bare Lambda Function URL -public struct FunctionURLRequest: Codable, Sendable { - public struct Context: Codable, Sendable { - public struct Authorizer: Codable, Sendable { - public struct IAMAuthorizer: Codable, Sendable { - public let accessKey: String - - public let accountId: String - public let callerId: String - public let cognitoIdentity: String? - - public let principalOrgId: String? - - public let userArn: String - public let userId: String - } - - public let iam: IAMAuthorizer? - } - - public struct HTTP: Codable, Sendable { - public let method: String - public let path: String - public let `protocol`: String - public let sourceIp: String - public let userAgent: String - } - - public let accountId: String - public let apiId: String - public let authentication: String? - public let authorizer: Authorizer? - public let domainName: String - public let domainPrefix: String - public let http: HTTP - - public let requestId: String - public let routeKey: String - public let stage: String - - public let time: String - public let timeEpoch: Int - } - - public let version: String - - public let routeKey: String - public let rawPath: String - public let rawQueryString: String - public let cookies: [String]? - public let headers: [String: String] - public let queryStringParameters: [String: String]? - - public let requestContext: Context - - public let body: String? - public let pathParameters: [String: String]? - public let isBase64Encoded: Bool - - public let stageVariables: [String: String]? -} - -// MARK: - Response - - -public struct FunctionURLResponse: Codable, Sendable { - public var statusCode: Int - public var headers: [String: String]? - public var body: String? - public let cookies: [String]? - public var isBase64Encoded: Bool? - - public init( - statusCode: Int, - headers: [String: String]? = nil, - body: String? = nil, - cookies: [String]? = nil, - isBase64Encoded: Bool? = nil - ) { - self.statusCode = statusCode - self.headers = headers - self.body = body - self.cookies = cookies - self.isBase64Encoded = isBase64Encoded - } -}