Skip to content

Commit bae9f27

Browse files
sebstoktoso
andauthored
Use a struct for ClientContext (fix #169) (#539)
Do not use a String for Lambdacontext.ClientContext, use a struct instead. Fix for #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 <[email protected]>
1 parent 76c3a44 commit bae9f27

File tree

2 files changed

+183
-5
lines changed

2 files changed

+183
-5
lines changed

Sources/AWSLambdaRuntime/LambdaContext.swift

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,70 @@ import Dispatch
1616
import Logging
1717
import NIOCore
1818

19+
// MARK: - Client Context
20+
21+
/// AWS Mobile SDK client fields.
22+
public struct ClientApplication: Codable, Sendable {
23+
/// The mobile app installation id
24+
public let installationID: String?
25+
/// The app title for the mobile app as registered with AWS' mobile services.
26+
public let appTitle: String?
27+
/// The version name of the application as registered with AWS' mobile services.
28+
public let appVersionName: String?
29+
/// The app version code.
30+
public let appVersionCode: String?
31+
/// The package name for the mobile application invoking the function
32+
public let appPackageName: String?
33+
34+
private enum CodingKeys: String, CodingKey {
35+
case installationID = "installation_id"
36+
case appTitle = "app_title"
37+
case appVersionName = "app_version_name"
38+
case appVersionCode = "app_version_code"
39+
case appPackageName = "app_package_name"
40+
}
41+
42+
public init(
43+
installationID: String? = nil,
44+
appTitle: String? = nil,
45+
appVersionName: String? = nil,
46+
appVersionCode: String? = nil,
47+
appPackageName: String? = nil
48+
) {
49+
self.installationID = installationID
50+
self.appTitle = appTitle
51+
self.appVersionName = appVersionName
52+
self.appVersionCode = appVersionCode
53+
self.appPackageName = appPackageName
54+
}
55+
}
56+
57+
/// For invocations from the AWS Mobile SDK, data about the client application and device.
58+
public struct ClientContext: Codable, Sendable {
59+
/// Information about the mobile application invoking the function.
60+
public let client: ClientApplication?
61+
/// Custom properties attached to the mobile event context.
62+
public let custom: [String: String]?
63+
/// Environment settings from the mobile client.
64+
public let environment: [String: String]?
65+
66+
private enum CodingKeys: String, CodingKey {
67+
case client
68+
case custom
69+
case environment = "env"
70+
}
71+
72+
public init(
73+
client: ClientApplication? = nil,
74+
custom: [String: String]? = nil,
75+
environment: [String: String]? = nil
76+
) {
77+
self.client = client
78+
self.custom = custom
79+
self.environment = environment
80+
}
81+
}
82+
1983
// MARK: - Context
2084

2185
/// Lambda runtime context.
@@ -27,7 +91,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
2791
let invokedFunctionARN: String
2892
let deadline: DispatchWallTime
2993
let cognitoIdentity: String?
30-
let clientContext: String?
94+
let clientContext: ClientContext?
3195
let logger: Logger
3296

3397
init(
@@ -36,7 +100,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
36100
invokedFunctionARN: String,
37101
deadline: DispatchWallTime,
38102
cognitoIdentity: String?,
39-
clientContext: String?,
103+
clientContext: ClientContext?,
40104
logger: Logger
41105
) {
42106
self.requestID = requestID
@@ -77,7 +141,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
77141
}
78142

79143
/// For invocations from the AWS Mobile SDK, data about the client application and device.
80-
public var clientContext: String? {
144+
public var clientContext: ClientContext? {
81145
self.storage.clientContext
82146
}
83147

@@ -94,7 +158,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
94158
invokedFunctionARN: String,
95159
deadline: DispatchWallTime,
96160
cognitoIdentity: String? = nil,
97-
clientContext: String? = nil,
161+
clientContext: ClientContext? = nil,
98162
logger: Logger
99163
) {
100164
self.storage = _Storage(
@@ -117,7 +181,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
117181
}
118182

119183
public var debugDescription: String {
120-
"\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(self.clientContext ?? "nil"), deadline: \(self.deadline))"
184+
"\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(String(describing: self.clientContext)), deadline: \(self.deadline))"
121185
}
122186

123187
/// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2022 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 Foundation
16+
import Testing
17+
18+
@testable import AWSLambdaRuntime
19+
20+
@Suite("LambdaContext ClientContext Tests")
21+
struct LambdaContextTests {
22+
23+
@Test("ClientContext with full data resolves correctly")
24+
func clientContextWithFullDataResolves() throws {
25+
let custom = ["key": "value"]
26+
let environment = ["key": "value"]
27+
let clientContext = ClientContext(
28+
client: ClientApplication(
29+
installationID: "test-id",
30+
appTitle: "test-app",
31+
appVersionName: "1.0",
32+
appVersionCode: "100",
33+
appPackageName: "com.test.app"
34+
),
35+
custom: custom,
36+
environment: environment
37+
)
38+
39+
let encoder = JSONEncoder()
40+
let clientContextData = try encoder.encode(clientContext)
41+
42+
// Verify JSON encoding/decoding works correctly
43+
let decoder = JSONDecoder()
44+
let decodedClientContext = try decoder.decode(ClientContext.self, from: clientContextData)
45+
46+
let decodedClient = try #require(decodedClientContext.client)
47+
let originalClient = try #require(clientContext.client)
48+
49+
#expect(decodedClient.installationID == originalClient.installationID)
50+
#expect(decodedClient.appTitle == originalClient.appTitle)
51+
#expect(decodedClient.appVersionName == originalClient.appVersionName)
52+
#expect(decodedClient.appVersionCode == originalClient.appVersionCode)
53+
#expect(decodedClient.appPackageName == originalClient.appPackageName)
54+
#expect(decodedClientContext.custom == clientContext.custom)
55+
#expect(decodedClientContext.environment == clientContext.environment)
56+
}
57+
58+
@Test("ClientContext with empty data resolves correctly")
59+
func clientContextWithEmptyDataResolves() throws {
60+
let emptyClientContextJSON = "{}"
61+
let emptyClientContextData = emptyClientContextJSON.data(using: .utf8)!
62+
63+
let decoder = JSONDecoder()
64+
let decodedClientContext = try decoder.decode(ClientContext.self, from: emptyClientContextData)
65+
66+
// With empty JSON, we expect nil values for optional fields
67+
#expect(decodedClientContext.client == nil)
68+
#expect(decodedClientContext.custom == nil)
69+
#expect(decodedClientContext.environment == nil)
70+
}
71+
72+
@Test("ClientContext with AWS Lambda JSON payload decodes correctly")
73+
func clientContextWithAWSLambdaJSONPayload() throws {
74+
let jsonPayload = """
75+
{
76+
"client": {
77+
"installation_id": "example-id",
78+
"app_title": "Example App",
79+
"app_version_name": "1.0",
80+
"app_version_code": "1",
81+
"app_package_name": "com.example.app"
82+
},
83+
"custom": {
84+
"customKey": "customValue"
85+
},
86+
"env": {
87+
"platform": "Android",
88+
"platform_version": "10"
89+
}
90+
}
91+
"""
92+
93+
let jsonData = jsonPayload.data(using: .utf8)!
94+
let decoder = JSONDecoder()
95+
let decodedClientContext = try decoder.decode(ClientContext.self, from: jsonData)
96+
97+
// Verify client application data
98+
let client = try #require(decodedClientContext.client)
99+
#expect(client.installationID == "example-id")
100+
#expect(client.appTitle == "Example App")
101+
#expect(client.appVersionName == "1.0")
102+
#expect(client.appVersionCode == "1")
103+
#expect(client.appPackageName == "com.example.app")
104+
105+
// Verify custom properties
106+
let custom = try #require(decodedClientContext.custom)
107+
#expect(custom["customKey"] == "customValue")
108+
109+
// Verify environment settings
110+
let environment = try #require(decodedClientContext.environment)
111+
#expect(environment["platform"] == "Android")
112+
#expect(environment["platform_version"] == "10")
113+
}
114+
}

0 commit comments

Comments
 (0)