From 2c6c3289941f42b7381051a34489439529645320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 13 Sep 2025 20:48:12 +0200 Subject: [PATCH 1/4] make sure the lib compiles when no traits are enabled --- .../FoundationSupport/Lambda+JSON.swift | 20 ++++++++++++++++++- .../LambdaResponseStreamWriter+Headers.swift | 20 ------------------- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 4 +--- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift index 4d74d48e..c8ca65ef 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift @@ -84,7 +84,25 @@ extension LambdaCodableAdapter { ) } } - +@available(LambdaSwift 2.0, *) +extension LambdaResponseStreamWriter { + /// Writes the HTTP status code and headers to the response stream. + /// + /// This method serializes the status and headers as JSON and writes them to the stream, + /// followed by eight null bytes as a separator before the response body. + /// + /// - Parameters: + /// - response: The status and headers response to write + /// - encoder: The encoder to use for serializing the response, use JSONEncoder by default + /// - Throws: An error if JSON serialization or writing fails + public func writeStatusAndHeaders( + _ response: StreamingLambdaStatusAndHeadersResponse, + encoder: JSONEncoder = JSONEncoder() + ) async throws { + encoder.outputFormatting = .withoutEscapingSlashes + try await self.writeStatusAndHeaders(response, encoder: LambdaJSONOutputEncoder(encoder)) + } +} @available(LambdaSwift 2.0, *) extension LambdaRuntime { /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**. diff --git a/Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift b/Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift index b1796fcd..0aeb84e5 100644 --- a/Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift +++ b/Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift @@ -86,23 +86,3 @@ extension LambdaResponseStreamWriter { try await self.write(buffer, hasCustomHeaders: false) } } - -@available(LambdaSwift 2.0, *) -extension LambdaResponseStreamWriter { - /// Writes the HTTP status code and headers to the response stream. - /// - /// This method serializes the status and headers as JSON and writes them to the stream, - /// followed by eight null bytes as a separator before the response body. - /// - /// - Parameters: - /// - response: The status and headers response to write - /// - encoder: The encoder to use for serializing the response, use JSONEncoder by default - /// - Throws: An error if JSON serialization or writing fails - public func writeStatusAndHeaders( - _ response: StreamingLambdaStatusAndHeadersResponse, - encoder: JSONEncoder = JSONEncoder() - ) async throws { - encoder.outputFormatting = .withoutEscapingSlashes - try await self.writeStatusAndHeaders(response, encoder: LambdaJSONOutputEncoder(encoder)) - } -} diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index d3a5d5bf..33bd46fd 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -59,14 +59,12 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb } #if !ServiceLifecycleSupport - @inlinable - internal func run() async throws { + public func run() async throws { try await _run() } #endif /// Make sure only one run() is called at a time - // @inlinable internal func _run() async throws { // we use an atomic global variable to ensure only one LambdaRuntime is running at the time From a0984ef75ae3f91b9bfdab51bb1875bb738fffc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 13 Sep 2025 20:48:24 +0200 Subject: [PATCH 2/4] add an example in the CI --- .github/workflows/pull_request.yml | 2 +- Examples/HelloWorldNoTraits/.gitignore | 4 ++ Examples/HelloWorldNoTraits/Package.swift | 54 +++++++++++++++++++ Examples/HelloWorldNoTraits/README.md | 12 +++++ .../HelloWorldNoTraits/Sources/main.swift | 22 ++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 Examples/HelloWorldNoTraits/.gitignore create mode 100644 Examples/HelloWorldNoTraits/Package.swift create mode 100644 Examples/HelloWorldNoTraits/README.md create mode 100644 Examples/HelloWorldNoTraits/Sources/main.swift diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index f668b9d5..a36a6fbd 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', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" + examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', '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/HelloWorldNoTraits/.gitignore b/Examples/HelloWorldNoTraits/.gitignore new file mode 100644 index 00000000..e41d0be5 --- /dev/null +++ b/Examples/HelloWorldNoTraits/.gitignore @@ -0,0 +1,4 @@ +response.json +samconfig.toml +template.yaml +Makefile diff --git a/Examples/HelloWorldNoTraits/Package.swift b/Examples/HelloWorldNoTraits/Package.swift new file mode 100644 index 00000000..51b7e2bf --- /dev/null +++ b/Examples/HelloWorldNoTraits/Package.swift @@ -0,0 +1,54 @@ +// swift-tools-version:6.1 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]) + ], + 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.3", traits: []) + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources" + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath, traits: []) + ] +} diff --git a/Examples/HelloWorldNoTraits/README.md b/Examples/HelloWorldNoTraits/README.md new file mode 100644 index 00000000..104b8c1a --- /dev/null +++ b/Examples/HelloWorldNoTraits/README.md @@ -0,0 +1,12 @@ +# Hello World, with no traits + +This is a simple example of an AWS Lambda function that takes a `String` as input parameter and returns a `String` as response. + +This function disables all the default traits: the support for JSON from Foundation, for Swift Service Lifecycle, and for the local server for testing. + +The main reasons of the existence of this example are + +1. to show you how to disable traits when using the Lambda Runtime Library +2. to add an integration test to our continous integration pipeline to make sure the library compiles with no traits enabled. + +For more details about this example, refer to the example in `Examples/HelloWorld`. \ No newline at end of file diff --git a/Examples/HelloWorldNoTraits/Sources/main.swift b/Examples/HelloWorldNoTraits/Sources/main.swift new file mode 100644 index 00000000..f0ab481a --- /dev/null +++ b/Examples/HelloWorldNoTraits/Sources/main.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 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 NIOCore + +let runtime = LambdaRuntime { event , response, context in + try await response.writeAndFinish(ByteBuffer(string: "Hello World!")) +} + +try await runtime.run() From d277c28c299680b5132d414e7b60064ea24292d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 13 Sep 2025 21:00:04 +0200 Subject: [PATCH 3/4] swift-format --- Examples/HelloWorldNoTraits/Sources/main.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/HelloWorldNoTraits/Sources/main.swift b/Examples/HelloWorldNoTraits/Sources/main.swift index f0ab481a..f0f167ab 100644 --- a/Examples/HelloWorldNoTraits/Sources/main.swift +++ b/Examples/HelloWorldNoTraits/Sources/main.swift @@ -15,7 +15,7 @@ import AWSLambdaRuntime import NIOCore -let runtime = LambdaRuntime { event , response, context in +let runtime = LambdaRuntime { event, response, context in try await response.writeAndFinish(ByteBuffer(string: "Hello World!")) } From cbd9aecc4c118d72e6473aaddad0b15926b476b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 13 Sep 2025 21:16:58 +0200 Subject: [PATCH 4/4] add more details to the README --- Examples/HelloWorldNoTraits/README.md | 105 ++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/Examples/HelloWorldNoTraits/README.md b/Examples/HelloWorldNoTraits/README.md index 104b8c1a..d04afd0c 100644 --- a/Examples/HelloWorldNoTraits/README.md +++ b/Examples/HelloWorldNoTraits/README.md @@ -1,12 +1,107 @@ # Hello World, with no traits -This is a simple example of an AWS Lambda function that takes a `String` as input parameter and returns a `String` as response. +This is an example of a low-level AWS Lambda function that takes a `ByteBuffer` as input parameter and writes its response on the provided `LambdaResponseStreamWriter`. -This function disables all the default traits: the support for JSON from Foundation, for Swift Service Lifecycle, and for the local server for testing. +This function disables all the default traits: the support for JSON Encoder and Decoder from Foundation, the support for Swift Service Lifecycle, and for the local tetsing server. The main reasons of the existence of this example are -1. to show you how to disable traits when using the Lambda Runtime Library -2. to add an integration test to our continous integration pipeline to make sure the library compiles with no traits enabled. +1. to show how to write a low-level Lambda function that doesn't rely on JSON encodinga and decoding. +2. to show you how to disable traits when using the Lambda Runtime Library. +3. to add an integration test to our continous integration pipeline to make sure the library compiles with no traits enabled. -For more details about this example, refer to the example in `Examples/HelloWorld`. \ No newline at end of file +## Disabling all traits + +Traits are functions of the AWS Lambda Runtime that you can disable at compile time to reduce the size of your binary, and therefore reduce the cold start time of your Lambda function. + +The library supports three traits: + +- "FoundationJSONSupport": adds the required API to encode and decode payloads with Foundation's `JSONEncoder` and `JSONDecoder`. + +- "ServiceLifecycleSupport": adds support for the Swift Service Lifecycle library. + +- "LocalServerSupport": adds support for testing your function locally with a built-in HTTP server. + +This example disables all the traits. To disable one or several traits, modify `Package.swift`: + +```swift + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0-beta", traits: []) + ], +``` + +## Code + +The code creates a `LambdaRuntime` struct. In its simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when an event triggers the Lambda function. + +The handler signature is `(event: ByteBuffer, response: LambdaResponseStreamWriter, context: LambdaContext)`. + +The function takes three arguments: +- the event argument is a `ByteBuffer`. It's the parameter passed when invoking the function. You are responsible of decoding this parameter, if necessary. +- the response writer provides you with functions to write the response stream back. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function return value will be encoded as your Lambda function response. + +## Test locally + +You cannot test this function locally, because the "LocalServer" trait is disabled. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip` + +## Deploy + +Here is how to deploy using the `aws` command line. + +```bash +aws lambda create-function \ +--function-name MyLambda \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam:::role/lambda_basic_execution +``` + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +Be sure to replace with your actual AWS account ID (for example: 012345678901). + +## Invoke your Lambda function + +To invoke the Lambda function, use this `aws` command line. + +```bash +aws lambda invoke \ +--function-name MyLambda \ +--payload $(echo "Seb" | base64) \ +out.txt && cat out.txt && rm out.txt +``` + +This should output the following result. + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} +"Hello World!" +``` + +## Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name MyLambda +``` \ No newline at end of file