├── .gitignore ├── CODE_OF_CONDUCT.md ├── .mailmap ├── .github ├── release.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── workflows │ └── pull_request.yml ├── NOTICE.txt ├── SECURITY.md ├── .license_header_template ├── .licenseignore ├── Package@swift-5.10.swift ├── Package.swift ├── Tests └── AWSLambdaEventsTests │ ├── Utils │ ├── HTTPHeadersTests.swift │ ├── Base64Tests.swift │ ├── IteratorTests.swift │ ├── RFC5322DateParseStrategyTests.swift │ └── DateWrapperTests.swift │ ├── APIGateway+EncodableTests.swift │ ├── ALBTests.swift │ ├── BedrockAgentTests.swift │ ├── SNSTests.swift │ ├── LambdaGatewayProxyEventTests.swift │ ├── APIGateway+WebsocketsTests.swift │ ├── SESTests.swift │ ├── S3Tests.swift │ ├── APIGateway+V2IAMTests.swift │ ├── CloudFormationTests.swift │ ├── SQSTests.swift │ └── APIGatewayLambdaAuthorizerTest.swift ├── scripts ├── preview_docc.sh ├── generate_contributors_list.sh ├── check_format.sh └── check_license.sh ├── .devcontainer └── devcontainer.json ├── Sources └── AWSLambdaEvents │ ├── BedrockAgent.swift │ ├── Codable Helpers │ ├── SQS+Decode.swift │ ├── DecodableRequest.swift │ └── EncodableResponse.swift │ ├── APIGateway+Encodable.swift │ ├── LambdaGatewayProxyEvent.swift │ ├── S3.swift │ ├── APIGateway+WebSockets.swift │ ├── Docs.docc │ └── index.md │ ├── ALB.swift │ ├── SES.swift │ ├── SQS.swift │ ├── SNS.swift │ ├── Utils │ ├── HTTP.swift │ ├── DateWrappers.swift │ └── Base64.swift │ ├── FunctionURL.swift │ ├── APIGatewayLambdaAuthorizers.swift │ ├── AWSRegion.swift │ ├── APIGateway.swift │ ├── CloudFormation.swift │ ├── AppSync.swift │ └── APIGateway+V2.swift ├── .swift-format ├── CONTRIBUTORS.txt ├── readme.md └── CONTRIBUTING.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.build 3 | *.index-build 4 | /.xcodeproj 5 | *.pem 6 | .podspecs 7 | .swiftpm 8 | .swift-version 9 | xcuserdata 10 | Package.resolved 11 | .serverless 12 | .devcontainer 13 | .amazonq 14 | .ash -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Tomer Doron 2 | Tomer Doron 3 | Tomer Doron 4 | Fabian Fett 5 | Fabian Fett 6 | Natan Rolnik -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: SemVer Major 4 | labels: 5 | - ⚠️ semver/major 6 | - title: SemVer Minor 7 | labels: 8 | - 🆕 semver/minor 9 | - title: SemVer Patch 10 | labels: 11 | - 🔨 semver/patch 12 | - title: Other Changes 13 | labels: 14 | - semver/none 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | _[One line description of your change]_ 2 | 3 | ### Motivation: 4 | 5 | _[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_ 6 | 7 | ### Modifications: 8 | 9 | _[Describe the modifications you've done.]_ 10 | 11 | ### Result: 12 | 13 | _[After your change, what will change.]_ 14 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | swift-aws-lambda-runtime 2 | Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | --- 5 | 6 | This product contains a derivation various code and scripts from SwiftNIO. 7 | 8 | * LICENSE (Apache License 2.0): 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * HOMEPAGE: 11 | * https://github.com/apple/swift-nio 12 | 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # SECURITY.md 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via 6 | our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email 7 | to [aws-security@amazon.com](mailto:aws-security@amazon.com). Please do not create a public GitHub issue. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | _[what you expected to happen]_ 3 | 4 | ### Actual behavior 5 | _[what actually happened]_ 6 | 7 | ### Steps to reproduce 8 | 9 | 1. ... 10 | 2. ... 11 | 12 | ### If possible, minimal yet complete reproducer code (or URL to code) 13 | 14 | _[anything to help us reproducing the issue]_ 15 | 16 | ### SwiftAWSLambdaRuntime version/commit hash 17 | 18 | _[the SwiftAWSLambdaRuntime tag/commit hash]_ 19 | 20 | ### Swift & OS version (output of `swift --version && uname -a`) 21 | -------------------------------------------------------------------------------- /.license_header_template: -------------------------------------------------------------------------------- 1 | @@===----------------------------------------------------------------------===@@ 2 | @@ 3 | @@ This source file is part of the SwiftAWSLambdaRuntime open source project 4 | @@ 5 | @@ Copyright SwiftAWSLambdaRuntime project authors 6 | @@ Copyright (c)Amazon.com, Inc. or its affiliates. 7 | @@ Licensed under Apache License v2.0 8 | @@ 9 | @@ See LICENSE.txt for license information 10 | @@ See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | @@ 12 | @@ SPDX-License-Identifier: Apache-2.0 13 | @@ 14 | @@===----------------------------------------------------------------------===@@ 15 | -------------------------------------------------------------------------------- /.licenseignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .licenseignore 3 | .swiftformatignore 4 | .spi.yml 5 | .swift-format 6 | .github/* 7 | *.md 8 | **/*.md 9 | CONTRIBUTORS.txt 10 | LICENSE.txt 11 | NOTICE.txt 12 | Package.swift 13 | Package@swift-*.swift 14 | Package.resolved 15 | **/*.docc/* 16 | **/.gitignore 17 | **/Package.swift 18 | **/Package.resolved 19 | **/docker-compose*.yaml 20 | **/docker/* 21 | **/.dockerignore 22 | **/Dockerfile 23 | **/Makefile 24 | **/*.html 25 | **/*-template.yml 26 | **/*.xcworkspace/* 27 | **/*.xcodeproj/* 28 | **/*.xcassets/* 29 | **/*.appiconset/* 30 | **/ResourcePackaging/hello.txt 31 | .mailmap 32 | .swiftformat 33 | **/*.json 34 | -------------------------------------------------------------------------------- /Package@swift-5.10.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | 3 | import PackageDescription 4 | 5 | let swiftSettings: [SwiftSetting] = [.enableExperimentalFeature("StrictConcurrency=complete")] 6 | 7 | let package = Package( 8 | name: "swift-aws-lambda-events", 9 | platforms: [.macOS(.v14)], 10 | products: [ 11 | .library(name: "AWSLambdaEvents", targets: ["AWSLambdaEvents"]) 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/apple/swift-http-types.git", from: "1.5.1") 15 | ], 16 | targets: [ 17 | .target( 18 | name: "AWSLambdaEvents", 19 | dependencies: [ 20 | .product(name: "HTTPTypes", package: "swift-http-types") 21 | ], 22 | swiftSettings: swiftSettings 23 | ) 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | 3 | import PackageDescription 4 | 5 | let swiftSettings: [SwiftSetting] = [.enableExperimentalFeature("StrictConcurrency=complete")] 6 | 7 | let package = Package( 8 | name: "swift-aws-lambda-events", 9 | products: [ 10 | .library(name: "AWSLambdaEvents", targets: ["AWSLambdaEvents"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-http-types.git", from: "1.5.1") 14 | ], 15 | targets: [ 16 | .target( 17 | name: "AWSLambdaEvents", 18 | dependencies: [ 19 | .product(name: "HTTPTypes", package: "swift-http-types") 20 | ], 21 | swiftSettings: swiftSettings 22 | ), 23 | .testTarget( 24 | name: "AWSLambdaEventsTests", 25 | dependencies: [ 26 | "AWSLambdaEvents" 27 | ], 28 | swiftSettings: swiftSettings 29 | ), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/Utils/HTTPHeadersTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import AWSLambdaEvents 17 | import Testing 18 | 19 | @Suite 20 | struct HTTPHeadersTests { 21 | @Test func first() throws { 22 | let headers: HTTPHeaders = [ 23 | ":method": "GET", 24 | "foo": "bar", 25 | "custom-key": "value-1,value-2", 26 | ] 27 | 28 | #expect(headers.first(name: ":method") == "GET") 29 | #expect(headers.first(name: "Foo") == "bar") 30 | #expect(headers.first(name: "custom-key") == "value-1,value-2") 31 | #expect(headers.first(name: "not-present") == nil) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scripts/preview_docc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftAWSLambdaRuntime open source project 5 | ## 6 | ## Copyright SwiftAWSLambdaRuntime project authors 7 | ## Copyright (c)Amazon.com, Inc. or its affiliates. 8 | ## Licensed under Apache License v2.0 9 | ## 10 | ## See LICENSE.txt for license information 11 | ## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 12 | ## 13 | ## SPDX-License-Identifier: Apache-2.0 14 | ## 15 | ##===----------------------------------------------------------------------===## 16 | 17 | ##===----------------------------------------------------------------------===## 18 | ## 19 | ## This source file is part of the Swift Distributed Actors open source project 20 | ## 21 | ## Copyright (c) 2018-2019 Apple Inc. and the Swift Distributed Actors project authors 22 | ## Licensed under Apache License v2.0 23 | ## 24 | ## See LICENSE.txt for license information 25 | ## See CONTRIBUTORS.md for the list of Swift Distributed Actors project authors 26 | ## 27 | ## SPDX-License-Identifier: Apache-2.0 28 | ## 29 | ##===----------------------------------------------------------------------===## 30 | 31 | swift package --disable-sandbox preview-documentation --target "$1" 32 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Swift", 3 | "image": "swift:6.0", 4 | "features": { 5 | "ghcr.io/devcontainers/features/common-utils:2": { 6 | "installZsh": "false", 7 | "username": "vscode", 8 | "upgradePackages": "false" 9 | }, 10 | "ghcr.io/devcontainers/features/git:1": { 11 | "version": "os-provided", 12 | "ppa": "false" 13 | } 14 | }, 15 | "runArgs": [ 16 | "--cap-add=SYS_PTRACE", 17 | "--security-opt", 18 | "seccomp=unconfined" 19 | ], 20 | // Configure tool-specific properties. 21 | "customizations": { 22 | // Configure properties specific to VS Code. 23 | "vscode": { 24 | // Set *default* container specific settings.json values on container create. 25 | "settings": { 26 | "lldb.library": "/usr/lib/liblldb.so" 27 | }, 28 | // Add the IDs of extensions you want installed when the container is created. 29 | "extensions": [ 30 | "sswg.swift-lang" 31 | ] 32 | } 33 | }, 34 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 35 | // "forwardPorts": [], 36 | 37 | // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 38 | "remoteUser": "vscode" 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | 7 | # As per Checkov CKV2_GHA_1 8 | permissions: read-all 9 | 10 | jobs: 11 | soundness: 12 | name: Soundness 13 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 14 | with: 15 | license_header_check_project_name: "SwiftAWSLambdaRuntime" 16 | shell_check_enabled: true 17 | python_lint_check_enabled: true 18 | api_breakage_check_container_image: "swift:6.2-jammy" 19 | docs_check_container_image: "swift:6.0-jammy" 20 | format_check_container_image: "swift:6.0-jammy" 21 | yamllint_check_enabled: true 22 | 23 | unit-tests: 24 | name: Unit tests 25 | uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main 26 | with: 27 | enable_windows_checks: false 28 | linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]" 29 | swift_flags: "--explicit-target-dependency-import-check error" 30 | swift_nightly_flags: "--explicit-target-dependency-import-check error" 31 | enable_linux_static_sdk_build: true 32 | 33 | semver-label-check: 34 | name: Semantic Version label check 35 | runs-on: ubuntu-latest 36 | timeout-minutes: 1 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | with: 41 | persist-credentials: false 42 | - name: Check for Semantic Version label 43 | uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main 44 | -------------------------------------------------------------------------------- /scripts/generate_contributors_list.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftAWSLambdaRuntime open source project 5 | ## 6 | ## Copyright SwiftAWSLambdaRuntime project authors 7 | ## Copyright (c)Amazon.com, Inc. or its affiliates. 8 | ## Licensed under Apache License v2.0 9 | ## 10 | ## See LICENSE.txt for license information 11 | ## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 12 | ## 13 | ## SPDX-License-Identifier: Apache-2.0 14 | ## 15 | ##===----------------------------------------------------------------------===## 16 | set -eu 17 | here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 18 | contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) 19 | 20 | cat > "$here/../CONTRIBUTORS.txt" <<- EOF 21 | For the purpose of tracking copyright, this is the list of individuals and 22 | organizations who have contributed source code to SwiftAWSLambdaRuntime. 23 | 24 | For employees of an organization/company where the copyright of work done 25 | by employees of that company is held by the company itself, only the company 26 | needs to be listed here. 27 | 28 | ## COPYRIGHT HOLDERS 29 | 30 | - Amazon.com, Inc. (all contributors with '@amazon.com') 31 | - Apple Inc. (all contributors with '@apple.com') 32 | 33 | ### Contributors 34 | 35 | $contributors 36 | 37 | **Updating this list** 38 | 39 | Please do not edit this file manually. It is generated using \`./scripts/generate_contributors_list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\` 40 | EOF 41 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/BedrockAgent.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import HTTPTypes 17 | 18 | // https://docs.aws.amazon.com/bedrock/latest/userguide/agents-lambda.html#agents-lambda-input 19 | public struct BedrockAgentRequest: Codable, Sendable { 20 | public let messageVersion: String 21 | public let agent: Agent? 22 | public let sessionId: String? 23 | public let sessionAttributes: [String: String]? 24 | public let promptSessionAttributes: [String: String]? 25 | public let inputText: String? 26 | public let apiPath: String? 27 | public let actionGroup: String? 28 | public let httpMethod: HTTPRequest.Method? 29 | public let parameters: [Parameter]? 30 | public let requestBody: RequestBody? 31 | 32 | public struct Agent: Codable, Sendable { 33 | public let alias: String 34 | public let name: String 35 | public let version: String 36 | public let id: String 37 | } 38 | 39 | public struct Parameter: Codable, Sendable { 40 | public let name: String 41 | public let type: String 42 | public let value: String 43 | } 44 | 45 | public struct RequestBody: Codable, Sendable { 46 | public let content: [String: Content] 47 | public struct Content: Codable, Sendable { 48 | public let properties: [Parameter] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/Codable Helpers/SQS+Decode.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | #if canImport(FoundationEssentials) 17 | import FoundationEssentials 18 | #else 19 | import Foundation 20 | #endif 21 | 22 | extension SQSEvent { 23 | /// Decodes the records included in the event into an array of decodable objects. 24 | /// 25 | /// - Parameters: 26 | /// - type: The type to decode the body into. 27 | /// - decoder: The decoder to use. Defaults to a new `JSONDecoder`. 28 | /// 29 | /// - Returns: The decoded records as `[T]`. 30 | /// - Throws: An error if any of the records cannot be decoded. 31 | public func decodeBody( 32 | _ type: T.Type, 33 | using decoder: JSONDecoder = JSONDecoder() 34 | ) throws -> [T] where T: Decodable { 35 | try records.map { 36 | try $0.decodeBody(type, using: decoder) 37 | } 38 | } 39 | } 40 | 41 | extension SQSEvent.Message { 42 | /// Decodes the body of the message into a decodable object. 43 | /// 44 | /// - Parameters: 45 | /// - type: The type to decode the body into. 46 | /// - decoder: The decoder to use. Defaults to a new `JSONDecoder`. 47 | /// 48 | /// - Returns: The decoded body as `T`. 49 | /// - Throws: An error if the body cannot be decoded. 50 | public func decodeBody( 51 | _ type: T.Type, 52 | using decoder: JSONDecoder = JSONDecoder() 53 | ) throws -> T where T: Decodable { 54 | try decoder.decode(T.self, from: body.data(using: .utf8) ?? Data()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/APIGateway+Encodable.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import HTTPTypes 17 | 18 | #if canImport(FoundationEssentials) 19 | import class FoundationEssentials.JSONEncoder 20 | import struct FoundationEssentials.Data 21 | #else 22 | import class Foundation.JSONEncoder 23 | import struct Foundation.Data 24 | #endif 25 | 26 | extension Encodable { 27 | fileprivate func string() throws -> String { 28 | let encoded = try JSONEncoder().encode(self) 29 | return String(decoding: encoded, as: UTF8.self) 30 | } 31 | } 32 | 33 | extension APIGatewayResponse { 34 | 35 | public init( 36 | statusCode: HTTPResponse.Status, 37 | headers: HTTPHeaders? = nil, 38 | multiValueHeaders: HTTPMultiValueHeaders? = nil, 39 | encodableBody: Input 40 | ) throws { 41 | self.init( 42 | statusCode: statusCode, 43 | headers: headers, 44 | multiValueHeaders: multiValueHeaders, 45 | body: try encodableBody.string(), 46 | isBase64Encoded: nil 47 | ) 48 | } 49 | } 50 | 51 | extension APIGatewayV2Response { 52 | 53 | public init( 54 | statusCode: HTTPResponse.Status, 55 | headers: HTTPHeaders? = nil, 56 | encodableBody: Input, 57 | cookies: [String]? = nil 58 | ) throws { 59 | self.init( 60 | statusCode: statusCode, 61 | headers: headers, 62 | body: try encodableBody.string(), 63 | isBase64Encoded: nil, 64 | cookies: cookies 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/check_format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftAWSLambdaRuntime open source project 5 | ## 6 | ## Copyright SwiftAWSLambdaRuntime project authors 7 | ## Copyright (c)Amazon.com, Inc. or its affiliates. 8 | ## Licensed under Apache License v2.0 9 | ## 10 | ## See LICENSE.txt for license information 11 | ## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 12 | ## 13 | ## SPDX-License-Identifier: Apache-2.0 14 | ## 15 | ##===----------------------------------------------------------------------===## 16 | ##===----------------------------------------------------------------------===## 17 | ## 18 | ## This source file is part of the Swift.org open source project 19 | ## 20 | ## Copyright (c) 2024 Apple Inc. and the Swift project authors 21 | ## Licensed under Apache License v2.0 with Runtime Library Exception 22 | ## 23 | ## See https://swift.org/LICENSE.txt for license information 24 | ## See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 25 | ## 26 | ##===----------------------------------------------------------------------===## 27 | 28 | set -euo pipefail 29 | 30 | log() { printf -- "** %s\n" "$*" >&2; } 31 | error() { printf -- "** ERROR: %s\n" "$*" >&2; } 32 | fatal() { error "$@"; exit 1; } 33 | 34 | 35 | if [[ -f .swiftformatignore ]]; then 36 | log "Found swiftformatignore file..." 37 | 38 | log "Running swift format format..." 39 | tr '\n' '\0' < .swiftformatignore| xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place 40 | 41 | log "Running swift format lint..." 42 | 43 | tr '\n' '\0' < .swiftformatignore | xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel 44 | else 45 | log "Running swift format format..." 46 | git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place 47 | 48 | log "Running swift format lint..." 49 | 50 | git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel 51 | fi 52 | 53 | 54 | 55 | log "Checking for modified files..." 56 | 57 | GIT_PAGER='' git diff --exit-code '*.swift' 58 | 59 | log "✅ Found no formatting issues." 60 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/APIGateway+EncodableTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Foundation 17 | import Testing 18 | 19 | @testable import AWSLambdaEvents 20 | 21 | struct APIGatewayEncodableResponseTests { 22 | 23 | // MARK: Encoding 24 | struct BusinessResponse: Codable, Equatable { 25 | let message: String 26 | let code: Int 27 | } 28 | 29 | @Test 30 | func testResponseEncodingV2() throws { 31 | 32 | // given 33 | let businessResponse = BusinessResponse(message: "Hello World", code: 200) 34 | 35 | var response: APIGatewayV2Response? = nil 36 | #expect(throws: Never.self) { 37 | try response = APIGatewayV2Response(statusCode: .ok, encodableBody: businessResponse) 38 | } 39 | 40 | // when 41 | let body = try #require(response?.body?.data(using: .utf8)) 42 | 43 | #expect(throws: Never.self) { 44 | let encodedBody = try JSONDecoder().decode(BusinessResponse.self, from: body) 45 | 46 | // then 47 | #expect(encodedBody == businessResponse) 48 | } 49 | } 50 | 51 | @Test 52 | func testResponseEncoding() throws { 53 | 54 | // given 55 | let businessResponse = BusinessResponse(message: "Hello World", code: 200) 56 | 57 | var response: APIGatewayResponse? = nil 58 | #expect(throws: Never.self) { 59 | try response = APIGatewayResponse(statusCode: .ok, encodableBody: businessResponse) 60 | } 61 | try #require(response?.body != nil) 62 | 63 | // when 64 | let body = response?.body?.data(using: .utf8) 65 | 66 | #expect(throws: Never.self) { 67 | let encodedBody = try JSONDecoder().decode(BusinessResponse.self, from: body!) 68 | 69 | // then 70 | #expect(encodedBody == businessResponse) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version" : 1, 3 | "indentation" : { 4 | "spaces" : 4 5 | }, 6 | "tabWidth" : 4, 7 | "fileScopedDeclarationPrivacy" : { 8 | "accessLevel" : "private" 9 | }, 10 | "spacesAroundRangeFormationOperators" : false, 11 | "indentConditionalCompilationBlocks" : false, 12 | "indentSwitchCaseLabels" : false, 13 | "lineBreakAroundMultilineExpressionChainComponents" : false, 14 | "lineBreakBeforeControlFlowKeywords" : false, 15 | "lineBreakBeforeEachArgument" : true, 16 | "lineBreakBeforeEachGenericRequirement" : true, 17 | "lineLength" : 120, 18 | "maximumBlankLines" : 1, 19 | "respectsExistingLineBreaks" : true, 20 | "prioritizeKeepingFunctionOutputTogether" : true, 21 | "rules" : { 22 | "AllPublicDeclarationsHaveDocumentation" : false, 23 | "AlwaysUseLiteralForEmptyCollectionInit" : false, 24 | "AlwaysUseLowerCamelCase" : false, 25 | "AmbiguousTrailingClosureOverload" : true, 26 | "BeginDocumentationCommentWithOneLineSummary" : false, 27 | "DoNotUseSemicolons" : true, 28 | "DontRepeatTypeInStaticProperties" : true, 29 | "FileScopedDeclarationPrivacy" : true, 30 | "FullyIndirectEnum" : true, 31 | "GroupNumericLiterals" : true, 32 | "IdentifiersMustBeASCII" : true, 33 | "NeverForceUnwrap" : false, 34 | "NeverUseForceTry" : false, 35 | "NeverUseImplicitlyUnwrappedOptionals" : false, 36 | "NoAccessLevelOnExtensionDeclaration" : true, 37 | "NoAssignmentInExpressions" : true, 38 | "NoBlockComments" : true, 39 | "NoCasesWithOnlyFallthrough" : true, 40 | "NoEmptyTrailingClosureParentheses" : true, 41 | "NoLabelsInCasePatterns" : true, 42 | "NoLeadingUnderscores" : false, 43 | "NoParensAroundConditions" : true, 44 | "NoVoidReturnOnFunctionSignature" : true, 45 | "OmitExplicitReturns" : true, 46 | "OneCasePerLine" : true, 47 | "OneVariableDeclarationPerLine" : true, 48 | "OnlyOneTrailingClosureArgument" : true, 49 | "OrderedImports" : true, 50 | "ReplaceForEachWithForLoop" : true, 51 | "ReturnVoidInsteadOfEmptyTuple" : true, 52 | "UseEarlyExits" : false, 53 | "UseExplicitNilCheckInConditions" : false, 54 | "UseLetInEveryBoundCaseVariable" : false, 55 | "UseShorthandTypeNames" : true, 56 | "UseSingleLinePropertyGetter" : false, 57 | "UseSynthesizedInitializer" : false, 58 | "UseTripleSlashForDocumentationComments" : true, 59 | "UseWhereClausesInForLoops" : false, 60 | "ValidateDocumentationComments" : false 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/Codable Helpers/DecodableRequest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import HTTPTypes 17 | 18 | #if canImport(FoundationEssentials) 19 | import FoundationEssentials 20 | #else 21 | import Foundation 22 | #endif 23 | 24 | public protocol DecodableRequest { 25 | var body: String? { get } 26 | var isBase64Encoded: Bool { get } 27 | 28 | func decodeBody() throws -> Data? 29 | func decodeBody( 30 | _ type: T.Type, 31 | using decoder: JSONDecoder 32 | ) throws -> T where T: Decodable 33 | } 34 | 35 | extension DecodableRequest { 36 | /// Decodes the body of the request into a `Data` object. 37 | /// 38 | /// - Returns: The decoded body as `Data` or `nil` if the body is empty. 39 | public func decodeBody() throws -> Data? { 40 | guard let body else { return nil } 41 | 42 | if isBase64Encoded, 43 | let base64Decoded = Data(base64Encoded: body) 44 | { 45 | return base64Decoded 46 | } 47 | 48 | return body.data(using: .utf8) 49 | } 50 | 51 | /// Decodes the body of the request into a decodable object. When the 52 | /// body is empty, an error is thrown. 53 | /// 54 | /// - Parameters: 55 | /// - type: The type to decode the body into. 56 | /// - decoder: The decoder to use. Defaults to `JSONDecoder()`. 57 | /// 58 | /// - Returns: The decoded body as `T`. 59 | /// - Throws: An error if the body cannot be decoded. 60 | public func decodeBody( 61 | _ type: T.Type, 62 | using decoder: JSONDecoder = JSONDecoder() 63 | ) throws -> T where T: Decodable { 64 | let bodyData = body?.data(using: .utf8) ?? Data() 65 | 66 | var requestData = bodyData 67 | 68 | if isBase64Encoded, 69 | let base64Decoded = Data(base64Encoded: requestData) 70 | { 71 | requestData = base64Decoded 72 | } 73 | 74 | return try decoder.decode(T.self, from: requestData) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/LambdaGatewayProxyEvent.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import HTTPTypes 17 | 18 | /// LambdaGatewayProxyEvent contains data coming from the new HTTP API Gateway Proxy 19 | public struct LambdaGatewayProxyEvent: Decodable, Sendable { 20 | public struct RequestContext: Decodable, Sendable { 21 | /// Authorizer contains authorizer information for the request context. 22 | public struct Authorizer: Codable, Sendable { 23 | public let claims: [String: String]? 24 | public let scopes: [String]? 25 | } 26 | 27 | public let accountID: String 28 | public let apiID: String 29 | public let domainName: String 30 | public let domainPrefix: String 31 | public let stage: String 32 | public let requestID: String 33 | 34 | public let httpMethod: HTTPRequest.Method 35 | public let authorizer: Authorizer? 36 | 37 | public let resourcePath: String? 38 | public let path: String? 39 | 40 | /// The request time in format: 23/Apr/2020:11:08:18 +0000 41 | public let requestTime: String? 42 | public let requestTimeEpoch: UInt64 43 | 44 | enum CodingKeys: String, CodingKey { 45 | case accountID = "accountId" 46 | case apiID = "apiId" 47 | case domainName 48 | case domainPrefix 49 | case stage 50 | 51 | case httpMethod 52 | case authorizer 53 | 54 | case requestID = "requestId" 55 | 56 | case resourcePath 57 | case path 58 | 59 | case requestTime 60 | case requestTimeEpoch 61 | } 62 | } 63 | 64 | public let resource: String 65 | public let path: String 66 | public let httpMethod: String 67 | public let stageVariables: [String: String]? 68 | 69 | public let cookies: [String]? 70 | public let headers: HTTPHeaders 71 | public let queryStringParameters: [String: String]? 72 | public let pathParameters: [String: String]? 73 | 74 | public let requestContext: RequestContext 75 | 76 | public let body: String? 77 | public let isBase64Encoded: Bool 78 | } 79 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/ALBTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Foundation 17 | import Testing 18 | 19 | @testable import AWSLambdaEvents 20 | 21 | @Suite 22 | struct ALBTests { 23 | static let exampleSingleValueHeadersEventBody = """ 24 | { 25 | "requestContext":{ 26 | "elb":{ 27 | "targetGroupArn": "arn:aws:elasticloadbalancing:eu-central-1:079477498937:targetgroup/EinSternDerDeinenNamenTraegt/621febf5a44b2ce5" 28 | } 29 | }, 30 | "httpMethod": "GET", 31 | "path": "/", 32 | "queryStringParameters": {}, 33 | "headers":{ 34 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 35 | "accept-encoding": "gzip, deflate", 36 | "accept-language": "en-us", 37 | "connection": "keep-alive", 38 | "host": "event-testl-1wa3wrvmroilb-358275751.eu-central-1.elb.amazonaws.com", 39 | "upgrade-insecure-requests": "1", 40 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15", 41 | "x-amzn-trace-id": "Root=1-5e189143-ad18a2b0a7728cd0dac45e10", 42 | "x-forwarded-for": "90.187.8.137", 43 | "x-forwarded-port": "80", 44 | "x-forwarded-proto": "http" 45 | }, 46 | "body":"", 47 | "isBase64Encoded":false 48 | } 49 | """ 50 | 51 | @Test func requestWithSingleValueHeadersEvent() { 52 | let data = ALBTests.exampleSingleValueHeadersEventBody.data(using: .utf8)! 53 | do { 54 | let decoder = JSONDecoder() 55 | 56 | let event = try decoder.decode(ALBTargetGroupRequest.self, from: data) 57 | 58 | #expect(event.httpMethod == .get) 59 | #expect(event.body == "") 60 | #expect(event.isBase64Encoded == false) 61 | #expect(event.headers?.count == 11) 62 | #expect(event.path == "/") 63 | #expect(event.queryStringParameters == [:]) 64 | } catch { 65 | Issue.record("Unexpected error: \(error)") 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | For the purpose of tracking copyright, this is the list of individuals and 2 | organizations who have contributed source code to SwiftAWSLambdaRuntime. 3 | 4 | For employees of an organization/company where the copyright of work done 5 | by employees of that company is held by the company itself, only the company 6 | needs to be listed here. 7 | 8 | ## COPYRIGHT HOLDERS 9 | 10 | - Amazon.com, Inc. (all contributors with '@amazon.com') 11 | - Apple Inc. (all contributors with '@apple.com') 12 | 13 | ### Contributors 14 | 15 | - Adam Fowler 16 | - Adolfo 17 | - Alessio Buratti <9006089+Buratti@users.noreply.github.com> 18 | - Andrea Scuderi 19 | - Brendan Kirchner 20 | - Bryan Bartow 21 | - Bryan Moffatt 22 | - Christoph Walcher 23 | - Colton Schlosser 24 | - DwayneCoussement 25 | - DwayneCoussement 26 | - Eneko Alonso 27 | - Fabian Fett 28 | - Filipp Fediakov 29 | - Franz Busch 30 | - George Barnett 31 | - Idelfonso Gutierrez 32 | - Joannis Orlandos 33 | - Johannes Bosecker 34 | - Johannes Weiss 35 | - Josh <29730338+mr-j-tree@users.noreply.github.com> 36 | - ML <44809298+mufumade@users.noreply.github.com> 37 | - Mahdi Bahrami 38 | - Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> 39 | - Max Desiatov 40 | - Mike Lewis 41 | - Natan Rolnik 42 | - Norman Maurer 43 | - Ralph Küpper 44 | - Renato Guimarães 45 | - Richard Kendall Wolf 46 | - Ro-M 47 | - Simon Leeb <52261246+sliemeobn@users.noreply.github.com> 48 | - Stefan Nienhuis 49 | - Sébastien Stormacq 50 | - Taylor 51 | - Tobias 52 | - Tomer Doron 53 | - Ugo Cottin 54 | - Yim Lee 55 | - Zhibin Cai 56 | - jsonfry 57 | - pmarrufo 58 | - pokryfka <5090827+pokryfka@users.noreply.github.com> 59 | - pokryfka 60 | - tachyonics 61 | 62 | **Updating this list** 63 | 64 | Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` 65 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/S3.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | #if canImport(FoundationEssentials) 17 | import FoundationEssentials 18 | #else 19 | import Foundation 20 | #endif 21 | 22 | // https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html 23 | 24 | public struct S3Event: Decodable, Sendable { 25 | public struct Record: Decodable, Sendable { 26 | public let eventVersion: String 27 | public let eventSource: String 28 | public let awsRegion: AWSRegion 29 | 30 | @ISO8601WithFractionalSecondsCoding 31 | public var eventTime: Date 32 | public let eventName: String 33 | public let userIdentity: UserIdentity 34 | public let requestParameters: RequestParameters 35 | public let responseElements: [String: String] 36 | public let s3: Entity 37 | } 38 | 39 | public let records: [Record] 40 | 41 | public enum CodingKeys: String, CodingKey { 42 | case records = "Records" 43 | } 44 | 45 | public struct RequestParameters: Codable, Equatable, Sendable { 46 | public let sourceIPAddress: String 47 | } 48 | 49 | public struct UserIdentity: Codable, Equatable, Sendable { 50 | public let principalId: String 51 | } 52 | 53 | public struct Entity: Codable, Sendable { 54 | public let configurationId: String 55 | public let schemaVersion: String 56 | public let bucket: Bucket 57 | public let object: Object 58 | 59 | enum CodingKeys: String, CodingKey { 60 | case configurationId 61 | case schemaVersion = "s3SchemaVersion" 62 | case bucket 63 | case object 64 | } 65 | } 66 | 67 | public struct Bucket: Codable, Sendable { 68 | public let name: String 69 | public let ownerIdentity: UserIdentity 70 | public let arn: String 71 | } 72 | 73 | public struct Object: Codable, Sendable { 74 | public let key: String 75 | /// The object's size in bytes. 76 | /// 77 | /// Note: This property is available for all event types except "ObjectRemoved:*" 78 | public let size: UInt64? 79 | public let urlDecodedKey: String? 80 | public let versionId: String? 81 | public let eTag: String? 82 | public let sequencer: String 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/APIGateway+WebSockets.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | /// `APIGatewayWebSocketRequest` is a variation of the`APIGatewayV2Request` 17 | /// and contains data coming from the WebSockets API Gateway. 18 | public struct APIGatewayWebSocketRequest: Codable { 19 | /// `Context` contains information to identify the AWS account and resources invoking the Lambda function. 20 | public struct Context: Codable { 21 | public struct Identity: Codable { 22 | public let sourceIp: String 23 | } 24 | 25 | public let routeKey: String 26 | public let eventType: String 27 | public let extendedRequestId: String 28 | /// The request time in format: 23/Apr/2020:11:08:18 +0000 29 | public let requestTime: String 30 | public let messageDirection: String 31 | public let stage: String 32 | public let connectedAt: UInt64 33 | public let requestTimeEpoch: UInt64 34 | public let identity: Identity 35 | public let requestId: String 36 | public let domainName: String 37 | public let connectionId: String 38 | public let apiId: String 39 | } 40 | 41 | public let headers: HTTPHeaders? 42 | public let queryStringParameters: [String: String]? 43 | public let multiValueHeaders: HTTPMultiValueHeaders? 44 | public let context: Context 45 | public let body: String? 46 | public let isBase64Encoded: Bool? 47 | 48 | enum CodingKeys: String, CodingKey { 49 | case headers 50 | case queryStringParameters 51 | case multiValueHeaders 52 | case context = "requestContext" 53 | case body 54 | case isBase64Encoded 55 | } 56 | } 57 | 58 | /// `APIGatewayWebSocketResponse` is a type alias for `APIGatewayV2Request`. 59 | /// Typically, lambda WebSockets servers send clients data via 60 | /// the ApiGatewayManagementApi mechanism. However, APIGateway does require 61 | /// lambda servers to return some kind of status when APIGateway invokes them. 62 | /// This can be as simple as always returning a 200 "OK" response for all 63 | /// WebSockets requests (the ApiGatewayManagementApi can return any errors to 64 | /// WebSockets clients). 65 | public typealias APIGatewayWebSocketResponse = APIGatewayV2Response 66 | 67 | #if swift(>=5.6) 68 | extension APIGatewayWebSocketRequest: Sendable {} 69 | extension APIGatewayWebSocketRequest.Context: Sendable {} 70 | extension APIGatewayWebSocketRequest.Context.Identity: Sendable {} 71 | #endif 72 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``AWSLambdaEvents`` 2 | 3 | A supporting library for Swift AWS Lambda Runtime. 4 | 5 | ## Overview 6 | 7 | Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift simple and safe. The library is an implementation of the [AWS Lambda Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) and uses an embedded asynchronous HTTP Client based on [SwiftNIO](http://github.com/apple/swift-nio) that is fine-tuned for performance in the AWS Runtime context. The library provides a multi-tier API that allows building a range of Lambda functions: From quick and simple closures to complex, performance-sensitive event handlers. 8 | 9 | Swift AWS Lambda Events is a supporting library for the [Swift AWS Lambda Runtime](http://github.com/awslabs/swift-aws-lambda-runtime) library, providing abstractions for popular AWS events. 10 | 11 | ## Integration with AWS Platform Events 12 | 13 | AWS Lambda functions can be invoked directly from the AWS Lambda console UI, AWS Lambda API, AWS SDKs, AWS CLI, and AWS toolkits. More commonly, they are invoked as a reaction to an events coming from the AWS platform. To make it easier to integrate with AWS platform events, this library includes an `AWSLambdaEvents` target which provides abstractions for many commonly used events. Additional events can be easily modeled when needed following the same patterns set by `AWSLambdaEvents`. Integration points with the AWS Platform include: 14 | 15 | * [APIGateway Proxy](https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html) 16 | * [S3 Events](https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html) 17 | * [SES Events](https://docs.aws.amazon.com/lambda/latest/dg/services-ses.html) 18 | * [SNS Events](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html) 19 | * [SQS Events](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html) 20 | * [CloudWatch Events](https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html) 21 | * [Cognito Lambda Triggers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html) 22 | 23 | **Note**: Each one of the integration points mentioned above includes a set of `Codable` structs that mirror AWS' data model for these APIs. 24 | 25 | ## Getting started 26 | 27 | If you have never used AWS Lambda or Docker before, check out this [getting started guide](https://swiftpackageindex.com/awslabs/swift-aws-lambda-runtime/2.3.0/documentation/awslambdaruntime/quick-setup) or [tutorial](https://swiftpackageindex.com/awslabs/swift-aws-lambda-runtime/2.3.0/tutorials/table-of-content) which helps you with every step from zero to a running Lambda. 28 | 29 | Swift AWS Lambda Events is a supporting library for the [Swift AWS Lambda Runtime](http://github.com/awslabs/swift-aws-lambda-runtime) library, where you can find further documentation and examples. 30 | 31 | ## Topics 32 | 33 | ### Events 34 | 35 | - ``AppSyncEvent`` 36 | - ``CloudwatchEvent`` 37 | - ``DynamoDBEvent`` 38 | - ``S3Event`` 39 | - ``SESEvent`` 40 | - ``SNSEvent`` 41 | - ``SQSEvent`` 42 | - ``CognitoEvent`` 43 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Testing 17 | 18 | @testable import AWSLambdaEvents 19 | 20 | #if canImport(FoundationEssentials) 21 | import FoundationEssentials 22 | #else 23 | import Foundation 24 | #endif 25 | 26 | @Suite 27 | struct Base64Tests { 28 | // MARK: - Decoding - 29 | 30 | @Test func decodeEmptyString() throws { 31 | let decoded = try "".base64decoded() 32 | #expect(decoded.count == 0) 33 | } 34 | 35 | @Test func base64DecodingArrayOfNulls() throws { 36 | let expected = Array(repeating: UInt8(0), count: 10) 37 | let decoded = try "AAAAAAAAAAAAAA==".base64decoded() 38 | #expect(decoded == expected) 39 | } 40 | 41 | @Test func base64DecodingAllTheBytesSequentially() throws { 42 | let base64 = 43 | "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==" 44 | 45 | let expected = Array(UInt8(0)...UInt8(255)) 46 | let decoded = try base64.base64decoded() 47 | 48 | #expect(decoded == expected) 49 | } 50 | 51 | @Test func base64UrlDecodingAllTheBytesSequentially() throws { 52 | let base64 = 53 | "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w==" 54 | 55 | let expected = Array(UInt8(0)...UInt8(255)) 56 | let decoded = try base64.base64decoded(options: .base64UrlAlphabet) 57 | 58 | #expect(decoded == expected) 59 | } 60 | 61 | @Test func base64DecodingWithPoop() { 62 | #expect(throws: (any Error).self) { 63 | try "💩".base64decoded() 64 | } 65 | } 66 | 67 | @Test func base64DecodingWithInvalidLength() { 68 | #expect(throws: (any Error).self) { 69 | try "AAAAA".base64decoded() 70 | } 71 | } 72 | 73 | @Test func nSStringToDecode() { 74 | let test = "1234567" 75 | let nsstring = test.data(using: .utf8)!.base64EncodedString() 76 | 77 | #expect(throws: Never.self) { try nsstring.base64decoded() } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/Codable Helpers/EncodableResponse.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import HTTPTypes 17 | 18 | #if canImport(FoundationEssentials) 19 | import FoundationEssentials 20 | #else 21 | import Foundation 22 | #endif 23 | 24 | public protocol EncodableResponse { 25 | static func encoding( 26 | _ encodable: T, 27 | status: HTTPResponse.Status, 28 | using encoder: JSONEncoder, 29 | headers: HTTPHeaders?, 30 | cookies: [String]?, 31 | onError: ((Error) -> Self) 32 | ) -> Self where T: Encodable 33 | 34 | init( 35 | statusCode: HTTPResponse.Status, 36 | headers: HTTPHeaders?, 37 | body: String?, 38 | isBase64Encoded: Bool?, 39 | cookies: [String]? 40 | ) 41 | } 42 | 43 | extension EncodableResponse { 44 | /// Encodes a given encodable object into a response object. 45 | /// 46 | /// - Parameters: 47 | /// - encodable: The object to encode. 48 | /// - status: The status code to use. Defaults to `ok`. 49 | /// - encoder: The encoder to use. Defaults to a new `JSONEncoder`. 50 | /// - onError: A closure to handle errors, and transform them into a `APIGatewayV2Response`. 51 | /// Defaults to converting the error into a 500 (Internal Server Error) response with the error message as 52 | /// the body. 53 | /// 54 | /// - Returns: a response object whose body is the encoded `encodable` type and with the 55 | /// other response parameters 56 | public static func encoding( 57 | _ encodable: T, 58 | status: HTTPResponse.Status = .ok, 59 | using encoder: JSONEncoder = JSONEncoder(), 60 | headers: HTTPHeaders? = nil, 61 | cookies: [String]? = nil, 62 | onError: ((Error) -> Self) = Self.defaultErrorHandler 63 | ) -> Self where T: Encodable { 64 | do { 65 | let encodedResponse = try encoder.encode(encodable) 66 | return Self( 67 | statusCode: status, 68 | headers: headers, 69 | body: String(data: encodedResponse, encoding: .utf8), 70 | isBase64Encoded: nil, 71 | cookies: cookies 72 | ) 73 | } catch { 74 | return onError(error) 75 | } 76 | } 77 | 78 | public static var defaultErrorHandler: ((Error) -> Self) { 79 | { error in 80 | Self( 81 | statusCode: .internalServerError, 82 | headers: nil, 83 | body: "Internal Server Error: \(String(describing: error))", 84 | isBase64Encoded: nil, 85 | cookies: nil 86 | ) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/ALB.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import HTTPTypes 17 | 18 | #if canImport(FoundationEssentials) 19 | import FoundationEssentials 20 | #else 21 | import Foundation 22 | #endif 23 | 24 | // https://github.com/aws/aws-lambda-go/blob/main/events/alb.go 25 | /// `ALBTargetGroupRequest` contains data originating from the ALB Lambda target group integration. 26 | public struct ALBTargetGroupRequest: Codable, Sendable { 27 | /// `Context` contains information to identify the load balancer invoking the lambda. 28 | public struct Context: Codable, Sendable { 29 | public let elb: ELBContext 30 | } 31 | 32 | public let httpMethod: HTTPRequest.Method 33 | public let path: String 34 | public let queryStringParameters: [String: String] 35 | 36 | /// Depending on your configuration of your target group either ``headers`` or ``multiValueHeaders`` 37 | /// are set. 38 | /// 39 | /// For more information visit: 40 | /// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers 41 | public let headers: HTTPHeaders? 42 | 43 | /// Depending on your configuration of your target group either ``headers`` or ``multiValueHeaders`` 44 | /// are set. 45 | /// 46 | /// For more information visit: 47 | /// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers 48 | public let multiValueHeaders: HTTPMultiValueHeaders? 49 | public let requestContext: Context 50 | public let isBase64Encoded: Bool 51 | public let body: String? 52 | 53 | /// `ELBContext` contains information to identify the ARN invoking the lambda. 54 | public struct ELBContext: Codable, Sendable { 55 | public let targetGroupArn: String 56 | } 57 | } 58 | 59 | public struct ALBTargetGroupResponse: Codable, Sendable { 60 | public var statusCode: HTTPResponse.Status 61 | public var statusDescription: String? 62 | public var headers: HTTPHeaders? 63 | public var multiValueHeaders: HTTPMultiValueHeaders? 64 | public var body: String 65 | public var isBase64Encoded: Bool 66 | 67 | public init( 68 | statusCode: HTTPResponse.Status, 69 | statusDescription: String? = nil, 70 | headers: HTTPHeaders? = nil, 71 | multiValueHeaders: HTTPMultiValueHeaders? = nil, 72 | body: String = "", 73 | isBase64Encoded: Bool = false 74 | ) { 75 | self.statusCode = statusCode 76 | self.statusDescription = statusDescription 77 | self.headers = headers 78 | self.multiValueHeaders = multiValueHeaders 79 | self.body = body 80 | self.isBase64Encoded = isBase64Encoded 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Swift AWS Lambda Events 2 | 3 | ## Overview 4 | 5 | Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift simple and safe. The library is an implementation of the [AWS Lambda Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) and uses an embedded asynchronous HTTP Client based on [SwiftNIO](http://github.com/apple/swift-nio) that is fine-tuned for performance in the AWS Runtime context. The library provides a multi-tier API that allows building a range of Lambda functions: From quick and simple closures to complex, performance-sensitive event handlers. 6 | 7 | Swift AWS Lambda Events is a supporting library for the [Swift AWS Lambda Runtime](http://github.com/awslabs/swift-aws-lambda-runtime) library, providing abstractions for popular AWS events. 8 | 9 | ## Integration with AWS Platform Events 10 | 11 | AWS Lambda functions can be invoked directly from the AWS Lambda console UI, AWS Lambda API, AWS SDKs, AWS CLI, and AWS toolkits. More commonly, they are invoked as a reaction to an events coming from the AWS platform. To make it easier to integrate with AWS platform events, this library includes an `AWSLambdaEvents` target which provides abstractions for many commonly used events. Additional events can be easily modeled when needed following the same patterns set by `AWSLambdaEvents`. Integration points with the AWS Platform include: 12 | 13 | * [APIGateway Proxy](https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html) 14 | * [S3 Events](https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html) 15 | * [SES Events](https://docs.aws.amazon.com/lambda/latest/dg/services-ses.html) 16 | * [SNS Events](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html) 17 | * [SQS Events](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html) 18 | * [CloudWatch Events](https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html) 19 | * [Cognito Lambda Triggers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html) 20 | 21 | **Note**: Each one of the integration points mentioned above includes a set of `Codable` structs that mirror AWS' data model for these APIs. 22 | 23 | ## Getting started 24 | 25 | If you have never used AWS Lambda or Docker before, check out this [tutorial](https://swiftpackageindex.com/awslabs/swift-aws-lambda-runtime/2.3.0/tutorials/table-of-content) or the [getting started guide](https://swiftpackageindex.com/awslabs/swift-aws-lambda-runtime/2.3.0/documentation/awslambdaruntime/quick-setup) which helps you with every step from zero to a running Lambda. 26 | 27 | Swift AWS Lambda Events is a supporting library for the [Swift AWS Lambda Runtime](http://github.com/awslabs/swift-aws-lambda-runtime) library, where you can find further documentation and examples. 28 | 29 | ## Project status 30 | 31 | This is the beginning of a community-driven open-source project actively seeking contributions. 32 | While the core API is considered stable, the API may still evolve as we get closer to a `1.0` version. 33 | There are several areas which need additional attention, including but not limited to: 34 | 35 | * Additional events 36 | * Additional documentation and best practices 37 | * Additional examples 38 | 39 | ## Security 40 | 41 | Please see [SECURITY.md](SECURITY.md) for details on the security process. 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE.txt) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/BedrockAgentTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Foundation 17 | import HTTPTypes 18 | import Testing 19 | 20 | @testable import AWSLambdaEvents 21 | 22 | @Suite 23 | struct BedrockAgentTests { 24 | static let eventBody = 25 | """ 26 | { 27 | "messageVersion": "1.0", 28 | "agent": { 29 | "alias": "AGENT_ID", 30 | "name": "StockQuoteAgent", 31 | "version": "DRAFT", 32 | "id": "PR3AHNYEAA" 33 | }, 34 | "sessionId": "486652066693565", 35 | "sessionAttributes": {}, 36 | "promptSessionAttributes": {}, 37 | "inputText": "what the price of amazon stock ?", 38 | "apiPath": "/stocks/{symbol}", 39 | "actionGroup": "StockQuoteService", 40 | "httpMethod": "GET", 41 | "parameters": [ 42 | { 43 | "name": "symbol", 44 | "type": "string", 45 | "value": "AMZN" 46 | } 47 | ], 48 | "requestBody": { 49 | "content": { 50 | "application/text": { 51 | "properties": [ 52 | { 53 | "name": "symbol", 54 | "type": "string", 55 | "value": "AMZN" 56 | } 57 | ] 58 | } 59 | } 60 | } 61 | } 62 | """ 63 | 64 | @Test func simpleEventFromJSON() throws { 65 | let data = BedrockAgentTests.eventBody.data(using: .utf8)! 66 | let event = try JSONDecoder().decode(BedrockAgentRequest.self, from: data) 67 | 68 | #expect(event.messageVersion == "1.0") 69 | 70 | #expect(event.agent?.alias == "AGENT_ID") 71 | #expect(event.agent?.name == "StockQuoteAgent") 72 | #expect(event.agent?.version == "DRAFT") 73 | #expect(event.agent?.id == "PR3AHNYEAA") 74 | 75 | #expect(event.sessionId == "486652066693565") 76 | #expect(event.inputText == "what the price of amazon stock ?") 77 | #expect(event.apiPath == "/stocks/{symbol}") 78 | #expect(event.actionGroup == "StockQuoteService") 79 | #expect(event.httpMethod == .get) 80 | 81 | #expect(event.parameters?.count == 1) 82 | #expect(event.parameters?[0].name == "symbol") 83 | #expect(event.parameters?[0].type == "string") 84 | #expect(event.parameters?[0].value == "AMZN") 85 | 86 | let body = try #require(event.requestBody?.content) 87 | let content = try #require(body["application/text"]) 88 | #expect(content.properties.count == 1) 89 | #expect(content.properties[0].name == "symbol") 90 | #expect(content.properties[0].type == "string") 91 | #expect(content.properties[0].value == "AMZN") 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/SES.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | #if canImport(FoundationEssentials) 17 | import FoundationEssentials 18 | #else 19 | import Foundation 20 | #endif 21 | 22 | // https://docs.aws.amazon.com/lambda/latest/dg/services-ses.html 23 | 24 | public struct SESEvent: Decodable, Sendable { 25 | public struct Record: Decodable, Sendable { 26 | public let eventSource: String 27 | public let eventVersion: String 28 | public let ses: Message 29 | } 30 | 31 | public let records: [Record] 32 | 33 | public enum CodingKeys: String, CodingKey { 34 | case records = "Records" 35 | } 36 | 37 | public struct Message: Decodable, Sendable { 38 | public let mail: Mail 39 | public let receipt: Receipt 40 | } 41 | 42 | public struct Mail: Decodable, Sendable { 43 | public let commonHeaders: CommonHeaders 44 | public let destination: [String] 45 | public let headers: [Header] 46 | public let headersTruncated: Bool 47 | public let messageId: String 48 | public let source: String 49 | @ISO8601WithFractionalSecondsCoding public var timestamp: Date 50 | } 51 | 52 | public struct CommonHeaders: Decodable, Sendable { 53 | public let bcc: [String]? 54 | public let cc: [String]? 55 | @RFC5322DateTimeCoding public var date: Date 56 | public let from: [String] 57 | public let messageId: String? 58 | public let returnPath: String? 59 | public let subject: String? 60 | public let to: [String]? 61 | } 62 | 63 | public struct Header: Decodable, Sendable { 64 | public let name: String 65 | public let value: String 66 | } 67 | 68 | public struct Receipt: Decodable, Sendable { 69 | public let action: Action 70 | public let dmarcPolicy: DMARCPolicy? 71 | public let dmarcVerdict: Verdict? 72 | public let dkimVerdict: Verdict 73 | public let processingTimeMillis: Int 74 | public let recipients: [String] 75 | public let spamVerdict: Verdict 76 | public let spfVerdict: Verdict 77 | @ISO8601WithFractionalSecondsCoding public var timestamp: Date 78 | public let virusVerdict: Verdict 79 | } 80 | 81 | public struct Action: Decodable, Sendable { 82 | public let functionArn: String 83 | public let invocationType: String 84 | public let type: String 85 | } 86 | 87 | public struct Verdict: Decodable, Sendable { 88 | public let status: Status 89 | } 90 | 91 | public enum DMARCPolicy: String, Decodable, Sendable { 92 | case none 93 | case quarantine 94 | case reject 95 | } 96 | 97 | public enum Status: String, Decodable, Sendable { 98 | case pass = "PASS" 99 | case fail = "FAIL" 100 | case gray = "GRAY" 101 | case processingFailed = "PROCESSING_FAILED" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Testing 17 | 18 | @testable import AWSLambdaEvents 19 | 20 | @Suite 21 | struct IteratorProtocolTests { 22 | @Test func expect() { 23 | // Test matching character 24 | var iterator = "abc".utf8.makeIterator() 25 | let result1 = iterator.expect(UInt8(ascii: "a")) 26 | #expect(result1) 27 | #expect(iterator.next() == UInt8(ascii: "b")) 28 | 29 | // Test non-matching character 30 | iterator = "abc".utf8.makeIterator() 31 | let result2 = iterator.expect(UInt8(ascii: "x")) 32 | #expect(!result2) 33 | } 34 | 35 | @Test func nextSkippingWhitespace() { 36 | // Test with leading spaces 37 | var iterator = " abc".utf8.makeIterator() 38 | #expect(iterator.nextSkippingWhitespace() == UInt8(ascii: "a")) 39 | 40 | // Test with no spaces 41 | iterator = "abc".utf8.makeIterator() 42 | #expect(iterator.nextSkippingWhitespace() == UInt8(ascii: "a")) 43 | 44 | // Test with only spaces 45 | iterator = " ".utf8.makeIterator() 46 | let result = iterator.nextSkippingWhitespace() 47 | #expect(result == nil) 48 | } 49 | 50 | @Test func nextAsciiDigit() { 51 | // Test basic digit 52 | var iterator = "123".utf8.makeIterator() 53 | #expect(iterator.nextAsciiDigit() == UInt8(ascii: "1")) 54 | 55 | // Test with leading spaces and skipping whitespace 56 | iterator = " 123".utf8.makeIterator() 57 | #expect(iterator.nextAsciiDigit(skippingWhitespace: true) == UInt8(ascii: "1")) 58 | 59 | // Test with leading spaces and not skipping whitespace 60 | iterator = " 123".utf8.makeIterator() 61 | let result1 = iterator.nextAsciiDigit() 62 | #expect(result1 == nil) 63 | 64 | // Test with non-digit 65 | iterator = "abc".utf8.makeIterator() 66 | let result2 = iterator.nextAsciiDigit() 67 | #expect(result2 == nil) 68 | } 69 | 70 | @Test func nextAsciiLetter() { 71 | // Test basic letter 72 | var iterator = "abc".utf8.makeIterator() 73 | #expect(iterator.nextAsciiLetter() == UInt8(ascii: "a")) 74 | 75 | // Test with leading spaces and skipping whitespace 76 | iterator = " abc".utf8.makeIterator() 77 | #expect(iterator.nextAsciiLetter(skippingWhitespace: true) == UInt8(ascii: "a")) 78 | 79 | // Test with leading spaces and not skipping whitespace 80 | iterator = " abc".utf8.makeIterator() 81 | let result1 = iterator.nextAsciiLetter() 82 | #expect(result1 == nil) 83 | 84 | // Test with non-letter 85 | iterator = "123".utf8.makeIterator() 86 | let result2 = iterator.nextAsciiLetter() 87 | #expect(result2 == nil) 88 | 89 | // Test with uppercase 90 | iterator = "ABC".utf8.makeIterator() 91 | #expect(iterator.nextAsciiLetter() == UInt8(ascii: "A")) 92 | 93 | // Test with empty string 94 | iterator = "".utf8.makeIterator() 95 | let result3 = iterator.nextAsciiLetter() 96 | #expect(result3 == nil) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/SQS.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | // https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html 17 | 18 | public struct SQSEvent: Decodable, Sendable { 19 | public let records: [Message] 20 | 21 | enum CodingKeys: String, CodingKey { 22 | case records = "Records" 23 | } 24 | 25 | public struct Message: Sendable { 26 | /// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html 27 | public enum Attribute: Sendable { 28 | case string(String) 29 | case binary([UInt8]) 30 | case number(String) 31 | } 32 | 33 | public let messageId: String 34 | public let receiptHandle: String 35 | public var body: String 36 | public let md5OfBody: String 37 | public let md5OfMessageAttributes: String? 38 | public let attributes: [String: String] 39 | public let messageAttributes: [String: Attribute] 40 | public let eventSourceArn: String 41 | public let eventSource: String 42 | public let awsRegion: AWSRegion 43 | } 44 | } 45 | 46 | extension SQSEvent.Message: Decodable { 47 | enum CodingKeys: String, CodingKey { 48 | case messageId 49 | case receiptHandle 50 | case body 51 | case md5OfBody 52 | case md5OfMessageAttributes 53 | case attributes 54 | case messageAttributes 55 | case eventSourceArn = "eventSourceARN" 56 | case eventSource 57 | case awsRegion 58 | } 59 | } 60 | 61 | extension SQSEvent.Message.Attribute: Equatable {} 62 | 63 | extension SQSEvent.Message.Attribute: Decodable { 64 | enum CodingKeys: String, CodingKey { 65 | case dataType 66 | case stringValue 67 | case binaryValue 68 | 69 | // BinaryListValue and StringListValue are unimplemented since 70 | // they are not implemented as discussed here: 71 | // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html 72 | } 73 | 74 | public init(from decoder: Decoder) throws { 75 | let container = try decoder.container(keyedBy: CodingKeys.self) 76 | 77 | let dataType = try container.decode(String.self, forKey: .dataType) 78 | switch dataType { 79 | case "String": 80 | let value = try container.decode(String.self, forKey: .stringValue) 81 | self = .string(value) 82 | case "Number": 83 | let value = try container.decode(String.self, forKey: .stringValue) 84 | self = .number(value) 85 | case "Binary": 86 | let base64encoded = try container.decode(String.self, forKey: .binaryValue) 87 | let bytes = try base64encoded.base64decoded() 88 | self = .binary(bytes) 89 | default: 90 | throw DecodingError.dataCorruptedError( 91 | forKey: .dataType, 92 | in: container, 93 | debugDescription: """ 94 | Unexpected value \"\(dataType)\" for key \(CodingKeys.dataType). 95 | Expected `String`, `Binary` or `Number`. 96 | """ 97 | ) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/SNS.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | #if canImport(FoundationEssentials) 17 | import FoundationEssentials 18 | #else 19 | import Foundation 20 | #endif 21 | 22 | // https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html 23 | 24 | public struct SNSEvent: Decodable, Sendable { 25 | public struct Record: Decodable, Sendable { 26 | public let eventVersion: String 27 | public let eventSubscriptionArn: String 28 | public let eventSource: String 29 | public let sns: Message 30 | 31 | public enum CodingKeys: String, CodingKey { 32 | case eventVersion = "EventVersion" 33 | case eventSubscriptionArn = "EventSubscriptionArn" 34 | case eventSource = "EventSource" 35 | case sns = "Sns" 36 | } 37 | } 38 | 39 | public let records: [Record] 40 | 41 | public enum CodingKeys: String, CodingKey { 42 | case records = "Records" 43 | } 44 | 45 | public struct Message: Sendable { 46 | public enum Attribute: Sendable { 47 | case string(String) 48 | case binary([UInt8]) 49 | } 50 | 51 | public let signature: String 52 | public let messageId: String 53 | public let type: String 54 | public let topicArn: String 55 | public let messageAttributes: [String: Attribute]? 56 | public let signatureVersion: String 57 | 58 | @ISO8601WithFractionalSecondsCoding 59 | public var timestamp: Date 60 | public let signingCertURL: String 61 | public let message: String 62 | public let unsubscribeURL: String 63 | public let subject: String? 64 | } 65 | } 66 | 67 | extension SNSEvent.Message: Decodable { 68 | enum CodingKeys: String, CodingKey { 69 | case signature = "Signature" 70 | case messageId = "MessageId" 71 | case type = "Type" 72 | case topicArn = "TopicArn" 73 | case messageAttributes = "MessageAttributes" 74 | case signatureVersion = "SignatureVersion" 75 | case timestamp = "Timestamp" 76 | case signingCertURL = "SigningCertUrl" 77 | case message = "Message" 78 | case unsubscribeURL = "UnsubscribeUrl" 79 | case subject = "Subject" 80 | } 81 | } 82 | 83 | extension SNSEvent.Message.Attribute: Equatable {} 84 | 85 | extension SNSEvent.Message.Attribute: Decodable { 86 | enum CodingKeys: String, CodingKey { 87 | case dataType = "Type" 88 | case dataValue = "Value" 89 | } 90 | 91 | public init(from decoder: Decoder) throws { 92 | let container = try decoder.container(keyedBy: CodingKeys.self) 93 | 94 | let dataType = try container.decode(String.self, forKey: .dataType) 95 | // https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html#SNSMessageAttributes.DataTypes 96 | switch dataType { 97 | case "String": 98 | let value = try container.decode(String.self, forKey: .dataValue) 99 | self = .string(value) 100 | case "Binary": 101 | let base64encoded = try container.decode(String.self, forKey: .dataValue) 102 | let bytes = try base64encoded.base64decoded() 103 | self = .binary(bytes) 104 | default: 105 | throw DecodingError.dataCorruptedError( 106 | forKey: .dataType, 107 | in: container, 108 | debugDescription: """ 109 | Unexpected value \"\(dataType)\" for key \(CodingKeys.dataType). 110 | Expected `String` or `Binary`. 111 | """ 112 | ) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/Utils/HTTP.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | // MARK: HTTPHeaders 17 | 18 | import HTTPTypes 19 | 20 | public typealias HTTPHeaders = [String: String] 21 | public typealias HTTPMultiValueHeaders = [String: [String]] 22 | 23 | extension HTTPHeaders { 24 | /// Retrieves the first value for a given header-field / dictionary-key (`name`) from the block. 25 | /// This method uses case-insensitive comparisons. 26 | /// 27 | /// - Parameter name: The header field name whose first value should be retrieved. 28 | /// - Returns: The first value for the header field name. 29 | public func first(name: String) -> String? { 30 | guard !self.isEmpty else { 31 | return nil 32 | } 33 | 34 | return self.first { header in header.0.isEqualCaseInsensitiveASCIIBytes(to: name) }?.1 35 | } 36 | } 37 | 38 | extension String { 39 | internal func isEqualCaseInsensitiveASCIIBytes(to: String) -> Bool { 40 | self.utf8.compareCaseInsensitiveASCIIBytes(to: to.utf8) 41 | } 42 | } 43 | 44 | extension String.UTF8View { 45 | /// Compares the collection of `UInt8`s to a case insensitive collection. 46 | /// 47 | /// This collection could be get from applying the `UTF8View` 48 | /// property on the string protocol. 49 | /// 50 | /// - Parameter bytes: The string constant in the form of a collection of `UInt8` 51 | /// - Returns: Whether the collection contains **EXACTLY** this array or no, but by ignoring case. 52 | internal func compareCaseInsensitiveASCIIBytes(to: String.UTF8View) -> Bool { 53 | // fast path: we can get the underlying bytes of both 54 | let maybeMaybeResult = self.withContiguousStorageIfAvailable { lhsBuffer -> Bool? in 55 | to.withContiguousStorageIfAvailable { rhsBuffer in 56 | if lhsBuffer.count != rhsBuffer.count { 57 | return false 58 | } 59 | 60 | for idx in 0.. Date? { 46 | #if canImport(FoundationEssentials) 47 | return try? Date(dateString, strategy: .iso8601) 48 | #else 49 | return Self.dateFormatter.date(from: dateString) 50 | #endif 51 | } 52 | 53 | #if !canImport(FoundationEssentials) 54 | private static var dateFormatter: DateFormatter { 55 | let formatter = DateFormatter() 56 | formatter.locale = Locale(identifier: "en_US_POSIX") 57 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 58 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 59 | return formatter 60 | } 61 | #endif 62 | } 63 | 64 | @propertyWrapper 65 | public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable { 66 | public let wrappedValue: Date 67 | 68 | public init(wrappedValue: Date) { 69 | self.wrappedValue = wrappedValue 70 | } 71 | 72 | public init(from decoder: Decoder) throws { 73 | let container = try decoder.singleValueContainer() 74 | let dateString = try container.decode(String.self) 75 | 76 | guard let date = Self.parseISO8601WithFractionalSeconds(dateString: dateString) else { 77 | throw DecodingError.dataCorruptedError( 78 | in: container, 79 | debugDescription: 80 | "Expected date to be in ISO8601 date format with fractional seconds, but `\(dateString)` is not in the correct format" 81 | ) 82 | } 83 | 84 | self.wrappedValue = date 85 | } 86 | 87 | private static func parseISO8601WithFractionalSeconds(dateString: String) -> Date? { 88 | #if canImport(FoundationEssentials) 89 | return try? Date(dateString, strategy: Self.iso8601WithFractionalSeconds) 90 | #else 91 | return Self.dateFormatter.date(from: dateString) 92 | #endif 93 | } 94 | 95 | #if canImport(FoundationEssentials) 96 | private static var iso8601WithFractionalSeconds: Date.ISO8601FormatStyle { 97 | Date.ISO8601FormatStyle(includingFractionalSeconds: true) 98 | } 99 | #else 100 | private static var dateFormatter: DateFormatter { 101 | let formatter = DateFormatter() 102 | formatter.locale = Locale(identifier: "en_US_POSIX") 103 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 104 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" 105 | return formatter 106 | } 107 | #endif 108 | } 109 | 110 | @propertyWrapper 111 | public struct RFC5322DateTimeCoding: Decodable, Sendable { 112 | public let wrappedValue: Date 113 | 114 | public init(wrappedValue: Date) { 115 | self.wrappedValue = wrappedValue 116 | } 117 | 118 | public init(from decoder: Decoder) throws { 119 | let container = try decoder.singleValueContainer() 120 | let string = try container.decode(String.self) 121 | 122 | do { 123 | #if canImport(FoundationEssentials) 124 | self.wrappedValue = try Date(string, strategy: Self.rfc5322DateParseStrategy) 125 | #else 126 | self.wrappedValue = try Self.rfc5322DateParseStrategy.parse(string) 127 | #endif 128 | } catch { 129 | throw DecodingError.dataCorruptedError( 130 | in: container, 131 | debugDescription: 132 | "Expected date to be in RFC5322 date-time format, but `\(string)` is not in the correct format" 133 | ) 134 | } 135 | } 136 | 137 | private static var rfc5322DateParseStrategy: RFC5322DateParseStrategy { 138 | RFC5322DateParseStrategy(calendar: Calendar(identifier: .gregorian)) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Foundation 17 | import Testing 18 | 19 | @testable import AWSLambdaEvents 20 | 21 | @Suite 22 | struct DateWrapperTests { 23 | @Test func iSO8601CodingWrapperSuccess() throws { 24 | struct TestEvent: Decodable { 25 | @ISO8601Coding 26 | var date: Date 27 | } 28 | 29 | let json = #"{"date":"2020-03-26T16:53:05Z"}"# 30 | let event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!) 31 | 32 | #expect(event.date == Date(timeIntervalSince1970: 1_585_241_585)) 33 | } 34 | 35 | @Test func iSO8601CodingWrapperFailure() { 36 | struct TestEvent: Decodable { 37 | @ISO8601Coding 38 | var date: Date 39 | } 40 | 41 | let date = "2020-03-26T16:53:05" // missing Z at end 42 | let json = #"{"date":"\#(date)"}"# 43 | #expect(throws: (any Error).self) { 44 | try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!) 45 | } 46 | } 47 | 48 | @Test func iSO8601WithFractionalSecondsCodingWrapperSuccess() throws { 49 | struct TestEvent: Decodable { 50 | @ISO8601WithFractionalSecondsCoding 51 | var date: Date 52 | } 53 | 54 | let json = #"{"date":"2020-03-26T16:53:05.123Z"}"# 55 | let event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!) 56 | 57 | #expect(abs(event.date.timeIntervalSince1970 - 1_585_241_585.123) < 0.001) 58 | } 59 | 60 | @Test func iSO8601WithFractionalSecondsCodingWrapperFailure() { 61 | struct TestEvent: Decodable { 62 | @ISO8601WithFractionalSecondsCoding 63 | var date: Date 64 | } 65 | 66 | let date = "2020-03-26T16:53:05Z" // missing fractional seconds 67 | let json = #"{"date":"\#(date)"}"# 68 | #if swift(<6.2) 69 | let error = (any Error).self 70 | #else 71 | #if canImport(FoundationEssentials) 72 | let error = Never.self 73 | #else 74 | let error = (any Error).self 75 | #endif 76 | #endif 77 | #expect(throws: error) { 78 | try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!) 79 | } 80 | } 81 | 82 | @Test func rFC5322DateTimeCodingWrapperSuccess() throws { 83 | struct TestEvent: Decodable { 84 | @RFC5322DateTimeCoding 85 | var date: Date 86 | } 87 | 88 | let json = #"{"date":"Thu, 5 Apr 2012 23:47:37 +0200"}"# 89 | let event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!) 90 | 91 | #expect(event.date.description == "2012-04-05 21:47:37 +0000") 92 | } 93 | 94 | @Test func rFC5322DateTimeCodingWrapperWithExtraTimeZoneSuccess() throws { 95 | struct TestEvent: Decodable { 96 | @RFC5322DateTimeCoding 97 | var date: Date 98 | } 99 | 100 | let json = #"{"date":"Fri, 26 Jun 2020 03:04:03 -0500 (CDT)"}"# 101 | let event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!) 102 | 103 | #expect(event.date.description == "2020-06-26 08:04:03 +0000") 104 | } 105 | 106 | @Test func rFC5322DateTimeCodingWrapperWithAlphabeticTimeZoneSuccess() throws { 107 | struct TestEvent: Decodable { 108 | @RFC5322DateTimeCoding 109 | var date: Date 110 | } 111 | 112 | let json = #"{"date":"Fri, 26 Jun 2020 03:04:03 CDT"}"# 113 | let event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!) 114 | 115 | #expect(event.date.description == "2020-06-26 08:04:03 +0000") 116 | } 117 | 118 | @Test func rFC5322DateTimeCodingWithoutDayWrapperSuccess() throws { 119 | struct TestEvent: Decodable { 120 | @RFC5322DateTimeCoding 121 | var date: Date 122 | } 123 | 124 | let json = #"{"date":"5 Apr 2012 23:47:37 +0200"}"# 125 | let event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!) 126 | 127 | #expect(event.date.description == "2012-04-05 21:47:37 +0000") 128 | } 129 | 130 | @Test func rFC5322DateTimeCodingWrapperFailure() { 131 | struct TestEvent: Decodable { 132 | @RFC5322DateTimeCoding 133 | var date: Date 134 | } 135 | 136 | let date = "Thu, 5 Apr 2012 23:47 +0200" // missing seconds 137 | let json = #"{"date":"\#(date)"}"# 138 | #expect(throws: (any Error).self) { 139 | try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!) 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /scripts/check_license.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftAWSLambdaRuntime open source project 5 | ## 6 | ## Copyright SwiftAWSLambdaRuntime project authors 7 | ## Copyright (c)Amazon.com, Inc. or its affiliates. 8 | ## Licensed under Apache License v2.0 9 | ## 10 | ## See LICENSE.txt for license information 11 | ## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 12 | ## 13 | ## SPDX-License-Identifier: Apache-2.0 14 | ## 15 | ##===----------------------------------------------------------------------===## 16 | 17 | # ===----------------------------------------------------------------------===// 18 | # 19 | # This source file is part of the Swift.org open source project 20 | # 21 | # Copyright (c) 2024 Apple Inc. and the Swift project authors 22 | # Licensed under Apache License v2.0 with Runtime Library Exception 23 | # 24 | # See https://swift.org/LICENSE.txt for license information 25 | # See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 26 | # 27 | # ===----------------------------------------------------------------------===// 28 | 29 | set -euo pipefail 30 | 31 | set +x 32 | 33 | log() { printf -- "** %s\n" "$*" >&2; } 34 | error() { printf -- "** ERROR: %s\n" "$*" >&2; } 35 | fatal() { error "$@"; exit 1; } 36 | 37 | test -n "${PROJECT_NAME:-}" || fatal "PROJECT_NAME unset" 38 | 39 | if [ -f .license_header_template ]; then 40 | log "Using custom license header template" 41 | # allow projects to override the license header template 42 | expected_file_header_template=$(cat .license_header_template) 43 | else 44 | expected_file_header_template="@@===----------------------------------------------------------------------===@@ 45 | @@ 46 | @@ This source file is part of the ${PROJECT_NAME} open source project 47 | @@ 48 | @@ Copyright ${PROJECT_NAME} project authors 49 | @@ Copyright (c)YEARS Amazon.com, Inc. or its affiliates. 50 | @@ 51 | @@ See LICENSE.txt for license information 52 | @@ See CONTRIBUTORS.txt for the list of ${PROJECT_NAME} project authors 53 | @@ 54 | @@ SPDX-License-Identifier: Apache-2.0 55 | @@ 56 | @@===----------------------------------------------------------------------===@@" 57 | fi 58 | 59 | paths_with_missing_license=( ) 60 | 61 | # file_excludes=".license_header_template 62 | # .licenseignore" 63 | # if [ -f .licenseignore ]; then 64 | # file_excludes=$(printf '%s\n%s' "$file_excludes" "$(cat .licenseignore)") 65 | # fi 66 | # file_paths=$(echo "$file_excludes" | tr '\n' '\0' | xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files) 67 | file_paths=$(tr '\n' '\0' < .licenseignore | xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files ":(exclude).licenseignore" ":(exclude).license_header_template" ) 68 | echo "${file_paths}" 69 | 70 | while IFS= read -r file_path; do 71 | file_basename=$(basename -- "${file_path}") 72 | file_extension="${file_basename##*.}" 73 | 74 | # shellcheck disable=SC2001 # We prefer to use sed here instead of bash search/replace 75 | case "${file_extension}" in 76 | swift) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;; 77 | h) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;; 78 | c) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;; 79 | sh) expected_file_header=$(cat <(echo '#!/bin/bash') <(sed -e 's|@@|##|g' <<<"${expected_file_header_template}")) ;; 80 | kts) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;; 81 | gradle) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;; 82 | groovy) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;; 83 | java) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;; 84 | py) expected_file_header=$(cat <(echo '#!/usr/bin/env python3') <(sed -e 's|@@|##|g' <<<"${expected_file_header_template}")) ;; 85 | rb) expected_file_header=$(cat <(echo '#!/usr/bin/env ruby') <(sed -e 's|@@|##|g' <<<"${expected_file_header_template}")) ;; 86 | in) expected_file_header=$(sed -e 's|@@|##|g' <<<"${expected_file_header_template}") ;; 87 | cmake) expected_file_header=$(sed -e 's|@@|##|g' <<<"${expected_file_header_template}") ;; 88 | *) 89 | error "Unsupported file extension ${file_extension} for file (exclude or update this script): ${file_path}" 90 | paths_with_missing_license+=("${file_path} ") 91 | ;; 92 | esac 93 | expected_file_header_linecount=$(wc -l <<<"${expected_file_header}") 94 | 95 | file_header=$(head -n "${expected_file_header_linecount}" "${file_path}") 96 | normalized_file_header=$( 97 | echo "${file_header}" \ 98 | | sed -e 's/20[12][0123456789]-20[12][0123456789]/YEARS/' -e 's/20[12][0123456789]/YEARS/' \ 99 | ) 100 | 101 | if ! diff -u \ 102 | --label "Expected header" <(echo "${expected_file_header}") \ 103 | --label "${file_path}" <(echo "${normalized_file_header}") 104 | then 105 | paths_with_missing_license+=("${file_path} ") 106 | fi 107 | done <<< "$file_paths" 108 | 109 | if [ "${#paths_with_missing_license[@]}" -gt 0 ]; then 110 | fatal "❌ Found missing license header in files: ${paths_with_missing_license[*]}." 111 | fi 112 | 113 | log "✅ Found no files with missing license header." 114 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/AWSRegion.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | // list all available regions using aws cli: 17 | // $ aws ssm get-parameters-by-path --path /aws/service/global-infrastructure/services/lambda/regions --output json 18 | 19 | /// Enumeration of the AWS Regions. 20 | public struct AWSRegion: RawRepresentable, Equatable, Sendable { 21 | public typealias RawValue = String 22 | 23 | public let rawValue: String 24 | 25 | public init?(rawValue: String) { 26 | self.rawValue = rawValue 27 | } 28 | 29 | static let all: [AWSRegion] = [ 30 | Self.af_south_1, 31 | Self.ap_northeast_1, 32 | Self.ap_northeast_2, 33 | Self.ap_northeast_3, 34 | Self.ap_east_1, 35 | Self.ap_southeast_1, 36 | Self.ap_southeast_2, 37 | Self.ap_southeast_3, 38 | Self.ap_southeast_4, 39 | Self.ap_south_1, 40 | Self.ap_south_2, 41 | Self.cn_north_1, 42 | Self.cn_northwest_1, 43 | Self.eu_north_1, 44 | Self.eu_south_1, 45 | Self.eu_south_2, 46 | Self.eu_west_1, 47 | Self.eu_west_2, 48 | Self.eu_west_3, 49 | Self.eu_central_1, 50 | Self.eu_central_2, 51 | Self.us_east_1, 52 | Self.us_east_2, 53 | Self.us_west_1, 54 | Self.us_west_2, 55 | Self.us_gov_east_1, 56 | Self.us_gov_west_1, 57 | Self.ca_central_1, 58 | Self.ca_west_1, 59 | Self.sa_east_1, 60 | Self.me_central_1, 61 | Self.me_south_1, 62 | Self.il_central_1, 63 | ] 64 | 65 | public static var af_south_1: Self { AWSRegion(rawValue: "af-south-1")! } 66 | 67 | public static var ap_northeast_1: Self { AWSRegion(rawValue: "ap-northeast-1")! } 68 | public static var ap_northeast_2: Self { AWSRegion(rawValue: "ap-northeast-2")! } 69 | public static var ap_northeast_3: Self { AWSRegion(rawValue: "ap-northeast-3")! } 70 | public static var ap_east_1: Self { AWSRegion(rawValue: "ap-east-1")! } 71 | public static var ap_southeast_1: Self { AWSRegion(rawValue: "ap-southeast-1")! } 72 | public static var ap_southeast_2: Self { AWSRegion(rawValue: "ap-southeast-2")! } 73 | public static var ap_southeast_3: Self { AWSRegion(rawValue: "ap-southeast-3")! } 74 | public static var ap_southeast_4: Self { AWSRegion(rawValue: "ap-southeast-4")! } 75 | public static var ap_south_1: Self { AWSRegion(rawValue: "ap-south-1")! } 76 | public static var ap_south_2: Self { AWSRegion(rawValue: "ap-south-2")! } 77 | 78 | public static var cn_north_1: Self { AWSRegion(rawValue: "cn-north-1")! } 79 | public static var cn_northwest_1: Self { AWSRegion(rawValue: "cn-northwest-1")! } 80 | 81 | public static var eu_north_1: Self { AWSRegion(rawValue: "eu-north-1")! } 82 | public static var eu_south_1: Self { AWSRegion(rawValue: "eu-south-1")! } 83 | public static var eu_south_2: Self { AWSRegion(rawValue: "eu-south-2")! } 84 | public static var eu_west_1: Self { AWSRegion(rawValue: "eu-west-1")! } 85 | public static var eu_west_2: Self { AWSRegion(rawValue: "eu-west-2")! } 86 | public static var eu_west_3: Self { AWSRegion(rawValue: "eu-west-3")! } 87 | public static var eu_central_1: Self { AWSRegion(rawValue: "eu-central-1")! } 88 | public static var eu_central_2: Self { AWSRegion(rawValue: "eu-central-2")! } 89 | 90 | public static var us_east_1: Self { AWSRegion(rawValue: "us-east-1")! } 91 | public static var us_east_2: Self { AWSRegion(rawValue: "us-east-2")! } 92 | public static var us_west_1: Self { AWSRegion(rawValue: "us-west-1")! } 93 | public static var us_west_2: Self { AWSRegion(rawValue: "us-west-2")! } 94 | public static var us_gov_east_1: Self { AWSRegion(rawValue: "us-gov-east-1")! } 95 | public static var us_gov_west_1: Self { AWSRegion(rawValue: "us-gov-west-1")! } 96 | 97 | public static var ca_central_1: Self { AWSRegion(rawValue: "ca-central-1")! } 98 | public static var ca_west_1: Self { AWSRegion(rawValue: "ca-west-1")! } 99 | public static var sa_east_1: Self { AWSRegion(rawValue: "sa-east-1")! } 100 | public static var me_central_1: Self { AWSRegion(rawValue: "me-central-1")! } 101 | public static var me_south_1: Self { AWSRegion(rawValue: "me-south-1")! } 102 | 103 | public static var il_central_1: Self { AWSRegion(rawValue: "il-central-1")! } 104 | } 105 | 106 | extension AWSRegion: Codable { 107 | public init(from decoder: Decoder) throws { 108 | let container = try decoder.singleValueContainer() 109 | let region = try container.decode(String.self) 110 | self.init(rawValue: region)! 111 | } 112 | 113 | public func encode(to encoder: Encoder) throws { 114 | var container = encoder.singleValueContainer() 115 | try container.encode(self.rawValue) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/APIGateway.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import HTTPTypes 17 | 18 | #if canImport(FoundationEssentials) 19 | import FoundationEssentials 20 | #else 21 | import Foundation 22 | #endif 23 | 24 | // https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html 25 | // https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html 26 | 27 | /// `APIGatewayRequest` contains data coming from the API Gateway. 28 | public struct APIGatewayRequest: Encodable, Sendable { 29 | public struct Context: Codable, Sendable { 30 | public struct Identity: Codable, Sendable { 31 | public let cognitoIdentityPoolId: String? 32 | 33 | public let apiKey: String? 34 | public let userArn: String? 35 | public let cognitoAuthenticationType: String? 36 | public let caller: String? 37 | public let userAgent: String? 38 | public let user: String? 39 | 40 | public let cognitoAuthenticationProvider: String? 41 | public let sourceIp: String? 42 | public let accountId: String? 43 | } 44 | 45 | public struct Authorizer: Codable, Sendable { 46 | public let claims: [String: String]? 47 | } 48 | 49 | public let resourceId: String 50 | public let apiId: String 51 | public let domainName: String? 52 | public let resourcePath: String 53 | public let httpMethod: String 54 | public let requestId: String 55 | public let accountId: String 56 | public let stage: String 57 | 58 | public let identity: Identity 59 | public let authorizer: Authorizer? 60 | public let extendedRequestId: String? 61 | public let path: String 62 | } 63 | 64 | public let resource: String 65 | public let path: String 66 | public let httpMethod: HTTPRequest.Method 67 | 68 | public let queryStringParameters: [String: String] 69 | public let multiValueQueryStringParameters: [String: [String]] 70 | public let headers: HTTPHeaders 71 | public let multiValueHeaders: HTTPMultiValueHeaders 72 | public let pathParameters: [String: String] 73 | public let stageVariables: [String: String] 74 | 75 | public let requestContext: Context 76 | public let body: String? 77 | public let isBase64Encoded: Bool 78 | } 79 | 80 | extension APIGatewayRequest: DecodableRequest {} 81 | 82 | // MARK: - Response - 83 | 84 | public struct APIGatewayResponse: Codable, Sendable { 85 | public var statusCode: HTTPResponse.Status 86 | public var headers: HTTPHeaders? 87 | public var multiValueHeaders: HTTPMultiValueHeaders? 88 | public var body: String? 89 | public var isBase64Encoded: Bool? 90 | 91 | public init( 92 | statusCode: HTTPResponse.Status, 93 | headers: HTTPHeaders? = nil, 94 | multiValueHeaders: HTTPMultiValueHeaders? = nil, 95 | body: String? = nil, 96 | isBase64Encoded: Bool? = nil 97 | ) { 98 | self.statusCode = statusCode 99 | self.headers = headers 100 | self.multiValueHeaders = multiValueHeaders 101 | self.body = body 102 | self.isBase64Encoded = isBase64Encoded 103 | } 104 | } 105 | 106 | extension APIGatewayRequest: Decodable { 107 | public init(from decoder: any Decoder) throws { 108 | let container = try decoder.container(keyedBy: CodingKeys.self) 109 | 110 | self.resource = try container.decode(String.self, forKey: .resource) 111 | self.path = try container.decode(String.self, forKey: .path) 112 | self.httpMethod = try container.decode(HTTPRequest.Method.self, forKey: .httpMethod) 113 | 114 | self.queryStringParameters = 115 | try container.decodeIfPresent([String: String].self, forKey: .queryStringParameters) ?? [:] 116 | self.multiValueQueryStringParameters = 117 | try container.decodeIfPresent([String: [String]].self, forKey: .multiValueQueryStringParameters) ?? [:] 118 | self.headers = try container.decodeIfPresent(HTTPHeaders.self, forKey: .headers) ?? HTTPHeaders() 119 | self.multiValueHeaders = 120 | try container.decodeIfPresent(HTTPMultiValueHeaders.self, forKey: .multiValueHeaders) 121 | ?? HTTPMultiValueHeaders() 122 | self.pathParameters = try container.decodeIfPresent([String: String].self, forKey: .pathParameters) ?? [:] 123 | self.stageVariables = try container.decodeIfPresent([String: String].self, forKey: .stageVariables) ?? [:] 124 | 125 | self.requestContext = try container.decode(Context.self, forKey: .requestContext) 126 | self.body = try container.decodeIfPresent(String.self, forKey: .body) 127 | self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/CloudFormation.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | // CloudFormation custom resource. 17 | public enum CloudFormation: Sendable { 18 | // Request represents the request body of AWS::CloudFormation::CustomResource. 19 | // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html 20 | public struct Request: Decodable { 21 | public enum RequestType: String, Decodable, Sendable { 22 | case create = "Create" 23 | case update = "Update" 24 | case delete = "Delete" 25 | } 26 | 27 | public let requestType: RequestType 28 | public let requestId: String 29 | public let responseURL: String 30 | public let physicalResourceId: String? 31 | public let logicalResourceId: String 32 | public let stackId: String 33 | public let resourceProperties: R? 34 | public let oldResourceProperties: O? 35 | 36 | enum CodingKeys: String, CodingKey { 37 | case requestType = "RequestType" 38 | case requestId = "RequestId" 39 | case responseURL = "ResponseURL" 40 | case physicalResourceId = "PhysicalResourceId" 41 | case logicalResourceId = "LogicalResourceId" 42 | case stackId = "StackId" 43 | case resourceProperties = "ResourceProperties" 44 | case oldResourceProperties = "OldResourceProperties" 45 | } 46 | 47 | public init(from decoder: Decoder) throws { 48 | let container = try decoder.container(keyedBy: CodingKeys.self) 49 | 50 | self.requestType = try container.decode(RequestType.self, forKey: .requestType) 51 | self.requestId = try container.decode(String.self, forKey: .requestId) 52 | self.responseURL = try container.decode(String.self, forKey: .responseURL) 53 | self.logicalResourceId = try container.decode(String.self, forKey: .logicalResourceId) 54 | self.stackId = try container.decode(String.self, forKey: .stackId) 55 | self.physicalResourceId = try container.decodeIfPresent(String.self, forKey: .physicalResourceId) 56 | self.resourceProperties = try container.decodeIfPresent(R.self, forKey: .resourceProperties) 57 | self.oldResourceProperties = try container.decodeIfPresent(O.self, forKey: .oldResourceProperties) 58 | } 59 | } 60 | 61 | // Response represents the response body of AWS::CloudFormation::CustomResource. 62 | // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html 63 | public struct Response: Encodable { 64 | public enum StatusType: String, Encodable, Sendable { 65 | case success = "SUCCESS" 66 | case failed = "FAILED" 67 | } 68 | 69 | public let status: StatusType 70 | public let requestId: String 71 | public let logicalResourceId: String 72 | public let stackId: String 73 | public let physicalResourceId: String? 74 | public let reason: String? 75 | public let noEcho: Bool? 76 | public let data: D? 77 | 78 | enum CodingKeys: String, CodingKey { 79 | case status = "Status" 80 | case requestId = "RequestId" 81 | case logicalResourceId = "LogicalResourceId" 82 | case stackId = "StackId" 83 | case physicalResourceId = "PhysicalResourceId" 84 | case reason = "Reason" 85 | case noEcho = "NoEcho" 86 | case data = "Data" 87 | } 88 | 89 | public init( 90 | status: StatusType, 91 | requestId: String, 92 | logicalResourceId: String, 93 | stackId: String, 94 | physicalResourceId: String?, 95 | reason: String?, 96 | noEcho: Bool?, 97 | data: D? 98 | ) { 99 | self.status = status 100 | self.requestId = requestId 101 | self.logicalResourceId = logicalResourceId 102 | self.stackId = stackId 103 | self.physicalResourceId = physicalResourceId 104 | self.reason = reason 105 | self.noEcho = noEcho 106 | self.data = data 107 | } 108 | 109 | public func encode(to encoder: Encoder) throws { 110 | var container = encoder.container(keyedBy: CodingKeys.self) 111 | 112 | try container.encode(self.status.rawValue, forKey: .status) 113 | try container.encode(self.requestId, forKey: .requestId) 114 | try container.encode(self.logicalResourceId, forKey: .logicalResourceId) 115 | try container.encode(self.stackId, forKey: .stackId) 116 | try container.encodeIfPresent(self.physicalResourceId, forKey: .physicalResourceId) 117 | try container.encodeIfPresent(self.reason, forKey: .reason) 118 | try container.encodeIfPresent(self.noEcho, forKey: .noEcho) 119 | try container.encodeIfPresent(self.data, forKey: .data) 120 | } 121 | } 122 | } 123 | 124 | extension CloudFormation.Request: Sendable where R: Sendable, O: Sendable {} 125 | extension CloudFormation.Response: Sendable where D: Sendable {} 126 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Foundation 17 | import Testing 18 | 19 | @testable import AWSLambdaEvents 20 | 21 | @Suite 22 | class APIGatewayWebSocketsTests { 23 | static let exampleConnectEventBody = """ 24 | { 25 | "headers": { 26 | "Host": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", 27 | "Origin": "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com", 28 | "Sec-WebSocket-Extensions": "", 29 | "Sec-WebSocket-Key": "am5ubWVpbHd3bmNyYXF0ag==", 30 | "Sec-WebSocket-Version": "13", 31 | "X-Amzn-Trace-Id": "Root=1-64b83950-42de8e247b4c2b43091ef67c", 32 | "X-Forwarded-For": "24.148.42.16", 33 | "X-Forwarded-Port": "443", 34 | "X-Forwarded-Proto": "https" 35 | }, 36 | "multiValueHeaders": { 37 | "Host": [ "lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], 38 | "Origin": [ "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], 39 | "Sec-WebSocket-Extensions": [ 40 | "permessage-deflate; client_max_window_bits; server_max_window_bits=15" 41 | ], 42 | "Sec-WebSocket-Key": [ "am5ubWVpbHd3bmNyYXF0ag==" ], 43 | "Sec-WebSocket-Version": [ "13" ], 44 | "X-Amzn-Trace-Id": [ "Root=1-64b83950-42de8e247b4c2b43091ef67c" ], 45 | "X-Forwarded-For": [ "24.148.42.16" ], 46 | "X-Forwarded-Port": [ "443" ], 47 | "X-Forwarded-Proto": [ "https" ] 48 | }, 49 | "requestContext": { 50 | "routeKey": "$connect", 51 | "eventType": "CONNECT", 52 | "extendedRequestId": "IU3kkGyEoAMFwZQ=", 53 | "requestTime": "19/Jul/2023:19:28:16 +0000", 54 | "messageDirection": "IN", 55 | "stage": "dev", 56 | "connectedAt": 1689794896145, 57 | "requestTimeEpoch": 1689794896162, 58 | "identity": { "sourceIp": "24.148.42.16" }, 59 | "requestId": "IU3kkGyEoAMFwZQ=", 60 | "domainName": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", 61 | "connectionId": "IU3kkeN4IAMCJwA=", 62 | "apiId": "lqrlmblaa2" 63 | }, 64 | "isBase64Encoded": false 65 | } 66 | """ 67 | 68 | static let exampleConnectEventBodyWithQueryParams = """ 69 | { 70 | "headers": { 71 | "Host": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", 72 | "Origin": "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com", 73 | "Sec-WebSocket-Extensions": "", 74 | "Sec-WebSocket-Key": "am5ubWVpbHd3bmNyYXF0ag==", 75 | "Sec-WebSocket-Version": "13", 76 | "X-Amzn-Trace-Id": "Root=1-64b83950-42de8e247b4c2b43091ef67c", 77 | "X-Forwarded-For": "24.148.42.16", 78 | "X-Forwarded-Port": "443", 79 | "X-Forwarded-Proto": "https" 80 | }, 81 | "queryStringParameters":{ 82 | "foo":"bar" 83 | }, 84 | "multiValueHeaders": { 85 | "Host": [ "lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], 86 | "Origin": [ "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], 87 | "Sec-WebSocket-Extensions": [ 88 | "permessage-deflate; client_max_window_bits; server_max_window_bits=15" 89 | ], 90 | "Sec-WebSocket-Key": [ "am5ubWVpbHd3bmNyYXF0ag==" ], 91 | "Sec-WebSocket-Version": [ "13" ], 92 | "X-Amzn-Trace-Id": [ "Root=1-64b83950-42de8e247b4c2b43091ef67c" ], 93 | "X-Forwarded-For": [ "24.148.42.16" ], 94 | "X-Forwarded-Port": [ "443" ], 95 | "X-Forwarded-Proto": [ "https" ] 96 | }, 97 | "requestContext": { 98 | "routeKey": "$connect", 99 | "eventType": "CONNECT", 100 | "extendedRequestId": "IU3kkGyEoAMFwZQ=", 101 | "requestTime": "19/Jul/2023:19:28:16 +0000", 102 | "messageDirection": "IN", 103 | "stage": "dev", 104 | "connectedAt": 1689794896145, 105 | "requestTimeEpoch": 1689794896162, 106 | "identity": { "sourceIp": "24.148.42.16" }, 107 | "requestId": "IU3kkGyEoAMFwZQ=", 108 | "domainName": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", 109 | "connectionId": "IU3kkeN4IAMCJwA=", 110 | "apiId": "lqrlmblaa2" 111 | }, 112 | "isBase64Encoded": false 113 | } 114 | """ 115 | 116 | // MARK: - Request - 117 | 118 | // MARK: Decoding 119 | @Test func testRequestDecodingExampleConnectRequest() async throws { 120 | let data = APIGatewayWebSocketsTests.exampleConnectEventBody.data(using: .utf8)! 121 | let req = try JSONDecoder().decode(APIGatewayWebSocketRequest.self, from: data) 122 | 123 | #expect(req.context.routeKey == "$connect") 124 | #expect(req.context.connectionId == "IU3kkeN4IAMCJwA=") 125 | #expect(req.body == nil) 126 | } 127 | 128 | @Test func testRequestDecodingExampleWithQueryParams() async throws { 129 | let data = APIGatewayWebSocketsTests.exampleConnectEventBodyWithQueryParams.data(using: .utf8)! 130 | let req = try JSONDecoder().decode(APIGatewayWebSocketRequest.self, from: data) 131 | 132 | #expect(req.queryStringParameters?["foo"] == "bar") 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/SESTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Foundation 17 | import Testing 18 | 19 | @testable import AWSLambdaEvents 20 | 21 | @Suite 22 | struct SESTests { 23 | static let eventBody = """ 24 | { 25 | "Records": [ 26 | { 27 | "eventSource": "aws:ses", 28 | "eventVersion": "1.0", 29 | "ses": { 30 | "mail": { 31 | "commonHeaders": { 32 | "date": "Wed, 7 Oct 2015 12:34:56 -0700", 33 | "from": [ 34 | "Jane Doe " 35 | ], 36 | "messageId": "<0123456789example.com>", 37 | "returnPath": "janedoe@example.com", 38 | "subject": "Test Subject", 39 | "to": [ 40 | "johndoe@example.com" 41 | ] 42 | }, 43 | "destination": [ 44 | "johndoe@example.com" 45 | ], 46 | "headers": [ 47 | { 48 | "name": "Return-Path", 49 | "value": "" 50 | }, 51 | { 52 | "name": "Received", 53 | "value": "from mailer.example.com (mailer.example.com [203.0.113.1]) by inbound-smtp.eu-west-1.amazonaws.com with SMTP id o3vrnil0e2ic28trm7dfhrc2v0cnbeccl4nbp0g1 for johndoe@example.com; Wed, 07 Oct 2015 12:34:56 +0000 (UTC)" 54 | } 55 | ], 56 | "headersTruncated": true, 57 | "messageId": "5h5auqp1oa1bg49b2q8f8tmli1oju8pcma2haao1", 58 | "source": "janedoe@example.com", 59 | "timestamp": "1970-01-01T00:00:00.000Z" 60 | }, 61 | "receipt": { 62 | "action": { 63 | "functionArn": "arn:aws:lambda:eu-west-1:123456789012:function:Example", 64 | "invocationType": "Event", 65 | "type": "Lambda" 66 | }, 67 | "dkimVerdict": { 68 | "status": "PASS" 69 | }, 70 | "processingTimeMillis": 574, 71 | "recipients": [ 72 | "test@swift-server.com", 73 | "test2@swift-server.com" 74 | ], 75 | "spamVerdict": { 76 | "status": "PASS" 77 | }, 78 | "spfVerdict": { 79 | "status": "PROCESSING_FAILED" 80 | }, 81 | "timestamp": "1970-01-01T00:00:00.000Z", 82 | "virusVerdict": { 83 | "status": "FAIL" 84 | } 85 | } 86 | } 87 | } 88 | ] 89 | } 90 | """ 91 | 92 | @Test func simpleEventFromJSON() throws { 93 | let data = Data(SESTests.eventBody.utf8) 94 | let event = try JSONDecoder().decode(SESEvent.self, from: data) 95 | let record = try #require(event.records.first) 96 | 97 | #expect(record.eventSource == "aws:ses") 98 | #expect(record.eventVersion == "1.0") 99 | #expect(record.ses.mail.commonHeaders.date.description == "2015-10-07 19:34:56 +0000") 100 | #expect(record.ses.mail.commonHeaders.from[0] == "Jane Doe ") 101 | #expect(record.ses.mail.commonHeaders.messageId == "<0123456789example.com>") 102 | #expect(record.ses.mail.commonHeaders.returnPath == "janedoe@example.com") 103 | #expect(record.ses.mail.commonHeaders.subject == "Test Subject") 104 | #expect(record.ses.mail.commonHeaders.to?[0] == "johndoe@example.com") 105 | #expect(record.ses.mail.destination[0] == "johndoe@example.com") 106 | #expect(record.ses.mail.headers[0].name == "Return-Path") 107 | #expect(record.ses.mail.headers[0].value == "") 108 | #expect(record.ses.mail.headers[1].name == "Received") 109 | #expect( 110 | record.ses.mail.headers[1].value 111 | == "from mailer.example.com (mailer.example.com [203.0.113.1]) by inbound-smtp.eu-west-1.amazonaws.com with SMTP id o3vrnil0e2ic28trm7dfhrc2v0cnbeccl4nbp0g1 for johndoe@example.com; Wed, 07 Oct 2015 12:34:56 +0000 (UTC)" 112 | ) 113 | #expect(record.ses.mail.headersTruncated == true) 114 | #expect(record.ses.mail.messageId == "5h5auqp1oa1bg49b2q8f8tmli1oju8pcma2haao1") 115 | #expect(record.ses.mail.source == "janedoe@example.com") 116 | #expect(record.ses.mail.timestamp.description == "1970-01-01 00:00:00 +0000") 117 | 118 | #expect(record.ses.receipt.action.functionArn == "arn:aws:lambda:eu-west-1:123456789012:function:Example") 119 | #expect(record.ses.receipt.action.invocationType == "Event") 120 | #expect(record.ses.receipt.action.type == "Lambda") 121 | #expect(record.ses.receipt.dkimVerdict.status == .pass) 122 | #expect(record.ses.receipt.processingTimeMillis == 574) 123 | #expect(record.ses.receipt.recipients[0] == "test@swift-server.com") 124 | #expect(record.ses.receipt.recipients[1] == "test2@swift-server.com") 125 | #expect(record.ses.receipt.spamVerdict.status == .pass) 126 | #expect(record.ses.receipt.spfVerdict.status == .processingFailed) 127 | #expect(record.ses.receipt.timestamp.description == "1970-01-01 00:00:00 +0000") 128 | #expect(record.ses.receipt.virusVerdict.status == .fail) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/AppSync.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import HTTPTypes 17 | 18 | // https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html 19 | public struct AppSyncEvent: Decodable, Sendable { 20 | public let arguments: [String: ArgumentValue] 21 | 22 | public enum ArgumentValue: Codable, Sendable { 23 | case string(String) 24 | case dictionary([String: String]) 25 | 26 | public init(from decoder: Decoder) throws { 27 | let container = try decoder.singleValueContainer() 28 | if let strValue = try? container.decode(String.self) { 29 | self = .string(strValue) 30 | } else if let dictionaryValue = try? container.decode([String: String].self) { 31 | self = .dictionary(dictionaryValue) 32 | } else { 33 | throw DecodingError.dataCorruptedError( 34 | in: container, 35 | debugDescription: """ 36 | Unexpected AppSync argument. 37 | Expected a String or a Dictionary. 38 | """ 39 | ) 40 | } 41 | } 42 | 43 | public func encode(to encoder: Encoder) throws { 44 | var container = encoder.singleValueContainer() 45 | switch self { 46 | case .dictionary(let array): 47 | try container.encode(array) 48 | case .string(let str): 49 | try container.encode(str) 50 | } 51 | } 52 | } 53 | 54 | public let request: Request 55 | public struct Request: Decodable, Sendable { 56 | let headers: HTTPHeaders 57 | } 58 | 59 | public let source: [String: String]? 60 | public let stash: [String: String]? 61 | 62 | public let info: Info 63 | public struct Info: Codable, Sendable { 64 | public var selectionSetList: [String] 65 | public var selectionSetGraphQL: String 66 | public var parentTypeName: String 67 | public var fieldName: String 68 | public var variables: [String: String] 69 | } 70 | 71 | public let identity: Identity? 72 | public enum Identity: Codable, Sendable { 73 | case iam(IAMIdentity) 74 | case cognitoUserPools(CognitoUserPoolIdentity) 75 | 76 | public struct IAMIdentity: Codable, Sendable { 77 | public let accountId: String 78 | public let cognitoIdentityPoolId: String 79 | public let cognitoIdentityId: String 80 | public let sourceIp: [String] 81 | public let username: String? 82 | public let userArn: String 83 | public let cognitoIdentityAuthType: String 84 | public let cognitoIdentityAuthProvider: String 85 | } 86 | 87 | public struct CognitoUserPoolIdentity: Codable, Sendable { 88 | public let defaultAuthStrategy: String 89 | public let issuer: String 90 | public let sourceIp: [String] 91 | public let sub: String 92 | public let username: String? 93 | 94 | public struct Claims { 95 | let sub: String 96 | let emailVerified: Bool 97 | let iss: String 98 | let phoneNumberVerified: Bool 99 | let cognitoUsername: String 100 | let aud: String 101 | let eventId: String 102 | let tokenUse: String 103 | let authTime: Int 104 | let phoneNumber: String? 105 | let exp: Int 106 | let iat: Int 107 | let email: String? 108 | 109 | enum CodingKeys: String, CodingKey { 110 | case sub 111 | case emailVerified = "email_verified" 112 | case iss 113 | case phoneNumberVerified = "phone_number_verified" 114 | case cognitoUsername = "cognito:username" 115 | case aud 116 | case eventId = "event_id" 117 | case tokenUse = "token_use" 118 | case authTime = "auth_time" 119 | case phoneNumber = "phone_number" 120 | case exp 121 | case iat 122 | case email 123 | } 124 | } 125 | } 126 | 127 | public init(from decoder: Decoder) throws { 128 | let container = try decoder.singleValueContainer() 129 | if let iamIdentity = try? container.decode(IAMIdentity.self) { 130 | self = .iam(iamIdentity) 131 | } else if let cognitoIdentity = try? container.decode(CognitoUserPoolIdentity.self) { 132 | self = .cognitoUserPools(cognitoIdentity) 133 | } else { 134 | throw DecodingError.dataCorruptedError( 135 | in: container, 136 | debugDescription: """ 137 | Unexpected Identity argument. 138 | Expected a IAM Identity or a Cognito User Pool Identity. 139 | """ 140 | ) 141 | } 142 | } 143 | 144 | public func encode(to encoder: Encoder) throws { 145 | var container = encoder.singleValueContainer() 146 | switch self { 147 | case .iam(let iamIdentity): 148 | try container.encode(iamIdentity) 149 | case .cognitoUserPools(let cognitoUserPool): 150 | try container.encode(cognitoUserPool) 151 | } 152 | } 153 | } 154 | } 155 | 156 | public enum AppSyncResponse: Encodable { 157 | public func encode(to encoder: Encoder) throws { 158 | var container = encoder.singleValueContainer() 159 | switch self { 160 | case .array(let array): 161 | try container.encode(array) 162 | case .object(let object): 163 | try container.encode(object) 164 | case .dictionary(let dictionary): 165 | try container.encode(dictionary) 166 | } 167 | } 168 | 169 | case object(ResultType) 170 | case array([ResultType]) 171 | case dictionary([String: ResultType]) 172 | } 173 | 174 | public typealias AppSyncJSONResponse = AppSyncResponse 175 | extension AppSyncResponse: Sendable where ResultType: Sendable {} 176 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/APIGateway+V2.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import HTTPTypes 17 | 18 | /// `APIGatewayV2Request` contains data coming from the new HTTP API Gateway. 19 | public struct APIGatewayV2Request: Encodable, Sendable { 20 | /// `Context` contains information to identify the AWS account and resources invoking the Lambda function. 21 | public struct Context: Codable, Sendable { 22 | public struct HTTP: Codable, Sendable { 23 | public let method: HTTPRequest.Method 24 | public let path: String 25 | public let `protocol`: String 26 | public let sourceIp: String 27 | public let userAgent: String 28 | } 29 | 30 | /// `Authorizer` contains authorizer information for the request context. 31 | public struct Authorizer: Codable, Sendable { 32 | /// `JWT` contains JWT authorizer information for the request context. 33 | public struct JWT: Codable, Sendable { 34 | public let claims: [String: String]? 35 | public let scopes: [String]? 36 | } 37 | 38 | public let jwt: JWT? 39 | 40 | // `IAM` contains AWS IAM authorizer information for the request context. 41 | public struct IAM: Codable, Sendable { 42 | public struct CognitoIdentity: Codable, Sendable { 43 | public let amr: [String]? 44 | public let identityId: String? 45 | public let identityPoolId: String? 46 | } 47 | 48 | public let accessKey: String? 49 | public let accountId: String? 50 | public let callerId: String? 51 | public let cognitoIdentity: CognitoIdentity? 52 | public let principalOrgId: String? 53 | public let userArn: String? 54 | public let userId: String? 55 | } 56 | 57 | public let iam: IAM? 58 | 59 | public let lambda: LambdaAuthorizerContext? 60 | } 61 | 62 | public struct Authentication: Codable, Sendable { 63 | public struct ClientCert: Codable, Sendable { 64 | public struct Validity: Codable, Sendable { 65 | public let notBefore: String 66 | public let notAfter: String 67 | } 68 | 69 | public let clientCertPem: String 70 | public let subjectDN: String 71 | public let issuerDN: String 72 | public let serialNumber: String 73 | public let validity: Validity 74 | } 75 | 76 | public let clientCert: ClientCert? 77 | } 78 | 79 | public let accountId: String 80 | public let apiId: String 81 | public let domainName: String 82 | public let domainPrefix: String 83 | public let stage: String 84 | public let requestId: String 85 | 86 | public let http: HTTP 87 | public let authorizer: Authorizer? 88 | public let authentication: Authentication? 89 | 90 | /// The request time in format: 23/Apr/2020:11:08:18 +0000 91 | public let time: String 92 | public let timeEpoch: UInt64 93 | } 94 | 95 | public let version: String 96 | public let routeKey: String 97 | public let rawPath: String 98 | public let rawQueryString: String 99 | 100 | public let cookies: [String] 101 | public let headers: HTTPHeaders 102 | public let queryStringParameters: [String: String] 103 | public let pathParameters: [String: String] 104 | 105 | public let context: Context 106 | public let stageVariables: [String: String] 107 | 108 | public let body: String? 109 | public let isBase64Encoded: Bool 110 | 111 | enum CodingKeys: String, CodingKey { 112 | case version 113 | case routeKey 114 | case rawPath 115 | case rawQueryString 116 | 117 | case cookies 118 | case headers 119 | case queryStringParameters 120 | case pathParameters 121 | 122 | case context = "requestContext" 123 | case stageVariables 124 | 125 | case body 126 | case isBase64Encoded 127 | } 128 | } 129 | 130 | extension APIGatewayV2Request: DecodableRequest {} 131 | 132 | public struct APIGatewayV2Response: Codable, Sendable { 133 | public var statusCode: HTTPResponse.Status 134 | public var headers: HTTPHeaders? 135 | public var body: String? 136 | public var isBase64Encoded: Bool? 137 | public var cookies: [String]? 138 | 139 | public init( 140 | statusCode: HTTPResponse.Status, 141 | headers: HTTPHeaders? = nil, 142 | body: String? = nil, 143 | isBase64Encoded: Bool? = nil, 144 | cookies: [String]? = nil 145 | ) { 146 | self.statusCode = statusCode 147 | self.headers = headers 148 | self.body = body 149 | self.isBase64Encoded = isBase64Encoded 150 | self.cookies = cookies 151 | } 152 | } 153 | 154 | extension APIGatewayV2Response: EncodableResponse {} 155 | 156 | extension APIGatewayV2Request: Decodable { 157 | public init(from decoder: Decoder) throws { 158 | let container = try decoder.container(keyedBy: CodingKeys.self) 159 | 160 | self.version = try container.decode(String.self, forKey: .version) 161 | self.routeKey = try container.decode(String.self, forKey: .routeKey) 162 | self.rawPath = try container.decode(String.self, forKey: .rawPath) 163 | self.rawQueryString = try container.decode(String.self, forKey: .rawQueryString) 164 | 165 | self.cookies = try container.decodeIfPresent([String].self, forKey: .cookies) ?? [] 166 | self.headers = try container.decodeIfPresent(HTTPHeaders.self, forKey: .headers) ?? HTTPHeaders() 167 | self.queryStringParameters = 168 | try container.decodeIfPresent([String: String].self, forKey: .queryStringParameters) ?? [:] 169 | self.pathParameters = try container.decodeIfPresent([String: String].self, forKey: .pathParameters) ?? [:] 170 | 171 | self.context = try container.decode(Context.self, forKey: .context) 172 | self.stageVariables = try container.decodeIfPresent([String: String].self, forKey: .stageVariables) ?? [:] 173 | 174 | self.body = try container.decodeIfPresent(String.self, forKey: .body) 175 | self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/S3Tests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Foundation 17 | import Testing 18 | 19 | @testable import AWSLambdaEvents 20 | 21 | @Suite 22 | struct S3Tests { 23 | static let eventBodyObjectCreated = """ 24 | { 25 | "Records": [ 26 | { 27 | "eventVersion":"2.1", 28 | "eventSource":"aws:s3", 29 | "awsRegion":"eu-central-1", 30 | "eventTime":"2020-01-13T09:25:40.621Z", 31 | "eventName":"ObjectCreated:Put", 32 | "userIdentity":{ 33 | "principalId":"AWS:AAAAAAAJ2MQ4YFQZ7AULJ" 34 | }, 35 | "requestParameters":{ 36 | "sourceIPAddress":"123.123.123.123" 37 | }, 38 | "responseElements":{ 39 | "x-amz-request-id":"01AFA1430E18C358", 40 | "x-amz-id-2":"JsbNw6sHGFwgzguQjbYcew//bfAeZITyTYLfjuu1U4QYqCq5CPlSyYLtvWQS+gw0RxcroItGwm8=" 41 | }, 42 | "s3":{ 43 | "s3SchemaVersion":"1.0", 44 | "configurationId":"98b55bc4-3c0c-4007-b727-c6b77a259dde", 45 | "bucket":{ 46 | "name":"eventsources", 47 | "ownerIdentity":{ 48 | "principalId":"AAAAAAAAAAAAAA" 49 | }, 50 | "arn":"arn:aws:s3:::eventsources" 51 | }, 52 | "object":{ 53 | "key":"Hi.md", 54 | "size":2880, 55 | "eTag":"91a7f2c3ae81bcc6afef83979b463f0e", 56 | "sequencer":"005E1C37948E783A6E" 57 | } 58 | } 59 | } 60 | ] 61 | } 62 | """ 63 | 64 | // A S3 ObjectRemoved:* event does not contain the object size 65 | static let eventBodyObjectRemoved = """ 66 | { 67 | "Records": [ 68 | { 69 | "eventVersion":"2.1", 70 | "eventSource":"aws:s3", 71 | "awsRegion":"eu-central-1", 72 | "eventTime":"2020-01-13T09:25:40.621Z", 73 | "eventName":"ObjectRemoved:DeleteMarkerCreated", 74 | "userIdentity":{ 75 | "principalId":"AWS:AAAAAAAJ2MQ4YFQZ7AULJ" 76 | }, 77 | "requestParameters":{ 78 | "sourceIPAddress":"123.123.123.123" 79 | }, 80 | "responseElements":{ 81 | "x-amz-request-id":"01AFA1430E18C358", 82 | "x-amz-id-2":"JsbNw6sHGFwgzguQjbYcew//bfAeZITyTYLfjuu1U4QYqCq5CPlSyYLtvWQS+gw0RxcroItGwm8=" 83 | }, 84 | "s3":{ 85 | "s3SchemaVersion":"1.0", 86 | "configurationId":"98b55bc4-3c0c-4007-b727-c6b77a259dde", 87 | "bucket":{ 88 | "name":"eventsources", 89 | "ownerIdentity":{ 90 | "principalId":"AAAAAAAAAAAAAA" 91 | }, 92 | "arn":"arn:aws:s3:::eventsources" 93 | }, 94 | "object":{ 95 | "key":"Hi.md", 96 | "eTag":"91a7f2c3ae81bcc6afef83979b463f0e", 97 | "sequencer":"005E1C37948E783A6E" 98 | } 99 | } 100 | } 101 | ] 102 | } 103 | """ 104 | 105 | @Test func simpleEventFromJSON() throws { 106 | let data = S3Tests.eventBodyObjectCreated.data(using: .utf8)! 107 | let event = try JSONDecoder().decode(S3Event.self, from: data) 108 | let record = try #require(event.records.first) 109 | 110 | #expect(record.eventVersion == "2.1") 111 | #expect(record.eventSource == "aws:s3") 112 | #expect(record.awsRegion == .eu_central_1) 113 | #expect(record.eventName == "ObjectCreated:Put") 114 | #expect(record.eventTime.ISO8601Format() == Date(timeIntervalSince1970: 1_578_907_540.621).ISO8601Format()) 115 | // see https://github.com/swiftlang/swift-foundation/issues/1561#issuecomment-3448853449 116 | #expect(abs(record.eventTime.timeIntervalSince1970 - 1_578_907_540.621) < 0.0005) 117 | #expect(record.userIdentity == S3Event.UserIdentity(principalId: "AWS:AAAAAAAJ2MQ4YFQZ7AULJ")) 118 | #expect(record.requestParameters == S3Event.RequestParameters(sourceIPAddress: "123.123.123.123")) 119 | #expect(record.responseElements.count == 2) 120 | #expect(record.s3.schemaVersion == "1.0") 121 | #expect(record.s3.configurationId == "98b55bc4-3c0c-4007-b727-c6b77a259dde") 122 | #expect(record.s3.bucket.name == "eventsources") 123 | #expect(record.s3.bucket.ownerIdentity == S3Event.UserIdentity(principalId: "AAAAAAAAAAAAAA")) 124 | #expect(record.s3.bucket.arn == "arn:aws:s3:::eventsources") 125 | #expect(record.s3.object.key == "Hi.md") 126 | #expect(record.s3.object.size == 2880) 127 | #expect(record.s3.object.eTag == "91a7f2c3ae81bcc6afef83979b463f0e") 128 | #expect(record.s3.object.sequencer == "005E1C37948E783A6E") 129 | } 130 | 131 | @Test func objectRemovedEvent() throws { 132 | let data = S3Tests.eventBodyObjectRemoved.data(using: .utf8)! 133 | let event = try JSONDecoder().decode(S3Event.self, from: data) 134 | let record = try #require(event.records.first) 135 | 136 | #expect(record.eventVersion == "2.1") 137 | #expect(record.eventSource == "aws:s3") 138 | #expect(record.awsRegion == .eu_central_1) 139 | #expect(record.eventName == "ObjectRemoved:DeleteMarkerCreated") 140 | #expect(record.eventTime.ISO8601Format() == Date(timeIntervalSince1970: 1_578_907_540.621).ISO8601Format()) 141 | // see https://github.com/swiftlang/swift-foundation/issues/1561#issuecomment-3448853449 142 | #expect(abs(record.eventTime.timeIntervalSince1970 - 1_578_907_540.621) < 0.0005) 143 | #expect(record.userIdentity == S3Event.UserIdentity(principalId: "AWS:AAAAAAAJ2MQ4YFQZ7AULJ")) 144 | #expect(record.requestParameters == S3Event.RequestParameters(sourceIPAddress: "123.123.123.123")) 145 | #expect(record.responseElements.count == 2) 146 | #expect(record.s3.schemaVersion == "1.0") 147 | #expect(record.s3.configurationId == "98b55bc4-3c0c-4007-b727-c6b77a259dde") 148 | #expect(record.s3.bucket.name == "eventsources") 149 | #expect(record.s3.bucket.ownerIdentity == S3Event.UserIdentity(principalId: "AAAAAAAAAAAAAA")) 150 | #expect(record.s3.bucket.arn == "arn:aws:s3:::eventsources") 151 | #expect(record.s3.object.key == "Hi.md") 152 | #expect(record.s3.object.size == nil) 153 | #expect(record.s3.object.eTag == "91a7f2c3ae81bcc6afef83979b463f0e") 154 | #expect(record.s3.object.sequencer == "005E1C37948E783A6E") 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/APIGateway+V2IAMTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Foundation 17 | import Testing 18 | 19 | @testable import AWSLambdaEvents 20 | 21 | @Suite 22 | struct APIGatewayV2IAMTests { 23 | static let getEventWithIAM = """ 24 | { 25 | "version": "2.0", 26 | "routeKey": "$default", 27 | "rawPath": "/hello", 28 | "rawQueryString": "", 29 | "headers": { 30 | "accept": "*/*", 31 | "authorization": "AWS4-HMAC-SHA256 Credential=ASIA-redacted/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=289b5fcef3d1156f019cc1140cb5565cc052880a5a0d5586c753e3e3c75556f9", 32 | "content-length": "0", 33 | "host": "74bxj8iqjc.execute-api.us-east-1.amazonaws.com", 34 | "user-agent": "curl/8.4.0", 35 | "x-amz-date": "20231214T203121Z", 36 | "x-amz-security-token": "IQoJb3JpZ2luX2VjEO3//////////-redacted", 37 | "x-amzn-trace-id": "Root=1-657b6619-3222de40051925dd66e1fd72", 38 | "x-forwarded-for": "191.95.150.52", 39 | "x-forwarded-port": "443", 40 | "x-forwarded-proto": "https" 41 | }, 42 | "requestContext": { 43 | "accountId": "012345678912", 44 | "apiId": "74bxj8iqjc", 45 | "authorizer": { 46 | "iam": { 47 | "accessKey": "ASIA-redacted", 48 | "accountId": "012345678912", 49 | "callerId": "AIDA-redacted", 50 | "cognitoIdentity": null, 51 | "principalOrgId": "aws:PrincipalOrgID", 52 | "userArn": "arn:aws:iam::012345678912:user/sst", 53 | "userId": "AIDA-redacted" 54 | } 55 | }, 56 | "domainName": "74bxj8iqjc.execute-api.us-east-1.amazonaws.com", 57 | "domainPrefix": "74bxj8iqjc", 58 | "http": { 59 | "method": "GET", 60 | "path": "/liveness", 61 | "protocol": "HTTP/1.1", 62 | "sourceIp": "191.95.150.52", 63 | "userAgent": "curl/8.4.0" 64 | }, 65 | "requestId": "P8zkDiQ8oAMEJsQ=", 66 | "routeKey": "$default", 67 | "stage": "$default", 68 | "time": "14/Dec/2023:20:31:21 +0000", 69 | "timeEpoch": 1702585881671 70 | }, 71 | "isBase64Encoded": false 72 | } 73 | """ 74 | 75 | static let getEventWithIAMAndCognito = """ 76 | { 77 | "version": "2.0", 78 | "routeKey": "$default", 79 | "rawPath": "/hello", 80 | "rawQueryString": "", 81 | "headers": { 82 | "accept": "*/*", 83 | "authorization": "AWS4-HMAC-SHA256 Credential=ASIA-redacted/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=289b5fcef3d1156f019cc1140cb5565cc052880a5a0d5586c753e3e3c75556f9", 84 | "content-length": "0", 85 | "host": "74bxj8iqjc.execute-api.us-east-1.amazonaws.com", 86 | "user-agent": "curl/8.4.0", 87 | "x-amz-date": "20231214T203121Z", 88 | "x-amz-security-token": "IQoJb3JpZ2luX2VjEO3//////////-redacted", 89 | "x-amzn-trace-id": "Root=1-657b6619-3222de40051925dd66e1fd72", 90 | "x-forwarded-for": "191.95.150.52", 91 | "x-forwarded-port": "443", 92 | "x-forwarded-proto": "https" 93 | }, 94 | "requestContext": { 95 | "accountId": "012345678912", 96 | "apiId": "74bxj8iqjc", 97 | "authorizer": { 98 | "iam": { 99 | "accessKey": "ASIA-redacted", 100 | "accountId": "012345678912", 101 | "callerId": "AROA-redacted:CognitoIdentityCredentials", 102 | "cognitoIdentity": { 103 | "amr": [ 104 | "authenticated", 105 | "cognito-idp.us-east-1.amazonaws.com/us-east-1_ABCD", 106 | "cognito-idp.us-east-1.amazonaws.com/us-east-1_ABCD:CognitoSignIn:04611e3d--redacted" 107 | ], 108 | "identityId": "us-east-1:68bc0ecd-9d5e--redacted", 109 | "identityPoolId": "us-east-1:e8b526df--redacted" 110 | }, 111 | "principalOrgId": "aws:PrincipalOrgID", 112 | "userArn": "arn:aws:sts::012345678912:assumed-role/authRole/CognitoIdentityCredentials", 113 | "userId": "AROA-redacted:CognitoIdentityCredentials" 114 | } 115 | }, 116 | "domainName": "74bxj8iqjc.execute-api.us-east-1.amazonaws.com", 117 | "domainPrefix": "74bxj8iqjc", 118 | "http": { 119 | "method": "GET", 120 | "path": "/liveness", 121 | "protocol": "HTTP/1.1", 122 | "sourceIp": "191.95.150.52", 123 | "userAgent": "curl/8.4.0" 124 | }, 125 | "requestId": "P8zkDiQ8oAMEJsQ=", 126 | "routeKey": "$default", 127 | "stage": "$default", 128 | "time": "14/Dec/2023:20:31:21 +0000", 129 | "timeEpoch": 1702585881671 130 | }, 131 | "isBase64Encoded": false 132 | } 133 | """ 134 | 135 | // MARK: - Request - 136 | 137 | // MARK: Decoding 138 | 139 | @Test func requestDecodingGetRequestWithIAM() throws { 140 | let data = APIGatewayV2IAMTests.getEventWithIAM.data(using: .utf8)! 141 | let req = try JSONDecoder().decode(APIGatewayV2Request.self, from: data) 142 | 143 | #expect(req.rawPath == "/hello") 144 | #expect(req.context.authorizer?.iam?.accessKey == "ASIA-redacted") 145 | #expect(req.context.authorizer?.iam?.accountId == "012345678912") 146 | #expect(req.body == nil) 147 | } 148 | 149 | @Test func requestDecodingGetRequestWithIAMWithCognito() throws { 150 | let data = APIGatewayV2IAMTests.getEventWithIAMAndCognito.data(using: .utf8)! 151 | let req = try JSONDecoder().decode(APIGatewayV2Request.self, from: data) 152 | 153 | #expect(req.rawPath == "/hello") 154 | #expect(req.context.authorizer?.iam?.accessKey == "ASIA-redacted") 155 | #expect(req.context.authorizer?.iam?.accountId == "012345678912") 156 | 157 | // test the cognito identity part 158 | #expect(req.context.authorizer?.iam?.cognitoIdentity?.identityId == "us-east-1:68bc0ecd-9d5e--redacted") 159 | #expect(req.context.authorizer?.iam?.cognitoIdentity?.amr?.count == 3) 160 | 161 | #expect(req.body == nil) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/CloudFormationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Testing 17 | 18 | @testable import AWSLambdaEvents 19 | 20 | #if canImport(FoundationEssentials) 21 | import FoundationEssentials 22 | #else 23 | import Foundation 24 | #endif 25 | 26 | @Suite 27 | struct CloudFormationTests { 28 | struct TestResourceProperties: Codable { 29 | let property1: String 30 | let property2: String 31 | let property3: [String] 32 | let property4: String? 33 | } 34 | 35 | struct EmptyTestResourceProperties: Codable {} 36 | 37 | static func eventBodyRequestRequiredFields() -> String { 38 | """ 39 | { 40 | "RequestType": "Create", 41 | "RequestId": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 42 | "StackId": "arn:aws:cloudformation:us-east-1:123456789:stack/TestStack", 43 | "ResponseURL": "http://localhost:7000/response/test", 44 | "LogicalResourceId": "TestLogicalResource" 45 | } 46 | """ 47 | } 48 | 49 | static func eventBodyRequestCreate() -> String { 50 | """ 51 | { 52 | "RequestType": "Create", 53 | "RequestId": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 54 | "StackId": "arn:aws:cloudformation:us-east-1:123456789:stack/TestStack", 55 | "ResponseURL": "http://localhost:7000/response/test", 56 | "LogicalResourceId": "TestLogicalResource", 57 | "PhysicalResourceId": "TestPhysicalResource", 58 | "ResourceProperties": { 59 | "property1": "value1", 60 | "property2": "", 61 | "property3": ["1", "2", "3"], 62 | "property4": null, 63 | } 64 | } 65 | """ 66 | } 67 | 68 | static func eventBodyRequestUpdate() -> String { 69 | """ 70 | { 71 | "RequestType": "Update", 72 | "RequestId": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 73 | "StackId": "arn:aws:cloudformation:us-east-1:123456789:stack/TestStack", 74 | "ResponseURL": "http://localhost:7000/response/test", 75 | "LogicalResourceId": "TestLogicalResource", 76 | "PhysicalResourceId": "TestPhysicalResource", 77 | "ResourceProperties": { 78 | "property1": "value1", 79 | "property2": "value2", 80 | "property3": ["1", "2", "3"], 81 | "property4": "value4", 82 | }, 83 | "OldResourceProperties": { 84 | "property1": "value1", 85 | "property2": "", 86 | "property3": ["1", "2", "3"], 87 | "property4": null, 88 | } 89 | } 90 | """ 91 | } 92 | 93 | static func eventBodyResponse() -> String { 94 | "{\"Data\":{\"property1\":\"value1\",\"property2\":\"\",\"property3\":[\"1\",\"2\",\"3\"]},\"LogicalResourceId\":\"TestLogicalResource\",\"NoEcho\":false,\"PhysicalResourceId\":\"TestPhysicalResource\",\"Reason\":\"See the details in CloudWatch Log Stream\",\"RequestId\":\"cdc73f9d-aea9-11e3-9d5a-835b769c0d9c\",\"StackId\":\"arn:aws:cloudformation:us-east-1:123456789:stack\\/TestStack\",\"Status\":\"SUCCESS\"}" 95 | } 96 | 97 | @Test func decodeRequestRequiredFieldsFromJSON() throws { 98 | let eventBody = CloudFormationTests.eventBodyRequestRequiredFields() 99 | let data = eventBody.data(using: .utf8)! 100 | let event: CloudFormation.Request? = try JSONDecoder() 101 | .decode(CloudFormation.Request.self, from: data) 102 | 103 | guard let event else { 104 | Issue.record("Expected to have an event") 105 | return 106 | } 107 | 108 | #expect(event.requestId == "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") 109 | #expect(event.requestType == CloudFormation.Request.RequestType.create) 110 | #expect(event.stackId == "arn:aws:cloudformation:us-east-1:123456789:stack/TestStack") 111 | #expect(event.responseURL == "http://localhost:7000/response/test") 112 | #expect(event.logicalResourceId == "TestLogicalResource") 113 | #expect(event.physicalResourceId == nil) 114 | #expect(event.resourceProperties == nil) 115 | #expect(event.oldResourceProperties == nil) 116 | } 117 | 118 | @Test func decodeRequestCreateFromJSON() throws { 119 | let eventBody = CloudFormationTests.eventBodyRequestCreate() 120 | let data = eventBody.data(using: .utf8)! 121 | let event: CloudFormation.Request? = try? JSONDecoder() 122 | .decode(CloudFormation.Request.self, from: data) 123 | 124 | guard let event else { 125 | Issue.record("Expected to have an event") 126 | return 127 | } 128 | 129 | #expect(event.requestId == "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") 130 | #expect(event.requestType == CloudFormation.Request.RequestType.create) 131 | #expect(event.stackId == "arn:aws:cloudformation:us-east-1:123456789:stack/TestStack") 132 | #expect(event.responseURL == "http://localhost:7000/response/test") 133 | #expect(event.logicalResourceId == "TestLogicalResource") 134 | #expect(event.physicalResourceId == "TestPhysicalResource") 135 | #expect(event.resourceProperties?.property1 == "value1") 136 | #expect(event.resourceProperties?.property2 == "") 137 | #expect(event.resourceProperties?.property3 == ["1", "2", "3"]) 138 | #expect(event.resourceProperties?.property4 == nil) 139 | #expect(event.oldResourceProperties == nil) 140 | } 141 | 142 | @Test func decodeRequestUpdateFromJSON() throws { 143 | let eventBody = CloudFormationTests.eventBodyRequestUpdate() 144 | let data = eventBody.data(using: .utf8)! 145 | let event: CloudFormation.Request? = try? JSONDecoder().decode( 146 | CloudFormation.Request.self, 147 | from: data 148 | ) 149 | 150 | guard let event else { 151 | Issue.record("Expected to have an event") 152 | return 153 | } 154 | 155 | #expect(event.requestId == "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") 156 | #expect(event.requestType == CloudFormation.Request.RequestType.update) 157 | #expect(event.stackId == "arn:aws:cloudformation:us-east-1:123456789:stack/TestStack") 158 | #expect(event.responseURL == "http://localhost:7000/response/test") 159 | #expect(event.logicalResourceId == "TestLogicalResource") 160 | #expect(event.physicalResourceId == "TestPhysicalResource") 161 | #expect(event.resourceProperties?.property1 == "value1") 162 | #expect(event.resourceProperties?.property2 == "value2") 163 | #expect(event.resourceProperties?.property3 == ["1", "2", "3"]) 164 | #expect(event.resourceProperties?.property4 == "value4") 165 | #expect(event.oldResourceProperties?.property1 == "value1") 166 | #expect(event.oldResourceProperties?.property2 == "") 167 | #expect(event.oldResourceProperties?.property3 == ["1", "2", "3"]) 168 | #expect(event.oldResourceProperties?.property4 == nil) 169 | } 170 | 171 | @Test func encodeResponseToJSON() throws { 172 | let resp = CloudFormation.Response( 173 | status: CloudFormation.Response.StatusType.success, 174 | requestId: "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 175 | logicalResourceId: "TestLogicalResource", 176 | stackId: "arn:aws:cloudformation:us-east-1:123456789:stack/TestStack", 177 | physicalResourceId: "TestPhysicalResource", 178 | reason: "See the details in CloudWatch Log Stream", 179 | noEcho: false, 180 | data: TestResourceProperties( 181 | property1: "value1", 182 | property2: "", 183 | property3: ["1", "2", "3"], 184 | property4: nil 185 | ) 186 | ) 187 | 188 | let encoder = JSONEncoder() 189 | encoder.outputFormatting = [.sortedKeys] 190 | 191 | let data = try #require(try? encoder.encode(resp)) 192 | 193 | var stringData: String? 194 | #expect(throws: Never.self) { 195 | stringData = String(data: data, encoding: .utf8) 196 | } 197 | 198 | #expect(CloudFormationTests.eventBodyResponse() == stringData) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/SQSTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Foundation 17 | import Testing 18 | 19 | @testable import AWSLambdaEvents 20 | 21 | @Suite 22 | struct SQSTests { 23 | static let eventBody = """ 24 | { 25 | "Records": [ 26 | { 27 | "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", 28 | "receiptHandle": "MessageReceiptHandle", 29 | "body": "Hello from SQS!", 30 | "attributes": { 31 | "ApproximateReceiveCount": "1", 32 | "SentTimestamp": "1523232000000", 33 | "SenderId": "123456789012", 34 | "ApproximateFirstReceiveTimestamp": "1523232000001" 35 | }, 36 | "messageAttributes": { 37 | "number":{ 38 | "stringValue":"123", 39 | "stringListValues":[], 40 | "binaryListValues":[], 41 | "dataType":"Number" 42 | }, 43 | "string":{ 44 | "stringValue":"abc123", 45 | "stringListValues":[], 46 | "binaryListValues":[], 47 | "dataType":"String" 48 | }, 49 | "binary":{ 50 | "dataType": "Binary", 51 | "stringListValues":[], 52 | "binaryListValues":[], 53 | "binaryValue":"YmFzZTY0" 54 | }, 55 | 56 | }, 57 | "md5OfBody": "7b270e59b47ff90a553787216d55d91d", 58 | "eventSource": "aws:sqs", 59 | "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", 60 | "awsRegion": "us-east-1" 61 | } 62 | ] 63 | } 64 | """ 65 | 66 | @Test func simpleEventFromJSON() throws { 67 | let data = SQSTests.eventBody.data(using: .utf8)! 68 | let event = try JSONDecoder().decode(SQSEvent.self, from: data) 69 | 70 | guard let message = event.records.first else { 71 | Issue.record("Expected to have one message in the event") 72 | return 73 | } 74 | 75 | #expect(message.messageId == "19dd0b57-b21e-4ac1-bd88-01bbb068cb78") 76 | #expect(message.receiptHandle == "MessageReceiptHandle") 77 | #expect(message.body == "Hello from SQS!") 78 | #expect(message.attributes.count == 4) 79 | 80 | #expect( 81 | message.messageAttributes == [ 82 | "number": .number("123"), 83 | "string": .string("abc123"), 84 | "binary": .binary([UInt8]("base64".utf8)), 85 | ] 86 | ) 87 | #expect(message.md5OfBody == "7b270e59b47ff90a553787216d55d91d") 88 | #expect(message.eventSource == "aws:sqs") 89 | #expect(message.eventSourceArn == "arn:aws:sqs:us-east-1:123456789012:MyQueue") 90 | #expect(message.awsRegion == .us_east_1) 91 | } 92 | 93 | // MARK: Codable Helpers Tests 94 | 95 | @Test func decodeBodyForSingleMessage() throws { 96 | struct TestPayload: Codable, Equatable { 97 | let message: String 98 | let count: Int 99 | } 100 | 101 | let testPayload = TestPayload(message: "test", count: 42) 102 | 103 | let eventBodyWithJSON = """ 104 | { 105 | "Records": [ 106 | { 107 | "messageId": "test-message-id", 108 | "receiptHandle": "test-receipt-handle", 109 | "body": "{\\"message\\":\\"test\\",\\"count\\":42}", 110 | "attributes": { 111 | "ApproximateReceiveCount": "1", 112 | "SentTimestamp": "1523232000000", 113 | "SenderId": "123456789012", 114 | "ApproximateFirstReceiveTimestamp": "1523232000001" 115 | }, 116 | "messageAttributes": {}, 117 | "md5OfBody": "test-md5", 118 | "eventSource": "aws:sqs", 119 | "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:TestQueue", 120 | "awsRegion": "us-east-1" 121 | } 122 | ] 123 | } 124 | """ 125 | 126 | let data = eventBodyWithJSON.data(using: .utf8)! 127 | let event = try JSONDecoder().decode(SQSEvent.self, from: data) 128 | 129 | guard let message = event.records.first else { 130 | Issue.record("Expected to have one message in the event") 131 | return 132 | } 133 | 134 | let decodedPayload = try message.decodeBody(TestPayload.self) 135 | #expect(decodedPayload == testPayload) 136 | } 137 | 138 | @Test func decodeBodyForSQSEvent() throws { 139 | struct TestPayload: Codable, Equatable { 140 | let message: String 141 | let count: Int 142 | } 143 | 144 | let testPayload1 = TestPayload(message: "test1", count: 42) 145 | let testPayload2 = TestPayload(message: "test2", count: 84) 146 | 147 | let eventBodyWithMultipleRecords = """ 148 | { 149 | "Records": [ 150 | { 151 | "messageId": "test-message-id-1", 152 | "receiptHandle": "test-receipt-handle-1", 153 | "body": "{\\"message\\":\\"test1\\",\\"count\\":42}", 154 | "attributes": { 155 | "ApproximateReceiveCount": "1", 156 | "SentTimestamp": "1523232000000", 157 | "SenderId": "123456789012", 158 | "ApproximateFirstReceiveTimestamp": "1523232000001" 159 | }, 160 | "messageAttributes": {}, 161 | "md5OfBody": "test-md5-1", 162 | "eventSource": "aws:sqs", 163 | "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:TestQueue", 164 | "awsRegion": "us-east-1" 165 | }, 166 | { 167 | "messageId": "test-message-id-2", 168 | "receiptHandle": "test-receipt-handle-2", 169 | "body": "{\\"message\\":\\"test2\\",\\"count\\":84}", 170 | "attributes": { 171 | "ApproximateReceiveCount": "1", 172 | "SentTimestamp": "1523232000000", 173 | "SenderId": "123456789012", 174 | "ApproximateFirstReceiveTimestamp": "1523232000001" 175 | }, 176 | "messageAttributes": {}, 177 | "md5OfBody": "test-md5-2", 178 | "eventSource": "aws:sqs", 179 | "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:TestQueue", 180 | "awsRegion": "us-east-1" 181 | } 182 | ] 183 | } 184 | """ 185 | 186 | let data = eventBodyWithMultipleRecords.data(using: .utf8)! 187 | let event = try JSONDecoder().decode(SQSEvent.self, from: data) 188 | 189 | let decodedPayloads = try event.decodeBody(TestPayload.self) 190 | #expect(decodedPayloads.count == 2) 191 | #expect(decodedPayloads[0] == testPayload1) 192 | #expect(decodedPayloads[1] == testPayload2) 193 | } 194 | 195 | @Test func decodeBodyWithCustomDecoder() throws { 196 | struct TestPayload: Codable, Equatable { 197 | let messageText: String 198 | let count: Int 199 | 200 | enum CodingKeys: String, CodingKey { 201 | case messageText = "message_text" 202 | case count 203 | } 204 | } 205 | 206 | let testPayload = TestPayload(messageText: "test", count: 42) 207 | 208 | // We need to create a decoder that can handle the explicit coding keys 209 | 210 | let eventBodyWithSnakeCase = """ 211 | { 212 | "Records": [ 213 | { 214 | "messageId": "test-message-id", 215 | "receiptHandle": "test-receipt-handle", 216 | "body": "{\\"message_text\\":\\"test\\",\\"count\\":42}", 217 | "attributes": { 218 | "ApproximateReceiveCount": "1", 219 | "SentTimestamp": "1523232000000", 220 | "SenderId": "123456789012", 221 | "ApproximateFirstReceiveTimestamp": "1523232000001" 222 | }, 223 | "messageAttributes": {}, 224 | "md5OfBody": "test-md5", 225 | "eventSource": "aws:sqs", 226 | "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:TestQueue", 227 | "awsRegion": "us-east-1" 228 | } 229 | ] 230 | } 231 | """ 232 | 233 | let data = eventBodyWithSnakeCase.data(using: .utf8)! 234 | let event = try JSONDecoder().decode(SQSEvent.self, from: data) 235 | 236 | let decodedPayloads = try event.decodeBody(TestPayload.self) 237 | #expect(decodedPayloads.count == 1) 238 | #expect(decodedPayloads[0] == testPayload) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Sources/AWSLambdaEvents/Utils/Base64.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | //===----------------------------------------------------------------------===// 17 | // This is a vendored version from: 18 | // https://github.com/fabianfett/swift-base64-kit 19 | 20 | struct Base64 {} 21 | 22 | // MARK: - Decode - 23 | 24 | extension Base64 { 25 | struct DecodingOptions: OptionSet { 26 | let rawValue: UInt 27 | init(rawValue: UInt) { self.rawValue = rawValue } 28 | 29 | static let base64UrlAlphabet = DecodingOptions(rawValue: UInt(1 << 0)) 30 | } 31 | 32 | enum DecodingError: Error, Equatable { 33 | case invalidLength 34 | case invalidCharacter(UInt8) 35 | case unexpectedPaddingCharacter 36 | case unexpectedEnd 37 | } 38 | 39 | @inlinable 40 | static func decode( 41 | encoded: Buffer, 42 | options: DecodingOptions = [] 43 | ) throws -> [UInt8] where Buffer.Element == UInt8 { 44 | let alphabet = 45 | options.contains(.base64UrlAlphabet) 46 | ? Base64.urlAlphabet 47 | : Base64.defaultAlphabet 48 | 49 | // In Base64 4 encoded bytes, become 3 decoded bytes. We pad to the 50 | // nearest multiple of three. 51 | let inputLength = encoded.count 52 | guard inputLength > 0 else { return [] } 53 | guard inputLength % 4 == 0 else { 54 | throw DecodingError.invalidLength 55 | } 56 | 57 | let inputBlocks = (inputLength + 3) / 4 58 | let fullQualified = inputBlocks - 1 59 | let outputLength = ((encoded.count + 3) / 4) * 3 60 | var iterator = encoded.makeIterator() 61 | var outputBytes = [UInt8]() 62 | outputBytes.reserveCapacity(outputLength) 63 | 64 | // fast loop. we don't expect any padding in here. 65 | for _ in 0..> 4)) 72 | outputBytes.append((secondValue << 4) | (thirdValue >> 2)) 73 | outputBytes.append((thirdValue << 6) | forthValue) 74 | } 75 | 76 | // last 4 bytes. we expect padding characters in three and four 77 | let firstValue: UInt8 = try iterator.nextValue(alphabet: alphabet) 78 | let secondValue: UInt8 = try iterator.nextValue(alphabet: alphabet) 79 | let thirdValue: UInt8? = try iterator.nextValueOrEmpty(alphabet: alphabet) 80 | let forthValue: UInt8? = try iterator.nextValueOrEmpty(alphabet: alphabet) 81 | 82 | outputBytes.append((firstValue << 2) | (secondValue >> 4)) 83 | if let thirdValue = thirdValue { 84 | outputBytes.append((secondValue << 4) | (thirdValue >> 2)) 85 | 86 | if let forthValue = forthValue { 87 | outputBytes.append((thirdValue << 6) | forthValue) 88 | } 89 | } 90 | 91 | return outputBytes 92 | } 93 | 94 | @inlinable 95 | static func decode(encoded: String, options: DecodingOptions = []) throws -> [UInt8] { 96 | // A string can be backed by a contiguous storage (pure swift string) 97 | // or a nsstring (bridged string from objc). We only get a pointer 98 | // to the contiguous storage, if the input string is a swift string. 99 | // Therefore to transform the nsstring backed input into a swift 100 | // string we concat the input with nothing, causing a copy on write 101 | // into a swift string. 102 | let decoded = try encoded.utf8.withContiguousStorageIfAvailable { pointer in 103 | try self.decode(encoded: pointer, options: options) 104 | } 105 | 106 | if decoded != nil { 107 | return decoded! 108 | } 109 | 110 | return try self.decode(encoded: encoded + "", options: options) 111 | } 112 | 113 | // MARK: Internal 114 | 115 | @usableFromInline 116 | static let defaultAlphabet: [UInt8] = [ 117 | // 0 1 2 3 4 5 6 7 8 9 118 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 0 119 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 1 120 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 2 121 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 3 122 | 255, 255, 255, 62, 255, 255, 255, 63, 52, 53, // 4 123 | 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, // 5 124 | 255, 254, 255, 255, 255, 0, 1, 2, 3, 4, // 6 125 | 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 7 126 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 8 127 | 25, 255, 255, 255, 255, 255, 255, 26, 27, 28, // 9 128 | 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // 10 129 | 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, // 11 130 | 49, 50, 51, 255, 255, 255, 255, 255, 255, 255, // 12 131 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 13 132 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 14 133 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 15 134 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 16 135 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 17 136 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 18 137 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 19 138 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 20 139 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 21 140 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 22 141 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 23 142 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 24 143 | 255, 255, 255, 255, 255, // 25 144 | ] 145 | 146 | @usableFromInline 147 | static let urlAlphabet: [UInt8] = [ 148 | // 0 1 2 3 4 5 6 7 8 9 149 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 0 150 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 1 151 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 2 152 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 3 153 | 255, 255, 255, 255, 255, 62, 255, 255, 52, 53, // 4 154 | 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, // 5 155 | 255, 254, 255, 255, 255, 0, 1, 2, 3, 4, // 6 156 | 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 7 157 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 8 158 | 25, 255, 255, 255, 255, 63, 255, 26, 27, 28, // 9 159 | 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // 10 160 | 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, // 11 161 | 49, 50, 51, 255, 255, 255, 255, 255, 255, 255, // 12 162 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 13 163 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 14 164 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 15 165 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 16 166 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 17 167 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 18 168 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 19 169 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 20 170 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 21 171 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 22 172 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 23 173 | 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 24 174 | 255, 255, 255, 255, 255, // 25 175 | ] 176 | 177 | @usableFromInline 178 | static let paddingCharacter: UInt8 = 254 179 | } 180 | 181 | extension IteratorProtocol where Self.Element == UInt8 { 182 | mutating func nextValue(alphabet: [UInt8]) throws -> UInt8 { 183 | let ascii = self.next()! 184 | 185 | let value = alphabet[Int(ascii)] 186 | 187 | if value < 64 { 188 | return value 189 | } 190 | 191 | if value == Base64.paddingCharacter { 192 | throw Base64.DecodingError.unexpectedPaddingCharacter 193 | } 194 | 195 | throw Base64.DecodingError.invalidCharacter(ascii) 196 | } 197 | 198 | mutating func nextValueOrEmpty(alphabet: [UInt8]) throws -> UInt8? { 199 | let ascii = self.next()! 200 | 201 | let value = alphabet[Int(ascii)] 202 | 203 | if value < 64 { 204 | return value 205 | } 206 | 207 | if value == Base64.paddingCharacter { 208 | return nil 209 | } 210 | 211 | throw Base64.DecodingError.invalidCharacter(ascii) 212 | } 213 | } 214 | 215 | // MARK: - Extensions - 216 | 217 | extension String { 218 | func base64decoded(options: Base64.DecodingOptions = []) throws -> [UInt8] { 219 | // In Base64, 3 bytes become 4 output characters, and we pad to the nearest multiple 220 | // of four. 221 | try Base64.decode(encoded: self, options: options) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /Tests/AWSLambdaEventsTests/APIGatewayLambdaAuthorizerTest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftAWSLambdaRuntime open source project 4 | // 5 | // Copyright SwiftAWSLambdaRuntime project authors 6 | // Copyright (c)Amazon.com, Inc. or its affiliates. 7 | // Licensed under Apache License v2.0 8 | // 9 | // See LICENSE.txt for license information 10 | // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import Foundation 17 | import Testing 18 | 19 | @testable import AWSLambdaEvents 20 | 21 | @Suite 22 | struct APIGatewayLambdaAuthorizerTests { 23 | static let getEventWithLambdaAuthorizer = """ 24 | { 25 | "version": "2.0", 26 | "routeKey": "$default", 27 | "rawPath": "/hello", 28 | "rawQueryString": "", 29 | "headers": { 30 | "accept": "*/*", 31 | "authorization": "AWS4-HMAC-SHA256 Credential=ASIA-redacted/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=289b5fcef3d1156f019cc1140cb5565cc052880a5a0d5586c753e3e3c75556f9", 32 | "content-length": "0", 33 | "host": "74bxj8iqjc.execute-api.us-east-1.amazonaws.com", 34 | "user-agent": "curl/8.4.0", 35 | "x-amz-date": "20231214T203121Z", 36 | "x-amz-security-token": "IQoJb3JpZ2luX2VjEO3//////////-redacted", 37 | "x-amzn-trace-id": "Root=1-657b6619-3222de40051925dd66e1fd72", 38 | "x-forwarded-for": "191.95.150.52", 39 | "x-forwarded-port": "443", 40 | "x-forwarded-proto": "https" 41 | }, 42 | "requestContext": { 43 | "accountId": "012345678912", 44 | "apiId": "74bxj8iqjc", 45 | "authorizer": { 46 | "lambda": { 47 | "abc1": "xyz1", 48 | "abc2": "xyz2", 49 | } 50 | }, 51 | "domainName": "74bxj8iqjc.execute-api.us-east-1.amazonaws.com", 52 | "domainPrefix": "74bxj8iqjc", 53 | "http": { 54 | "method": "GET", 55 | "path": "/liveness", 56 | "protocol": "HTTP/1.1", 57 | "sourceIp": "191.95.150.52", 58 | "userAgent": "curl/8.4.0" 59 | }, 60 | "requestId": "P8zkDiQ8oAMEJsQ=", 61 | "routeKey": "$default", 62 | "stage": "$default", 63 | "time": "14/Dec/2023:20:31:21 +0000", 64 | "timeEpoch": 1702585881671 65 | }, 66 | "isBase64Encoded": false 67 | } 68 | """ 69 | 70 | static let lambdaAuthorizerRequest = """ 71 | { 72 | "version": "2.0", 73 | "type": "REQUEST", 74 | "routeArn": "arn:aws:execute-api:eu-north-1:000000000000:0000000000/dev/GET/applications", 75 | "identitySource": [ 76 | "abc.xyz.123" 77 | ], 78 | "routeKey": "GET /applications", 79 | "rawPath": "/dev/applications", 80 | "rawQueryString": "", 81 | "headers": { 82 | "accept": "*/*", 83 | "authorization": "abc.xyz.123", 84 | "content-length": "0", 85 | "host": "0000000000.execute-api.eu-north-1.amazonaws.com", 86 | "user-agent": "curl/8.1.2", 87 | "x-amzn-trace-id": "Root=1-00000000-000000000000000000000000", 88 | "x-forwarded-for": "0.0.0.0", 89 | "x-forwarded-port": "443", 90 | "x-forwarded-proto": "https" 91 | }, 92 | "requestContext": { 93 | "accountId": "000000000000", 94 | "apiId": "0000000000", 95 | "domainName": "0000000000.execute-api.eu-north-1.amazonaws.com", 96 | "domainPrefix": "0000000000", 97 | "http": { 98 | "method": "GET", 99 | "path": "/dev/applications", 100 | "protocol": "HTTP/1.1", 101 | "sourceIp": "0.0.0.0", 102 | "userAgent": "curl/8.1.2" 103 | }, 104 | "requestId": "QHACgr8sig0MELg=", 105 | "routeKey": "GET /applications", 106 | "stage": "dev", 107 | "time": "15/Dec/2023:20:35:03 +0000", 108 | "timeEpoch": 1702672503230 109 | } 110 | } 111 | """ 112 | 113 | static let lambdaAuthorizerSimpleResponse = """ 114 | { 115 | "isAuthorized": true, 116 | "context": { 117 | "exampleKey": "exampleValue" 118 | } 119 | } 120 | """ 121 | 122 | static let lambdaAuthorizerPolicyResponse = """ 123 | { 124 | "principalId": "abcdef", 125 | "policyDocument": { 126 | "Version": "2012-10-17", 127 | "Statement": [ 128 | { 129 | "Action": "execute-api:Invoke", 130 | "Effect": "Allow|Deny", 131 | "Resource": "arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]" 132 | } 133 | ] 134 | }, 135 | "context": { 136 | "exampleKey": "exampleValue" 137 | } 138 | } 139 | """ 140 | 141 | // MARK: - Request - 142 | 143 | // MARK: Decoding 144 | 145 | @Test func requestDecodingGetRequestWithLambdaAuthorizer() throws { 146 | let data = APIGatewayLambdaAuthorizerTests.getEventWithLambdaAuthorizer.data(using: .utf8)! 147 | let req = try JSONDecoder().decode(APIGatewayV2Request.self, from: data) 148 | 149 | #expect(req.rawPath == "/hello") 150 | #expect(req.context.authorizer?.lambda?.count == 2) 151 | #expect(req.context.authorizer?.lambda?["abc1"] == "xyz1") 152 | #expect(req.context.authorizer?.lambda?["abc2"] == "xyz2") 153 | #expect(req.body == nil) 154 | } 155 | 156 | @Test func lambdaAuthorizerRequestRequestDecoding() throws { 157 | let data = APIGatewayLambdaAuthorizerTests.lambdaAuthorizerRequest.data(using: .utf8)! 158 | let req = try JSONDecoder().decode(APIGatewayLambdaAuthorizerRequest.self, from: data) 159 | 160 | #expect(req.rawPath == "/dev/applications") 161 | #expect(req.version == "2.0") 162 | } 163 | 164 | // MARK: Encoding 165 | 166 | @Test func decodingLambdaAuthorizerSimpleResponse() throws { 167 | var resp = APIGatewayLambdaAuthorizerSimpleResponse( 168 | isAuthorized: true, 169 | context: ["abc1": "xyz1", "abc2": "xyz2"] 170 | ) 171 | 172 | let data = try #require(try? JSONEncoder().encode(resp)) 173 | let stringData = try #require(String(data: data, encoding: .utf8)) 174 | let newData = try #require(stringData.data(using: .utf8)) 175 | 176 | resp = try JSONDecoder().decode(APIGatewayLambdaAuthorizerSimpleResponse.self, from: newData) 177 | 178 | #expect(resp.isAuthorized == true) 179 | #expect(resp.context?.count == 2) 180 | #expect(resp.context?["abc1"] == "xyz1") 181 | } 182 | 183 | @Test func decodingLambdaAuthorizerPolicyResponse() throws { 184 | let statement = APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument.Statement( 185 | action: "s3:getObject", 186 | effect: .allow, 187 | resource: "*" 188 | ) 189 | let policy = APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument(statement: [statement]) 190 | var resp = APIGatewayLambdaAuthorizerPolicyResponse( 191 | principalId: "John Appleseed", 192 | policyDocument: policy, 193 | context: ["abc1": "xyz1", "abc2": "xyz2"] 194 | ) 195 | 196 | let data = try #require(try? JSONEncoder().encode(resp)) 197 | let stringData = try #require(String(data: data, encoding: .utf8)) 198 | let newData = try #require(stringData.data(using: .utf8)) 199 | 200 | resp = try JSONDecoder().decode(APIGatewayLambdaAuthorizerPolicyResponse.self, from: newData) 201 | 202 | #expect(resp.principalId == "John Appleseed") 203 | #expect(resp.policyDocument.statement.count == 1) 204 | #expect(resp.policyDocument.statement[0].action == ["s3:getObject"]) 205 | #expect(resp.context?.count == 2) 206 | #expect(resp.context?["abc1"] == "xyz1") 207 | } 208 | 209 | @Test func decodingLambdaAuthorizerPolicyResponseWithMultipleResources() throws { 210 | let statement = APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument.Statement( 211 | action: ["execute-api:Invoke"], 212 | effect: .allow, 213 | resource: [ 214 | "arn:aws:execute-api:*:*:*/*/GET/v1/user/0123", 215 | "arn:aws:execute-api:*:*:*/*/POST/v1/user", 216 | ] 217 | ) 218 | let policy = APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument(statement: [statement]) 219 | var resp = APIGatewayLambdaAuthorizerPolicyResponse( 220 | principalId: "John Appleseed", 221 | policyDocument: policy, 222 | context: ["abc1": "xyz1", "abc2": "xyz2"] 223 | ) 224 | 225 | let data = try #require(try? JSONEncoder().encode(resp)) 226 | let stringData = try #require(String(data: data, encoding: .utf8)) 227 | let newData = try #require(stringData.data(using: .utf8)) 228 | 229 | resp = try JSONDecoder().decode(APIGatewayLambdaAuthorizerPolicyResponse.self, from: newData) 230 | 231 | #expect(resp.principalId == "John Appleseed") 232 | #expect(resp.policyDocument.statement.count == 1) 233 | #expect(resp.policyDocument.statement[0].action == ["execute-api:Invoke"]) 234 | #expect( 235 | resp.policyDocument.statement[0].resource == [ 236 | "arn:aws:execute-api:*:*:*/*/GET/v1/user/0123", 237 | "arn:aws:execute-api:*:*:*/*/POST/v1/user", 238 | ] 239 | ) 240 | #expect(resp.context?.count == 2) 241 | #expect(resp.context?["abc1"] == "xyz1") 242 | } 243 | } 244 | --------------------------------------------------------------------------------