Use a struct for ClientContext (fix #169) (#539)

Do not use a String for Lambdacontext.ClientContext, use a struct instead.
Fix for https://github.com/swift-server/swift-aws-lambda-runtime/issues/169

Note: this PR introduces an API change that will break function using
`LambdaContext`, we should integrate this change during the beta
otherwise it will require a major version bump.

### Motivation:

Let the compiler detect type errors for us

### Modifications:

- Create a struct for ClientContext and it's embedded ClientApplication
- add three unit test to validate the struct 

### Result:

No more String?

---------

Co-authored-by: Konrad `ktoso` Malawski <konrad.malawski@project13.pl>
This commit is contained in:
Sébastien Stormacq
2025-08-04 08:24:46 +02:00
committed by GitHub
parent 76c3a441de
commit bae9f27ccb
2 changed files with 183 additions and 5 deletions
+69 -5
View File
@@ -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.
@@ -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")
}
}