Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 69 additions & 5 deletions Sources/AWSLambdaRuntime/LambdaContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(
Expand All @@ -36,7 +100,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
invokedFunctionARN: String,
deadline: DispatchWallTime,
cognitoIdentity: String?,
clientContext: String?,
clientContext: ClientContext?,
logger: Logger
) {
self.requestID = requestID
Expand Down Expand Up @@ -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
}

Expand All @@ -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(
Expand All @@ -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.
Expand Down
114 changes: 114 additions & 0 deletions Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading