diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index fbf84158..81fa72b9 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..c1108b68 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import Testing + +@testable import AWSLambdaRuntime + +@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") + } +}