Skip to content

Commit 1843cdc

Browse files
authored
[fix] The library must compile when no traits are enabled (#563)
Make required changed to ensure the library compiles when no traits are enabled (fix: #562) Add an example project that serves as test in the CI ### Motivation: Recent changes introduced compilation errors when no traits are enabled. ### Modifications: - `LambdaResponseStreamWriter.writeStatusAndHeaders()` moved to the `FoundationSupport` directory where all classes and struct depending on `Encodable` and `Decodable` are located, protected by `#if FoundationJSONSupport` - `LambdaRuntime.run()` method when ServiceLifeCycle is disabled in now public (and therefore can not be `@inlinable` anymore) - Add an example that disables all traits. - Add this example to the CI ### Result: The Library now compiles when no default traits are enabled. This is flagged `semver/major` because we change the public API `LambdaRuntime.run()`
1 parent 303559d commit 1843cdc

File tree

8 files changed

+208
-25
lines changed

8 files changed

+208
-25
lines changed

.github/workflows/pull_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
# We pass the list of examples here, but we can't pass an array as argument
3737
# Instead, we pass a String with a valid JSON array.
3838
# The workaround is mentioned here https://github.com/orgs/community/discussions/11692
39-
examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HummingbirdLambda', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]"
39+
examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]"
4040
archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]"
4141
archive_plugin_enabled: true
4242

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
response.json
2+
samconfig.toml
3+
template.yaml
4+
Makefile
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// swift-tools-version:6.1
2+
3+
import PackageDescription
4+
5+
// needed for CI to test the local version of the library
6+
import struct Foundation.URL
7+
8+
let package = Package(
9+
name: "swift-aws-lambda-runtime-example",
10+
platforms: [.macOS(.v15)],
11+
products: [
12+
.executable(name: "MyLambda", targets: ["MyLambda"])
13+
],
14+
dependencies: [
15+
// during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below
16+
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0-beta.3", traits: [])
17+
],
18+
targets: [
19+
.executableTarget(
20+
name: "MyLambda",
21+
dependencies: [
22+
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime")
23+
],
24+
path: "Sources"
25+
)
26+
]
27+
)
28+
29+
if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"],
30+
localDepsPath != "",
31+
let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]),
32+
v.isDirectory == true
33+
{
34+
// when we use the local runtime as deps, let's remove the dependency added above
35+
let indexToRemove = package.dependencies.firstIndex { dependency in
36+
if case .sourceControl(
37+
name: _,
38+
location: "https://github.com/swift-server/swift-aws-lambda-runtime.git",
39+
requirement: _
40+
) = dependency.kind {
41+
return true
42+
}
43+
return false
44+
}
45+
if let indexToRemove {
46+
package.dependencies.remove(at: indexToRemove)
47+
}
48+
49+
// then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..)
50+
print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)")
51+
package.dependencies += [
52+
.package(name: "swift-aws-lambda-runtime", path: localDepsPath, traits: [])
53+
]
54+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Hello World, with no traits
2+
3+
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`.
4+
5+
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.
6+
7+
The main reasons of the existence of this example are
8+
9+
1. to show how to write a low-level Lambda function that doesn't rely on JSON encodinga and decoding.
10+
2. to show you how to disable traits when using the Lambda Runtime Library.
11+
3. to add an integration test to our continous integration pipeline to make sure the library compiles with no traits enabled.
12+
13+
## Disabling all traits
14+
15+
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.
16+
17+
The library supports three traits:
18+
19+
- "FoundationJSONSupport": adds the required API to encode and decode payloads with Foundation's `JSONEncoder` and `JSONDecoder`.
20+
21+
- "ServiceLifecycleSupport": adds support for the Swift Service Lifecycle library.
22+
23+
- "LocalServerSupport": adds support for testing your function locally with a built-in HTTP server.
24+
25+
This example disables all the traits. To disable one or several traits, modify `Package.swift`:
26+
27+
```swift
28+
dependencies: [
29+
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0-beta", traits: [])
30+
],
31+
```
32+
33+
## Code
34+
35+
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.
36+
37+
The handler signature is `(event: ByteBuffer, response: LambdaResponseStreamWriter, context: LambdaContext)`.
38+
39+
The function takes three arguments:
40+
- the event argument is a `ByteBuffer`. It's the parameter passed when invoking the function. You are responsible of decoding this parameter, if necessary.
41+
- the response writer provides you with functions to write the response stream back.
42+
- the context argument is a `Lambda Context`. It is a description of the runtime context.
43+
44+
The function return value will be encoded as your Lambda function response.
45+
46+
## Test locally
47+
48+
You cannot test this function locally, because the "LocalServer" trait is disabled.
49+
50+
## Build & Package
51+
52+
To build & archive the package, type the following commands.
53+
54+
```bash
55+
swift build
56+
swift package archive --allow-network-connections docker
57+
```
58+
59+
If there is no error, there is a ZIP file ready to deploy.
60+
The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip`
61+
62+
## Deploy
63+
64+
Here is how to deploy using the `aws` command line.
65+
66+
```bash
67+
aws lambda create-function \
68+
--function-name MyLambda \
69+
--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \
70+
--runtime provided.al2 \
71+
--handler provided \
72+
--architectures arm64 \
73+
--role arn:aws:iam::<YOUR_ACCOUNT_ID>:role/lambda_basic_execution
74+
```
75+
76+
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`.
77+
78+
Be sure to replace <YOUR_ACCOUNT_ID> with your actual AWS account ID (for example: 012345678901).
79+
80+
## Invoke your Lambda function
81+
82+
To invoke the Lambda function, use this `aws` command line.
83+
84+
```bash
85+
aws lambda invoke \
86+
--function-name MyLambda \
87+
--payload $(echo "Seb" | base64) \
88+
out.txt && cat out.txt && rm out.txt
89+
```
90+
91+
This should output the following result.
92+
93+
```
94+
{
95+
"StatusCode": 200,
96+
"ExecutedVersion": "$LATEST"
97+
}
98+
"Hello World!"
99+
```
100+
101+
## Undeploy
102+
103+
When done testing, you can delete the Lambda function with this command.
104+
105+
```bash
106+
aws lambda delete-function --function-name MyLambda
107+
```
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import AWSLambdaRuntime
16+
import NIOCore
17+
18+
let runtime = LambdaRuntime { event, response, context in
19+
try await response.writeAndFinish(ByteBuffer(string: "Hello World!"))
20+
}
21+
22+
try await runtime.run()

Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,25 @@ extension LambdaCodableAdapter {
8484
)
8585
}
8686
}
87-
87+
@available(LambdaSwift 2.0, *)
88+
extension LambdaResponseStreamWriter {
89+
/// Writes the HTTP status code and headers to the response stream.
90+
///
91+
/// This method serializes the status and headers as JSON and writes them to the stream,
92+
/// followed by eight null bytes as a separator before the response body.
93+
///
94+
/// - Parameters:
95+
/// - response: The status and headers response to write
96+
/// - encoder: The encoder to use for serializing the response, use JSONEncoder by default
97+
/// - Throws: An error if JSON serialization or writing fails
98+
public func writeStatusAndHeaders(
99+
_ response: StreamingLambdaStatusAndHeadersResponse,
100+
encoder: JSONEncoder = JSONEncoder()
101+
) async throws {
102+
encoder.outputFormatting = .withoutEscapingSlashes
103+
try await self.writeStatusAndHeaders(response, encoder: LambdaJSONOutputEncoder(encoder))
104+
}
105+
}
88106
@available(LambdaSwift 2.0, *)
89107
extension LambdaRuntime {
90108
/// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**.

Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,23 +86,3 @@ extension LambdaResponseStreamWriter {
8686
try await self.write(buffer, hasCustomHeaders: false)
8787
}
8888
}
89-
90-
@available(LambdaSwift 2.0, *)
91-
extension LambdaResponseStreamWriter {
92-
/// Writes the HTTP status code and headers to the response stream.
93-
///
94-
/// This method serializes the status and headers as JSON and writes them to the stream,
95-
/// followed by eight null bytes as a separator before the response body.
96-
///
97-
/// - Parameters:
98-
/// - response: The status and headers response to write
99-
/// - encoder: The encoder to use for serializing the response, use JSONEncoder by default
100-
/// - Throws: An error if JSON serialization or writing fails
101-
public func writeStatusAndHeaders(
102-
_ response: StreamingLambdaStatusAndHeadersResponse,
103-
encoder: JSONEncoder = JSONEncoder()
104-
) async throws {
105-
encoder.outputFormatting = .withoutEscapingSlashes
106-
try await self.writeStatusAndHeaders(response, encoder: LambdaJSONOutputEncoder(encoder))
107-
}
108-
}

Sources/AWSLambdaRuntime/LambdaRuntime.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,12 @@ public final class LambdaRuntime<Handler>: Sendable where Handler: StreamingLamb
5959
}
6060

6161
#if !ServiceLifecycleSupport
62-
@inlinable
63-
internal func run() async throws {
62+
public func run() async throws {
6463
try await _run()
6564
}
6665
#endif
6766

6867
/// Make sure only one run() is called at a time
69-
// @inlinable
7068
internal func _run() async throws {
7169

7270
// we use an atomic global variable to ensure only one LambdaRuntime is running at the time

0 commit comments

Comments
 (0)