From 8f651e8ba6fc8b4e5c7d4a69d2c0bb2e319b4ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 31 Jul 2025 09:26:48 +0400 Subject: [PATCH 1/5] Use a struct for LambdaContext (fix #169) --- Sources/AWSLambdaRuntime/LambdaContext.swift | 74 +++++++++++- .../LambdaContextTests.swift | 113 ++++++++++++++++++ 2 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index fbf84158..c79bc3e5 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -16,6 +16,70 @@ import Dispatch import Logging import NIOCore +// MARK: - Client Context + +/// AWS Mobile SDK client fields. +public struct ClientApplication: Codable, Sendable { + /// The mobile app installation id + public let installationId: String? + /// The app title for the mobile app as registered with AWS' mobile services. + public let appTitle: String? + /// The version name of the application as registered with AWS' mobile services. + public let appVersionName: String? + /// The app version code. + public let appVersionCode: String? + /// The package name for the mobile application invoking the function + public let appPackageName: String? + + private enum CodingKeys: String, CodingKey { + case installationId = "installation_id" + case appTitle = "app_title" + case appVersionName = "app_version_name" + case appVersionCode = "app_version_code" + case appPackageName = "app_package_name" + } + + public init( + installationId: String? = nil, + appTitle: String? = nil, + appVersionName: String? = nil, + appVersionCode: String? = nil, + appPackageName: String? = nil + ) { + self.installationId = installationId + self.appTitle = appTitle + self.appVersionName = appVersionName + self.appVersionCode = appVersionCode + self.appPackageName = appPackageName + } +} + +/// For invocations from the AWS Mobile SDK, data about the client application and device. +public struct ClientContext: Codable, Sendable { + /// Information about the mobile application invoking the function. + public let client: ClientApplication? + /// Custom properties attached to the mobile event context. + public let custom: [String: String]? + /// Environment settings from the mobile client. + public let environment: [String: String]? + + private enum CodingKeys: String, CodingKey { + case client + case custom + case environment = "env" + } + + public init( + client: ClientApplication? = nil, + custom: [String: String]? = nil, + environment: [String: String]? = nil + ) { + self.client = client + self.custom = custom + self.environment = environment + } +} + // MARK: - Context /// Lambda runtime context. @@ -27,7 +91,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { let invokedFunctionARN: String let deadline: DispatchWallTime let cognitoIdentity: String? - let clientContext: String? + let clientContext: ClientContext? let logger: Logger init( @@ -36,7 +100,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { invokedFunctionARN: String, deadline: DispatchWallTime, cognitoIdentity: String?, - clientContext: String?, + clientContext: ClientContext?, logger: Logger ) { self.requestID = requestID @@ -77,7 +141,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { } /// For invocations from the AWS Mobile SDK, data about the client application and device. - public var clientContext: String? { + public var clientContext: ClientContext? { self.storage.clientContext } @@ -94,7 +158,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { invokedFunctionARN: String, deadline: DispatchWallTime, cognitoIdentity: String? = nil, - clientContext: String? = nil, + clientContext: ClientContext? = nil, logger: Logger ) { self.storage = _Storage( @@ -117,7 +181,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { } public var debugDescription: String { - "\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(self.clientContext ?? "nil"), deadline: \(self.deadline))" + "\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(String(describing: self.clientContext)), deadline: \(self.deadline))" } /// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning. diff --git a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift new file mode 100644 index 00000000..678be749 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Testing +@testable import AWSLambdaRuntime +import Foundation + +@Suite("LambdaContext ClientContext Tests") +struct LambdaContextTests { + + @Test("ClientContext with full data resolves correctly") + func clientContextWithFullDataResolves() throws { + let custom = ["key": "value"] + let environment = ["key": "value"] + let clientContext = ClientContext( + client: ClientApplication( + installationId: "test-id", + appTitle: "test-app", + appVersionName: "1.0", + appVersionCode: "100", + appPackageName: "com.test.app" + ), + custom: custom, + environment: environment + ) + + let encoder = JSONEncoder() + let clientContextData = try encoder.encode(clientContext) + + // Verify JSON encoding/decoding works correctly + let decoder = JSONDecoder() + let decodedClientContext = try decoder.decode(ClientContext.self, from: clientContextData) + + let decodedClient = try #require(decodedClientContext.client) + let originalClient = try #require(clientContext.client) + + #expect(decodedClient.installationId == originalClient.installationId) + #expect(decodedClient.appTitle == originalClient.appTitle) + #expect(decodedClient.appVersionName == originalClient.appVersionName) + #expect(decodedClient.appVersionCode == originalClient.appVersionCode) + #expect(decodedClient.appPackageName == originalClient.appPackageName) + #expect(decodedClientContext.custom == clientContext.custom) + #expect(decodedClientContext.environment == clientContext.environment) + } + + @Test("ClientContext with empty data resolves correctly") + func clientContextWithEmptyDataResolves() throws { + let emptyClientContextJSON = "{}" + let emptyClientContextData = emptyClientContextJSON.data(using: .utf8)! + + let decoder = JSONDecoder() + let decodedClientContext = try decoder.decode(ClientContext.self, from: emptyClientContextData) + + // With empty JSON, we expect nil values for optional fields + #expect(decodedClientContext.client == nil) + #expect(decodedClientContext.custom == nil) + #expect(decodedClientContext.environment == nil) + } + + @Test("ClientContext with AWS Lambda JSON payload decodes correctly") + func clientContextWithAWSLambdaJSONPayload() throws { + let jsonPayload = """ + { + "client": { + "installation_id": "example-id", + "app_title": "Example App", + "app_version_name": "1.0", + "app_version_code": "1", + "app_package_name": "com.example.app" + }, + "custom": { + "customKey": "customValue" + }, + "env": { + "platform": "Android", + "platform_version": "10" + } + } + """ + + let jsonData = jsonPayload.data(using: .utf8)! + let decoder = JSONDecoder() + let decodedClientContext = try decoder.decode(ClientContext.self, from: jsonData) + + // Verify client application data + let client = try #require(decodedClientContext.client) + #expect(client.installationId == "example-id") + #expect(client.appTitle == "Example App") + #expect(client.appVersionName == "1.0") + #expect(client.appVersionCode == "1") + #expect(client.appPackageName == "com.example.app") + + // Verify custom properties + let custom = try #require(decodedClientContext.custom) + #expect(custom["customKey"] == "customValue") + + // Verify environment settings + let environment = try #require(decodedClientContext.environment) + #expect(environment["platform"] == "Android") + #expect(environment["platform_version"] == "10") + } +} \ No newline at end of file From d8ffed641573a984aa84ff07c4848204402c5dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 31 Jul 2025 19:04:13 +0400 Subject: [PATCH 2/5] swift-format --- Sources/AWSLambdaRuntime/LambdaContext.swift | 8 +-- .../LambdaContextTests.swift | 65 ++++++++++--------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index c79bc3e5..e5ea0aed 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -30,7 +30,7 @@ public struct ClientApplication: Codable, Sendable { public let appVersionCode: String? /// The package name for the mobile application invoking the function public let appPackageName: String? - + private enum CodingKeys: String, CodingKey { case installationId = "installation_id" case appTitle = "app_title" @@ -38,7 +38,7 @@ public struct ClientApplication: Codable, Sendable { case appVersionCode = "app_version_code" case appPackageName = "app_package_name" } - + public init( installationId: String? = nil, appTitle: String? = nil, @@ -62,13 +62,13 @@ public struct ClientContext: Codable, Sendable { public let custom: [String: String]? /// Environment settings from the mobile client. public let environment: [String: String]? - + private enum CodingKeys: String, CodingKey { case client case custom case environment = "env" } - + public init( client: ClientApplication? = nil, custom: [String: String]? = nil, diff --git a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift index 678be749..047a1cf9 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -12,13 +12,14 @@ // //===----------------------------------------------------------------------===// +import Foundation import Testing + @testable import AWSLambdaRuntime -import Foundation @Suite("LambdaContext ClientContext Tests") struct LambdaContextTests { - + @Test("ClientContext with full data resolves correctly") func clientContextWithFullDataResolves() throws { let custom = ["key": "value"] @@ -34,17 +35,17 @@ struct LambdaContextTests { custom: custom, environment: environment ) - + let encoder = JSONEncoder() let clientContextData = try encoder.encode(clientContext) - + // Verify JSON encoding/decoding works correctly let decoder = JSONDecoder() let decodedClientContext = try decoder.decode(ClientContext.self, from: clientContextData) - + let decodedClient = try #require(decodedClientContext.client) let originalClient = try #require(clientContext.client) - + #expect(decodedClient.installationId == originalClient.installationId) #expect(decodedClient.appTitle == originalClient.appTitle) #expect(decodedClient.appVersionName == originalClient.appVersionName) @@ -53,46 +54,46 @@ struct LambdaContextTests { #expect(decodedClientContext.custom == clientContext.custom) #expect(decodedClientContext.environment == clientContext.environment) } - + @Test("ClientContext with empty data resolves correctly") func clientContextWithEmptyDataResolves() throws { let emptyClientContextJSON = "{}" let emptyClientContextData = emptyClientContextJSON.data(using: .utf8)! - + let decoder = JSONDecoder() let decodedClientContext = try decoder.decode(ClientContext.self, from: emptyClientContextData) - + // With empty JSON, we expect nil values for optional fields #expect(decodedClientContext.client == nil) #expect(decodedClientContext.custom == nil) #expect(decodedClientContext.environment == nil) } - + @Test("ClientContext with AWS Lambda JSON payload decodes correctly") func clientContextWithAWSLambdaJSONPayload() throws { let jsonPayload = """ - { - "client": { - "installation_id": "example-id", - "app_title": "Example App", - "app_version_name": "1.0", - "app_version_code": "1", - "app_package_name": "com.example.app" - }, - "custom": { - "customKey": "customValue" - }, - "env": { - "platform": "Android", - "platform_version": "10" - } - } - """ - + { + "client": { + "installation_id": "example-id", + "app_title": "Example App", + "app_version_name": "1.0", + "app_version_code": "1", + "app_package_name": "com.example.app" + }, + "custom": { + "customKey": "customValue" + }, + "env": { + "platform": "Android", + "platform_version": "10" + } + } + """ + let jsonData = jsonPayload.data(using: .utf8)! let decoder = JSONDecoder() let decodedClientContext = try decoder.decode(ClientContext.self, from: jsonData) - + // Verify client application data let client = try #require(decodedClientContext.client) #expect(client.installationId == "example-id") @@ -100,14 +101,14 @@ struct LambdaContextTests { #expect(client.appVersionName == "1.0") #expect(client.appVersionCode == "1") #expect(client.appPackageName == "com.example.app") - + // Verify custom properties let custom = try #require(decodedClientContext.custom) #expect(custom["customKey"] == "customValue") - + // Verify environment settings let environment = try #require(decodedClientContext.environment) #expect(environment["platform"] == "Android") #expect(environment["platform_version"] == "10") } -} \ No newline at end of file +} From fa260738a43aa437f1b54521af938535db99295c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 4 Aug 2025 07:42:52 +0200 Subject: [PATCH 3/5] Update Sources/AWSLambdaRuntime/LambdaContext.swift Co-authored-by: Konrad `ktoso` Malawski --- Sources/AWSLambdaRuntime/LambdaContext.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index e5ea0aed..8cb9ff86 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -21,7 +21,7 @@ import NIOCore /// AWS Mobile SDK client fields. public struct ClientApplication: Codable, Sendable { /// The mobile app installation id - public let installationId: String? + public let installationID: String? /// The app title for the mobile app as registered with AWS' mobile services. public let appTitle: String? /// The version name of the application as registered with AWS' mobile services. From 66f6d8fc83a864f90f92debfee3e61cd0a19a15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 4 Aug 2025 07:52:46 +0200 Subject: [PATCH 4/5] update Id to ID --- Sources/AWSLambdaRuntime/LambdaContext.swift | 6 +++--- Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index 8cb9ff86..81fa72b9 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -32,7 +32,7 @@ public struct ClientApplication: Codable, Sendable { public let appPackageName: String? private enum CodingKeys: String, CodingKey { - case installationId = "installation_id" + case installationID = "installation_id" case appTitle = "app_title" case appVersionName = "app_version_name" case appVersionCode = "app_version_code" @@ -40,13 +40,13 @@ public struct ClientApplication: Codable, Sendable { } public init( - installationId: String? = nil, + installationID: String? = nil, appTitle: String? = nil, appVersionName: String? = nil, appVersionCode: String? = nil, appPackageName: String? = nil ) { - self.installationId = installationId + self.installationID = installationID self.appTitle = appTitle self.appVersionName = appVersionName self.appVersionCode = appVersionCode diff --git a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift index 047a1cf9..80ca90b0 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -26,7 +26,7 @@ struct LambdaContextTests { let environment = ["key": "value"] let clientContext = ClientContext( client: ClientApplication( - installationId: "test-id", + installationID: "test-id", appTitle: "test-app", appVersionName: "1.0", appVersionCode: "100", @@ -44,9 +44,9 @@ struct LambdaContextTests { let decodedClientContext = try decoder.decode(ClientContext.self, from: clientContextData) let decodedClient = try #require(decodedClientContext.client) - let originalClient = try #require(clientContext.client) + let originalClient = try #require(clientContext.client) - #expect(decodedClient.installationId == originalClient.installationId) + #expect(decodedClient.installationID == originalClient.installationID) #expect(decodedClient.appTitle == originalClient.appTitle) #expect(decodedClient.appVersionName == originalClient.appVersionName) #expect(decodedClient.appVersionCode == originalClient.appVersionCode) @@ -96,7 +96,7 @@ struct LambdaContextTests { // Verify client application data let client = try #require(decodedClientContext.client) - #expect(client.installationId == "example-id") + #expect(client.installationID == "example-id") #expect(client.appTitle == "Example App") #expect(client.appVersionName == "1.0") #expect(client.appVersionCode == "1") From c314b770f90f79c1ad624bff95e227b32e9dcefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 4 Aug 2025 08:08:04 +0200 Subject: [PATCH 5/5] swift-format --- Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift index 80ca90b0..c1108b68 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -44,7 +44,7 @@ struct LambdaContextTests { let decodedClientContext = try decoder.decode(ClientContext.self, from: clientContextData) let decodedClient = try #require(decodedClientContext.client) - let originalClient = try #require(clientContext.client) + let originalClient = try #require(clientContext.client) #expect(decodedClient.installationID == originalClient.installationID) #expect(decodedClient.appTitle == originalClient.appTitle)