diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..068475e9 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +{ + "name": "Swift", + "image": "swift:6.0", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "false", + "username": "vscode", + "upgradePackages": "false" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "os-provided", + "ppa": "false" + } + }, + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.library": "/usr/lib/liblldb.so" + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "sswg.swift-lang" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 1c2f5167..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,20 +0,0 @@ -### Expected behavior -_[what you expected to happen]_ - -### Actual behavior -_[what actually happened]_ - -### Steps to reproduce - -1. ... -2. ... - -### If possible, minimal yet complete reproducer code (or URL to code) - -_[anything to help us reproducing the issue]_ - -### SwiftAWSLambdaRuntime version/commit hash - -_[the SwiftAWSLambdaRuntime tag/commit hash]_ - -### Swift & OS version (output of `swift --version && uname -a`) diff --git a/.github/ISSUE_TEMPLATE/issue-report.yml b/.github/ISSUE_TEMPLATE/issue-report.yml new file mode 100644 index 00000000..ae6e0ba3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-report.yml @@ -0,0 +1,71 @@ +name: Swift AWS Lambda SDK issue +description: File an issue report with the usage of the Swift AWS Lambda Runtime +body: + - type: markdown + attributes: + value: "Thanks for taking the time to fill out this issue report" + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: What you expected to happen? + placeholder: Describe with your own words the expected result + validations: + required: true + - type: textarea + id: what-happend + attributes: + label: Actual behavior + description: What actually happened + placeholder: Describe + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce + description: List the steps followed to reproduce the behaviour you are reporting + placeholder: | + 1. First I... + 2. Then... + 3. Finally... + validations: + required: true + - type: textarea + id: code-snippet + attributes: + label: If possible, minimal yet complete reproducer code (or URL to code) + description: List the steps followed to reproduce the behaviour you are reporting + placeholder: | + You can add any relevant code snippet that you consider or an URL to code. + + URL could be a link to a GitHub Gist, for example + validations: + required: false + - type: input + id: swift-aws-lambda-runtime-version + attributes: + label: What version of this project (`swift-aws-lambda-runtime`) are you using? + description: The release, branch or commit hash related with this issue. + placeholder: 1.0.0-alpha.1 + validations: + required: true + - type: textarea + id: swift-version + attributes: + label: Swift version + description: Swift environment version. + placeholder: | + Open a Terminal and execute the following command + + swift --version && uname -a + validations: + required: true + - type: input + id: amazon-linux-2-version + attributes: + label: Amazon Linux 2 docker image version + description: The docker image tag used to archive the lambda, if available. + placeholder: 5.7.3-amazonlinux2 + validations: + required: false diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..e29eb846 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,14 @@ +changelog: + categories: + - title: SemVer Major + labels: + - ⚠️ semver/major + - title: SemVer Minor + labels: + - 🆕 semver/minor + - title: SemVer Patch + labels: + - 🔨 semver/patch + - title: Other Changes + labels: + - semver/none diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml new file mode 100644 index 00000000..422ef82e --- /dev/null +++ b/.github/workflows/integration_tests.yml @@ -0,0 +1,136 @@ +name: IntegrationTests + +on: + workflow_call: + inputs: + name: + type: string + description: "The name of the workflow used for the concurrency group." + required: true + # We pass the list of examples here, but we can't pass an array as argument + # Instead, we pass a String with a valid JSON array. + # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 + examples: + type: string + description: "The list of examples to run. Pass a String with a valid JSON array such as \"[ 'HelloWorld', 'APIGateway' ]\"" + required: true + default: "" + examples_enabled: + type: boolean + description: "Boolean to enable the compilation of examples. Defaults to true." + default: true + archive_plugin_examples: + type: string + description: "The list of examples to run through the archive plugin test. Pass a String with a valid JSON array such as \"[ 'HelloWorld', 'APIGateway' ]\"" + required: true + default: "" + archive_plugin_enabled: + type: boolean + description: "Boolean to enable the test of the archive plugin. Defaults to true." + default: true + check_foundation_enabled: + type: boolean + description: "Boolean to enable the check for Foundation dependency. Defaults to true." + default: true + matrix_linux_command: + type: string + description: "The command of the current Swift version linux matrix job to execute." + required: true + matrix_linux_swift_container_image: + type: string + description: "Container image for the matrix job. Defaults to matching latest Swift 6.1 Amazon Linux 2 image." + default: "swiftlang/swift:nightly-6.1-amazonlinux2" + +## We are cancelling previously triggered workflow runs +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.name }} + cancel-in-progress: true + +jobs: + test-examples: + name: Test Examples/${{ matrix.examples }} on ${{ matrix.swift.swift_version }} + if: ${{ inputs.examples_enabled }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + examples: ${{ fromJson(inputs.examples) }} + + # We are using only one Swift version + swift: + - image: ${{ inputs.matrix_linux_swift_container_image }} + container: + image: ${{ matrix.swift.image }} + steps: + # GitHub checkout action has a dep on NodeJS 20 which is not running on Amazonlinux2 + # workaround is to manually checkout the repository + # https://github.com/actions/checkout/issues/1487 + - name: Manually Clone repository and checkout PR + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # Clone the repository + git clone https://github.com/${{ github.repository }} + cd ${{ github.event.repository.name }} + + # Fetch the pull request + git fetch origin +refs/pull/$PR_NUMBER/merge: + + # Checkout the pull request + git checkout -qf FETCH_HEAD + + # - name: Checkout repository + # uses: actions/checkout@v4 + # with: + # persist-credentials: false + + - name: Mark the workspace as safe + working-directory: ${{ github.event.repository.name }} # until we can use action/checkout@v4 + # https://github.com/actions/checkout/issues/766 + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + + - name: Run matrix job + working-directory: ${{ github.event.repository.name }} # until we can use action/checkout@v4 + env: + COMMAND: ${{ inputs.matrix_linux_command }} + EXAMPLE: ${{ matrix.examples }} + run: | + .github/workflows/scripts/integration_tests.sh + + test-archive-plugin: + name: Test archive plugin + if: ${{ inputs.archive_plugin_enabled }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + examples: ${{ fromJson(inputs.archive_plugin_examples) }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Mark the workspace as safe + # https://github.com/actions/checkout/issues/766 + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + - name: Test the archive plugin + env: + EXAMPLE: ${{ matrix.examples }} + run: | + .github/workflows/scripts/check-archive-plugin.sh + + check-foundation: + name: No dependencies on Foundation + if: ${{ inputs.check_foundation_enabled }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Mark the workspace as safe + # https://github.com/actions/checkout/issues/766 + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + - name: Check for Foundation or ICU dependency + run: | + .github/workflows/scripts/check-link-foundation.sh diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..520c539d --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,71 @@ +name: PR + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "SwiftAWSLambdaRuntime" + shell_check_enabled: true + python_lint_check_enabled: true + api_breakage_check_container_image: "swiftlang/swift:nightly-6.1-jammy" + docs_check_container_image: "swift:6.0-noble" + format_check_container_image: "swiftlang/swift:nightly-6.1-jammy" + yamllint_check_enabled: true + + unit-tests: + name: Unit tests + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + with: + linux_5_9_enabled: false + linux_5_10_enabled: false + linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error" + linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" + + integration-tests: + name: Integration Tests + uses: ./.github/workflows/integration_tests.yml + with: + name: "Integration tests" + examples_enabled: true + matrix_linux_command: "LAMBDA_USE_LOCAL_DEPS=../.. swift build" + # We pass the list of examples here, but we can't pass an array as argument + # Instead, we pass a String with a valid JSON array. + # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 + examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Testing', 'Tutorial' ]" + archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" + archive_plugin_enabled: true + + swift-6-language-mode: + name: Swift 6 Language Mode + uses: apple/swift-nio/.github/workflows/swift_6_language_mode.yml@main + + semver-label-check: + name: Semantic Version label check + runs-on: ubuntu-latest + timeout-minutes: 1 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Check for Semantic Version label + uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main + + # until there is a support for musl in swiftlang/github-workflows + # https://github.com/swiftlang/github-workflows/issues/34 + musl: + runs-on: ubuntu-latest + container: swift:6.0.2-noble + timeout-minutes: 30 + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Install SDK + run: swift sdk install https://download.swift.org/swift-6.0.2-release/static-sdk/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum aa5515476a403797223fc2aad4ca0c3bf83995d5427fb297cab1d93c68cee075 + - name: Build + run: swift build --swift-sdk x86_64-swift-linux-musl diff --git a/.github/workflows/scripts/check-archive-plugin.sh b/.github/workflows/scripts/check-archive-plugin.sh new file mode 100755 index 00000000..218ee79a --- /dev/null +++ b/.github/workflows/scripts/check-archive-plugin.sh @@ -0,0 +1,54 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +test -n "${EXAMPLE:-}" || fatal "EXAMPLE unset" + +OUTPUT_DIR=.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager +OUTPUT_FILE=${OUTPUT_DIR}/MyLambda/bootstrap +ZIP_FILE=${OUTPUT_DIR}/MyLambda/MyLambda.zip + +pushd "Examples/${EXAMPLE}" || exit 1 + +# package the example (docker and swift toolchain are installed on the GH runner) +LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker || exit 1 + +# did the plugin generated a Linux binary? +[ -f "${OUTPUT_FILE}" ] +file "${OUTPUT_FILE}" | grep --silent ELF + +# did the plugin created a ZIP file? +[ -f "${ZIP_FILE}" ] + +# does the ZIP file contain the bootstrap? +unzip -l "${ZIP_FILE}" | grep --silent bootstrap + +# if EXAMPLE is ResourcesPackaging, check if the ZIP file contains hello.txt +if [ "$EXAMPLE" == "ResourcesPackaging" ]; then + echo "Checking if resource was added to the ZIP file" + unzip -l "${ZIP_FILE}" | grep --silent hello.txt + SUCCESS=$? + if [ "$SUCCESS" -eq 1 ]; then + log "❌ Resource not found." && exit 1 + else + log "✅ Resource found." + fi +fi + +echo "✅ The archive plugin is OK with example ${EXAMPLE}" +popd || exit 1 diff --git a/.github/workflows/scripts/check-link-foundation.sh b/.github/workflows/scripts/check-link-foundation.sh new file mode 100755 index 00000000..be84116c --- /dev/null +++ b/.github/workflows/scripts/check-link-foundation.sh @@ -0,0 +1,60 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +EXAMPLE=APIGateway +OUTPUT_DIR=.build/release +OUTPUT_FILE=${OUTPUT_DIR}/APIGatewayLambda +LIBS_TO_CHECK="libFoundation.so libFoundationInternationalization.so lib_FoundationICU.so" + +pushd Examples/${EXAMPLE} || fatal "Failed to change directory to Examples/${EXAMPLE}." + +# recompile the example without the --static-swift-stdlib flag +LAMBDA_USE_LOCAL_DEPS=../.. swift build -c release -Xlinker -s || fatal "Failed to build the example." + +# check if the binary exists +if [ ! -f "${OUTPUT_FILE}" ]; then + error "❌ ${OUTPUT_FILE} does not exist." +fi + +# Checking for Foundation or ICU dependencies +echo "Checking for Foundation or ICU dependencies in ${OUTPUT_DIR}/${OUTPUT_FILE}." +LIBRARIES=$(ldd ${OUTPUT_FILE} | awk '{print $1}') +for LIB in ${LIBS_TO_CHECK}; do + echo -n "Checking for ${LIB}... " + + # check if the binary has a dependency on Foundation or ICU + echo "${LIBRARIES}" | grep "${LIB}" # return 1 if not found + + # 1 is success (grep failed to find the lib), 0 is failure (grep successly found the lib) + SUCCESS=$? + if [ "$SUCCESS" -eq 0 ]; then + log "❌ ${LIB} found." && break + else + log "✅ ${LIB} not found." + fi +done + +popd || fatal "Failed to change directory back to the root directory." + +# exit code is the opposite of the grep exit code +if [ "$SUCCESS" -eq 0 ]; then + fatal "❌ At least one foundation lib was found, reporting the error." +else + log "✅ No foundation lib found, congrats!" && exit 0 +fi \ No newline at end of file diff --git a/Examples/LambdaFunctions/scripts/serverless-deploy.sh b/.github/workflows/scripts/integration_tests.sh similarity index 56% rename from Examples/LambdaFunctions/scripts/serverless-deploy.sh rename to .github/workflows/scripts/integration_tests.sh index 241ee7bf..8d11b313 100755 --- a/Examples/LambdaFunctions/scripts/serverless-deploy.sh +++ b/.github/workflows/scripts/integration_tests.sh @@ -13,17 +13,20 @@ ## ##===----------------------------------------------------------------------===## -set -eu +set -euo pipefail -DIR="$(cd "$(dirname "$0")" && pwd)" -source $DIR/config.sh +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } -echo -e "\ndeploying $executable" +SWIFT_VERSION=$(swift --version) +test -n "${SWIFT_VERSION:-}" || fatal "SWIFT_VERSION unset" +test -n "${COMMAND:-}" || fatal "COMMAND unset" +test -n "${EXAMPLE:-}" || fatal "EXAMPLE unset" -$DIR/build-and-package.sh "$executable" +pushd Examples/"$EXAMPLE" > /dev/null -echo "-------------------------------------------------------------------------" -echo "deploying using Serverless" -echo "-------------------------------------------------------------------------" +log "Running command with Swift $SWIFT_VERSION" +eval "$COMMAND" -serverless deploy --config "./scripts/serverless/$executable-template.yml" --stage dev -v +popd diff --git a/.gitignore b/.gitignore index 04708b0f..f7a26a78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store *.build +*.index-build /.xcodeproj *.pem .podspecs @@ -8,3 +9,6 @@ xcuserdata Package.resolved .serverless +.vscode +Makefile +.devcontainer \ No newline at end of file diff --git a/.licenseignore b/.licenseignore new file mode 100644 index 00000000..d47f45a2 --- /dev/null +++ b/.licenseignore @@ -0,0 +1,37 @@ +.gitignore +.licenseignore +.swiftformatignore +.spi.yml +.swift-format +.github/* +*.md +**/*.md +CONTRIBUTORS.txt +LICENSE.txt +NOTICE.txt +Package.swift +Package@swift-*.swift +Package.resolved +**/*.docc/* +**/.gitignore +**/Package.swift +**/Package.resolved +**/docker-compose*.yaml +**/docker/* +**/.dockerignore +**/Dockerfile +**/Makefile +**/*.html +**/*-template.yml +**/*.xcworkspace/* +**/*.xcodeproj/* +**/*.xcassets/* +**/*.appiconset/* +**/ResourcePackaging/hello.txt +.mailmap +.swiftformat +*.yaml +*.yml +**/.npmignore +**/*.json +**/*.txt \ No newline at end of file diff --git a/.mailmap b/.mailmap index c8ad743d..91b18339 100644 --- a/.mailmap +++ b/.mailmap @@ -1,3 +1,5 @@ Tomer Doron Tomer Doron Tomer Doron +Fabian Fett +Fabian Fett \ No newline at end of file diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 00000000..9c13e3e4 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [AWSLambdaRuntime] diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..7fa06fb3 --- /dev/null +++ b/.swift-format @@ -0,0 +1,62 @@ +{ + "version" : 1, + "indentation" : { + "spaces" : 4 + }, + "tabWidth" : 4, + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "spacesAroundRangeFormationOperators" : false, + "indentConditionalCompilationBlocks" : false, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : true, + "lineBreakBeforeEachGenericRequirement" : true, + "lineLength" : 120, + "maximumBlankLines" : 1, + "respectsExistingLineBreaks" : true, + "prioritizeKeepingFunctionOutputTogether" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : false, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : true, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : false, + "UseLetInEveryBoundCaseVariable" : false, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : false, + "UseSynthesizedInitializer" : false, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + } +} diff --git a/.swiftformat b/.swiftformat deleted file mode 100644 index 864dbb8d..00000000 --- a/.swiftformat +++ /dev/null @@ -1,13 +0,0 @@ -# file options - ---swiftversion 5.1 ---exclude .build - -# format options - ---self insert ---patternlet inline ---stripunusedargs unnamed-only ---ifdef no-indent - -# rules diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ebdb302e..0776b8a0 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,55 +1,3 @@ # Code of Conduct -To be a truly great community, SwiftAWSLambdaRuntime needs to welcome developers from all walks of life, -with different backgrounds, and with a wide range of experience. A diverse and friendly -community will have more great ideas, more unique perspectives, and produce more great -code. We will work diligently to make the SwiftAWSLambdaRuntime community welcoming to everyone. -To give clarity of what is expected of our members, SwiftAWSLambdaRuntime has adopted the code of conduct -defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source -communities, and we think it articulates our values well. The full text is copied below: - -### Contributor Code of Conduct v1.3 -As contributors and maintainers of this project, and in the interest of fostering an open and -welcoming community, we pledge to respect all people who contribute through reporting -issues, posting feature requests, updating documentation, submitting pull requests or patches, -and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, sexual -orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or -nationality. - -Examples of unacceptable behavior by participants include: -- The use of sexualized language or imagery -- Personal attacks -- Trolling or insulting/derogatory comments -- Public or private harassment -- Publishing other’s private information, such as physical or electronic addresses, without explicit permission -- Other unethical or unprofessional conduct - -Project maintainers have the right and responsibility to remove, edit, or reject comments, -commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of -Conduct, or to ban temporarily or permanently any contributor for other behaviors that they -deem inappropriate, threatening, offensive, or harmful. - -By adopting this Code of Conduct, project maintainers commit themselves to fairly and -consistently applying these principles to every aspect of managing this project. Project -maintainers who do not follow or enforce the Code of Conduct may be permanently removed -from the project team. - -This code of conduct applies both within project spaces and in public spaces when an -individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by -contacting a project maintainer at [swift-server-conduct@group.apple.com](mailto:swift-server-conduct@group.apple.com). All complaints will be reviewed and -investigated and will result in a response that is deemed necessary and appropriate to the -circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter -of an incident. - -*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](https://contributor-covenant.org/version/1/3/0/).* - -### Reporting -A working group of community members is committed to promptly addressing any [reported issues](mailto:swift-server-conduct@group.apple.com). -Working group members are volunteers appointed by the project lead, with a -preference for individuals with varied backgrounds and perspectives. Membership is expected -to change regularly, and may grow or shrink. +The code of conduct for this project can be found at https://swift.org/code-of-conduct. diff --git a/Examples/APIGateway+LambdaAuthorizer/.gitignore b/Examples/APIGateway+LambdaAuthorizer/.gitignore new file mode 100644 index 00000000..e4044f6f --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/.gitignore @@ -0,0 +1,2 @@ +samconfig.toml +Makefile diff --git a/Examples/APIGateway+LambdaAuthorizer/Package.swift b/Examples/APIGateway+LambdaAuthorizer/Package.swift new file mode 100644 index 00000000..03117835 --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/Package.swift @@ -0,0 +1,63 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]), + .executable(name: "AuthorizerLambda", targets: ["AuthorizerLambda"]), + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "APIGatewayLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ), + .executableTarget( + name: "AuthorizerLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ), + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/APIGateway+LambdaAuthorizer/README.md b/Examples/APIGateway+LambdaAuthorizer/README.md new file mode 100644 index 00000000..a7a55812 --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/README.md @@ -0,0 +1,112 @@ +# Lambda Authorizer with API Gateway + +This is an example of a Lambda Authorizer function. There are two Lambda functions in this example. The first one is the authorizer function. The second one is the business function. The business function is exposed through a REST API using the API Gateway. The API Gateway is configured to use the authorizer function to implement a custom logic to authorize the requests. + +>[!NOTE] +> If your application is protected by JWT tokens, it's recommended to use [the native JWT authorizer provided by the API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html). The Lambda authorizer is useful when you need to implement a custom authorization logic. See the [OAuth 2.0/JWT authorizer example for AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-controlling-access-to-apis-oauth2-authorizer.html) to learn how to use the native JWT authorizer with SAM. + +## Code + +The authorizer function is a simple function that checks data received from the API Gateway. In this example, the API Gateway is configured to pass the content of the `Authorization` header to the authorizer Lambda function. + +There are two possible responses from a Lambda Authorizer function: policy and simple. The policy response returns an IAM policy document that describes the permissions of the caller. The simple response returns a boolean value that indicates if the caller is authorized or not. You can read more about the two types of responses in the [Lambda authorizer response format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html) section of the API Gateway documentation. + +This example uses an authorizer that returns the simple response. The authorizer function is defined in the `Sources/AuthorizerLambda` directory. The business function is defined in the `Sources/APIGatewayLambda` directory. + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there are two ZIP files ready to deploy, one for the authorizer function and one for the business function. +The ZIP file are located under `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager` + +## Deploy + +The deployment must include the Lambda functions and the API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name APIGatewayWithLambdaAuthorizer \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URI +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com/demo +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +When invoking the Lambda function without `Authorization` header, the response is a `401 Unauthorized` error. + +```bash +curl -v https://a5q74es3k2.execute-api.us-east-1.amazonaws.com/demo +... +> GET /demo HTTP/2 +> Host: 6sm6270j21.execute-api.us-east-1.amazonaws.com +> User-Agent: curl/8.7.1 +> Accept: */* +> +* Request completely sent off +< HTTP/2 401 +< date: Sat, 04 Jan 2025 14:03:02 GMT +< content-type: application/json +< content-length: 26 +< apigw-requestid: D3bfpidOoAMESiQ= +< +* Connection #0 to host 6sm6270j21.execute-api.us-east-1.amazonaws.com left intact +{"message":"Unauthorized"} +``` + +When invoking the Lambda function with the `Authorization` header, the response is a `200 OK` status code. Note that the Lambda Authorizer function is configured to accept any value in the `Authorization` header. + +```bash +curl -v -H 'Authorization: 123' https://a5q74es3k2.execute-api.us-east-1.amazonaws.com/demo +... +> GET /demo HTTP/2 +> Host: 6sm6270j21.execute-api.us-east-1.amazonaws.com +> User-Agent: curl/8.7.1 +> Accept: */* +> Authorization: 123 +> +* Request completely sent off +< HTTP/2 200 +< date: Sat, 04 Jan 2025 14:04:43 GMT +< content-type: application/json +< content-length: 911 +< apigw-requestid: D3bvRjJcoAMEaig= +< +* Connection #0 to host 6sm6270j21.execute-api.us-east-1.amazonaws.com left intact +{"headers":{"x-forwarded-port":"443","x-forwarded-proto":"https","host":"6sm6270j21.execute-api.us-east-1.amazonaws.com","user-agent":"curl\/8.7.1","accept":"*\/*","content-length":"0","x-amzn-trace-id":"Root=1-67793ffa-05f1296f1a52f8a066180020","authorization":"123","x-forwarded-for":"81.49.207.77"},"routeKey":"ANY \/demo","version":"2.0","rawQueryString":"","isBase64Encoded":false,"queryStringParameters":{},"pathParameters":{},"rawPath":"\/demo","cookies":[],"requestContext":{"domainPrefix":"6sm6270j21","requestId":"D3bvRjJcoAMEaig=","domainName":"6sm6270j21.execute-api.us-east-1.amazonaws.com","stage":"$default","authorizer":{"lambda":{"abc1":"xyz1"}},"timeEpoch":1735999482988,"accountId":"401955065246","time":"04\/Jan\/2025:14:04:42 +0000","http":{"method":"GET","sourceIp":"81.49.207.77","path":"\/demo","userAgent":"curl\/8.7.1","protocol":"HTTP\/1.1"},"apiId":"6sm6270j21"},"stageVariables":{}} +``` + +## Undeploy + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete --stack-name APIGatewayWithLambdaAuthorizer +``` \ No newline at end of file diff --git a/Examples/APIGateway+LambdaAuthorizer/Sources/APIGatewayLambda/main.swift b/Examples/APIGateway+LambdaAuthorizer/Sources/APIGatewayLambda/main.swift new file mode 100644 index 00000000..f7662d1c --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/Sources/APIGatewayLambda/main.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime + +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + + var header = HTTPHeaders() + context.logger.debug("HTTP API Message received") + + header["content-type"] = "application/json" + + // echo the request in the response + return try APIGatewayV2Response(statusCode: .ok, headers: header, encodableBody: event) +} + +try await runtime.run() diff --git a/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift b/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift new file mode 100644 index 00000000..60ea2b7b --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime + +// +// This is an example of a policy authorizer that always authorizes the request. +// The policy authorizer returns an IAM policy document that defines what the Lambda function caller can do and optional context key-value pairs +// +// This code is shown for the example only and is not used in this demo. +// This code doesn't perform any type of token validation. It should be used as a reference only. +let policyAuthorizerHandler: + (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerPolicyResponse = { + (request: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in + + context.logger.debug("+++ Policy Authorizer called +++") + + // typically, this function will check the validity of the incoming token received in the request + + // then it creates and returns a response + return APIGatewayLambdaAuthorizerPolicyResponse( + principalId: "John Appleseed", + + // this policy allows the caller to invoke any API Gateway endpoint + policyDocument: .init(statement: [ + .init( + action: "execute-api:Invoke", + effect: .allow, + resource: "*" + ) + + ]), + + // this is additional context we want to return to the caller + context: [ + "abc1": "xyz1", + "abc2": "xyz2", + ] + ) + } + +// +// This is an example of a simple authorizer that always authorizes the request. +// A simple authorizer returns a yes/no decision and optional context key-value pairs +// +// This code doesn't perform any type of token validation. It should be used as a reference only. +let simpleAuthorizerHandler: + (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerSimpleResponse = { + (_: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in + + context.logger.debug("+++ Simple Authorizer called +++") + + // typically, this function will check the validity of the incoming token received in the request + + return APIGatewayLambdaAuthorizerSimpleResponse( + // this is the authorization decision: yes or no + isAuthorized: true, + + // this is additional context we want to return to the caller + context: ["abc1": "xyz1"] + ) + } + +// create the runtime and start polling for new events. +// in this demo we use the simple authorizer handler +let runtime = LambdaRuntime(body: simpleAuthorizerHandler) +try await runtime.run() diff --git a/Examples/APIGateway+LambdaAuthorizer/template.yaml b/Examples/APIGateway+LambdaAuthorizer/template.yaml new file mode 100644 index 00000000..ae9a026f --- /dev/null +++ b/Examples/APIGateway+LambdaAuthorizer/template.yaml @@ -0,0 +1,77 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for APIGateway Lambda Example + +Resources: + # The API Gateway + MyProtectedApi: + Type: AWS::Serverless::HttpApi + Properties: + Auth: + DefaultAuthorizer: MyLambdaRequestAuthorizer + Authorizers: + MyLambdaRequestAuthorizer: + FunctionArn: !GetAtt AuthorizerLambda.Arn + Identity: + Headers: + - Authorization + AuthorizerPayloadFormatVersion: "2.0" + EnableSimpleResponses: true + + # Give the API Gateway permissions to invoke the Lambda authorizer + AuthorizerPermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref AuthorizerLambda + Principal: apigateway.amazonaws.com + SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${MyProtectedApi}/* + + # Lambda business function + APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 512 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + Events: + HttpApiEvent: + Type: HttpApi + Properties: + ApiId: !Ref MyProtectedApi + Path: /demo + Method: ANY + + # Lambda authorizer function + AuthorizerLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/AuthorizerLambda/AuthorizerLambda.zip + Timeout: 29 # max 29 seconds for Lambda authorizers + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 512 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + +Outputs: + # print API Gateway endpoint + APIGatewayEndpoint: + Description: API Gateway endpoint URI + Value: !Sub "https://${MyProtectedApi}.execute-api.${AWS::Region}.amazonaws.com/demo" diff --git a/Examples/APIGateway/.gitignore b/Examples/APIGateway/.gitignore new file mode 100644 index 00000000..e4044f6f --- /dev/null +++ b/Examples/APIGateway/.gitignore @@ -0,0 +1,2 @@ +samconfig.toml +Makefile diff --git a/Examples/APIGateway/Package.swift b/Examples/APIGateway/Package.swift new file mode 100644 index 00000000..b2373801 --- /dev/null +++ b/Examples/APIGateway/Package.swift @@ -0,0 +1,56 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]) + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "APIGatewayLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "Sources" + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/APIGateway/README.md b/Examples/APIGateway/README.md new file mode 100644 index 00000000..d03c5343 --- /dev/null +++ b/Examples/APIGateway/README.md @@ -0,0 +1,124 @@ +# API Gateway + +This is a simple example of an AWS Lambda function invoked through an Amazon API Gateway. + +## Code + +The Lambda function takes all HTTP headers it receives as input and returns them as output. + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when the API Gateway receives an HTTP request. + +The handler is `(event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response`. The function takes two arguments: +- the event argument is a `APIGatewayV2Request`. It is the parameter passed by the API Gateway. It contains all data passed in the HTTP request and some meta data. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function must return a `APIGatewayV2Response`. + +`APIGatewayV2Request` and `APIGatewayV2Response` are defined in the [Swift AWS Lambda Events](https://github.com/swift-server/swift-aws-lambda-events) library. + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip` + +## Deploy + +The deployment must include the Lambda function and the API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name APIGatewayLambda \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URL" +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print a JSON similar to + +```bash +{"version":"2.0","rawPath":"\/","isBase64Encoded":false,"rawQueryString":"","headers":{"user-agent":"curl\/8.7.1","accept":"*\/*","host":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","content-length":"0","x-amzn-trace-id":"Root=1-66fb0388-691f744d4bd3c99c7436a78d","x-forwarded-port":"443","x-forwarded-for":"81.0.0.43","x-forwarded-proto":"https"},"requestContext":{"requestId":"e719cgNpoAMEcwA=","http":{"sourceIp":"81.0.0.43","path":"\/","protocol":"HTTP\/1.1","userAgent":"curl\/8.7.1","method":"GET"},"stage":"$default","apiId":"a5q74es3k2","time":"30\/Sep\/2024:20:01:12 +0000","timeEpoch":1727726472922,"domainPrefix":"a5q74es3k2","domainName":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","accountId":"012345678901"} +``` + +If you have `jq` installed, you can use it to pretty print the output. + +```bash +curl -s https://a5q74es3k2.execute-api.us-east-1.amazonaws.com | jq +{ + "version": "2.0", + "rawPath": "/", + "requestContext": { + "domainPrefix": "a5q74es3k2", + "stage": "$default", + "timeEpoch": 1727726558220, + "http": { + "protocol": "HTTP/1.1", + "method": "GET", + "userAgent": "curl/8.7.1", + "path": "/", + "sourceIp": "81.0.0.43" + }, + "apiId": "a5q74es3k2", + "accountId": "012345678901", + "requestId": "e72KxgsRoAMEMSA=", + "domainName": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "time": "30/Sep/2024:20:02:38 +0000" + }, + "rawQueryString": "", + "routeKey": "$default", + "headers": { + "x-forwarded-for": "81.0.0.43", + "user-agent": "curl/8.7.1", + "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "accept": "*/*", + "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", + "content-length": "0", + "x-forwarded-proto": "https", + "x-forwarded-port": "443" + }, + "isBase64Encoded": false +} +``` + +## Undeploy + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` \ No newline at end of file diff --git a/Examples/APIGateway/Sources/main.swift b/Examples/APIGateway/Sources/main.swift new file mode 100644 index 00000000..f7662d1c --- /dev/null +++ b/Examples/APIGateway/Sources/main.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime + +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + + var header = HTTPHeaders() + context.logger.debug("HTTP API Message received") + + header["content-type"] = "application/json" + + // echo the request in the response + return try APIGatewayV2Response(statusCode: .ok, headers: header, encodableBody: event) +} + +try await runtime.run() diff --git a/Examples/APIGateway/template.yaml b/Examples/APIGateway/template.yaml new file mode 100644 index 00000000..939f09f8 --- /dev/null +++ b/Examples/APIGateway/template.yaml @@ -0,0 +1,31 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for APIGateway Lambda Example + +Resources: + # Lambda function + APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 512 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + Events: + HttpApiEvent: + Type: HttpApi + +Outputs: + # print API Gateway endpoint + APIGatewayEndpoint: + Description: API Gateway endpoint UR" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" diff --git a/Examples/BackgroundTasks/.gitignore b/Examples/BackgroundTasks/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/BackgroundTasks/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/BackgroundTasks/Package.swift b/Examples/BackgroundTasks/Package.swift new file mode 100644 index 00000000..3d5b52bb --- /dev/null +++ b/Examples/BackgroundTasks/Package.swift @@ -0,0 +1,54 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "BackgroundTasks", targets: ["BackgroundTasks"]) + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "BackgroundTasks", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources" + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/BackgroundTasks/README.md b/Examples/BackgroundTasks/README.md new file mode 100644 index 00000000..e1bf0ddd --- /dev/null +++ b/Examples/BackgroundTasks/README.md @@ -0,0 +1,119 @@ +# Background Tasks + +This is an example for running background tasks in an AWS Lambda function. + +Background tasks allow code to execute asynchronously after the main response has been returned, enabling additional processing without affecting response latency. This approach is ideal for scenarios like logging, data updates, or notifications that can be deferred. The code leverages Lambda's "Response Streaming" feature, which is effective for balancing real-time user responsiveness with the ability to perform extended tasks post-response. + +For more information about Lambda background tasks, see [this AWS blog post](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/). + +## Code + +The sample code creates a `BackgroundProcessingHandler` struct that conforms to the `LambdaWithBackgroundProcessingHandler` protocol provided by the Swift AWS Lambda Runtime. + +The `BackgroundProcessingHandler` struct defines the input and output JSON received and returned by the Handler. + +The `handle(...)` method of this protocol receives incoming events as `Input` and returns the output as a `Greeting`. The `handle(...)` methods receives an `outputWriter` parameter to write the output before the function returns, giving some opportunities to run long-lasting tasks after the response has been returned to the client but before the function returns. + +The `handle(...)` method uses the `outputWriter` to return the response as soon as possible. It then waits for 10 seconds to simulate a long background work. When the 10 seconds elapsed, the function returns. The billing cycle ends when the function returns. + +The `handle(...)` method is marked as `mutating` to allow handlers to be implemented with a `struct`. + +Once the struct is created and the `handle(...)` method is defined, the sample code creates a `LambdaCodableAdapter` adapter to adapt the `LambdaWithBackgroundProcessingHandler` to a type accepted by the `LambdaRuntime` struct. Then, the sample code initializes the `LambdaRuntime` with the adapter just created. Finally, the code calls `run()` to start the interaction with the AWS Lambda control plane. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip` + +## Deploy with the AWS CLI + +Here is how to deploy using the `aws` command line. + +### Create the function + +```bash +AWS_ACCOUNT_ID=012345678901 +aws lambda create-function \ +--function-name BackgroundTasks \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \ +--environment "Variables={LOG_LEVEL=debug}" \ +--timeout 15 +``` + +> [!IMPORTANT] +> The timeout value must be bigger than the time it takes for your function to complete its background tasks. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish the tasks. Here, the sample function waits for 10 seconds and we set the timeout for 15 seconds. + +The `--environment` arguments sets the `LOG_LEVEL` environment variable to `debug`. This will ensure the debugging statements in the handler `context.logger.debug("...")` are printed in the Lambda function logs. + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +Be sure to set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). + +### Invoke your Lambda function + +To invoke the Lambda function, use `aws` command line. +```bash +aws lambda invoke \ + --function-name BackgroundTasks \ + --cli-binary-format raw-in-base64-out \ + --payload '{ "message" : "Hello Background Tasks" }' \ + response.json +``` + +This should immediately output the following result. + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} +``` + +The response is visible in the `response.json` file. + +```bash +cat response.json +{"echoedMessage":"Hello Background Tasks"} +``` + +### View the function's logs + +You can observe additional messages being logged after the response is received. + +To tail the log, use the AWS CLI: +```bash +aws logs tail /aws/lambda/BackgroundTasks --follow +``` + +This produces an output like: +```text +INIT_START Runtime Version: provided:al2.v59 Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:974c4a90f22278a2ef1c3f53c5c152167318aaf123fbb07c055a4885a4e97e52 +START RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd Version: $LATEST +debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - message received +debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - response sent. Performing background tasks. +debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - Background tasks completed. Returning +END RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd +REPORT RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd Duration: 10160.89 ms Billed Duration: 10250 ms Memory Size: 128 MB Max Memory Used: 27 MB Init Duration: 88.20 ms +``` +> [!NOTE] +> The `debug` message are sent by the code inside the `handler()` function. Note that the `Duration` and `Billed Duration` on the last line are for 10.1 and 10.2 seconds respectively. + +Type CTRL-C to stop tailing the logs. + +## Cleanup + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name BackgroundTasks +``` \ No newline at end of file diff --git a/Examples/BackgroundTasks/Sources/main.swift b/Examples/BackgroundTasks/Sources/main.swift new file mode 100644 index 00000000..1985fc34 --- /dev/null +++ b/Examples/BackgroundTasks/Sources/main.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + context.logger.debug("BackgroundProcessingHandler - message received") + try await outputWriter.write(Greeting(echoedMessage: event.message)) + + // Perform some background work, e.g: + context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.") + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning") + return + } +} + +let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler()) +let runtime = LambdaRuntime.init(handler: adapter) +try await runtime.run() diff --git a/Examples/CDK/Package.swift b/Examples/CDK/Package.swift new file mode 100644 index 00000000..b2373801 --- /dev/null +++ b/Examples/CDK/Package.swift @@ -0,0 +1,56 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]) + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "APIGatewayLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "Sources" + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/CDK/README.md b/Examples/CDK/README.md new file mode 100644 index 00000000..69b772ec --- /dev/null +++ b/Examples/CDK/README.md @@ -0,0 +1,121 @@ +# API Gateway and Cloud Development Kit + +This is a simple example of an AWS Lambda function invoked through an Amazon API Gateway and deployed with the Cloud Development Kit (CDK). + +## Code + +The Lambda function takes all HTTP headers it receives as input and returns them as output. See the [API Gateway example](Examples/APIGateway/README.md) for a complete description of the code. + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip` + +## Deploy + +>[NOTE] +>Before deploying the infrastructure, you need to have NodeJS and the AWS CDK installed and configured. +>For more information, see the [AWS CDK documentation](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). + +To deploy the infrastructure, type the following commands. + +```sh +# Change to the infra directory +cd infra + +# Install the dependencies (only before the first deployment) +npm install + +cdk deploy + +✨ Synthesis time: 2.88s +... redacted for brevity ... +Do you wish to deploy these changes (y/n)? y +... redacted for brevity ... + ✅ LambdaApiStack + +✨ Deployment time: 42.96s + +Outputs: +LambdaApiStack.ApiUrl = https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com/ +Stack ARN: +arn:aws:cloudformation:eu-central-1:401955065246:stack/LambdaApiStack/e0054390-be05-11ef-9504-065628de4b89 + +✨ Total time: 45.84s +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print a JSON similar to + +```bash +{"version":"2.0","rawPath":"\/","isBase64Encoded":false,"rawQueryString":"","headers":{"user-agent":"curl\/8.7.1","accept":"*\/*","host":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","content-length":"0","x-amzn-trace-id":"Root=1-66fb0388-691f744d4bd3c99c7436a78d","x-forwarded-port":"443","x-forwarded-for":"81.0.0.43","x-forwarded-proto":"https"},"requestContext":{"requestId":"e719cgNpoAMEcwA=","http":{"sourceIp":"81.0.0.43","path":"\/","protocol":"HTTP\/1.1","userAgent":"curl\/8.7.1","method":"GET"},"stage":"$default","apiId":"a5q74es3k2","time":"30\/Sep\/2024:20:01:12 +0000","timeEpoch":1727726472922,"domainPrefix":"a5q74es3k2","domainName":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","accountId":"012345678901"} +``` + +If you have `jq` installed, you can use it to pretty print the output. + +```bash +curl -s https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com | jq +{ + "version": "2.0", + "rawPath": "/", + "requestContext": { + "domainPrefix": "a5q74es3k2", + "stage": "$default", + "timeEpoch": 1727726558220, + "http": { + "protocol": "HTTP/1.1", + "method": "GET", + "userAgent": "curl/8.7.1", + "path": "/", + "sourceIp": "81.0.0.43" + }, + "apiId": "a5q74es3k2", + "accountId": "012345678901", + "requestId": "e72KxgsRoAMEMSA=", + "domainName": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "time": "30/Sep/2024:20:02:38 +0000" + }, + "rawQueryString": "", + "routeKey": "$default", + "headers": { + "x-forwarded-for": "81.0.0.43", + "user-agent": "curl/8.7.1", + "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "accept": "*/*", + "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", + "content-length": "0", + "x-forwarded-proto": "https", + "x-forwarded-port": "443" + }, + "isBase64Encoded": false +} +``` + +## Undeploy + +When done testing, you can delete the infrastructure with this command. + +```bash +cdk destroy + +Are you sure you want to delete: LambdaApiStack (y/n)? y +LambdaApiStack: destroying... [1/1] +... redacted for brevity ... + ✅ LambdaApiStack: destroyed +``` \ No newline at end of file diff --git a/Examples/CDK/Sources/main.swift b/Examples/CDK/Sources/main.swift new file mode 100644 index 00000000..2d5707d6 --- /dev/null +++ b/Examples/CDK/Sources/main.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +let encoder = JSONEncoder() +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + + var header = HTTPHeaders() + context.logger.debug("HTTP API Message received") + + header["content-type"] = "application/json" + + // echo the request in the response + let data = try encoder.encode(event) + let response = String(decoding: data, as: Unicode.UTF8.self) + + return APIGatewayV2Response(statusCode: .ok, headers: header, body: response) +} + +try await runtime.run() diff --git a/Examples/CDK/infra/.gitignore b/Examples/CDK/infra/.gitignore new file mode 100644 index 00000000..a08f1af9 --- /dev/null +++ b/Examples/CDK/infra/.gitignore @@ -0,0 +1,9 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out + diff --git a/Examples/CDK/infra/.npmignore b/Examples/CDK/infra/.npmignore new file mode 100644 index 00000000..c1d6d45d --- /dev/null +++ b/Examples/CDK/infra/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/Examples/CDK/infra/README.md b/Examples/CDK/infra/README.md new file mode 100644 index 00000000..9315fe5b --- /dev/null +++ b/Examples/CDK/infra/README.md @@ -0,0 +1,14 @@ +# Welcome to your CDK TypeScript project + +This is a blank project for CDK development with TypeScript. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +## Useful commands + +* `npm run build` compile typescript to js +* `npm run watch` watch for changes and compile +* `npm run test` perform the jest unit tests +* `npx cdk deploy` deploy this stack to your default AWS account/region +* `npx cdk diff` compare deployed stack with current state +* `npx cdk synth` emits the synthesized CloudFormation template diff --git a/Examples/LocalDebugging/Shared/Tests/LinuxMain.swift b/Examples/CDK/infra/bin/deploy.ts similarity index 66% rename from Examples/LocalDebugging/Shared/Tests/LinuxMain.swift rename to Examples/CDK/infra/bin/deploy.ts index c46de763..a83096da 100644 --- a/Examples/LocalDebugging/Shared/Tests/LinuxMain.swift +++ b/Examples/CDK/infra/bin/deploy.ts @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,4 +12,8 @@ // //===----------------------------------------------------------------------===// -preconditionFailure("use `swift test --enable-test-discovery`") +import * as cdk from 'aws-cdk-lib'; +import { LambdaApiStack } from '../lib/lambda-api-project-stack'; + +const app = new cdk.App(); +new LambdaApiStack(app, 'LambdaApiStack'); diff --git a/Examples/CDK/infra/cdk.json b/Examples/CDK/infra/cdk.json new file mode 100644 index 00000000..06b03d2f --- /dev/null +++ b/Examples/CDK/infra/cdk.json @@ -0,0 +1,81 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/deploy.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true + } +} diff --git a/Examples/CDK/infra/lib/lambda-api-project-stack.ts b/Examples/CDK/infra/lib/lambda-api-project-stack.ts new file mode 100644 index 00000000..3598478c --- /dev/null +++ b/Examples/CDK/infra/lib/lambda-api-project-stack.ts @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2'; +import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; + +export class LambdaApiStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Create the Lambda function + const lambdaFunction = new lambda.Function(this, 'SwiftLambdaFunction', { + runtime: lambda.Runtime.PROVIDED_AL2, + architecture: lambda.Architecture.ARM_64, + handler: 'bootstrap', + code: lambda.Code.fromAsset('../.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip'), + memorySize: 512, + timeout: cdk.Duration.seconds(30), + environment: { + LOG_LEVEL: 'debug', + }, + }); + + // Create the integration + const integration = new HttpLambdaIntegration( + 'LambdaIntegration', + lambdaFunction + ); + + // Create HTTP API with the integration + const httpApi = new apigateway.HttpApi(this, 'HttpApi', { + defaultIntegration: integration, + }); + + // Output the API URL + new cdk.CfnOutput(this, 'ApiUrl', { + value: httpApi.url ?? 'Something went wrong', + }); + } +} + diff --git a/Examples/CDK/infra/package-lock.json b/Examples/CDK/infra/package-lock.json new file mode 100644 index 00000000..c3c85d14 --- /dev/null +++ b/Examples/CDK/infra/package-lock.json @@ -0,0 +1,1182 @@ +{ + "name": "deploy", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deploy", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "^2.183.0", + "constructs": "^10.4.2" + }, + "bin": { + "deploy": "bin/deploy.js" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.13.10", + "aws-cdk": "2.1003.0", + "ts-node": "^10.9.2", + "typescript": "~5.8.2" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.227", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.227.tgz", + "integrity": "sha512-BAZwZGtX166K9KRYnRBbRj/fU0FY00LOnki11OsDQMfZ9H6tno+LIhv/ZY5U4LGIaI8yP881pAhbrVxpKQnYLg==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "40.7.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-40.7.0.tgz", + "integrity": "sha512-00wVKn9pOOGXbeNwA4E8FUFt0zIB4PmSO7PvIiDWgpaFX3G/sWyy0A3s6bg/n2Yvkghu8r4a8ckm+mAzkAYmfA==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-cdk": { + "version": "2.1003.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1003.0.tgz", + "integrity": "sha512-FORPDGW8oUg4tXFlhX+lv/j+152LO9wwi3/CwNr1WY3c3HwJUtc0fZGb2B3+Fzy6NhLWGHJclUsJPEhjEt8Nhg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.183.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.183.0.tgz", + "integrity": "sha512-xwdDMm7qKBgN+dRjn8XxwS0YDRFM9JnUavFWM2bzaOzFeaBCiwFMrG0xLZaZs6GBImV804/jj8PnjmbOCsDZdw==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.208", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^40.6.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.2.0", + "ignore": "^5.3.2", + "jsonschema": "^1.4.1", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.6.3", + "table": "^6.8.2", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.6.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", + "license": "Apache-2.0" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/Examples/CDK/infra/package.json b/Examples/CDK/infra/package.json new file mode 100644 index 00000000..40859162 --- /dev/null +++ b/Examples/CDK/infra/package.json @@ -0,0 +1,23 @@ +{ + "name": "deploy", + "version": "0.1.0", + "bin": { + "deploy": "bin/deploy.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.13.10", + "aws-cdk": "2.1003.0", + "ts-node": "^10.9.2", + "typescript": "~5.8.2" + }, + "dependencies": { + "aws-cdk-lib": "^2.183.0", + "constructs": "^10.4.2" + } +} diff --git a/Examples/CDK/infra/tsconfig.json b/Examples/CDK/infra/tsconfig.json new file mode 100644 index 00000000..aaa7dc51 --- /dev/null +++ b/Examples/CDK/infra/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/Examples/HelloJSON/.gitignore b/Examples/HelloJSON/.gitignore new file mode 100644 index 00000000..e41d0be5 --- /dev/null +++ b/Examples/HelloJSON/.gitignore @@ -0,0 +1,4 @@ +response.json +samconfig.toml +template.yaml +Makefile diff --git a/Examples/HelloJSON/Package.swift b/Examples/HelloJSON/Package.swift new file mode 100644 index 00000000..9f26ff9d --- /dev/null +++ b/Examples/HelloJSON/Package.swift @@ -0,0 +1,59 @@ +// swift-tools-version:6.1 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "HelloJSON", targets: ["HelloJSON"]) + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package( + url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + branch: "ff-package-traits", + traits: [ + .trait(name: "FoundationJSONSupport") + ] + ) + ], + targets: [ + .executableTarget( + name: "HelloJSON", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ] + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/HelloJSON/README.md b/Examples/HelloJSON/README.md new file mode 100644 index 00000000..d3f1dc29 --- /dev/null +++ b/Examples/HelloJSON/README.md @@ -0,0 +1,80 @@ +# Hello JSON + +This is a simple example of an AWS Lambda function that takes a JSON structure as an input parameter and returns a JSON structure as a response. + +The runtime takes care of decoding the input and encoding the output. + +## Code + +The code defines `HelloRequest` and `HelloResponse` data structures to represent the input and output payloads. These structures are typically shared with a client project, such as an iOS application. + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as an argument. The function is the handler that will be invoked when an event triggers the Lambda function. + +The handler is `(event: HelloRequest, context: LambdaContext)`. The function takes two arguments: +- the event argument is a `HelloRequest`. It is the parameter passed when invoking the function. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function return value will be encoded to a `HelloResponse` as your Lambda function response. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip` + +## Deploy + +Here is how to deploy using the `aws` command line. + +```bash +# Replace with your AWS Account ID +AWS_ACCOUNT_ID=012345678901 + +aws lambda create-function \ +--function-name HelloJSON \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution +``` + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +Be sure to define the `AWS_ACCOUNT_ID` environment variable with your actual AWS account ID (for example: 012345678901). + +## Invoke your Lambda function + +To invoke the Lambda function, use this `aws` command line. + +```bash +aws lambda invoke \ +--function-name HelloJSON \ +--payload $(echo '{ "name" : "Seb", "age" : 50 }' | base64) \ +out.txt && cat out.txt && rm out.txt +``` + +Note that the payload is expected to be a valid JSON string. + +This should output the following result. + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} +{"greetings":"Hello Seb. You look younger than your age."} +``` + +## Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name HelloJSON +``` \ No newline at end of file diff --git a/Examples/HelloJSON/Sources/main.swift b/Examples/HelloJSON/Sources/main.swift new file mode 100644 index 00000000..7e48971b --- /dev/null +++ b/Examples/HelloJSON/Sources/main.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +// in this example we are receiving and responding with JSON structures + +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// the Lambda runtime +let runtime = LambdaRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} + +// start the loop +try await runtime.run() diff --git a/Examples/HelloWorld/.gitignore b/Examples/HelloWorld/.gitignore new file mode 100644 index 00000000..e41d0be5 --- /dev/null +++ b/Examples/HelloWorld/.gitignore @@ -0,0 +1,4 @@ +response.json +samconfig.toml +template.yaml +Makefile diff --git a/Examples/HelloWorld/Package.swift b/Examples/HelloWorld/Package.swift new file mode 100644 index 00000000..17d5e4a4 --- /dev/null +++ b/Examples/HelloWorld/Package.swift @@ -0,0 +1,54 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]) + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources" + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/HelloWorld/README.md b/Examples/HelloWorld/README.md new file mode 100644 index 00000000..c4eacfc2 --- /dev/null +++ b/Examples/HelloWorld/README.md @@ -0,0 +1,103 @@ +# Hello World + +This is a simple example of an AWS Lambda function that takes a `String` as input parameter and returns a `String` as response. + +## Code + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when an event triggers the Lambda function. + +The handler is `(event: String, context: LambdaContext)`. The function takes two arguments: +- the event argument is a `String`. It is the parameter passed when invoking the function. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function return value will be encoded as your Lambda function response. + +## Test locally + +You can test your function locally before deploying it to AWS Lambda. + +To start the local function, type the following commands: + +```bash +swift run +``` + +It will compile your code and start the local server. You know the local server is ready to accept connections when you see this message. + +```txt +Building for debugging... +[1/1] Write swift-version--644A47CB88185983.txt +Build of product 'MyLambda' complete! (0.31s) +2025-01-29T12:44:48+0100 info LocalServer : host="127.0.0.1" port=7000 [AWSLambdaRuntime] Server started and listening +``` + +Then, from another Terminal, send your payload with `curl`. Note that the payload must be a valid JSON string. In the case of this function that accepts a simple String, it means the String must be wrapped in between double quotes. + +```bash +curl -d '"seb"' http://127.0.0.1:7000/invoke +"Hello seb" +``` + +> [!IMPORTANT] +> The local server is only available in `DEBUG` mode. It will not start with `swift -c release run`. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip` + +## Deploy + +Here is how to deploy using the `aws` command line. + +```bash +aws lambda create-function \ +--function-name MyLambda \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam:::role/lambda_basic_execution +``` + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +Be sure to replace with your actual AWS account ID (for example: 012345678901). + +## Invoke your Lambda function + +To invoke the Lambda function, use this `aws` command line. + +```bash +aws lambda invoke \ +--function-name MyLambda \ +--payload $(echo \"Seb\" | base64) \ +out.txt && cat out.txt && rm out.txt +``` + +Note that the payload is expected to be a valid JSON string, hence the surroundings quotes (`"`). + +This should output the following result. + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} +"Hello Seb" +``` + +## Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name MyLambda +``` \ No newline at end of file diff --git a/Examples/LambdaFunctions/Sources/HelloWorld/main.swift b/Examples/HelloWorld/Sources/main.swift similarity index 66% rename from Examples/LambdaFunctions/Sources/HelloWorld/main.swift rename to Examples/HelloWorld/Sources/main.swift index 7535da97..5aab1a79 100644 --- a/Examples/LambdaFunctions/Sources/HelloWorld/main.swift +++ b/Examples/HelloWorld/Sources/main.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -14,7 +14,11 @@ import AWSLambdaRuntime -// introductory example, the obligatory "hello, world!" -Lambda.run { (_: Lambda.Context, _: String, callback: (Result) -> Void) in - callback(.success("hello, world!")) +// in this example we are receiving and responding with strings + +let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + "Hello \(event)" } + +try await runtime.run() diff --git a/Examples/LambdaFunctions/.dockerignore b/Examples/LambdaFunctions/.dockerignore deleted file mode 100644 index 24e5b0a1..00000000 --- a/Examples/LambdaFunctions/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.build diff --git a/Examples/LambdaFunctions/Dockerfile b/Examples/LambdaFunctions/Dockerfile deleted file mode 100644 index d5315703..00000000 --- a/Examples/LambdaFunctions/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM swift:5.2-amazonlinux2 - -RUN yum -y install zip diff --git a/Examples/LambdaFunctions/Package.swift b/Examples/LambdaFunctions/Package.swift deleted file mode 100644 index ae79d287..00000000 --- a/Examples/LambdaFunctions/Package.swift +++ /dev/null @@ -1,46 +0,0 @@ -// swift-tools-version:5.2 - -import PackageDescription - -let package = Package( - name: "swift-aws-lambda-runtime-samples", - platforms: [ - .macOS(.v10_13), - ], - products: [ - // introductory example - .executable(name: "HelloWorld", targets: ["HelloWorld"]), - // good for benchmarking - .executable(name: "Benchmark", targets: ["Benchmark"]), - // demonstrate different types of error handling - .executable(name: "ErrorHandling", targets: ["ErrorHandling"]), - // demostrate how to integrate with AWS API Gateway - .executable(name: "APIGateway", targets: ["APIGateway"]), - // fully featured example with domain specific business logic - .executable(name: "CurrencyExchange", targets: ["CurrencyExchange"]), - ], - dependencies: [ - // this is the dependency on the swift-aws-lambda-runtime library - // in real-world projects this would say - // .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0") - .package(name: "swift-aws-lambda-runtime", path: "../.."), - ], - targets: [ - .target(name: "HelloWorld", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ]), - .target(name: "Benchmark", dependencies: [ - .product(name: "AWSLambdaRuntimeCore", package: "swift-aws-lambda-runtime"), - ]), - .target(name: "ErrorHandling", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ]), - .target(name: "APIGateway", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"), - ]), - .target(name: "CurrencyExchange", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ]), - ] -) diff --git a/Examples/LambdaFunctions/README.md b/Examples/LambdaFunctions/README.md deleted file mode 100644 index 3c46a092..00000000 --- a/Examples/LambdaFunctions/README.md +++ /dev/null @@ -1,177 +0,0 @@ -# Lambda Functions Examples - -This sample project is a collection of Lambda functions that demonstrates -how to write a simple Lambda function in Swift, and how to package and deploy it -to the AWS Lambda platform. - -The scripts are prepared to work from the `LambdaFunctions` folder. - -``` -git clone https://github.com/swift-server/swift-aws-lambda-runtime.git -cd swift-aws-lambda-runtime/Examples/LambdaFunctions -``` - -Note: The example scripts assume you have [jq](https://stedolan.github.io/jq/download/) command line tool installed. - -## Deployment instructions using AWS CLI - -Steps to deploy this sample to AWS Lambda using the AWS CLI: - -1. Login to AWS Console and create an AWS Lambda with the following settings: - * Runtime: Custom runtime - * Handler: Can be any string, does not matter in this case - -2. Build, package and deploy the Lambda - - ``` - ./scripts/deploy.sh - ``` - - Notes: - - This script assumes you have AWS CLI installed and credentials setup in `~/.aws/credentials`. - - The default lambda function name is `SwiftSample`. You can specify a different one updating `lambda_name` in `deploy.sh` - - Update `s3_bucket=swift-lambda-test` in `deploy.sh` before running (AWS S3 buckets require a unique global name) - - Both lambda function and S3 bucket must exist before deploying for the first time. - -### Deployment instructions using AWS SAM (Serverless Application Model) - -AWS [Serverless Application Model](https://aws.amazon.com/serverless/sam/) (SAM) is an open-source framework for building serverless applications. This framework allows you to easily deploy other AWS resources and more complex deployment mechanisms such a CI pipelines. - -***Note:*** Deploying using SAM will automatically create resources within your AWS account. Charges may apply for these resources. - -To use SAM to deploy this sample to AWS: - -1. Install the AWS CLI by following the [instructions](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html). - -2. Install SAM CLI by following the [instructions](https://aws.amazon.com/serverless/sam/). - -3. Build, package and deploy the Lambda - - ``` - ./scripts/sam-deploy.sh --guided - ``` - -The script will ask you which sample Lambda you wish to deploy. It will then guide you through the SAM setup process. - - ``` - Setting default arguments for 'sam deploy' - ========================================= - Stack Name [sam-app]: swift-aws-lambda-runtime-sample - AWS Region [us-east-1]: - #Shows you resources changes to be deployed and require a 'Y' to initiate deploy - Confirm changes before deploy [y/N]: Y - #SAM needs permission to be able to create roles to connect to the resources in your template - Allow SAM CLI IAM role creation [Y/n]: Y - Save arguments to samconfig.toml [Y/n]: Y - ``` - -If you said yes to confirm changes, SAM will ask you to accept changes to the infrastructure you are setting up. For more on this, see [Cloud Formation](https://aws.amazon.com/cloudformation/). - -The `sam-deploy` script passes through any parameters to the SAM deploy command. - -4. Subsequent deploys can just use the command minus the `guided` parameter: - - ``` - ./scripts/sam-deploy.sh - ``` - -The script will ask you which sample Lambda you wish to deploy. If you are deploying a different sample lambda, the deploy process will pull down the previous Lambda. - -SAM will still ask you to confirm changes if you said yes to that initially. - -5. Testing - -For the API Gateway sample: - -The SAM template will provide an output labelled `LambdaApiGatewayEndpoint` which you can use to test the Lambda. For example: - - ``` - curl <> - ``` - -***Warning:*** This SAM template is only intended as a sample and creates a publicly accessible HTTP endpoint. - -For all other samples use the AWS Lambda console. - -### Deployment instructions using Serverless Framework (serverless.com) - -[Serverless framework](https://www.serverless.com/open-source/) (Serverless) is a provider agnostic, open-source framework for building serverless applications. This framework allows you to easily deploy other AWS resources and more complex deployment mechanisms such a CI pipelines. Serverless Framework offers solutions for not only deploying but also testing, monitoring, alerting, and security and is widely adopted by the industry and offers along the open-source version a paid one. - -***Note:*** Deploying using Serverless will automatically create resources within your AWS account. Charges may apply for these resources. - -To use Serverless to deploy this sample to AWS: - -1. Install the AWS CLI by following the [instructions](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html). - -2. Install Serverless by following the [instructions](https://www.serverless.com/framework/docs/getting-started/). -If you already have installed be sure you have the latest version. -The examples have been tested with the version 1.72.0. - -``` -Serverless --version -Framework Core: 1.72.0 (standalone) -Plugin: 3.6.13 -SDK: 2.3.1 -Components: 2.30.12 -``` - -3. Build, package and deploy the Lambda - - ``` - ./scripts/serverless-deploy.sh - ``` - -The script will ask you which sample Lambda you wish to deploy. - -The `serverless-deploy.sh` script passes through any parameters to the Serverless deploy command. - -4. Testing - -For the APIGateway sample: - -The Serverless template will provide an endpoint which you can use to test the Lambda. - -Outuput example: - -``` -... -... -Serverless: Stack update finished... -Service Information -service: apigateway-swift-aws -stage: dev -region: us-east-1 -stack: apigateway-swift-aws-dev -resources: 12 -api keys: - None -endpoints: - GET - https://r39lvhfng3.execute-api.us-east-1.amazonaws.com/api -functions: - httpGet: apigateway-swift-aws-dev-httpGet -layers: - None - -Stack Outputs -HttpGetLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:XXXXXXXXX:function:apigateway-swift-aws-dev-httpGet:1 -ServerlessDeploymentBucketName: apigateway-swift-aws-dev-serverlessdeploymentbuck-ud51msgcrj1e -HttpApiUrl: https://r39lvhfng3.execute-api.us-east-1.amazonaws.com -``` - -For example: - - ``` - curl https://r39lvhfng3.execute-api.us-east-1.amazonaws.com/api - ``` - -***Warning:*** This Serverless template is only intended as a sample and creates a publicly accessible HTTP endpoint. - -For all other samples use the AWS Lambda console. - -4. Remove - - ``` - ./scripts/serverless-remove.sh - ``` - -The script will ask you which sample Lambda you wish to remove from the previous depolyment. \ No newline at end of file diff --git a/Examples/LambdaFunctions/Sources/APIGateway/main.swift b/Examples/LambdaFunctions/Sources/APIGateway/main.swift deleted file mode 100644 index abe0f5a5..00000000 --- a/Examples/LambdaFunctions/Sources/APIGateway/main.swift +++ /dev/null @@ -1,34 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaEvents -import AWSLambdaRuntime -import NIO - -// MARK: - Run Lambda - -Lambda.run(APIGatewayProxyLambda()) - -// MARK: - Handler, Request and Response - -// FIXME: Use proper Event abstractions once added to AWSLambdaRuntime -struct APIGatewayProxyLambda: EventLoopLambdaHandler { - public typealias In = APIGateway.V2.Request - public typealias Out = APIGateway.V2.Response - - public func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { - context.logger.debug("hello, api gateway!") - return context.eventLoop.makeSucceededFuture(APIGateway.V2.Response(statusCode: .ok, body: "hello, world!")) - } -} diff --git a/Examples/LambdaFunctions/Sources/Benchmark/main.swift b/Examples/LambdaFunctions/Sources/Benchmark/main.swift deleted file mode 100644 index 6e902a56..00000000 --- a/Examples/LambdaFunctions/Sources/Benchmark/main.swift +++ /dev/null @@ -1,31 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntimeCore -import NIO - -// If you would like to benchmark Swift's Lambda Runtime, -// use this example which is more performant. -// `EventLoopLambdaHandler` does not offload the Lambda processing to a separate thread -// while the closure-based handlers do. -Lambda.run(BenchmarkHandler()) - -struct BenchmarkHandler: EventLoopLambdaHandler { - typealias In = String - typealias Out = String - - func handle(context: Lambda.Context, event: String) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture("hello, world!") - } -} diff --git a/Examples/LambdaFunctions/Sources/CurrencyExchange/main.swift b/Examples/LambdaFunctions/Sources/CurrencyExchange/main.swift deleted file mode 100644 index 22c30563..00000000 --- a/Examples/LambdaFunctions/Sources/CurrencyExchange/main.swift +++ /dev/null @@ -1,247 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntime -import Dispatch -import Foundation -#if canImport(FoundationNetworking) && canImport(FoundationXML) -import FoundationNetworking -import FoundationXML -#endif -import Logging - -// MARK: - Run Lambda - -Lambda.run { (context: Lambda.Context, _: Request, callback: @escaping (Result<[Exchange], Error>) -> Void) in - let calculator = ExchangeRatesCalculator() - calculator.run(logger: context.logger, callback: callback) -} - -// MARK: - Business Logic - -// This is a contrived example performing currency exchange rate lookup and conversion using URLSession and XML parsing -struct ExchangeRatesCalculator { - static let currencies = ["EUR", "USD", "JPY"] - static let currenciesEmojies = [ - "EUR": "💶", - "JPY": "💴", - "USD": "💵", - ] - - let locale: Locale - let calendar: Calendar - - init() { - // This is data from HMRC, the UK tax authority. Therefore we want to use their locale when interpreting data from the server. - self.locale = Locale(identifier: "en_GB") - // Use the UK calendar, not the system one. - var calendar = self.locale.calendar - calendar.timeZone = TimeZone(identifier: "UTC")! - self.calendar = calendar - } - - func run(logger: Logger, callback: @escaping (Result<[Exchange], Swift.Error>) -> Void) { - let startDate = Date() - let months = (1 ... 12).map { - self.calendar.date(byAdding: DateComponents(month: -$0), to: startDate)! - } - - self.download(logger: logger, - months: months, - monthIndex: months.startIndex, - currencies: Self.currencies, - state: [:]) { result in - - switch result { - case .failure(let error): - return callback(.failure(error)) - case .success(let downloadedDataByMonth): - logger.debug("Downloads complete") - - var result = [Exchange]() - var previousData: [String: Decimal?] = [:] - for (_, exchangeRateData) in downloadedDataByMonth.filter({ $1.period != nil }).sorted(by: { $0.key < $1.key }) { - for (currencyCode, rate) in exchangeRateData.ratesByCurrencyCode.sorted(by: { $0.key < $1.key }) { - if let rate = rate, let currencyEmoji = Self.currenciesEmojies[currencyCode] { - let change: Exchange.Change - switch previousData[currencyCode] { - case .some(.some(let previousRate)) where rate > previousRate: - change = .up - case .some(.some(let previousRate)) where rate < previousRate: - change = .down - case .some(.some(let previousRate)) where rate == previousRate: - change = .none - default: - change = .unknown - } - result.append(Exchange(date: exchangeRateData.period!.start, - from: .init(symbol: "GBP", emoji: "💷"), - to: .init(symbol: currencyCode, emoji: currencyEmoji), - rate: rate, - change: change)) - } - } - previousData = exchangeRateData.ratesByCurrencyCode - } - callback(.success(result)) - } - } - } - - private func download(logger: Logger, - months: [Date], - monthIndex: Array.Index, - currencies: [String], - state: [Date: ExchangeRates], - callback: @escaping ((Result<[Date: ExchangeRates], Swift.Error>) -> Void)) { - if monthIndex == months.count { - return callback(.success(state)) - } - - var newState = state - - let month = months[monthIndex] - let url = self.exchangeRatesURL(forMonthContaining: month) - logger.debug("requesting exchange rate from \(url)") - let dataTask = URLSession.shared.dataTask(with: url) { data, _, error in - do { - guard let data = data else { - throw error! - } - let exchangeRates = try self.parse(data: data, currencyCodes: Set(currencies)) - newState[month] = exchangeRates - logger.debug("Finished downloading month: \(month)") - if let period = exchangeRates.period { - logger.debug("Got data covering period: \(period)") - } - } catch { - return callback(.failure(error)) - } - self.download(logger: logger, - months: months, - monthIndex: monthIndex.advanced(by: 1), - currencies: currencies, - state: newState, - callback: callback) - } - dataTask.resume() - } - - private func parse(data: Data, currencyCodes: Set) throws -> ExchangeRates { - let document = try XMLDocument(data: data) - let dateFormatter = DateFormatter() - dateFormatter.timeZone = TimeZone(identifier: "Etc/UTC")! - dateFormatter.dateFormat = "dd/MMM/yy" - let interval: DateInterval? - if let period = try document.nodes(forXPath: "/exchangeRateMonthList/@Period").first?.stringValue, - period.count == 26 { - // "01/Sep/2018 to 30/Sep/2018" - let startString = period[period.startIndex ..< period.index(period.startIndex, offsetBy: 11)] - let to = period[startString.endIndex ..< period.index(startString.endIndex, offsetBy: 4)] - let endString = period[to.endIndex ..< period.index(to.endIndex, offsetBy: 11)] - if let startDate = dateFormatter.date(from: String(startString)), - let startDay = calendar.dateInterval(of: .day, for: startDate), - to == " to ", - let endDate = dateFormatter.date(from: String(endString)), - let endDay = calendar.dateInterval(of: .day, for: endDate) { - interval = DateInterval(start: startDay.start, end: endDay.end) - } else { - interval = nil - } - } else { - interval = nil - } - - let ratesByCurrencyCode: [String: Decimal?] = Dictionary(uniqueKeysWithValues: try currencyCodes.map { - let xpathCurrency = $0.replacingOccurrences(of: "'", with: "'") - if let rateString = try document.nodes(forXPath: "/exchangeRateMonthList/exchangeRate/currencyCode[text()='\(xpathCurrency)']/../rateNew/text()").first?.stringValue, - // We must parse the decimal data using the UK locale, not the system one. - let rate = Decimal(string: rateString, locale: self.locale) { - return ($0, rate) - } else { - return ($0, nil) - } - }) - - return (period: interval, ratesByCurrencyCode: ratesByCurrencyCode) - } - - private func makeUTCDateFormatter(dateFormat: String) -> DateFormatter { - let utcTimeZone = TimeZone(identifier: "UTC")! - let result = DateFormatter() - result.locale = Locale(identifier: "en_US_POSIX") - result.timeZone = utcTimeZone - result.dateFormat = dateFormat - return result - } - - private func exchangeRatesURL(forMonthContaining date: Date) -> URL { - let exchangeRatesBaseURL = URL(string: "https://www.hmrc.gov.uk/softwaredevelopers/rates")! - let dateFormatter = self.makeUTCDateFormatter(dateFormat: "MMyy") - return exchangeRatesBaseURL.appendingPathComponent("exrates-monthly-\(dateFormatter.string(from: date)).xml") - } - - private typealias ExchangeRates = (period: DateInterval?, ratesByCurrencyCode: [String: Decimal?]) - - private struct Error: Swift.Error, CustomStringConvertible { - let description: String - } -} - -// MARK: - Request and Response - -struct Request: Decodable {} - -struct Exchange: Encodable { - @DateCoding - var date: Date - let from: Currency - let to: Currency - let rate: Decimal - let change: Change - - struct Currency: Encodable { - let symbol: String - let emoji: String - } - - enum Change: String, Encodable { - case up - case down - case none - case unknown - } - - @propertyWrapper - public struct DateCoding: Encodable { - public let wrappedValue: Date - - public init(wrappedValue: Date) { - self.wrappedValue = wrappedValue - } - - func encode(to encoder: Encoder) throws { - let string = Self.dateFormatter.string(from: self.wrappedValue) - var container = encoder.singleValueContainer() - try container.encode(string) - } - - private static var dateFormatter: ISO8601DateFormatter { - let dateFormatter = ISO8601DateFormatter() - dateFormatter.timeZone = TimeZone(identifier: "UTC")! - dateFormatter.formatOptions = [.withYear, .withMonth, .withDashSeparatorInDate] - return dateFormatter - } - } -} diff --git a/Examples/LambdaFunctions/Sources/ErrorHandling/main.swift b/Examples/LambdaFunctions/Sources/ErrorHandling/main.swift deleted file mode 100644 index 9f2fce2e..00000000 --- a/Examples/LambdaFunctions/Sources/ErrorHandling/main.swift +++ /dev/null @@ -1,101 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntime - -// MARK: - Run Lambda - -// switch over the error type "requested" by the request, and trigger such error accordingly -Lambda.run { (context: Lambda.Context, request: Request, callback: (Result) -> Void) in - switch request.error { - // no error here! - case .none: - callback(.success(Response(awsRequestID: context.requestID, requestID: request.requestID, status: .ok))) - // trigger a "managed" error - domain specific business logic failure - case .managed: - callback(.success(Response(awsRequestID: context.requestID, requestID: request.requestID, status: .error))) - // trigger an "unmanaged" error - an unexpected Swift Error triggered while processing the request - case .unmanaged(let error): - callback(.failure(UnmanagedError(description: error))) - // trigger a "fatal" error - a panic type error which will crash the process - case .fatal: - fatalError("crash!") - } -} - -// MARK: - Request and Response - -struct Request: Codable { - let requestID: String - let error: Error - - public init(requestID: String, error: Error? = nil) { - self.requestID = requestID - self.error = error ?? .none - } - - public enum Error: Codable, RawRepresentable { - case none - case managed - case unmanaged(String) - case fatal - - public init?(rawValue: String) { - switch rawValue { - case "none": - self = .none - case "managed": - self = .managed - case "fatal": - self = .fatal - default: - self = .unmanaged(rawValue) - } - } - - public var rawValue: String { - switch self { - case .none: - return "none" - case .managed: - return "managed" - case .fatal: - return "fatal" - case .unmanaged(let error): - return error - } - } - } -} - -struct Response: Codable { - let awsRequestID: String - let requestID: String - let status: Status - - public init(awsRequestID: String, requestID: String, status: Status) { - self.awsRequestID = awsRequestID - self.requestID = requestID - self.status = status - } - - public enum Status: Int, Codable { - case ok - case error - } -} - -struct UnmanagedError: Error { - let description: String -} diff --git a/Examples/LambdaFunctions/scripts/SAM/APIGateway-template.yml b/Examples/LambdaFunctions/scripts/SAM/APIGateway-template.yml deleted file mode 100644 index 7776921b..00000000 --- a/Examples/LambdaFunctions/scripts/SAM/APIGateway-template.yml +++ /dev/null @@ -1,30 +0,0 @@ -AWSTemplateFormatVersion : '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: A sample SAM template for deploying Lambda functions. - -Resources: -# APIGateway Function - apiGatewayFunction: - Type: AWS::Serverless::Function - Properties: - Handler: Provided - Runtime: provided - CodeUri: ../../.build/lambda/APIGateway/lambda.zip -# Add an API Gateway event source for the Lambda - Events: - HttpGet: - Type: HttpApi - Properties: - ApiId: !Ref lambdaApiGateway - Path: '/samples/apig' - Method: GET -# Instructs new versions to be published to an alias named "live". - AutoPublishAlias: live - - lambdaApiGateway: - Type: AWS::Serverless::HttpApi - -Outputs: - LambdaApiGatewayEndpoint: - Description: 'API Gateway endpoint URL.' - Value: !Sub 'https://${lambdaApiGateway}.execute-api.${AWS::Region}.amazonaws.com/samples/apig' diff --git a/Examples/LambdaFunctions/scripts/SAM/Benchmark-template.yml b/Examples/LambdaFunctions/scripts/SAM/Benchmark-template.yml deleted file mode 100644 index 55100d12..00000000 --- a/Examples/LambdaFunctions/scripts/SAM/Benchmark-template.yml +++ /dev/null @@ -1,14 +0,0 @@ -AWSTemplateFormatVersion : '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: A sample SAM template for deploying Lambda functions. - -Resources: -# Benchmark Function - benchmarkFunction: - Type: AWS::Serverless::Function - Properties: - Handler: Provided - Runtime: provided - CodeUri: ../../.build/lambda/Benchmark/lambda.zip -# Instructs new versions to be published to an alias named "live". - AutoPublishAlias: live diff --git a/Examples/LambdaFunctions/scripts/SAM/CurrencyExchange-template.yml b/Examples/LambdaFunctions/scripts/SAM/CurrencyExchange-template.yml deleted file mode 100644 index b7b4f250..00000000 --- a/Examples/LambdaFunctions/scripts/SAM/CurrencyExchange-template.yml +++ /dev/null @@ -1,15 +0,0 @@ -AWSTemplateFormatVersion : '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: A sample SAM template for deploying Lambda functions. - -Resources: -# CurrencyExchange Function - currencyExchangeFunction: - Type: AWS::Serverless::Function - Properties: - Handler: Provided - Runtime: provided - CodeUri: ../../.build/lambda/CurrencyExchange/lambda.zip - Timeout: 300 -# Instructs new versions to be published to an alias named "live". - AutoPublishAlias: live diff --git a/Examples/LambdaFunctions/scripts/SAM/ErrorHandling-template.yml b/Examples/LambdaFunctions/scripts/SAM/ErrorHandling-template.yml deleted file mode 100644 index c277ec72..00000000 --- a/Examples/LambdaFunctions/scripts/SAM/ErrorHandling-template.yml +++ /dev/null @@ -1,14 +0,0 @@ -AWSTemplateFormatVersion : '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: A sample SAM template for deploying Lambda functions. - -Resources: -# ErrorHandling Function - errorHandlingFunction: - Type: AWS::Serverless::Function - Properties: - Handler: Provided - Runtime: provided - CodeUri: ../../.build/lambda/ErrorHandling/lambda.zip -# Instructs new versions to be published to an alias named "live". - AutoPublishAlias: live diff --git a/Examples/LambdaFunctions/scripts/SAM/HelloWorld-template.yml b/Examples/LambdaFunctions/scripts/SAM/HelloWorld-template.yml deleted file mode 100644 index 22b09df7..00000000 --- a/Examples/LambdaFunctions/scripts/SAM/HelloWorld-template.yml +++ /dev/null @@ -1,14 +0,0 @@ -AWSTemplateFormatVersion : '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: A sample SAM template for deploying Lambda functions. - -Resources: -# HelloWorld Function - helloWorldFunction: - Type: AWS::Serverless::Function - Properties: - Handler: Provided - Runtime: provided - CodeUri: ../../.build/lambda/HelloWorld/lambda.zip -# Instructs new versions to be published to an alias named "live". - AutoPublishAlias: live diff --git a/Examples/LambdaFunctions/scripts/build-and-package.sh b/Examples/LambdaFunctions/scripts/build-and-package.sh deleted file mode 100755 index 4e45c486..00000000 --- a/Examples/LambdaFunctions/scripts/build-and-package.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu - -executable=$1 -workspace="$(pwd)/../.." - -echo "-------------------------------------------------------------------------" -echo "preparing docker build image" -echo "-------------------------------------------------------------------------" -docker build . -t builder -echo "done" - -echo "-------------------------------------------------------------------------" -echo "building \"$executable\" lambda" -echo "-------------------------------------------------------------------------" -docker run --rm -v "$workspace":/workspace -w /workspace/Examples/LambdaFunctions builder \ - bash -cl "swift build --product $executable -c release" -echo "done" - -echo "-------------------------------------------------------------------------" -echo "packaging \"$executable\" lambda" -echo "-------------------------------------------------------------------------" -docker run --rm -v "$workspace":/workspace -w /workspace/Examples/LambdaFunctions builder \ - bash -cl "./scripts/package.sh $executable" -echo "done" diff --git a/Examples/LambdaFunctions/scripts/config.sh b/Examples/LambdaFunctions/scripts/config.sh deleted file mode 100755 index d4ab9f6f..00000000 --- a/Examples/LambdaFunctions/scripts/config.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -DIR="$(cd "$(dirname "$0")" && pwd)" -executables=( $(swift package dump-package | sed -e 's|: null|: ""|g' | jq '.products[] | (select(.type.executable)) | .name' | sed -e 's|"||g') ) - -if [[ ${#executables[@]} = 0 ]]; then - echo "no executables found" - exit 1 -elif [[ ${#executables[@]} = 1 ]]; then - executable=${executables[0]} -elif [[ ${#executables[@]} > 1 ]]; then - echo "multiple executables found:" - for executable in ${executables[@]}; do - echo " * $executable" - done - echo "" - read -p "select which executables to deploy: " executable -fi - -echo "-------------------------------------------------------------------------" -echo "configuration" -echo "-------------------------------------------------------------------------" -echo "current dir: $DIR" -echo "executable: $executable" -echo "-------------------------------------------------------------------------" diff --git a/Examples/LambdaFunctions/scripts/deploy.sh b/Examples/LambdaFunctions/scripts/deploy.sh deleted file mode 100755 index 3720b4d0..00000000 --- a/Examples/LambdaFunctions/scripts/deploy.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu - -DIR="$(cd "$(dirname "$0")" && pwd)" -source $DIR/config.sh - -workspace="$DIR/../.." - -echo -e "\ndeploying $executable" - -$DIR/build-and-package.sh "$executable" - -echo "-------------------------------------------------------------------------" -echo "uploading \"$executable\" lambda to AWS S3" -echo "-------------------------------------------------------------------------" - -read -p "S3 bucket name to upload zip file (must exist in AWS S3): " s3_bucket -s3_bucket=${s3_bucket:-swift-lambda-test} # default for easy testing - -aws s3 cp ".build/lambda/$executable/lambda.zip" "s3://$s3_bucket/" - -echo "-------------------------------------------------------------------------" -echo "updating AWS Lambda to use \"$executable\"" -echo "-------------------------------------------------------------------------" - -read -p "Lambda Function name (must exist in AWS Lambda): " lambda_name -lambda_name=${lambda_name:-SwiftSample} # default for easy testing - -aws lambda update-function-code --function "$lambda_name" --s3-bucket "$s3_bucket" --s3-key lambda.zip diff --git a/Examples/LambdaFunctions/scripts/package.sh b/Examples/LambdaFunctions/scripts/package.sh deleted file mode 100755 index 17d5853b..00000000 --- a/Examples/LambdaFunctions/scripts/package.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu - -executable=$1 - -target=".build/lambda/$executable" -rm -rf "$target" -mkdir -p "$target" -cp ".build/release/$executable" "$target/" -# add the target deps based on ldd -ldd ".build/release/$executable" | grep swift | awk '{print $3}' | xargs cp -Lv -t "$target" -cd "$target" -ln -s "$executable" "bootstrap" -zip --symlinks lambda.zip * diff --git a/Examples/LambdaFunctions/scripts/sam-deploy.sh b/Examples/LambdaFunctions/scripts/sam-deploy.sh deleted file mode 100755 index d87d966d..00000000 --- a/Examples/LambdaFunctions/scripts/sam-deploy.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -DIR="$(cd "$(dirname "$0")" && pwd)" -source $DIR/config.sh - -echo -e "\ndeploying $executable" - -$DIR/build-and-package.sh "$executable" - -echo "-------------------------------------------------------------------------" -echo "deploying using SAM" -echo "-------------------------------------------------------------------------" - -sam deploy --template "./scripts/SAM/$executable-template.yml" $@ diff --git a/Examples/LambdaFunctions/scripts/serverless-remove.sh b/Examples/LambdaFunctions/scripts/serverless-remove.sh deleted file mode 100755 index 262c07cb..00000000 --- a/Examples/LambdaFunctions/scripts/serverless-remove.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu - -DIR="$(cd "$(dirname "$0")" && pwd)" -source $DIR/config.sh - -echo -e "\nremoving $executable" - -echo "-------------------------------------------------------------------------" -echo "removing using Serverless" -echo "-------------------------------------------------------------------------" - -serverless remove --config "./scripts/serverless/$executable-template.yml" --stage dev -v diff --git a/Examples/LambdaFunctions/scripts/serverless/APIGateway-template.yml b/Examples/LambdaFunctions/scripts/serverless/APIGateway-template.yml deleted file mode 100644 index 6787ad0f..00000000 --- a/Examples/LambdaFunctions/scripts/serverless/APIGateway-template.yml +++ /dev/null @@ -1,28 +0,0 @@ -service: apigateway-swift-aws - -package: - artifact: .build/lambda/APIGateway/lambda.zip - -provider: - name: aws - httpApi: - payload: '2.0' - runtime: provided - logs: - httpApi: true - iamRoleStatements: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: "*" - -functions: - httpGet: - handler: APIGateway - memorySize: 128 - events: - - httpApi: - method: GET - path: /api \ No newline at end of file diff --git a/Examples/LambdaFunctions/scripts/serverless/Benchmark-template.yml b/Examples/LambdaFunctions/scripts/serverless/Benchmark-template.yml deleted file mode 100644 index 74099441..00000000 --- a/Examples/LambdaFunctions/scripts/serverless/Benchmark-template.yml +++ /dev/null @@ -1,20 +0,0 @@ -service: benchmark-swift-aws - -package: - artifact: .build/lambda/Benchmark/lambda.zip - -provider: - name: aws - runtime: provided - iamRoleStatements: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: "*" - -functions: - benchmarkFunction: - handler: Benchmark - memorySize: 128 \ No newline at end of file diff --git a/Examples/LambdaFunctions/scripts/serverless/CurrencyExchange-template.yml b/Examples/LambdaFunctions/scripts/serverless/CurrencyExchange-template.yml deleted file mode 100644 index 7e5c6b09..00000000 --- a/Examples/LambdaFunctions/scripts/serverless/CurrencyExchange-template.yml +++ /dev/null @@ -1,20 +0,0 @@ -service: currency-swift-aws - -package: - artifact: .build/lambda/CurrencyExchange/lambda.zip - -provider: - name: aws - runtime: provided - iamRoleStatements: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: "*" - -functions: - currencyExchangeFunction: - handler: CurrencyExchange - memorySize: 128 \ No newline at end of file diff --git a/Examples/LambdaFunctions/scripts/serverless/ErrorHandling-template.yml b/Examples/LambdaFunctions/scripts/serverless/ErrorHandling-template.yml deleted file mode 100644 index 367be490..00000000 --- a/Examples/LambdaFunctions/scripts/serverless/ErrorHandling-template.yml +++ /dev/null @@ -1,20 +0,0 @@ -service: errorhandling-swift-aws - -package: - artifact: .build/lambda/ErrorHandling/lambda.zip - -provider: - name: aws - runtime: provided - iamRoleStatements: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: "*" - -functions: - errorHandlingFunction: - handler: ErrorHandling - memorySize: 128 \ No newline at end of file diff --git a/Examples/LambdaFunctions/scripts/serverless/HelloWorld-template.yml b/Examples/LambdaFunctions/scripts/serverless/HelloWorld-template.yml deleted file mode 100644 index 276f9909..00000000 --- a/Examples/LambdaFunctions/scripts/serverless/HelloWorld-template.yml +++ /dev/null @@ -1,20 +0,0 @@ -service: helloworld-swift-aws - -package: - artifact: .build/lambda/HelloWorld/lambda.zip - -provider: - name: aws - runtime: provided - iamRoleStatements: - - Effect: Allow - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Resource: "*" - -functions: - hello: - handler: HelloWorld - memorySize: 128 \ No newline at end of file diff --git a/Examples/LocalDebugging/Example.xcworkspace/contents.xcworkspacedata b/Examples/LocalDebugging/Example.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index e42d285c..00000000 --- a/Examples/LocalDebugging/Example.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - diff --git a/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/Examples/LocalDebugging/Example.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/Examples/LocalDebugging/MyApp/MyApp.xcodeproj/project.pbxproj b/Examples/LocalDebugging/MyApp/MyApp.xcodeproj/project.pbxproj deleted file mode 100644 index fcf97682..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp.xcodeproj/project.pbxproj +++ /dev/null @@ -1,365 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 52; - objects = { - -/* Begin PBXBuildFile section */ - F7B6C1FE246121E800607A89 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B6C1FD246121E800607A89 /* AppDelegate.swift */; }; - F7B6C200246121E800607A89 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B6C1FF246121E800607A89 /* SceneDelegate.swift */; }; - F7B6C202246121E800607A89 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B6C201246121E800607A89 /* ContentView.swift */; }; - F7B6C204246121E900607A89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7B6C203246121E900607A89 /* Assets.xcassets */; }; - F7B6C207246121E900607A89 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7B6C206246121E900607A89 /* Preview Assets.xcassets */; }; - F7B6C20A246121E900607A89 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7B6C208246121E900607A89 /* LaunchScreen.storyboard */; }; - F7EA8D6024762E4000B0D09E /* Shared in Frameworks */ = {isa = PBXBuildFile; productRef = F7EA8D5F24762E4000B0D09E /* Shared */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - F7B6C1FA246121E800607A89 /* MyApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - F7B6C1FD246121E800607A89 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - F7B6C1FF246121E800607A89 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - F7B6C201246121E800607A89 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - F7B6C203246121E900607A89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - F7B6C206246121E900607A89 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - F7B6C209246121E900607A89 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - F7B6C20B246121E900607A89 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - F7B6C1F7246121E800607A89 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F7EA8D6024762E4000B0D09E /* Shared in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - F7B6C1F1246121E800607A89 = { - isa = PBXGroup; - children = ( - F7B6C1FC246121E800607A89 /* MyApp */, - F7B6C1FB246121E800607A89 /* Products */, - F7EA8D5E24762E4000B0D09E /* Frameworks */, - ); - sourceTree = ""; - }; - F7B6C1FB246121E800607A89 /* Products */ = { - isa = PBXGroup; - children = ( - F7B6C1FA246121E800607A89 /* MyApp.app */, - ); - name = Products; - sourceTree = ""; - }; - F7B6C1FC246121E800607A89 /* MyApp */ = { - isa = PBXGroup; - children = ( - F7B6C1FD246121E800607A89 /* AppDelegate.swift */, - F7B6C1FF246121E800607A89 /* SceneDelegate.swift */, - F7B6C201246121E800607A89 /* ContentView.swift */, - F7B6C203246121E900607A89 /* Assets.xcassets */, - F7B6C208246121E900607A89 /* LaunchScreen.storyboard */, - F7B6C20B246121E900607A89 /* Info.plist */, - F7B6C205246121E900607A89 /* Preview Content */, - ); - path = MyApp; - sourceTree = ""; - }; - F7B6C205246121E900607A89 /* Preview Content */ = { - isa = PBXGroup; - children = ( - F7B6C206246121E900607A89 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - F7EA8D5E24762E4000B0D09E /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - F7B6C1F9246121E800607A89 /* MyApp */ = { - isa = PBXNativeTarget; - buildConfigurationList = F7B6C20E246121E900607A89 /* Build configuration list for PBXNativeTarget "MyApp" */; - buildPhases = ( - F7B6C1F6246121E800607A89 /* Sources */, - F7B6C1F7246121E800607A89 /* Frameworks */, - F7B6C1F8246121E800607A89 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = MyApp; - packageProductDependencies = ( - F7EA8D5F24762E4000B0D09E /* Shared */, - ); - productName = MyApp; - productReference = F7B6C1FA246121E800607A89 /* MyApp.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - F7B6C1F2246121E800607A89 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1140; - LastUpgradeCheck = 1140; - ORGANIZATIONNAME = "Tom Doron"; - TargetAttributes = { - F7B6C1F9246121E800607A89 = { - CreatedOnToolsVersion = 11.4.1; - }; - }; - }; - buildConfigurationList = F7B6C1F5246121E800607A89 /* Build configuration list for PBXProject "MyApp" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = F7B6C1F1246121E800607A89; - productRefGroup = F7B6C1FB246121E800607A89 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - F7B6C1F9246121E800607A89 /* MyApp */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - F7B6C1F8246121E800607A89 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F7B6C20A246121E900607A89 /* LaunchScreen.storyboard in Resources */, - F7B6C207246121E900607A89 /* Preview Assets.xcassets in Resources */, - F7B6C204246121E900607A89 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - F7B6C1F6246121E800607A89 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F7B6C1FE246121E800607A89 /* AppDelegate.swift in Sources */, - F7B6C200246121E800607A89 /* SceneDelegate.swift in Sources */, - F7B6C202246121E800607A89 /* ContentView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - F7B6C208246121E900607A89 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F7B6C209246121E900607A89 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - F7B6C20C246121E900607A89 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - F7B6C20D246121E900607A89 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - F7B6C20F246121E900607A89 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"MyApp/Preview Content\""; - ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = MyApp/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.apple.swift.MyApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - F7B6C210246121E900607A89 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"MyApp/Preview Content\""; - ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = MyApp/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.apple.swift.MyApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - F7B6C1F5246121E800607A89 /* Build configuration list for PBXProject "MyApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F7B6C20C246121E900607A89 /* Debug */, - F7B6C20D246121E900607A89 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F7B6C20E246121E900607A89 /* Build configuration list for PBXNativeTarget "MyApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F7B6C20F246121E900607A89 /* Debug */, - F7B6C210246121E900607A89 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCSwiftPackageProductDependency section */ - F7EA8D5F24762E4000B0D09E /* Shared */ = { - isa = XCSwiftPackageProductDependency; - productName = Shared; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = F7B6C1F2246121E800607A89 /* Project object */; -} diff --git a/Examples/LocalDebugging/MyApp/MyApp.xcodeproj/xcshareddata/xcschemes/MyApp.xcscheme b/Examples/LocalDebugging/MyApp/MyApp.xcodeproj/xcshareddata/xcschemes/MyApp.xcscheme deleted file mode 100644 index dc471464..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp.xcodeproj/xcshareddata/xcschemes/MyApp.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/LocalDebugging/MyApp/MyApp/AppDelegate.swift b/Examples/LocalDebugging/MyApp/MyApp/AppDelegate.swift deleted file mode 100644 index 068d2b7a..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/AppDelegate.swift +++ /dev/null @@ -1,37 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import UIKit - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } -} diff --git a/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 9221b9bb..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/Contents.json b/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/LocalDebugging/MyApp/MyApp/Base.lproj/LaunchScreen.storyboard b/Examples/LocalDebugging/MyApp/MyApp/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/LocalDebugging/MyApp/MyApp/ContentView.swift b/Examples/LocalDebugging/MyApp/MyApp/ContentView.swift deleted file mode 100644 index a88ff962..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/ContentView.swift +++ /dev/null @@ -1,91 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Shared -import SwiftUI - -struct ContentView: View { - @State var name: String = "" - @State var password: String = "" - @State var response: String = "" - - var body: some View { - VStack(alignment: .leading, spacing: 20) { - TextField("Username", text: $name) - SecureField("Password", text: $password) - Button( - action: self.register, - label: { - Text("Register") - .padding() - .foregroundColor(.white) - .background(Color.black) - .border(Color.black, width: 2) - } - ) - Text(response) - }.padding(100) - } - - func register() { - guard let url = URL(string: "http://localhost:7000/invoke") else { - fatalError("invalid url") - } - var request = URLRequest(url: url) - request.httpMethod = "POST" - - guard let jsonRequest = try? JSONEncoder().encode(Request(name: self.name, password: self.password)) else { - fatalError("encoding error") - } - request.httpBody = jsonRequest - - let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in - do { - if let error = error { - throw CommunicationError(reason: error.localizedDescription) - } - guard let httpResponse = response as? HTTPURLResponse else { - throw CommunicationError(reason: "invalid response, expected HTTPURLResponse") - } - guard httpResponse.statusCode == 200 else { - throw CommunicationError(reason: "invalid response code: \(httpResponse.statusCode)") - } - guard let data = data else { - throw CommunicationError(reason: "invald response, empty body") - } - let response = try JSONDecoder().decode(Response.self, from: data) - self.setResponse(response.message) - } catch { - self.setResponse("\(error)") - } - } - task.resume() - } - - func setResponse(_ text: String) { - DispatchQueue.main.async { - self.response = text - } - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} - -struct CommunicationError: Error { - let reason: String -} diff --git a/Examples/LocalDebugging/MyApp/MyApp/Info.plist b/Examples/LocalDebugging/MyApp/MyApp/Info.plist deleted file mode 100644 index 9742bf0f..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/Info.plist +++ /dev/null @@ -1,60 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/Examples/LocalDebugging/MyApp/MyApp/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/LocalDebugging/MyApp/MyApp/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/LocalDebugging/MyApp/MyApp/SceneDelegate.swift b/Examples/LocalDebugging/MyApp/MyApp/SceneDelegate.swift deleted file mode 100644 index 71e700d4..00000000 --- a/Examples/LocalDebugging/MyApp/MyApp/SceneDelegate.swift +++ /dev/null @@ -1,65 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import SwiftUI -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - - // Create the SwiftUI view that provides the window contents. - let contentView = ContentView() - - // Use a UIHostingController as window root view controller. - if let windowScene = scene as? UIWindowScene { - let window = UIWindow(windowScene: windowScene) - window.rootViewController = UIHostingController(rootView: contentView) - self.window = window - window.makeKeyAndVisible() - } - } - - func sceneDidDisconnect(_: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } -} diff --git a/Examples/LocalDebugging/MyLambda/.dockerignore b/Examples/LocalDebugging/MyLambda/.dockerignore deleted file mode 100644 index 24e5b0a1..00000000 --- a/Examples/LocalDebugging/MyLambda/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.build diff --git a/Examples/LocalDebugging/MyLambda/Dockerfile b/Examples/LocalDebugging/MyLambda/Dockerfile deleted file mode 100644 index d5315703..00000000 --- a/Examples/LocalDebugging/MyLambda/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM swift:5.2-amazonlinux2 - -RUN yum -y install zip diff --git a/Examples/LocalDebugging/MyLambda/Package.swift b/Examples/LocalDebugging/MyLambda/Package.swift deleted file mode 100644 index b7c4e7fe..00000000 --- a/Examples/LocalDebugging/MyLambda/Package.swift +++ /dev/null @@ -1,28 +0,0 @@ -// swift-tools-version:5.2 - -import PackageDescription - -let package = Package( - name: "MyLambda", - platforms: [ - .macOS(.v10_13), - ], - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - // this is the dependency on the swift-aws-lambda-runtime library - // in real-world projects this would say - // .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0") - .package(name: "swift-aws-lambda-runtime", path: "../../.."), - .package(name: "Shared", path: "../Shared"), - ], - targets: [ - .target( - name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "Shared", package: "Shared"), - ] - ), - ] -) diff --git a/Examples/LocalDebugging/MyLambda/Tests/LinuxMain.swift b/Examples/LocalDebugging/MyLambda/Tests/LinuxMain.swift deleted file mode 100644 index c46de763..00000000 --- a/Examples/LocalDebugging/MyLambda/Tests/LinuxMain.swift +++ /dev/null @@ -1,15 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -preconditionFailure("use `swift test --enable-test-discovery`") diff --git a/Examples/LocalDebugging/MyLambda/scripts/deploy.sh b/Examples/LocalDebugging/MyLambda/scripts/deploy.sh deleted file mode 100755 index 75be0ceb..00000000 --- a/Examples/LocalDebugging/MyLambda/scripts/deploy.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu - -executable=MyLambda -lambda_name=SwiftSample -s3_bucket=swift-lambda-test - -echo -e "\ndeploying $executable" - -echo "-------------------------------------------------------------------------" -echo "preparing docker build image" -echo "-------------------------------------------------------------------------" -docker build . -t builder -echo "done" - -echo "-------------------------------------------------------------------------" -echo "building \"$executable\" lambda" -echo "-------------------------------------------------------------------------" -docker run --rm -v `pwd`/../../..:/workspace -w /workspace/Examples/LocalDebugging/MyLambda builder \ - bash -cl "swift build --product $executable -c release" -echo "done" - -echo "-------------------------------------------------------------------------" -echo "packaging \"$executable\" lambda" -echo "-------------------------------------------------------------------------" -docker run --rm -v `pwd`:/workspace -w /workspace builder \ - bash -cl "./scripts/package.sh $executable" -echo "done" - -echo "-------------------------------------------------------------------------" -echo "uploading \"$executable\" lambda to s3" -echo "-------------------------------------------------------------------------" - -aws s3 cp .build/lambda/$executable/lambda.zip s3://$s3_bucket/ - -echo "-------------------------------------------------------------------------" -echo "updating \"$lambda_name\" to latest \"$executable\"" -echo "-------------------------------------------------------------------------" -aws lambda update-function-code --function $lambda_name --s3-bucket $s3_bucket --s3-key lambda.zip diff --git a/Examples/LocalDebugging/MyLambda/scripts/package.sh b/Examples/LocalDebugging/MyLambda/scripts/package.sh deleted file mode 100755 index 17d5853b..00000000 --- a/Examples/LocalDebugging/MyLambda/scripts/package.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu - -executable=$1 - -target=".build/lambda/$executable" -rm -rf "$target" -mkdir -p "$target" -cp ".build/release/$executable" "$target/" -# add the target deps based on ldd -ldd ".build/release/$executable" | grep swift | awk '{print $3}' | xargs cp -Lv -t "$target" -cd "$target" -ln -s "$executable" "bootstrap" -zip --symlinks lambda.zip * diff --git a/Examples/LocalDebugging/README.md b/Examples/LocalDebugging/README.md deleted file mode 100644 index 25ee92ba..00000000 --- a/Examples/LocalDebugging/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Local Debugging Example - -This sample project demonstrates how to write a simple Lambda function in Swift, -and how to use local debugging techniques that simulate how the Lambda function -would be invoked by the AWS Lambda Runtime engine. - -The example includes an Xcode workspace with three modules: - -1. [MyApp](MyApp) is a SwiftUI iOS application that calls the Lambda function. -2. [MyLambda](MyLambda) is a SwiftPM executable package for the Lambda function. -3. [Shared](Shared) is a SwiftPM library package used for shared code between the iOS application and the Lambda function, -such as the Request and Response model objects. - -The local debugging experience is achieved by running the Lambda function in the context of the -debug-only local lambda engine simulator which starts a local HTTP server enabling the communication -between the iOS application and the Lambda function over HTTP. - -To try out this example, open the workspace in Xcode and "run" the two targets, -using the relevant `MyLambda` and `MyApp` Xcode schemes. - -Start with running the `MyLambda` target. -* Switch to the `MyLambda` scheme and select the "My Mac" destination -* Set the `LOCAL_LAMBDA_SERVER_ENABLED` environment variable to `true` by editing the `MyLambda` scheme Run/Arguments options. -* Hit `Run` -* Once it is up you should see a log message in the Xcode console saying -`LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke` -which means the local emulator is up and receiving traffic on port `7000` and expecting events on the `/invoke` endpoint. - -Continue to run the `MyApp` target -* Switch to the `MyApp` scheme and select a simulator destination. -* Hit `Run` -* Once up, the application's UI should appear in the simulator allowing you -to interact with it. - -Once both targets are running, set up breakpoints in the iOS application or Lambda function to observe the system behavior. diff --git a/Examples/LocalDebugging/Shared/Package.swift b/Examples/LocalDebugging/Shared/Package.swift deleted file mode 100644 index 3ab44d3f..00000000 --- a/Examples/LocalDebugging/Shared/Package.swift +++ /dev/null @@ -1,16 +0,0 @@ -// swift-tools-version:5.2 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "Shared", - products: [ - .library(name: "Shared", targets: ["Shared"]), - ], - dependencies: [ - ], - targets: [ - .target(name: "Shared", dependencies: []), - ] -) diff --git a/Examples/LocalDebugging/Shared/Sources/Shared/Shared.swift b/Examples/LocalDebugging/Shared/Sources/Shared/Shared.swift deleted file mode 100644 index 8189eac3..00000000 --- a/Examples/LocalDebugging/Shared/Sources/Shared/Shared.swift +++ /dev/null @@ -1,35 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -public struct Request: Codable, CustomStringConvertible { - public let name: String - public let password: String - - public init(name: String, password: String) { - self.name = name - self.password = password - } - - public var description: String { - "name: \(self.name), password: ***" - } -} - -public struct Response: Codable { - public let message: String - - public init(message: String) { - self.message = message - } -} diff --git a/Examples/README.md b/Examples/README.md new file mode 100644 index 00000000..973df897 --- /dev/null +++ b/Examples/README.md @@ -0,0 +1,69 @@ +This directory contains example code for Lambda functions. + +## Pre-requisites + +- Ensure you have the Swift 6.x toolchain installed. You can [install Swift toolchains](https://www.swift.org/install/macos/) from Swift.org + +- When developing on macOS, be sure you use macOS 15 (Sequoia) or a more recent macOS version. + +- To build and archive your Lambda functions, you need to [install docker](https://docs.docker.com/desktop/install/mac-install/). + +- To deploy your Lambda functions and invoke them, you must have [an AWS account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) and [install and configure the `aws` command line](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). + +- Some examples are using [AWS SAM](https://aws.amazon.com/serverless/sam/). Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) before deploying these examples. + +- Some examples are using the [AWS CDK](https://aws.amazon.com/cdk/). Install the [CDK CLI](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) before deploying these examples. + +## Examples + +- **[API Gateway](APIGateway/README.md)**: an HTTPS REST API with [Amazon API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) and a Lambda function as backend (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[API Gateway with Lambda Authorizer](APIGateway+LambdaAuthorizer/README.md)**: an HTTPS REST API with [Amazon API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) protected by a Lambda authorizer (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[BackgroundTasks](BackgroundTasks/README.md)**: a Lambda function that continues to run background tasks after having sent the response (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). + +- **[CDK](CDK/README.md)**: a simple example of an AWS Lambda function invoked through an Amazon API Gateway and deployed with the Cloud Development Kit (CDK). + +- **[HelloJSON](HelloJSON/README.md)**: a Lambda function that accepts JSON as an input parameter and responds with a JSON output (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). + +- **[HelloWorld](HelloWorld/README.md)**: a simple Lambda function (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). + +- **[S3EventNotifier](S3EventNotifier/README.md)**: a Lambda function that receives object-upload notifications from an [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) bucket. + +- **[S3_AWSSDK](S3_AWSSDK/README.md)**: a Lambda function that uses the [AWS SDK for Swift](https://docs.aws.amazon.com/sdk-for-swift/latest/developer-guide/getting-started.html) to invoke an [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) API (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[S3_Soto](S3_Soto/README.md)**: a Lambda function that uses [Soto](https://github.com/soto-project/soto) to invoke an [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) API (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[Streaming]**: create a Lambda function exposed as an URL. The Lambda function streams its response over time. (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). + +- **[Testing](Testing/README.md)**: a test suite for Lambda functions. + +## AWS Credentials and Signature + +This section is a short tutorial on the AWS Signature protocol and the AWS credentials. + +**What is AWS SigV4?** + +AWS SigV4, short for "Signature Version 4," is a protocol AWS uses to authenticate and secure requests. When you, as a developer, send a request to an AWS service, AWS SigV4 makes sure the request is verified and hasn’t been tampered with. This is done through a digital signature, which is created by combining your request details with your secret AWS credentials. This signature tells AWS that the request is genuine and is coming from a user who has the right permissions. + +**How to Obtain AWS Access Keys and Session Tokens** + +To start making authenticated requests with AWS SigV4, you’ll need three main pieces of information: + +1. **Access Key ID**: This is a unique identifier for your AWS account, IAM (Identity and Access Management) user, or federated user. + +2. **Secret Access Key**: This is a secret code that only you and AWS know. It works together with your access key ID to sign requests. + +3. **Session Token (Optional)**: If you're using temporary security credentials, AWS will also provide a session token. This is usually required if you're using temporary access (e.g., through AWS STS, which provides short-lived, temporary credentials, or for federated users). + +To obtain these keys, you need an AWS account: + +1. **Sign up or Log in to AWS Console**: Go to the [AWS Management Console](https://aws.amazon.com/console/), log in, or create an AWS account if you don’t have one. + +2. **Create IAM User**: In the console, go to IAM (Identity and Access Management) and create a new user. Ensure you set permissions that match what the user will need for your application (e.g., permissions to access specific AWS services, such as AWS Lambda). + +3. **Generate Access Key and Secret Access Key**: In the IAM user credentials section, find the option to generate an "Access Key" and "Secret Access Key." Save these securely! You’ll need them to authenticate your requests. + +4. **(Optional) Generate Temporary Security Credentials**: If you’re using temporary credentials (which are more secure for short-term access), use AWS Security Token Service (STS). You can call the `GetSessionToken` or `AssumeRole` API to generate temporary credentials, including a session token. + +With these in hand, you can use AWS SigV4 to securely sign your requests and interact with AWS services from your Swift app. diff --git a/Examples/ResourcesPackaging/.gitignore b/Examples/ResourcesPackaging/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/ResourcesPackaging/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/ResourcesPackaging/Package.swift b/Examples/ResourcesPackaging/Package.swift new file mode 100644 index 00000000..4680b74a --- /dev/null +++ b/Examples/ResourcesPackaging/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "ResourcesPackaging", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]) + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: ".", + resources: [ + .process("hello.txt") + ] + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/LocalDebugging/MyLambda/Sources/MyLambda/main.swift b/Examples/ResourcesPackaging/Sources/main.swift similarity index 55% rename from Examples/LocalDebugging/MyLambda/Sources/MyLambda/main.swift rename to Examples/ResourcesPackaging/Sources/main.swift index a7e52304..dccbd863 100644 --- a/Examples/LocalDebugging/MyLambda/Sources/MyLambda/main.swift +++ b/Examples/ResourcesPackaging/Sources/main.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,11 +13,14 @@ //===----------------------------------------------------------------------===// import AWSLambdaRuntime -import Shared +import Foundation -// set LOCAL_LAMBDA_SERVER_ENABLED env variable to "true" to start -// a local server simulator which will allow local debugging -Lambda.run { (_, request: Request, callback: @escaping (Result) -> Void) in - // TODO: something useful - callback(.success(Response(message: "Hello, \(request.name)!"))) +let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + guard let fileURL = Bundle.module.url(forResource: "hello", withExtension: "txt") else { + fatalError("no file url") + } + return try String(contentsOf: fileURL, encoding: .utf8) } + +try await runtime.run() diff --git a/Examples/ResourcesPackaging/hello.txt b/Examples/ResourcesPackaging/hello.txt new file mode 100644 index 00000000..557db03d --- /dev/null +++ b/Examples/ResourcesPackaging/hello.txt @@ -0,0 +1 @@ +Hello World diff --git a/Examples/S3EventNotifier/.gitignore b/Examples/S3EventNotifier/.gitignore new file mode 100644 index 00000000..10edc03d --- /dev/null +++ b/Examples/S3EventNotifier/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/.index-build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/S3EventNotifier/Package.swift b/Examples/S3EventNotifier/Package.swift new file mode 100644 index 00000000..6554b385 --- /dev/null +++ b/Examples/S3EventNotifier/Package.swift @@ -0,0 +1,50 @@ +// swift-tools-version: 6.0 +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "S3EventNotifier", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"), + ], + targets: [ + .executableTarget( + name: "S3EventNotifier", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/S3EventNotifier/README.md b/Examples/S3EventNotifier/README.md new file mode 100644 index 00000000..3ccee239 --- /dev/null +++ b/Examples/S3EventNotifier/README.md @@ -0,0 +1,94 @@ +# S3 Event Notifier + +This example demonstrates how to write a Lambda that is invoked by an event originating from Amazon S3, such as a new object being uploaded to a bucket. + +## Code + +In this example the Lambda function receives an `S3Event` object defined in the `AWSLambdaEvents` library as input object. The `S3Event` object contains all the information about the S3 event that triggered the function, but what we are interested in is the bucket name and the object key, which are inside of a notification `Record`. The object contains an array of records, however since the Lambda function is triggered by a single event, we can safely assume that there is only one record in the array: the first one. Inside of this record, we can find the bucket name and the object key: + +```swift +guard let s3NotificationRecord = event.records.first else { + throw LambdaError.noNotificationRecord +} + +let bucket = s3NotificationRecord.s3.bucket.name +let key = s3NotificationRecord.s3.object.key.replacingOccurrences(of: "+", with: " ") +``` + +The key is URL encoded, so we replace the `+` with a space. + +## Build & Package + +To build & archive the package you can use the following commands: + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there are no errors, a ZIP file should be ready to deploy, located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip`. + +## Deploy + +> [!IMPORTANT] +> The Lambda function and the S3 bucket must be located in the same AWS Region. In the code below, we use `eu-west-1` (Ireland). + +To deploy the Lambda function, you can use the `aws` command line: + +```bash +REGION=eu-west-1 +aws lambda create-function \ + --region "${REGION}" \ + --function-name S3EventNotifier \ + --zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip \ + --runtime provided.al2 \ + --handler provided \ + --architectures arm64 \ + --role arn:aws:iam:::role/lambda_basic_execution +``` + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +Be sure to define `REGION` with the region where you want to deploy your Lambda function and replace `` with your actual AWS account ID (for example: 012345678901). + +Besides deploying the Lambda function you also need to create the S3 bucket and configure it to send events to the Lambda function. You can do this using the following commands: + +```bash +REGION=eu-west-1 + +aws s3api create-bucket \ + --region "${REGION}" \ + --bucket my-test-bucket \ + --create-bucket-configuration LocationConstraint="${REGION}" + +aws lambda add-permission \ + --region "${REGION}" \ + --function-name S3EventNotifier \ + --statement-id S3InvokeFunction \ + --action lambda:InvokeFunction \ + --principal s3.amazonaws.com \ + --source-arn arn:aws:s3:::my-test-bucket + +aws s3api put-bucket-notification-configuration \ + --region "${REGION}" \ + --bucket my-test-bucket \ + --notification-configuration '{ + "LambdaFunctionConfigurations": [{ + "LambdaFunctionArn": "arn:aws:lambda:${REGION}::function:S3EventNotifier", + "Events": ["s3:ObjectCreated:*"] + }] + }' + +touch testfile.txt && aws s3 cp testfile.txt s3://my-test-bucket/ +``` + +This will: + - create a bucket named `my-test-bucket` in the `$REGION` region; + - add a permission to the Lambda function to be invoked by Amazon S3; + - configure the bucket to send `s3:ObjectCreated:*` events to the Lambda function named `S3EventNotifier`; + - upload a file named `testfile.txt` to the bucket. + +Replace `my-test-bucket` with your bucket name (bucket names are unique globaly and this one is already taken). Also replace `REGION` environment variable with the AWS Region where you deployed the Lambda function and `` with your actual AWS account ID. + +> [!IMPORTANT] +> The Lambda function and the S3 bucket must be located in the same AWS Region. Adjust the code above according to your closest AWS Region. \ No newline at end of file diff --git a/Examples/S3EventNotifier/Sources/main.swift b/Examples/S3EventNotifier/Sources/main.swift new file mode 100644 index 00000000..9a55974e --- /dev/null +++ b/Examples/S3EventNotifier/Sources/main.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime +import Foundation + +let runtime = LambdaRuntime { (event: S3Event, context: LambdaContext) async throws in + guard let s3NotificationRecord = event.records.first else { + context.logger.error("No S3 notification record found in the event") + return + } + + let bucket = s3NotificationRecord.s3.bucket.name + let key = s3NotificationRecord.s3.object.key.replacingOccurrences(of: "+", with: " ") + + context.logger.info("Received notification from S3 bucket '\(bucket)' for object with key '\(key)'") + + // Here you could, for example, notify an API or a messaging service +} + +try await runtime.run() diff --git a/Examples/S3_AWSSDK/.gitignore b/Examples/S3_AWSSDK/.gitignore new file mode 100644 index 00000000..70799e05 --- /dev/null +++ b/Examples/S3_AWSSDK/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +.aws-sam/ +.build +samtemplate.toml +*/build/* +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc \ No newline at end of file diff --git a/Examples/S3_AWSSDK/Package.swift b/Examples/S3_AWSSDK/Package.swift new file mode 100644 index 00000000..0eec7c6b --- /dev/null +++ b/Examples/S3_AWSSDK/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "AWSSDKExample", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "AWSSDKExample", targets: ["AWSSDKExample"]) + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", from: "1.0.0"), + .package(url: "https://github.com/awslabs/aws-sdk-swift", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "AWSSDKExample", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + .product(name: "AWSS3", package: "aws-sdk-swift"), + ] + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/S3_AWSSDK/README.md b/Examples/S3_AWSSDK/README.md new file mode 100644 index 00000000..4c84c277 --- /dev/null +++ b/Examples/S3_AWSSDK/README.md @@ -0,0 +1,89 @@ +# List Amazon S3 Buckets with the AWS SDK for Swift + +This is a simple example of an AWS Lambda function that uses the [AWS SDK for Swift](https://github.com/awslabs/aws-sdk-swift) to read data from Amazon S3. + +## Code + +The Lambda function reads all bucket names from your AWS account and returns them as a String. + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when the API Gateway receives an HTTP request. + +The handler is `(event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response`. The function takes two arguments: +- the event argument is a `APIGatewayV2Request`. It is the parameter passed by the API Gateway. It contains all data passed in the HTTP request and some meta data. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function must return a `APIGatewayV2Response`. + +`APIGatewayV2Request` and `APIGatewayV2Response` are defined in the [Swift AWS Lambda Events](https://github.com/swift-server/swift-aws-lambda-events) library. + +The handler creates an S3 client and `ListBucketsInput` object. It passes the input object to the client and receives an output response. +It then extracts the list of bucket names from the output and creates a `\n`-separated list of names, as a `String` + +## Build & Package + +To build the package, type the following commands. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/AWSSDKExample/AWSSDKExample.zip` + +## Deploy + +The deployment must include the Lambda function and an API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name AWSSDKExample \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URL" +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print text similar to + +```bash +my_bucket_1 +my_bucket_2 +... +``` + +## Delete the infrastructure + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` \ No newline at end of file diff --git a/Examples/S3_AWSSDK/Sources/main.swift b/Examples/S3_AWSSDK/Sources/main.swift new file mode 100644 index 00000000..6665893c --- /dev/null +++ b/Examples/S3_AWSSDK/Sources/main.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime +@preconcurrency import AWSS3 + +let client = try await S3Client() + +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response in + + var response: APIGatewayV2Response + do { + // read the list of buckets + context.logger.debug("Reading list of buckets") + let output = try await client.listBuckets(input: ListBucketsInput()) + let bucketList = output.buckets?.compactMap { $0.name } + response = APIGatewayV2Response(statusCode: .ok, body: bucketList?.joined(separator: "\n")) + } catch { + context.logger.error("\(error)") + response = APIGatewayV2Response(statusCode: .internalServerError, body: "[ERROR] \(error)") + } + return response +} + +try await runtime.run() diff --git a/Examples/S3_AWSSDK/template.yaml b/Examples/S3_AWSSDK/template.yaml new file mode 100644 index 00000000..46e29ec8 --- /dev/null +++ b/Examples/S3_AWSSDK/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for AWS SDK Example + +Resources: + # Lambda function + AWSSDKExample: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/AWSSDKExample/AWSSDKExample.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 512 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + + # Handles all methods of the REST API + Events: + Api: + Type: HttpApi + + # Add an IAM policy to this function. + # It grants the function permissions to read the list of buckets in your account. + Policies: + - Statement: + - Sid: ListAllS3BucketsInYourAccount + Effect: Allow + Action: + - s3:ListAllMyBuckets + Resource: '*' + +# print API endpoint +Outputs: + SwiftAPIEndpoint: + Description: "API Gateway endpoint URL for your application" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" diff --git a/Examples/S3_Soto/.gitignore b/Examples/S3_Soto/.gitignore new file mode 100644 index 00000000..70799e05 --- /dev/null +++ b/Examples/S3_Soto/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +.aws-sam/ +.build +samtemplate.toml +*/build/* +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc \ No newline at end of file diff --git a/Examples/S3_Soto/Package.swift b/Examples/S3_Soto/Package.swift new file mode 100644 index 00000000..97e5a9fb --- /dev/null +++ b/Examples/S3_Soto/Package.swift @@ -0,0 +1,58 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "SotoExample", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "SotoExample", targets: ["SotoExample"]) + ], + dependencies: [ + .package(url: "https://github.com/soto-project/soto.git", from: "7.0.0"), + + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "SotoExample", + dependencies: [ + .product(name: "SotoS3", package: "soto"), + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/S3_Soto/README.md b/Examples/S3_Soto/README.md new file mode 100644 index 00000000..7ae0fa16 --- /dev/null +++ b/Examples/S3_Soto/README.md @@ -0,0 +1,89 @@ +# List Amazon S3 Buckets with Soto + +This is a simple example of an AWS Lambda function that uses the [Soto SDK for AWS](https://github.com/soto-project/soto) to read data from Amazon S3. + +## Code + +The Lambda function reads all bucket names from your AWS account and returns them as a String. + +The code creates a `LambdaRuntime` struct. In it's simplest form, the initializer takes a function as argument. The function is the handler that will be invoked when the API Gateway receives an HTTP request. + +The handler is `(event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response`. The function takes two arguments: +- the event argument is a `APIGatewayV2Request`. It is the parameter passed by the API Gateway. It contains all data passed in the HTTP request and some meta data. +- the context argument is a `Lambda Context`. It is a description of the runtime context. + +The function must return a `APIGatewayV2Response`. + +`APIGatewayV2Request` and `APIGatewayV2Response` are defined in the [Swift AWS Lambda Events](https://github.com/swift-server/swift-aws-lambda-events) library. + +The handler creates two clients : an AWS client that manages the communication with AWS API and and the S3 client that expose the S3 API. Then, the handler calls `listBuckets()` on the S3 client and receives an output response. +Finally, the handler extracts the list of bucket names from the output to create a `\n`-separated list of names, as a `String`. + +## Build & Package + +To build the package, type the following command. + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/SotoExample/SotoExample.zip` + +## Deploy + +The deployment must include the Lambda function and an API Gateway. We use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to deploy the infrastructure. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +The example directory contains a file named `template.yaml` that describes the deployment. + +To actually deploy your Lambda function and create the infrastructure, type the following `sam` command. + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name SotoExample \ +--capabilities CAPABILITY_IAM +``` + +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URL" +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +----------------------------------------------------------------------------------------------------------------------------- +``` + +## Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print text similar to + +```bash +my_bucket_1 +my_bucket_2 +... +``` + +## Delete the infrastructure + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` \ No newline at end of file diff --git a/Examples/S3_Soto/Sources/main.swift b/Examples/S3_Soto/Sources/main.swift new file mode 100644 index 00000000..caa70116 --- /dev/null +++ b/Examples/S3_Soto/Sources/main.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime +import SotoS3 + +let client = AWSClient() +let s3 = S3(client: client, region: .useast1) + +func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { + + var response: APIGatewayV2Response + do { + context.logger.debug("Reading list of buckets") + + // read the list of buckets + let bucketResponse = try await s3.listBuckets() + let bucketList = bucketResponse.buckets?.compactMap { $0.name } + response = APIGatewayV2Response(statusCode: .ok, body: bucketList?.joined(separator: "\n")) + } catch { + context.logger.error("\(error)") + response = APIGatewayV2Response(statusCode: .internalServerError, body: "[ERROR] \(error)") + } + return response +} + +let runtime = LambdaRuntime.init(body: handler) + +try await runtime.run() +try await client.shutdown() diff --git a/Examples/S3_Soto/template.yaml b/Examples/S3_Soto/template.yaml new file mode 100644 index 00000000..bfc04d1e --- /dev/null +++ b/Examples/S3_Soto/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for AWS SDK Example + +Resources: + # Lambda function + SotoExample: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/SotoExample/SotoExample.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 512 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: debug + + # Handles all methods of the REST API + Events: + Api: + Type: HttpApi + + # Add an IAM policy to this function. + # It grants the function permissions to read the list of buckets in your account. + Policies: + - Statement: + - Sid: ListAllS3BucketsInYourAccount + Effect: Allow + Action: + - s3:ListAllMyBuckets + Resource: '*' + +# print API endpoint +Outputs: + SwiftAPIEndpoint: + Description: "API Gateway endpoint URL for your application" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" diff --git a/Examples/Streaming/.gitignore b/Examples/Streaming/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/Streaming/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/Streaming/Package.swift b/Examples/Streaming/Package.swift new file mode 100644 index 00000000..b29dc961 --- /dev/null +++ b/Examples/Streaming/Package.swift @@ -0,0 +1,54 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "StreamingNumbers", targets: ["StreamingNumbers"]) + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "StreamingNumbers", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources" + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/Streaming/README.md b/Examples/Streaming/README.md new file mode 100644 index 00000000..86a42754 --- /dev/null +++ b/Examples/Streaming/README.md @@ -0,0 +1,222 @@ +# Streaming Lambda function + +You can configure your Lambda function to stream response payloads back to clients. Response streaming can benefit latency sensitive applications by improving time to first byte (TTFB) performance. This is because you can send partial responses back to the client as they become available. Additionally, you can use response streaming to build functions that return larger payloads. Response stream payloads have a soft limit of 20 MB as compared to the 6 MB limit for buffered responses. Streaming a response also means that your function doesn’t need to fit the entire response in memory. For very large responses, this can reduce the amount of memory you need to configure for your function. + +Streaming responses incurs a cost. For more information, see [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/). + +You can stream responses through [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), the AWS SDK, or using the Lambda [InvokeWithResponseStream](https://docs.aws.amazon.com/lambda/latest/dg/API_InvokeWithResponseStream.html) API. In this example, we create an authenticated Lambda function URL. + + +## Code + +The sample code creates a `SendNumbersWithPause` struct that conforms to the `StreamingLambdaHandler` protocol provided by the Swift AWS Lambda Runtime. + +The `handle(...)` method of this protocol receives incoming events as a Swift NIO `ByteBuffer` and returns the output as a `ByteBuffer`. + +The response is streamed through the `LambdaResponseStreamWriter`, which is passed as an argument in the `handle` function. The code calls the `write(_:)` function of the `LambdaResponseStreamWriter` with partial data repeatedly written before +finally closing the response stream by calling `finish()`. Developers can also choose to return the entire output and not +stream the response by calling `writeAndFinish(_:)`. + +An error is thrown if `finish()` is called multiple times or if it is called after having called `writeAndFinish(_:)`. + +The `handle(...)` method is marked as `mutating` to allow handlers to be implemented with a `struct`. + +Once the struct is created and the `handle(...)` method is defined, the sample code creates a `LambdaRuntime` struct and initializes it with the handler just created. Then, the code calls `run()` to start the interaction with the AWS Lambda control plane. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip` + +## Deploy with the AWS CLI + +Here is how to deploy using the `aws` command line. + +### Step 1: Create the function + +```bash +# Replace with your AWS Account ID +AWS_ACCOUNT_ID=012345678901 +aws lambda create-function \ +--function-name StreamingNumbers \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \ +--timeout 15 +``` + +> [!IMPORTANT] +> The timeout value must be bigger than the time it takes for your function to stream its output. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish writing the stream. Here, the sample function stream responses during 10 seconds and we set the timeout for 15 seconds. + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +Be sure to set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). + +### Step2: Give permission to invoke that function through an URL + +Anyone with a valid signature from your AWS account will have permission to invoke the function through its URL. + +```bash +aws lambda add-permission \ + --function-name StreamingNumbers \ + --action lambda:InvokeFunctionUrl \ + --principal ${AWS_ACCOUNT_ID} \ + --function-url-auth-type AWS_IAM \ + --statement-id allowURL +``` + +### Step3: Create the URL + +This creates [a URL with IAM authentication](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html). Only calls with a valid signature will be authorized. + +```bash +aws lambda create-function-url-config \ + --function-name StreamingNumbers \ + --auth-type AWS_IAM \ + --invoke-mode RESPONSE_STREAM +``` +This calls return various information, including the URL to invoke your function. + +```json +{ + "FunctionUrl": "https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/", + "FunctionArn": "arn:aws:lambda:us-east-1:012345678901:function:StreamingNumbers", + "AuthType": "AWS_IAM", + "CreationTime": "2024-10-22T07:57:23.112599Z", + "InvokeMode": "RESPONSE_STREAM" +} +``` + +### Invoke your Lambda function + +To invoke the Lambda function, use `curl` with the AWS Sigv4 option to generate the signature. + +Read the [AWS Credentials and Signature](../README.md/#AWS-Credentials-and-Signature) section for more details about the AWS Sigv4 protocol and how to obtain AWS credentials. + +When you have the `aws` command line installed and configured, you will find the credentials in the `~/.aws/credentials` file. + +```bash +URL=https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/ +REGION=us-east-1 +ACCESS_KEY=AK... +SECRET_KEY=... +AWS_SESSION_TOKEN=... + +curl "$URL" \ + --user "${ACCESS_KEY}":"${SECRET_KEY}" \ + --aws-sigv4 "aws:amz:${REGION}:lambda" \ + -H "x-amz-security-token: ${AWS_SESSION_TOKEN}" \ + --no-buffer +``` + +Note that there is no payload required for this example. + +This should output the following result, with a one-second delay between each numbers. + +``` +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +``` + +### Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name StreamingNumbers +``` + +## Deploy with AWS SAM + +Alternatively, you can use [AWS SAM](https://aws.amazon.com/serverless/sam/) to deploy the Lambda function. + +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) + +### SAM Template + +The template file is provided as part of the example in the `template.yaml` file. It defines a Lambda function based on the binary ZIP file. It creates the function url with IAM authentication and sets the function timeout to 15 seconds. + +```yaml +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for StreamingLambda Example + +Resources: + # Lambda function + StreamingNumbers: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip + Timeout: 15 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + FunctionUrlConfig: + AuthType: AWS_IAM + InvokeMode: RESPONSE_STREAM + +Outputs: + # print Lambda function URL + LambdaURL: + Description: Lambda URL + Value: !GetAtt StreamingNumbersUrl.FunctionUrl +``` + +### Deploy with SAM + +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name StreamingNumbers \ +--capabilities CAPABILITY_IAM +``` + +The URL of the function is provided as part of the output. + +``` +CloudFormation outputs from deployed stack +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key LambdaURL +Description Lambda URL +Value https://gaudpin2zjqizfujfnqxstnv6u0czrfu.lambda-url.us-east-1.on.aws/ +----------------------------------------------------------------------------------------------------------------------------- +``` + +Once the function is deployed, you can invoke it with `curl`, similarly to what you did when deploying with the AWS CLI. + +```bash +curl "$URL" \ + --user "$ACCESS_KEY":"$SECRET_KEY" \ + --aws-sigv4 "aws:amz:${REGION}:lambda" \ + -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ + --no-buffer +``` + +### Undeploy with SAM + +When done testing, you can delete the infrastructure with this command. + +```bash +sam delete +``` diff --git a/Examples/Streaming/Sources/main.swift b/Examples/Streaming/Sources/main.swift new file mode 100644 index 00000000..ce92560c --- /dev/null +++ b/Examples/Streaming/Sources/main.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import NIOCore + +struct SendNumbersWithPause: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + for i in 1...10 { + // Send partial data + try await responseWriter.write(ByteBuffer(string: "\(i)\n")) + // Perform some long asynchronous work + try await Task.sleep(for: .milliseconds(1000)) + } + // All data has been sent. Close off the response stream. + try await responseWriter.finish() + } +} + +let runtime = LambdaRuntime.init(handler: SendNumbersWithPause()) +try await runtime.run() diff --git a/Examples/Streaming/template.yaml b/Examples/Streaming/template.yaml new file mode 100644 index 00000000..2cc72839 --- /dev/null +++ b/Examples/Streaming/template.yaml @@ -0,0 +1,25 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for Streaming Example + +Resources: + # Lambda function + StreamingNumbers: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip + Timeout: 15 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 128 + Architectures: + - arm64 + FunctionUrlConfig: + AuthType: AWS_IAM + InvokeMode: RESPONSE_STREAM + +Outputs: + # print Lambda function URL + LambdaURL: + Description: Lambda URL + Value: !GetAtt StreamingNumbersUrl.FunctionUrl diff --git a/Examples/Testing/Package.swift b/Examples/Testing/Package.swift new file mode 100644 index 00000000..db196325 --- /dev/null +++ b/Examples/Testing/Package.swift @@ -0,0 +1,64 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "APIGatewayLambda", targets: ["APIGatewayLambda"]) + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "APIGatewayLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "Sources" + ), + .testTarget( + name: "LambdaFunctionTests", + dependencies: ["APIGatewayLambda"], + path: "Tests", + resources: [ + .process("event.json") + ] + ), + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/Testing/README.md b/Examples/Testing/README.md new file mode 100644 index 00000000..9bfd0e28 --- /dev/null +++ b/Examples/Testing/README.md @@ -0,0 +1,171 @@ +# Swift Testing Example + +This is a simple example to show different testing strategies for your Swift Lambda functions. +For this example, we developed a simple Lambda function that returns the body of the API Gateway payload in lowercase, except for the first letter, which is in uppercase. + +In this document, we describe four different testing strategies: + * [Unit Testing your business logic](#unit-testing-your-business-logic) + * [Integration testing the handler function](#integration-testing-the-handler-function) + * [Local invocation using the Swift AWS Lambda Runtime](#local-invocation-using-the-swift-aws-lambda-runtime) + * [Local invocation using the AWS SAM CLI](#local-invocation-using-the-aws-sam-cli) + +> [!IMPORTANT] +> In this example, the API Gateway sends an event to your Lambda function as a JSON string. Your business payload is in the `body` section of the API Gateway event. It is base64-encoded. You can find an example of the API Gateway event in the `event.json` file. The API Gateway event format is documented in [Create AWS Lambda proxy integrations for HTTP APIs in API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). + +To include a sample event in your test targets, you must add the `event.json` file from the `Tests` directory to the binary bundle. To do so, add a `resources` section in your `Package.swift` file: + +```swift + .testTarget( + name: "LambdaFunctionTests", + dependencies: ["APIGatewayLambda"], + path: "Tests", + resources: [ + .process("event.json") + ] + ) +``` + +## Unit Testing your business logic + +You can test the business logic of your Lambda function by writing unit tests for your business code used in the handler function, just like usual. + +1. Create your Swift Test code in the `Tests` directory. + +```swift +let valuesToTest: [(String, String)] = [ + ("hello world", "Hello world"), // happy path + ("", ""), // Empty string + ("a", "A"), // Single character +] + +@Suite("Business Tests") +class BusinessTests { + + @Test("Uppercased First", arguments: valuesToTest) + func uppercasedFirst(_ arg: (String,String)) { + let input = arg.0 + let expectedOutput = arg.1 + #expect(input.uppercasedFirst() == expectedOutput) + } +} +``` + +2. Add a test target to your `Package.swift` file. +```swift + .testTarget( + name: "BusinessTests", + dependencies: ["APIGatewayLambda"], + path: "Tests" + ) +``` + +3. run `swift test` to run the tests. + +## Integration Testing the handler function + +You can test the handler function by creating an input event, a mock Lambda context, and calling your Lambda handler function from your test. +Your Lambda handler function must be declared separatly from the `LambdaRuntime`. For example: + +```swift +public struct MyHandler: Sendable { + + public func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { + context.logger.debug("HTTP API Message received") + context.logger.trace("Event: \(event)") + + var header = HTTPHeaders() + header["content-type"] = "application/json" + + if let payload = event.body { + // call our business code to process the payload and return a response + return APIGatewayV2Response(statusCode: .ok, headers: header, body: payload.uppercasedFirst()) + } else { + return APIGatewayV2Response(statusCode: .badRequest) + } + } +} + +let runtime = LambdaRuntime(body: MyHandler().handler) +try await runtime.run() +``` + +Then, the test looks like this: + +```swift +@Suite("Handler Tests") +public struct HandlerTest { + + @Test("Invoke handler") + public func invokeHandler() async throws { + + // read event.json file + let testBundle = Bundle.module + guard let eventURL = testBundle.url(forResource: "event", withExtension: "json") else { + Issue.record("event.json not found in test bundle") + return + } + let eventData = try Data(contentsOf: eventURL) + + // decode the event + let apiGatewayRequest = try JSONDecoder().decode(APIGatewayV2Request.self, from: eventData) + + // create a mock LambdaContext + let lambdaContext = LambdaContext.__forTestsOnly( + requestID: UUID().uuidString, + traceID: UUID().uuidString, + invokedFunctionARN: "arn:", + timeout: .milliseconds(6000), + logger: Logger(label: "fakeContext") + ) + + // call the handler with the event and context + let response = try await MyHandler().handler(event: apiGatewayRequest, context: lambdaContext) + + // assert the response + #expect(response.statusCode == .ok) + #expect(response.body == "Hello world of swift lambda!") + } +} +``` + +## Local invocation using the Swift AWS Lambda Runtime + +You can test your Lambda function locally by invoking it with the Swift AWS Lambda Runtime. + +You must pass an event to the Lambda function. You can use the `Tests/event.json` file for this purpose. The return value is a `APIGatewayV2Response` object in this example. + +Just type `swift run` to run the Lambda function locally, this starts a local HTTP endpoint on localhost:7000. + +```sh +LOG_LEVEL=trace swift run + +# from another terminal +# the `-X POST` flag is implied when using `--data`. It is here for clarity only. +curl -X POST "http://127.0.0.1:7000/invoke" --data @Tests/event.json +``` + +This returns the following response: + +```text +{"statusCode":200,"headers":{"content-type":"application\/json"},"body":"Hello world of swift lambda!"} +``` + +## Local invocation using the AWS SAM CLI + +The AWS SAM CLI provides you with a local testing environment for your Lambda functions. It deploys and invokes your function locally in a Docker container designed to mimic the AWS Lambda environment. + +You must pass an event to the Lambda function. You can use the `event.json` file for this purpose. The return value is a `APIGatewayV2Response` object in this example. + +```sh +sam local invoke -e Tests/event.json + +START RequestId: 3270171f-46d3-45f9-9bb6-3c2e5e9dc625 Version: $LATEST +2024-12-21T16:49:31+0000 debug LambdaRuntime : [AWSLambdaRuntime] LambdaRuntime initialized +2024-12-21T16:49:31+0000 trace LambdaRuntime : lambda_ip=127.0.0.1 lambda_port=9001 [AWSLambdaRuntime] Connection to control plane created +2024-12-21T16:49:31+0000 debug LambdaRuntime : [APIGatewayLambda] HTTP API Message received +2024-12-21T16:49:31+0000 trace LambdaRuntime : [APIGatewayLambda] Event: APIGatewayV2Request(version: "2.0", routeKey: "$default", rawPath: "/", rawQueryString: "", cookies: [], headers: ["x-forwarded-proto": "https", "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", "content-length": "0", "x-forwarded-for": "81.0.0.43", "accept": "*/*", "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", "x-forwarded-port": "443", "user-agent": "curl/8.7.1"], queryStringParameters: [:], pathParameters: [:], context: AWSLambdaEvents.APIGatewayV2Request.Context(accountId: "012345678901", apiId: "a5q74es3k2", domainName: "a5q74es3k2.execute-api.us-east-1.amazonaws.com", domainPrefix: "a5q74es3k2", stage: "$default", requestId: "e72KxgsRoAMEMSA=", http: AWSLambdaEvents.APIGatewayV2Request.Context.HTTP(method: GET, path: "/", protocol: "HTTP/1.1", sourceIp: "81.0.0.43", userAgent: "curl/8.7.1"), authorizer: nil, authentication: nil, time: "30/Sep/2024:20:02:38 +0000", timeEpoch: 1727726558220), stageVariables: [:], body: Optional("aGVsbG8gd29ybGQgb2YgU1dJRlQgTEFNQkRBIQ=="), isBase64Encoded: false) +END RequestId: 5b71587a-39da-445e-855d-27a700e57efd +REPORT RequestId: 5b71587a-39da-445e-855d-27a700e57efd Init Duration: 0.04 ms Duration: 21.57 ms Billed Duration: 22 ms Memory Size: 512 MB Max Memory Used: 512 MB + +{"body": "Hello world of swift lambda!", "statusCode": 200, "headers": {"content-type": "application/json"}} +``` diff --git a/Examples/Testing/Sources/Business.swift b/Examples/Testing/Sources/Business.swift new file mode 100644 index 00000000..af95b8e5 --- /dev/null +++ b/Examples/Testing/Sources/Business.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension String { + /// Returns a new string with the first character capitalized and the remaining characters in lowercase. + /// + /// This method capitalizes the first character of the string and converts the remaining characters to lowercase. + /// It is useful for formatting strings where only the first character should be uppercase. + /// + /// - Returns: A new string with the first character capitalized and the remaining characters in lowercase. + /// + /// - Example: + /// ``` + /// let example = "hello world" + /// print(example.uppercasedFirst()) // Prints "Hello world" + /// ``` + func uppercasedFirst() -> String { + let firstCharacter = prefix(1).capitalized + let remainingCharacters = dropFirst().lowercased() + return firstCharacter + remainingCharacters + } +} diff --git a/Examples/Testing/Sources/main.swift b/Examples/Testing/Sources/main.swift new file mode 100644 index 00000000..af76e02c --- /dev/null +++ b/Examples/Testing/Sources/main.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +public struct MyHandler: Sendable { + + public func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { + context.logger.debug("HTTP API Message received") + context.logger.trace("Event: \(event)") + + var header = HTTPHeaders() + header["content-type"] = "application/json" + + // API Gateway sends text or URL encoded data as a Base64 encoded string + if let base64EncodedString = event.body, + let decodedData = Data(base64Encoded: base64EncodedString), + let decodedString = String(data: decodedData, encoding: .utf8) + { + + // call our business code to process the payload and return a response + return APIGatewayV2Response(statusCode: .ok, headers: header, body: decodedString.uppercasedFirst()) + } else { + return APIGatewayV2Response(statusCode: .badRequest) + } + } +} + +let runtime = LambdaRuntime(body: MyHandler().handler) +try await runtime.run() diff --git a/Examples/Testing/Tests/BusinessTests.swift b/Examples/Testing/Tests/BusinessTests.swift new file mode 100644 index 00000000..85f821e1 --- /dev/null +++ b/Examples/Testing/Tests/BusinessTests.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import APIGatewayLambda // to access the business code + +let valuesToTest: [(String, String)] = [ + ("hello world", "Hello world"), // happy path + ("", ""), // Empty string + ("a", "A"), // Single character + ("A", "A"), // Single uppercase character + ("HELLO WORLD", "Hello world"), // All uppercase + ("hello world", "Hello world"), // All lowercase + ("hElLo WoRlD", "Hello world"), // Mixed case + ("123abc", "123abc"), // Numeric string + ("!@#abc", "!@#abc"), // Special characters +] + +@Suite("Business Tests") +class BusinessTests { + + @Test("Uppercased First", arguments: valuesToTest) + func uppercasedFirst(_ arg: (String, String)) { + let input = arg.0 + let expectedOutput = arg.1 + #expect(input.uppercasedFirst() == expectedOutput) + } +} diff --git a/Examples/Testing/Tests/HandlerTests.swift b/Examples/Testing/Tests/HandlerTests.swift new file mode 100644 index 00000000..85cc4e4e --- /dev/null +++ b/Examples/Testing/Tests/HandlerTests.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime +import Logging +import Testing + +@testable import APIGatewayLambda // to access the business code +@testable import AWSLambdaRuntime // to access the LambdaContext + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite("Handler Tests") +public struct HandlerTest { + + @Test("Invoke handler") + public func invokeHandler() async throws { + + // read event.json file + let testBundle = Bundle.module + guard let eventURL = testBundle.url(forResource: "event", withExtension: "json") else { + Issue.record("event.json not found in test bundle") + return + } + let eventData = try Data(contentsOf: eventURL) + + // decode the event + let apiGatewayRequest = try JSONDecoder().decode(APIGatewayV2Request.self, from: eventData) + + // create a mock LambdaContext + let lambdaContext = LambdaContext.__forTestsOnly( + requestID: UUID().uuidString, + traceID: UUID().uuidString, + invokedFunctionARN: "arn:", + timeout: .milliseconds(6000), + logger: Logger(label: "fakeContext") + ) + + // call the handler with the event and context + let response = try await MyHandler().handler(event: apiGatewayRequest, context: lambdaContext) + + // assert the response + #expect(response.statusCode == .ok) + #expect(response.body == "Hello world of swift lambda!") + } +} diff --git a/Examples/Testing/Tests/event.json b/Examples/Testing/Tests/event.json new file mode 100644 index 00000000..213f8bee --- /dev/null +++ b/Examples/Testing/Tests/event.json @@ -0,0 +1,35 @@ +{ + "version": "2.0", + "rawPath": "/", + "body": "aGVsbG8gd29ybGQgb2YgU1dJRlQgTEFNQkRBIQ==", + "requestContext": { + "domainPrefix": "a5q74es3k2", + "stage": "$default", + "timeEpoch": 1727726558220, + "http": { + "protocol": "HTTP/1.1", + "method": "GET", + "userAgent": "curl/8.7.1", + "path": "/", + "sourceIp": "81.0.0.43" + }, + "apiId": "a5q74es3k2", + "accountId": "012345678901", + "requestId": "e72KxgsRoAMEMSA=", + "domainName": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "time": "30/Sep/2024:20:02:38 +0000" + }, + "rawQueryString": "", + "routeKey": "$default", + "headers": { + "x-forwarded-for": "81.0.0.43", + "user-agent": "curl/8.7.1", + "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "accept": "*/*", + "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", + "content-length": "0", + "x-forwarded-proto": "https", + "x-forwarded-port": "443" + }, + "isBase64Encoded": false +} diff --git a/Examples/Testing/template.yaml b/Examples/Testing/template.yaml new file mode 100644 index 00000000..c981c978 --- /dev/null +++ b/Examples/Testing/template.yaml @@ -0,0 +1,31 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for APIGateway Lambda Example + +Resources: + # Lambda function + APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 512 + Architectures: + - arm64 + Environment: + Variables: + # by default, AWS Lambda runtime produces no log + # use `LOG_LEVEL: debug` for for lifecycle and event handling information + # use `LOG_LEVEL: trace` for detailed input event information + LOG_LEVEL: trace + Events: + HttpApiEvent: + Type: HttpApi + +Outputs: + # print API Gateway endpoint + APIGatewayEndpoint: + Description: API Gateway endpoint UR" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" diff --git a/Examples/Tutorial/.gitignore b/Examples/Tutorial/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/Tutorial/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/Tutorial/Package.swift b/Examples/Tutorial/Package.swift new file mode 100644 index 00000000..8fd031c1 --- /dev/null +++ b/Examples/Tutorial/Package.swift @@ -0,0 +1,51 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +import struct Foundation.URL + +let package = Package( + name: "Palindrome", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "Palindrome", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ] + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/Tutorial/Sources/main.swift b/Examples/Tutorial/Sources/main.swift new file mode 100644 index 00000000..db28931d --- /dev/null +++ b/Examples/Tutorial/Sources/main.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} + +// the lambda handler function +let runtime = LambdaRuntime { + (event: Request, context: LambdaContext) -> Response in + + let result = isPalindrome(event.text) + return Response( + text: event.text, + isPalindrome: result, + message: "Your text is \(result ? "a" : "not a") palindrome" + ) +} + +// start the runtime +try await runtime.run() diff --git a/Examples/_MyFirstFunction/.gitignore b/Examples/_MyFirstFunction/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/_MyFirstFunction/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/_MyFirstFunction/clean.sh b/Examples/_MyFirstFunction/clean.sh new file mode 100755 index 00000000..457e1c8a --- /dev/null +++ b/Examples/_MyFirstFunction/clean.sh @@ -0,0 +1,36 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +echo "This script deletes the Lambda function and the IAM role created in the previous step and deletes the project files." +read -r -p "Are you you sure you want to delete everything that was created? [y/n] " continue +if [[ ! $continue =~ ^[Yy]$ ]]; then + echo "OK, try again later when you feel ready" + exit 1 +fi + +echo "🚀 Deleting the Lambda function and the role" +aws lambda delete-function --function-name MyLambda +aws iam detach-role-policy \ + --role-name lambda_basic_execution \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole +aws iam delete-role --role-name lambda_basic_execution + +echo "🚀 Deleting the project files" +rm -rf .build +rm -rf ./Sources +rm trust-policy.json +rm Package.swift Package.resolved + +echo "🎉 Done! Your project is cleaned up and ready for a fresh start." \ No newline at end of file diff --git a/Examples/_MyFirstFunction/create_and_deploy_function.sh b/Examples/_MyFirstFunction/create_and_deploy_function.sh new file mode 100755 index 00000000..eb8f6d4f --- /dev/null +++ b/Examples/_MyFirstFunction/create_and_deploy_function.sh @@ -0,0 +1,194 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +# Stop the script execution if an error occurs +set -e -o pipefail + +check_prerequisites() { + # check if docker is installed + which docker > /dev/null || (echo "Docker is not installed. Please install Docker and try again." && exit 1) + + # check if aws cli is installed + which aws > /dev/null || (echo "AWS CLI is not installed. Please install AWS CLI and try again." && exit 1) + + # check if user has an access key and secret access key + echo "This script creates and deploys a Lambda function on your AWS Account. + + You must have an AWS account and know an AWS access key, secret access key, and an optional session token. + These values are read from '~/.aws/credentials'. + " + + read -r -p "Are you ready to create your first Lambda function in Swift? [y/n] " continue + if [[ ! $continue =~ ^[Yy]$ ]]; then + echo "OK, try again later when you feel ready" + exit 1 + fi +} + +create_lambda_execution_role() { + role_name=$1 + + # Allow the Lambda service to assume the IAM role + cat < trust-policy.json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +EOF + + # Create the IAM role + echo "🔐 Create the IAM role for the Lambda function" + aws iam create-role \ + --role-name "${role_name}" \ + --assume-role-policy-document file://trust-policy.json > /dev/null 2>&1 + + # Attach basic permissions to the role + # The AWSLambdaBasicExecutionRole policy grants permissions to write logs to CloudWatch Logs + echo "🔒 Attach basic permissions to the role" + aws iam attach-role-policy \ + --role-name "${role_name}" \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole > /dev/null 2>&1 + + echo "⏰ Waiting 10 secs for IAM role to propagate..." + sleep 10 +} + +create_swift_project() { + echo "⚡️ Create your Swift Lambda project" + swift package init --type executable --name MyLambda > /dev/null + + echo "📦 Add the AWS Lambda Swift runtime to your project" + # The following commands are commented out until the `lambad-init` plugin will be release + # swift package add-dependency https://github.com/swift-server/swift-aws-lambda-runtime.git --branch main + # swift package add-dependency https://github.com/swift-server/swift-aws-lambda-events.git --branch main + # swift package add-target-dependency AWSLambdaRuntime MyLambda --package swift-aws-lambda-runtime + # swift package add-target-dependency AWSLambdaEvents MyLambda --package swift-aws-lambda-events + cat < Package.swift +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]) + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "." + ) + ] +) +EOF + + echo "📝 Write the Swift code" + # The following command is commented out until the `lambad-init` plugin will be release + # swift package lambda-init --allow-writing-to-package-directory + cat < Sources/main.swift +import AWSLambdaRuntime + +let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + "Hello \(event)" +} + +try await runtime.run() +EOF + + echo "📦 Compile and package the function for deployment (this might take a while)" + swift package archive --allow-network-connections docker > /dev/null 2>&1 +} + +deploy_lambda_function() { + echo "🚀 Deploy to AWS Lambda" + + # retrieve your AWS Account ID + echo "🔑 Retrieve your AWS Account ID" + AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + export AWS_ACCOUNT_ID + + # Check if the role already exists + echo "🔍 Check if a Lambda execution IAM role already exists" + aws iam get-role --role-name lambda_basic_execution > /dev/null 2>&1 || create_lambda_execution_role lambda_basic_execution + + # Create the Lambda function + echo "🚀 Create the Lambda function" + aws lambda create-function \ + --function-name MyLambda \ + --zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ + --runtime provided.al2 \ + --handler provided \ + --architectures "$(uname -m)" \ + --role arn:aws:iam::"${AWS_ACCOUNT_ID}":role/lambda_basic_execution > /dev/null 2>&1 + + echo "⏰ Waiting 10 secs for the Lambda function to be ready..." + sleep 10 +} + +invoke_lambda_function() { + # Invoke the Lambda function + echo "🔗 Invoke the Lambda function" + aws lambda invoke \ + --function-name MyLambda \ + --cli-binary-format raw-in-base64-out \ + --payload '"Lambda Swift"' \ + output.txt > /dev/null 2>&1 + + echo "👀 Your Lambda function returned:" + cat output.txt && rm output.txt +} + +main() { + # + # Check prerequisites + # + check_prerequisites + + # + # Create the Swift project + # + create_swift_project + + # + # Now the function is ready to be deployed to AWS Lambda + # + deploy_lambda_function + + # + # Invoke the Lambda function + # + invoke_lambda_function + + echo "" + echo "🎉 Done! Your first Lambda function in Swift is now deployed on AWS Lambda. 🚀" +} + +main "$@" \ No newline at end of file diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..0ff96e27 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,44 @@ + + The SwiftAWSLambdaRuntime Project + ================================= + +Please visit the SwiftAWSLambdaRuntime web site for more information: + + * https://github.com/swift-server/swift-aws-lambda-runtime + +Copyright 2017-2021 The SwiftAWSLambdaRuntime Project + +The SwiftAWSLambdaRuntime Project licenses this file to you under the Apache License, +version 2.0 (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at: + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. + +Also, please refer to each LICENSE.txt file, which is located in +the 'license' directory of the distribution file, for the license terms of the +components that this product depends on. + +------------------------------------------------------------------------------- + + +This product contains a derivation various code and scripts from SwiftNIO. + + * LICENSE (Apache License 2.0): + * https://www.apache.org/licenses/LICENSE-2.0 + * HOMEPAGE: + * https://github.com/apple/swift-nio + +--- + +This product contains a derivation of the swift-extras' 'swift-extras-uuid'. + + * LICENSE (MIT): + * https://github.com/swift-extras/swift-extras-uuid/blob/main/LICENSE + * HOMEPAGE: + * https://github.com/swift-extras/swift-extras-uuid diff --git a/Package.swift b/Package.swift index 0e5823f6..a94ba22b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,57 +1,83 @@ -// swift-tools-version:5.2 +// swift-tools-version:6.1 import PackageDescription let package = Package( name: "swift-aws-lambda-runtime", + platforms: [.macOS(.v15)], products: [ - // this library exports `AWSLambdaRuntimeCore` and adds Foundation convenience methods .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), - // this has all the main functionality for lambda and it does not link Foundation - .library(name: "AWSLambdaRuntimeCore", targets: ["AWSLambdaRuntimeCore"]), - // common AWS events - .library(name: "AWSLambdaEvents", targets: ["AWSLambdaEvents"]), - // for testing only - .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), + // plugin to package the lambda, creating an archive that can be uploaded to AWS + // requires Linux or at least macOS v15 + .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), + ], + traits: [ + "FoundationJSONSupport", + "ServiceLifecycleSupport", + "LocalServerSupport", + .default( + enabledTraits: [ + "FoundationJSONSupport", + "ServiceLifecycleSupport", + "LocalServerSupport", + ] + ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.17.0")), - .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/swift-server/swift-backtrace.git", .upToNextMajor(from: "1.1.0")), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), ], targets: [ - .target(name: "AWSLambdaRuntime", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), - .product(name: "NIO", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), - ]), - .target(name: "AWSLambdaRuntimeCore", dependencies: [ - .product(name: "Logging", package: "swift-log"), - .product(name: "Backtrace", package: "swift-backtrace"), - .product(name: "NIOHTTP1", package: "swift-nio"), - ]), - .testTarget(name: "AWSLambdaRuntimeCoreTests", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), - .product(name: "NIOTestUtils", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), - ]), - .testTarget(name: "AWSLambdaRuntimeTests", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), - .byName(name: "AWSLambdaRuntime"), - ]), - .target(name: "AWSLambdaEvents", dependencies: []), - .testTarget(name: "AWSLambdaEventsTests", dependencies: ["AWSLambdaEvents"]), - // testing helper - .target(name: "AWSLambdaTesting", dependencies: [ - .byName(name: "AWSLambdaRuntime"), - .product(name: "NIO", package: "swift-nio"), - ]), - .testTarget(name: "AWSLambdaTestingTests", dependencies: ["AWSLambdaTesting"]), + .target( + name: "AWSLambdaRuntime", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "DequeModule", package: "swift-collections"), + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product( + name: "ServiceLifecycle", + package: "swift-service-lifecycle", + condition: .when(traits: ["ServiceLifecycleSupport"]) + ), + ] + ), + .plugin( + name: "AWSLambdaPackager", + capability: .command( + intent: .custom( + verb: "archive", + description: + "Archive the Lambda binary and prepare it for uploading to AWS. Requires docker on macOS or non Amazonlinux 2 distributions." + ), + permissions: [ + .allowNetworkConnections( + scope: .docker, + reason: "This plugin uses Docker to create the AWS Lambda ZIP package." + ) + ] + ) + ), + .testTarget( + name: "AWSLambdaRuntimeTests", + dependencies: [ + .byName(name: "AWSLambdaRuntime"), + .product(name: "NIOTestUtils", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + ] + ), // for perf testing - .target(name: "MockServer", dependencies: [ - .product(name: "NIOHTTP1", package: "swift-nio"), - ]), - .target(name: "StringSample", dependencies: ["AWSLambdaRuntime"]), - .target(name: "CodableSample", dependencies: ["AWSLambdaRuntime"]), + .executableTarget( + name: "MockServer", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + ] + ), ] ) diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 00000000..fb4e82d0 --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,72 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "swift-aws-lambda-runtime", + platforms: [.macOS(.v15)], + products: [ + .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), + // plugin to package the lambda, creating an archive that can be uploaded to AWS + // requires Linux or at least macOS v15 + .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), + ], + targets: [ + .target( + name: "AWSLambdaRuntime", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "DequeModule", package: "swift-collections"), + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), + ], + swiftSettings: [ + .define("FoundationJSONSupport"), + .define("ServiceLifecycleSupport"), + .define("LocalServerSupport"), + ] + ), + .plugin( + name: "AWSLambdaPackager", + capability: .command( + intent: .custom( + verb: "archive", + description: + "Archive the Lambda binary and prepare it for uploading to AWS. Requires docker on macOS or non Amazonlinux 2 distributions." + ), + permissions: [ + .allowNetworkConnections( + scope: .docker, + reason: "This plugin uses Docker to create the AWS Lambda ZIP package." + ) + ] + ) + ), + .testTarget( + name: "AWSLambdaRuntimeTests", + dependencies: [ + .byName(name: "AWSLambdaRuntime"), + .product(name: "NIOTestUtils", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + ] + ), + // for perf testing + .executableTarget( + name: "MockServer", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + ] + ), + ] +) diff --git a/Plugins/AWSLambdaPackager/Plugin.swift b/Plugins/AWSLambdaPackager/Plugin.swift new file mode 100644 index 00000000..10d9c8a3 --- /dev/null +++ b/Plugins/AWSLambdaPackager/Plugin.swift @@ -0,0 +1,531 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +@main +@available(macOS 15.0, *) +struct AWSLambdaPackager: CommandPlugin { + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { + let configuration = try Configuration(context: context, arguments: arguments) + + if configuration.help { + self.displayHelpMessage() + return + } + + guard !configuration.products.isEmpty else { + throw Errors.unknownProduct("no appropriate products found to package") + } + + if configuration.products.count > 1 && !configuration.explicitProducts { + let productNames = configuration.products.map(\.name) + print( + "No explicit products named, building all executable products: '\(productNames.joined(separator: "', '"))'" + ) + } + + let builtProducts: [LambdaProduct: URL] + if self.isAmazonLinux2() { + // build directly on the machine + builtProducts = try self.build( + packageIdentity: context.package.id, + products: configuration.products, + buildConfiguration: configuration.buildConfiguration, + verboseLogging: configuration.verboseLogging + ) + } else { + // build with docker + builtProducts = try self.buildInDocker( + packageIdentity: context.package.id, + packageDirectory: context.package.directoryURL, + products: configuration.products, + toolsProvider: { name in try context.tool(named: name).url }, + outputDirectory: configuration.outputDirectory, + baseImage: configuration.baseDockerImage, + disableDockerImageUpdate: configuration.disableDockerImageUpdate, + buildConfiguration: configuration.buildConfiguration, + verboseLogging: configuration.verboseLogging + ) + } + + // create the archive + let archives = try self.package( + packageName: context.package.displayName, + products: builtProducts, + toolsProvider: { name in try context.tool(named: name).url }, + outputDirectory: configuration.outputDirectory, + verboseLogging: configuration.verboseLogging + ) + + print( + "\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created" + ) + for (product, archivePath) in archives { + print(" * \(product.name) at \(archivePath.path())") + } + } + + private func buildInDocker( + packageIdentity: Package.ID, + packageDirectory: URL, + products: [Product], + toolsProvider: (String) throws -> URL, + outputDirectory: URL, + baseImage: String, + disableDockerImageUpdate: Bool, + buildConfiguration: PackageManager.BuildConfiguration, + verboseLogging: Bool + ) throws -> [LambdaProduct: URL] { + let dockerToolPath = try toolsProvider("docker") + + print("-------------------------------------------------------------------------") + print("building \"\(packageIdentity)\" in docker") + print("-------------------------------------------------------------------------") + + if !disableDockerImageUpdate { + // update the underlying docker image, if necessary + print("updating \"\(baseImage)\" docker image") + try Utils.execute( + executable: dockerToolPath, + arguments: ["pull", baseImage], + logLevel: verboseLogging ? .debug : .output + ) + } + + // get the build output path + let buildOutputPathCommand = "swift build -c \(buildConfiguration.rawValue) --show-bin-path" + let dockerBuildOutputPath = try Utils.execute( + executable: dockerToolPath, + arguments: [ + "run", "--rm", "-v", "\(packageDirectory.path()):/workspace", "-w", "/workspace", baseImage, "bash", + "-cl", buildOutputPathCommand, + ], + logLevel: verboseLogging ? .debug : .silent + ) + guard let buildPathOutput = dockerBuildOutputPath.split(separator: "\n").last else { + throw Errors.failedParsingDockerOutput(dockerBuildOutputPath) + } + let buildOutputPath = URL( + string: buildPathOutput.replacingOccurrences(of: "/workspace/", with: packageDirectory.description) + )! + + // build the products + var builtProducts = [LambdaProduct: URL]() + for product in products { + print("building \"\(product.name)\"") + let buildCommand = + "swift build -c \(buildConfiguration.rawValue) --product \(product.name) --static-swift-stdlib" + if let localPath = ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] { + // when developing locally, we must have the full swift-aws-lambda-runtime project in the container + // because Examples' Package.swift have a dependency on ../.. + // just like Package.swift's examples assume ../.., we assume we are two levels below the root project + let slice = packageDirectory.pathComponents.suffix(2) + try Utils.execute( + executable: dockerToolPath, + arguments: [ + "run", "--rm", "--env", "LAMBDA_USE_LOCAL_DEPS=\(localPath)", "-v", + "\(packageDirectory.path())../..:/workspace", "-w", + "/workspace/\(slice.joined(separator: "/"))", baseImage, "bash", "-cl", buildCommand, + ], + logLevel: verboseLogging ? .debug : .output + ) + } else { + try Utils.execute( + executable: dockerToolPath, + arguments: [ + "run", "--rm", "-v", "\(packageDirectory.path()):/workspace", "-w", "/workspace", baseImage, + "bash", "-cl", buildCommand, + ], + logLevel: verboseLogging ? .debug : .output + ) + } + let productPath = buildOutputPath.appending(path: product.name) + + guard FileManager.default.fileExists(atPath: productPath.path()) else { + Diagnostics.error("expected '\(product.name)' binary at \"\(productPath.path())\"") + throw Errors.productExecutableNotFound(product.name) + } + builtProducts[.init(product)] = productPath + } + return builtProducts + } + + private func build( + packageIdentity: Package.ID, + products: [Product], + buildConfiguration: PackageManager.BuildConfiguration, + verboseLogging: Bool + ) throws -> [LambdaProduct: URL] { + print("-------------------------------------------------------------------------") + print("building \"\(packageIdentity)\"") + print("-------------------------------------------------------------------------") + + var results = [LambdaProduct: URL]() + for product in products { + print("building \"\(product.name)\"") + var parameters = PackageManager.BuildParameters() + parameters.configuration = buildConfiguration + parameters.otherSwiftcFlags = ["-static-stdlib"] + parameters.logging = verboseLogging ? .verbose : .concise + + let result = try packageManager.build( + .product(product.name), + parameters: parameters + ) + guard let artifact = result.executableArtifact(for: product) else { + throw Errors.productExecutableNotFound(product.name) + } + results[.init(product)] = artifact.url + } + return results + } + + // TODO: explore using ziplib or similar instead of shelling out + private func package( + packageName: String, + products: [LambdaProduct: URL], + toolsProvider: (String) throws -> URL, + outputDirectory: URL, + verboseLogging: Bool + ) throws -> [LambdaProduct: URL] { + let zipToolPath = try toolsProvider("zip") + + var archives = [LambdaProduct: URL]() + for (product, artifactPath) in products { + print("-------------------------------------------------------------------------") + print("archiving \"\(product.name)\"") + print("-------------------------------------------------------------------------") + + // prep zipfile location + let workingDirectory = outputDirectory.appending(path: product.name) + let zipfilePath = workingDirectory.appending(path: "\(product.name).zip") + if FileManager.default.fileExists(atPath: workingDirectory.path()) { + try FileManager.default.removeItem(atPath: workingDirectory.path()) + } + try FileManager.default.createDirectory(atPath: workingDirectory.path(), withIntermediateDirectories: true) + + // rename artifact to "bootstrap" + let relocatedArtifactPath = workingDirectory.appending(path: "bootstrap") + try FileManager.default.copyItem(atPath: artifactPath.path(), toPath: relocatedArtifactPath.path()) + + var arguments: [String] = [] + #if os(macOS) || os(Linux) + arguments = [ + "--recurse-paths", + "--symlinks", + zipfilePath.lastPathComponent, + relocatedArtifactPath.lastPathComponent, + ] + #else + throw Errors.unsupportedPlatform("can't or don't know how to create a zip file on this platform") + #endif + + // add resources + var artifactPathComponents = artifactPath.pathComponents + _ = artifactPathComponents.removeFirst() // Get rid of beginning "/" + _ = artifactPathComponents.removeLast() // Get rid of the name of the package + let artifactDirectory = "/\(artifactPathComponents.joined(separator: "/"))" + for fileInArtifactDirectory in try FileManager.default.contentsOfDirectory(atPath: artifactDirectory) { + guard let artifactURL = URL(string: "\(artifactDirectory)/\(fileInArtifactDirectory)") else { + continue + } + + guard artifactURL.pathExtension == "resources" else { + continue // Not resources, so don't copy + } + let resourcesDirectoryName = artifactURL.lastPathComponent + let relocatedResourcesDirectory = workingDirectory.appending(path: resourcesDirectoryName) + if FileManager.default.fileExists(atPath: artifactURL.path()) { + do { + arguments.append(resourcesDirectoryName) + try FileManager.default.copyItem( + atPath: artifactURL.path(), + toPath: relocatedResourcesDirectory.path() + ) + } catch let error as CocoaError { + + // On Linux, when the build has been done with Docker, + // the source file are owned by root + // this causes a permission error **after** the files have been copied + // see https://github.com/swift-server/swift-aws-lambda-runtime/issues/449 + // see https://forums.swift.org/t/filemanager-copyitem-on-linux-fails-after-copying-the-files/77282 + + // because this error happens after the files have been copied, we can ignore it + // this code checks if the destination file exists + // if they do, just ignore error, otherwise throw it up to the caller. + if !(error.code == CocoaError.Code.fileWriteNoPermission + && FileManager.default.fileExists(atPath: relocatedResourcesDirectory.path())) + { + throw error + } // else just ignore it + } + } + } + + // run the zip tool + try Utils.execute( + executable: zipToolPath, + arguments: arguments, + customWorkingDirectory: workingDirectory, + logLevel: verboseLogging ? .debug : .silent + ) + + archives[product] = zipfilePath + } + return archives + } + + private func isAmazonLinux2() -> Bool { + if let data = FileManager.default.contents(atPath: "/etc/system-release"), + let release = String(data: data, encoding: .utf8) + { + return release.hasPrefix("Amazon Linux release 2") + } else { + return false + } + } + + private func displayHelpMessage() { + print( + """ + OVERVIEW: A SwiftPM plugin to build and package your lambda function. + + REQUIREMENTS: To use this plugin, you must have docker installed and started. + + USAGE: swift package --allow-network-connections docker archive + [--help] [--verbose] + [--output-path ] + [--products ] + [--configuration debug | release] + [--swift-version ] + [--base-docker-image ] + [--disable-docker-image-update] + + + OPTIONS: + --verbose Produce verbose output for debugging. + --output-path The path of the binary package. + (default is `.build/plugins/AWSLambdaPackager/outputs/...`) + --products The list of executable targets to build. + (default is taken from Package.swift) + --configuration The build configuration (debug or release) + (default is release) + --swift-version The swift version to use for building. + (default is latest) + This parameter cannot be used when --base-docker-image is specified. + --base-docker-image The name of the base docker image to use for the build. + (default : swift-:amazonlinux2) + This parameter cannot be used when --swift-version is specified. + --disable-docker-image-update Do not attempt to update the docker image + --help Show help information. + """ + ) + } +} + +@available(macOS 15.0, *) +private struct Configuration: CustomStringConvertible { + public let help: Bool + public let outputDirectory: URL + public let products: [Product] + public let explicitProducts: Bool + public let buildConfiguration: PackageManager.BuildConfiguration + public let verboseLogging: Bool + public let baseDockerImage: String + public let disableDockerImageUpdate: Bool + + public init( + context: PluginContext, + arguments: [String] + ) throws { + var argumentExtractor = ArgumentExtractor(arguments) + let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 + let outputPathArgument = argumentExtractor.extractOption(named: "output-path") + let productsArgument = argumentExtractor.extractOption(named: "products") + let configurationArgument = argumentExtractor.extractOption(named: "configuration") + let swiftVersionArgument = argumentExtractor.extractOption(named: "swift-version") + let baseDockerImageArgument = argumentExtractor.extractOption(named: "base-docker-image") + let disableDockerImageUpdateArgument = argumentExtractor.extractFlag(named: "disable-docker-image-update") > 0 + let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 + + // help required ? + self.help = helpArgument + + // verbose logging required ? + self.verboseLogging = verboseArgument + + if let outputPath = outputPathArgument.first { + #if os(Linux) + var isDirectory: Bool = false + #else + var isDirectory: ObjCBool = false + #endif + guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory) + else { + throw Errors.invalidArgument("invalid output directory '\(outputPath)'") + } + self.outputDirectory = URL(string: outputPath)! + } else { + self.outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaPackager.self)") + } + + self.explicitProducts = !productsArgument.isEmpty + if self.explicitProducts { + let products = try context.package.products(named: productsArgument) + for product in products { + guard product is ExecutableProduct else { + throw Errors.invalidArgument("product named '\(product.name)' is not an executable product") + } + } + self.products = products + + } else { + self.products = context.package.products.filter { $0 is ExecutableProduct } + } + + if let buildConfigurationName = configurationArgument.first { + guard let buildConfiguration = PackageManager.BuildConfiguration(rawValue: buildConfigurationName) else { + throw Errors.invalidArgument("invalid build configuration named '\(buildConfigurationName)'") + } + self.buildConfiguration = buildConfiguration + } else { + self.buildConfiguration = .release + } + + guard !(!swiftVersionArgument.isEmpty && !baseDockerImageArgument.isEmpty) else { + throw Errors.invalidArgument("--swift-version and --base-docker-image are mutually exclusive") + } + + let swiftVersion = swiftVersionArgument.first ?? .none // undefined version will yield the latest docker image + self.baseDockerImage = + baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2" + + self.disableDockerImageUpdate = disableDockerImageUpdateArgument + + if self.verboseLogging { + print("-------------------------------------------------------------------------") + print("configuration") + print("-------------------------------------------------------------------------") + print(self) + } + } + + var description: String { + """ + { + outputDirectory: \(self.outputDirectory) + products: \(self.products.map(\.name)) + buildConfiguration: \(self.buildConfiguration) + baseDockerImage: \(self.baseDockerImage) + disableDockerImageUpdate: \(self.disableDockerImageUpdate) + } + """ + } +} + +private enum ProcessLogLevel: Comparable { + case silent + case output(outputIndent: Int) + case debug(outputIndent: Int) + + var naturalOrder: Int { + switch self { + case .silent: + return 0 + case .output: + return 1 + case .debug: + return 2 + } + } + + static var output: Self { + .output(outputIndent: 2) + } + + static var debug: Self { + .debug(outputIndent: 2) + } + + static func < (lhs: ProcessLogLevel, rhs: ProcessLogLevel) -> Bool { + lhs.naturalOrder < rhs.naturalOrder + } +} + +private enum Errors: Error, CustomStringConvertible { + case invalidArgument(String) + case unsupportedPlatform(String) + case unknownProduct(String) + case productExecutableNotFound(String) + case failedWritingDockerfile + case failedParsingDockerOutput(String) + case processFailed([String], Int32) + + var description: String { + switch self { + case .invalidArgument(let description): + return description + case .unsupportedPlatform(let description): + return description + case .unknownProduct(let description): + return description + case .productExecutableNotFound(let product): + return "product executable not found '\(product)'" + case .failedWritingDockerfile: + return "failed writing dockerfile" + case .failedParsingDockerOutput(let output): + return "failed parsing docker output: '\(output)'" + case .processFailed(let arguments, let code): + return "\(arguments.joined(separator: " ")) failed with code \(code)" + } + } +} + +private struct LambdaProduct: Hashable { + let underlying: Product + + init(_ underlying: Product) { + self.underlying = underlying + } + + var name: String { + self.underlying.name + } + + func hash(into hasher: inout Hasher) { + self.underlying.id.hash(into: &hasher) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.underlying.id == rhs.underlying.id + } +} + +extension PackageManager.BuildResult { + // find the executable produced by the build + func executableArtifact(for product: Product) -> PackageManager.BuildResult.BuiltArtifact? { + let executables = self.builtArtifacts.filter { + $0.kind == .executable && $0.url.lastPathComponent == product.name + } + guard !executables.isEmpty else { + return nil + } + guard executables.count == 1, let executable = executables.first else { + return nil + } + return executable + } +} diff --git a/Plugins/AWSLambdaPackager/PluginUtils.swift b/Plugins/AWSLambdaPackager/PluginUtils.swift new file mode 100644 index 00000000..f4e8cb02 --- /dev/null +++ b/Plugins/AWSLambdaPackager/PluginUtils.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Dispatch +import Foundation +import PackagePlugin +import Synchronization + +@available(macOS 15.0, *) +struct Utils { + @discardableResult + static func execute( + executable: URL, + arguments: [String], + customWorkingDirectory: URL? = .none, + logLevel: ProcessLogLevel + ) throws -> String { + if logLevel >= .debug { + print("\(executable.path()) \(arguments.joined(separator: " "))") + if let customWorkingDirectory { + print("Working directory: \(customWorkingDirectory.path())") + } + } + + let fd = dup(1) + let stdout = fdopen(fd, "rw") + defer { if let so = stdout { fclose(so) } } + + // We need to use an unsafe transfer here to get the fd into our Sendable closure. + // This transfer is fine, because we write to the variable from a single SerialDispatchQueue here. + // We wait until the process is run below process.waitUntilExit(). + // This means no further writes to output will happen. + // This makes it save for us to read the output + struct UnsafeTransfer: @unchecked Sendable { + let value: Value + } + + let outputMutex = Mutex("") + let outputSync = DispatchGroup() + let outputQueue = DispatchQueue(label: "AWSLambdaPackager.output") + let unsafeTransfer = UnsafeTransfer(value: stdout) + let outputHandler = { @Sendable (data: Data?) in + dispatchPrecondition(condition: .onQueue(outputQueue)) + + outputSync.enter() + defer { outputSync.leave() } + + guard + let _output = data.flatMap({ + String(data: $0, encoding: .utf8)?.trimmingCharacters(in: CharacterSet(["\n"])) + }), !_output.isEmpty + else { + return + } + + outputMutex.withLock { output in + output += _output + "\n" + } + + switch logLevel { + case .silent: + break + case .debug(let outputIndent), .output(let outputIndent): + print(String(repeating: " ", count: outputIndent), terminator: "") + print(_output) + fflush(unsafeTransfer.value) + } + } + + let pipe = Pipe() + pipe.fileHandleForReading.readabilityHandler = { fileHandle in + outputQueue.async { outputHandler(fileHandle.availableData) } + } + + let process = Process() + process.standardOutput = pipe + process.standardError = pipe + process.executableURL = executable + process.arguments = arguments + if let customWorkingDirectory { + process.currentDirectoryURL = URL(fileURLWithPath: customWorkingDirectory.path()) + } + process.terminationHandler = { _ in + outputQueue.async { + outputHandler(try? pipe.fileHandleForReading.readToEnd()) + } + } + + try process.run() + process.waitUntilExit() + + // wait for output to be full processed + outputSync.wait() + + let output = outputMutex.withLock { $0 } + + if process.terminationStatus != 0 { + // print output on failure and if not already printed + if logLevel < .output { + print(output) + fflush(stdout) + } + throw ProcessError.processFailed([executable.path()] + arguments, process.terminationStatus) + } + + return output + } + + enum ProcessError: Error, CustomStringConvertible { + case processFailed([String], Int32) + + var description: String { + switch self { + case .processFailed(let arguments, let code): + return "\(arguments.joined(separator: " ")) failed with code \(code)" + } + } + } + + enum ProcessLogLevel: Comparable { + case silent + case output(outputIndent: Int) + case debug(outputIndent: Int) + + var naturalOrder: Int { + switch self { + case .silent: + return 0 + case .output: + return 1 + case .debug: + return 2 + } + } + + static var output: Self { + .output(outputIndent: 2) + } + + static var debug: Self { + .debug(outputIndent: 2) + } + + static func < (lhs: ProcessLogLevel, rhs: ProcessLogLevel) -> Bool { + lhs.naturalOrder < rhs.naturalOrder + } + } +} diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..1430c662 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,43 @@ +# Security + +This document specifies the security process for the Swift AWS Lambda Runtime project. + +## Disclosures + +### Private Disclosure Process + +The Swift AWS Lambda Runtime maintainers ask that known and suspected vulnerabilities be +privately and responsibly disclosed by emailing +[sswg-security-reports@forums.swift.org](mailto:sswg-security-reports@forums.swift.org) +with the all the required detail. +**Do not file a public issue.** + +#### When to report a vulnerability + +* You think you have discovered a potential security vulnerability in Swift AWS Lambda Runtime. +* You are unsure how a vulnerability affects Swift AWS Lambda Runtime. + +#### What happens next? + +* A member of the team will acknowledge receipt of the report within 3 + working days (United States). This may include a request for additional + information about reproducing the vulnerability. +* We will privately inform the Swift Server Work Group ([SSWG][sswg]) of the + vulnerability within 10 days of the report as per their [security + guidelines][sswg-security]. +* Once we have identified a fix we may ask you to validate it. We aim to do this + within 30 days. In some cases this may not be possible, for example when the + vulnerability exists at the protocol level and the industry must coordinate on + the disclosure process. +* If a CVE number is required, one will be requested from [MITRE][mitre] + providing you with full credit for the discovery. +* We will decide on a planned release date and let you know when it is. +* Prior to release, we will inform major dependents that a security-related + patch is impending. +* Once the fix has been released we will publish a security advisory on GitHub + and in the Server → Security Updates category on the [Swift forums][swift-forums-sec]. + +[sswg]: https://github.com/swift-server/sswg +[sswg-security]: https://github.com/swift-server/sswg/blob/main/security/README.md +[swift-forums-sec]: https://forums.swift.org/c/server/security-updates/ +[mitre]: https://cveform.mitre.org/ diff --git a/Sources/AWSLambdaEvents/ALB.swift b/Sources/AWSLambdaEvents/ALB.swift deleted file mode 100644 index 121531a6..00000000 --- a/Sources/AWSLambdaEvents/ALB.swift +++ /dev/null @@ -1,77 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import class Foundation.JSONEncoder - -// https://github.com/aws/aws-lambda-go/blob/master/events/alb.go -public enum ALB { - /// ALBTargetGroupRequest contains data originating from the ALB Lambda target group integration - public struct TargetGroupRequest: Codable { - /// ALBTargetGroupRequestContext contains the information to identify the load balancer invoking the lambda - public struct Context: Codable { - public let elb: ELBContext - } - - public let httpMethod: HTTPMethod - public let path: String - public let queryStringParameters: [String: [String]] - - /// Depending on your configuration of your target group either `headers` or `multiValueHeaders` - /// are set. - /// - /// For more information visit: - /// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers - public let headers: HTTPHeaders? - - /// Depending on your configuration of your target group either `headers` or `multiValueHeaders` - /// are set. - /// - /// For more information visit: - /// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers - public let multiValueHeaders: HTTPMultiValueHeaders? - public let requestContext: Context - public let isBase64Encoded: Bool - public let body: String? - } - - /// ELBContext contains the information to identify the ARN invoking the lambda - public struct ELBContext: Codable { - public let targetGroupArn: String - } - - public struct TargetGroupResponse: Codable { - public let statusCode: HTTPResponseStatus - public let statusDescription: String? - public let headers: HTTPHeaders? - public let multiValueHeaders: HTTPMultiValueHeaders? - public let body: String - public let isBase64Encoded: Bool - - public init( - statusCode: HTTPResponseStatus, - statusDescription: String? = nil, - headers: HTTPHeaders? = nil, - multiValueHeaders: HTTPMultiValueHeaders? = nil, - body: String = "", - isBase64Encoded: Bool = false - ) { - self.statusCode = statusCode - self.statusDescription = statusDescription - self.headers = headers - self.multiValueHeaders = multiValueHeaders - self.body = body - self.isBase64Encoded = isBase64Encoded - } - } -} diff --git a/Sources/AWSLambdaEvents/APIGateway+V2.swift b/Sources/AWSLambdaEvents/APIGateway+V2.swift deleted file mode 100644 index 12c4c2ce..00000000 --- a/Sources/AWSLambdaEvents/APIGateway+V2.swift +++ /dev/null @@ -1,119 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -extension APIGateway { - public struct V2 {} -} - -extension APIGateway.V2 { - /// APIGateway.V2.Request contains data coming from the new HTTP API Gateway - public struct Request: Codable { - /// Context contains the information to identify the AWS account and resources invoking the Lambda function. - public struct Context: Codable { - public struct HTTP: Codable { - public let method: HTTPMethod - public let path: String - public let `protocol`: String - public let sourceIp: String - public let userAgent: String - } - - /// Authorizer contains authorizer information for the request context. - public struct Authorizer: Codable { - /// JWT contains JWT authorizer information for the request context. - public struct JWT: Codable { - public let claims: [String: String] - public let scopes: [String]? - } - - let jwt: JWT - } - - public let accountId: String - public let apiId: String - public let domainName: String - public let domainPrefix: String - public let stage: String - public let requestId: String - - public let http: HTTP - public let authorizer: Authorizer? - - /// The request time in format: 23/Apr/2020:11:08:18 +0000 - public let time: String - public let timeEpoch: UInt64 - } - - public let version: String - public let routeKey: String - public let rawPath: String - public let rawQueryString: String - - public let cookies: [String]? - public let headers: HTTPHeaders - public let queryStringParameters: [String: String]? - public let pathParameters: [String: String]? - - public let context: Context - public let stageVariables: [String: String]? - - public let body: String? - public let isBase64Encoded: Bool - - enum CodingKeys: String, CodingKey { - case version - case routeKey - case rawPath - case rawQueryString - - case cookies - case headers - case queryStringParameters - case pathParameters - - case context = "requestContext" - case stageVariables - - case body - case isBase64Encoded - } - } -} - -extension APIGateway.V2 { - public struct Response: Codable { - public let statusCode: HTTPResponseStatus - public let headers: HTTPHeaders? - public let multiValueHeaders: HTTPMultiValueHeaders? - public let body: String? - public let isBase64Encoded: Bool? - public let cookies: [String]? - - public init( - statusCode: HTTPResponseStatus, - headers: HTTPHeaders? = nil, - multiValueHeaders: HTTPMultiValueHeaders? = nil, - body: String? = nil, - isBase64Encoded: Bool? = nil, - cookies: [String]? = nil - ) { - self.statusCode = statusCode - self.headers = headers - self.multiValueHeaders = multiValueHeaders - self.body = body - self.isBase64Encoded = isBase64Encoded - self.cookies = cookies - } - } -} diff --git a/Sources/AWSLambdaEvents/APIGateway.swift b/Sources/AWSLambdaEvents/APIGateway.swift deleted file mode 100644 index 5ae961e2..00000000 --- a/Sources/AWSLambdaEvents/APIGateway.swift +++ /dev/null @@ -1,93 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import class Foundation.JSONEncoder - -// https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html -// https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html - -public enum APIGateway { - /// APIGatewayRequest contains data coming from the API Gateway - public struct Request: Codable { - public struct Context: Codable { - public struct Identity: Codable { - public let cognitoIdentityPoolId: String? - - public let apiKey: String? - public let userArn: String? - public let cognitoAuthenticationType: String? - public let caller: String? - public let userAgent: String? - public let user: String? - - public let cognitoAuthenticationProvider: String? - public let sourceIp: String? - public let accountId: String? - } - - public let resourceId: String - public let apiId: String - public let resourcePath: String - public let httpMethod: String - public let requestId: String - public let accountId: String - public let stage: String - - public let identity: Identity - public let extendedRequestId: String? - public let path: String - } - - public let resource: String - public let path: String - public let httpMethod: HTTPMethod - - public let queryStringParameters: [String: String]? - public let multiValueQueryStringParameters: [String: [String]]? - public let headers: HTTPHeaders - public let multiValueHeaders: HTTPMultiValueHeaders - public let pathParameters: [String: String]? - public let stageVariables: [String: String]? - - public let requestContext: Context - public let body: String? - public let isBase64Encoded: Bool - } -} - -// MARK: - Response - - -extension APIGateway { - public struct Response: Codable { - public let statusCode: HTTPResponseStatus - public let headers: HTTPHeaders? - public let multiValueHeaders: HTTPMultiValueHeaders? - public let body: String? - public let isBase64Encoded: Bool? - - public init( - statusCode: HTTPResponseStatus, - headers: HTTPHeaders? = nil, - multiValueHeaders: HTTPMultiValueHeaders? = nil, - body: String? = nil, - isBase64Encoded: Bool? = nil - ) { - self.statusCode = statusCode - self.headers = headers - self.multiValueHeaders = multiValueHeaders - self.body = body - self.isBase64Encoded = isBase64Encoded - } - } -} diff --git a/Sources/AWSLambdaEvents/AWSRegion.swift b/Sources/AWSLambdaEvents/AWSRegion.swift deleted file mode 100644 index e884d3a6..00000000 --- a/Sources/AWSLambdaEvents/AWSRegion.swift +++ /dev/null @@ -1,92 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -// list all available regions using aws cli: -// $ aws ssm get-parameters-by-path --path /aws/service/global-infrastructure/services/lambda/regions --output json - -/// Enumeration of the AWS Regions. -public struct AWSRegion: RawRepresentable, Equatable { - public typealias RawValue = String - - public let rawValue: String - - public init?(rawValue: String) { - self.rawValue = rawValue - } - - static var all: [AWSRegion] = [ - Self.ap_northeast_1, - Self.ap_northeast_2, - Self.ap_east_1, - Self.ap_southeast_1, - Self.ap_southeast_2, - Self.ap_south_1, - Self.cn_north_1, - Self.cn_northwest_1, - Self.eu_north_1, - Self.eu_west_1, - Self.eu_west_2, - Self.eu_west_3, - Self.eu_central_1, - Self.us_east_1, - Self.us_east_2, - Self.us_west_1, - Self.us_west_2, - Self.us_gov_east_1, - Self.us_gov_west_1, - Self.ca_central_1, - Self.sa_east_1, - Self.me_south_1, - ] - - public static var ap_northeast_1: Self { AWSRegion(rawValue: "ap-northeast-1")! } - public static var ap_northeast_2: Self { AWSRegion(rawValue: "ap-northeast-2")! } - public static var ap_east_1: Self { AWSRegion(rawValue: "ap-east-1")! } - public static var ap_southeast_1: Self { AWSRegion(rawValue: "ap-southeast-1")! } - public static var ap_southeast_2: Self { AWSRegion(rawValue: "ap-southeast-2")! } - public static var ap_south_1: Self { AWSRegion(rawValue: "ap-south-1")! } - - public static var cn_north_1: Self { AWSRegion(rawValue: "cn-north-1")! } - public static var cn_northwest_1: Self { AWSRegion(rawValue: "cn-northwest-1")! } - - public static var eu_north_1: Self { AWSRegion(rawValue: "eu-north-1")! } - public static var eu_west_1: Self { AWSRegion(rawValue: "eu-west-1")! } - public static var eu_west_2: Self { AWSRegion(rawValue: "eu-west-2")! } - public static var eu_west_3: Self { AWSRegion(rawValue: "eu-west-3")! } - public static var eu_central_1: Self { AWSRegion(rawValue: "eu-central-1")! } - - public static var us_east_1: Self { AWSRegion(rawValue: "us-east-1")! } - public static var us_east_2: Self { AWSRegion(rawValue: "us-east-2")! } - public static var us_west_1: Self { AWSRegion(rawValue: "us-west-1")! } - public static var us_west_2: Self { AWSRegion(rawValue: "us-west-2")! } - public static var us_gov_east_1: Self { AWSRegion(rawValue: "us-gov-east-1")! } - public static var us_gov_west_1: Self { AWSRegion(rawValue: "us-gov-west-1")! } - - public static var ca_central_1: Self { AWSRegion(rawValue: "ca-central-1")! } - public static var sa_east_1: Self { AWSRegion(rawValue: "sa-east-1")! } - public static var me_south_1: Self { AWSRegion(rawValue: "me-south-1")! } -} - -extension AWSRegion: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let region = try container.decode(String.self) - self.init(rawValue: region)! - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.rawValue) - } -} diff --git a/Sources/AWSLambdaEvents/Cloudwatch.swift b/Sources/AWSLambdaEvents/Cloudwatch.swift deleted file mode 100644 index 3ef3eb9e..00000000 --- a/Sources/AWSLambdaEvents/Cloudwatch.swift +++ /dev/null @@ -1,129 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import struct Foundation.Date - -/// EventBridge has the same events/notification types as CloudWatch -typealias EventBridge = Cloudwatch - -public protocol CloudwatchDetail: Decodable { - static var name: String { get } -} - -public extension CloudwatchDetail { - var detailType: String { - Self.name - } -} - -public enum Cloudwatch { - /// CloudWatch.Event is the outer structure of an event sent via CloudWatch Events. - /// - /// **NOTE**: For examples of events that come via CloudWatch Events, see - /// https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html - /// https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html - /// https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html - public struct Event: Decodable { - public let id: String - public let source: String - public let accountId: String - public let time: Date - public let region: AWSRegion - public let resources: [String] - public let detail: Detail - - enum CodingKeys: String, CodingKey { - case id - case source - case accountId = "account" - case time - case region - case resources - case detailType = "detail-type" - case detail - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.id = try container.decode(String.self, forKey: .id) - self.source = try container.decode(String.self, forKey: .source) - self.accountId = try container.decode(String.self, forKey: .accountId) - self.time = (try container.decode(ISO8601Coding.self, forKey: .time)).wrappedValue - self.region = try container.decode(AWSRegion.self, forKey: .region) - self.resources = try container.decode([String].self, forKey: .resources) - - let detailType = try container.decode(String.self, forKey: .detailType) - guard detailType.lowercased() == Detail.name.lowercased() else { - throw DetailTypeMismatch(name: detailType, type: Detail.self) - } - - self.detail = try container.decode(Detail.self, forKey: .detail) - } - } - - // MARK: - Common Event Types - - public typealias ScheduledEvent = Event - public struct Scheduled: CloudwatchDetail { - public static let name = "Scheduled Event" - } - - public enum EC2 { - public typealias InstanceStateChangeNotificationEvent = Event - public struct InstanceStateChangeNotification: CloudwatchDetail { - public static let name = "EC2 Instance State-change Notification" - - public enum State: String, Codable { - case running - case shuttingDown = "shutting-down" - case stopped - case stopping - case terminated - } - - public let instanceId: String - public let state: State - - enum CodingKeys: String, CodingKey { - case instanceId = "instance-id" - case state - } - } - - public typealias SpotInstanceInterruptionNoticeEvent = Event - public struct SpotInstanceInterruptionNotice: CloudwatchDetail { - public static let name = "EC2 Spot Instance Interruption Warning" - - public enum Action: String, Codable { - case hibernate - case stop - case terminate - } - - public let instanceId: String - public let action: Action - - enum CodingKeys: String, CodingKey { - case instanceId = "instance-id" - case action = "instance-action" - } - } - } - - struct DetailTypeMismatch: Error { - let name: String - let type: Any - } -} diff --git a/Sources/AWSLambdaEvents/DynamoDB.swift b/Sources/AWSLambdaEvents/DynamoDB.swift deleted file mode 100644 index ebc9f645..00000000 --- a/Sources/AWSLambdaEvents/DynamoDB.swift +++ /dev/null @@ -1,942 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import struct Foundation.Date - -// https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html -public struct DynamoDB { - public struct Event: Decodable { - public let records: [EventRecord] - - public enum CodingKeys: String, CodingKey { - case records = "Records" - } - } - - public enum KeyType: String, Codable { - case hash = "HASH" - case range = "RANGE" - } - - public enum OperationType: String, Codable { - case insert = "INSERT" - case modify = "MODIFY" - case remove = "REMOVE" - } - - public enum SharedIteratorType: String, Codable { - case trimHorizon = "TRIM_HORIZON" - case latest = "LATEST" - case atSequenceNumber = "AT_SEQUENCE_NUMBER" - case afterSequenceNumber = "AFTER_SEQUENCE_NUMBER" - } - - public enum StreamStatus: String, Codable { - case enabling = "ENABLING" - case enabled = "ENABLED" - case disabling = "DISABLING" - case disabled = "DISABLED" - } - - public enum StreamViewType: String, Codable { - /// the entire item, as it appeared after it was modified. - case newImage = "NEW_IMAGE" - /// the entire item, as it appeared before it was modified. - case oldImage = "OLD_IMAGE" - /// both the new and the old item images of the item. - case newAndOldImages = "NEW_AND_OLD_IMAGES" - /// only the key attributes of the modified item. - case keysOnly = "KEYS_ONLY" - } - - public struct EventRecord: Decodable { - /// The region in which the GetRecords request was received. - public let awsRegion: AWSRegion - - /// The main body of the stream record, containing all of the DynamoDB-specific - /// fields. - public let change: StreamRecord - - /// A globally unique identifier for the event that was recorded in this stream - /// record. - public let eventId: String - - /// The type of data modification that was performed on the DynamoDB table: - /// * INSERT - a new item was added to the table. - /// * MODIFY - one or more of an existing item's attributes were modified. - /// * REMOVE - the item was deleted from the table - public let eventName: OperationType - - /// The AWS service from which the stream record originated. For DynamoDB Streams, - /// this is aws:dynamodb. - public let eventSource: String - - /// The version number of the stream record format. This number is updated whenever - /// the structure of Record is modified. - /// - /// Client applications must not assume that eventVersion will remain at a particular - /// value, as this number is subject to change at any time. In general, eventVersion - /// will only increase as the low-level DynamoDB Streams API evolves. - public let eventVersion: String - - /// The event source ARN of DynamoDB - public let eventSourceArn: String - - /// Items that are deleted by the Time to Live process after expiration have - /// the following fields: - /// * Records[].userIdentity.type - /// - /// "Service" - /// * Records[].userIdentity.principalId - /// - /// "dynamodb.amazonaws.com" - public let userIdentity: UserIdentity? - - public enum CodingKeys: String, CodingKey { - case awsRegion - case change = "dynamodb" - case eventId = "eventID" - case eventName - case eventSource - case eventVersion - case eventSourceArn = "eventSourceARN" - case userIdentity - } - } - - public struct StreamRecord { - /// The approximate date and time when the stream record was created, in UNIX - /// epoch time (http://www.epochconverter.com/) format. - public let approximateCreationDateTime: Date? - - /// The primary key attribute(s) for the DynamoDB item that was modified. - public let keys: [String: AttributeValue] - - /// The item in the DynamoDB table as it appeared after it was modified. - public let newImage: [String: AttributeValue]? - - /// The item in the DynamoDB table as it appeared before it was modified. - public let oldImage: [String: AttributeValue]? - - /// The sequence number of the stream record. - public let sequenceNumber: String - - /// The size of the stream record, in bytes. - public let sizeBytes: Int64 - - /// The type of data from the modified DynamoDB item that was captured in this - /// stream record. - public let streamViewType: StreamViewType - } - - public struct UserIdentity: Codable { - public let type: String - public let principalId: String - } -} - -extension DynamoDB.StreamRecord: Decodable { - enum CodingKeys: String, CodingKey { - case approximateCreationDateTime = "ApproximateCreationDateTime" - case keys = "Keys" - case newImage = "NewImage" - case oldImage = "OldImage" - case sequenceNumber = "SequenceNumber" - case sizeBytes = "SizeBytes" - case streamViewType = "StreamViewType" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.keys = try container.decode( - [String: DynamoDB.AttributeValue].self, - forKey: .keys - ) - - self.newImage = try container.decodeIfPresent( - [String: DynamoDB.AttributeValue].self, - forKey: .newImage - ) - self.oldImage = try container.decodeIfPresent( - [String: DynamoDB.AttributeValue].self, - forKey: .oldImage - ) - - self.sequenceNumber = try container.decode(String.self, forKey: .sequenceNumber) - self.sizeBytes = try container.decode(Int64.self, forKey: .sizeBytes) - self.streamViewType = try container.decode(DynamoDB.StreamViewType.self, forKey: .streamViewType) - - if let timestamp = try container.decodeIfPresent(Double.self, forKey: .approximateCreationDateTime) { - self.approximateCreationDateTime = Date(timeIntervalSince1970: timestamp) - } else { - self.approximateCreationDateTime = nil - } - } -} - -// MARK: - AttributeValue - - -extension DynamoDB { - public enum AttributeValue { - case boolean(Bool) - case binary([UInt8]) - case binarySet([[UInt8]]) - case string(String) - case stringSet([String]) - case null - case number(String) - case numberSet([String]) - - case list([AttributeValue]) - case map([String: AttributeValue]) - } -} - -extension DynamoDB.AttributeValue: Decodable { - enum CodingKeys: String, CodingKey { - case binary = "B" - case bool = "BOOL" - case binarySet = "BS" - case list = "L" - case map = "M" - case number = "N" - case numberSet = "NS" - case null = "NULL" - case string = "S" - case stringSet = "SS" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - guard container.allKeys.count == 1, let key = container.allKeys.first else { - let context = DecodingError.Context( - codingPath: container.codingPath, - debugDescription: "Expected exactly one key, but got \(container.allKeys.count)" - ) - throw DecodingError.dataCorrupted(context) - } - - switch key { - case .binary: - let encoded = try container.decode(String.self, forKey: .binary) - self = .binary(try encoded.base64decoded()) - - case .bool: - let value = try container.decode(Bool.self, forKey: .bool) - self = .boolean(value) - - case .binarySet: - let values = try container.decode([String].self, forKey: .binarySet) - let buffers = try values.map { try $0.base64decoded() } - self = .binarySet(buffers) - - case .list: - let values = try container.decode([DynamoDB.AttributeValue].self, forKey: .list) - self = .list(values) - - case .map: - let value = try container.decode([String: DynamoDB.AttributeValue].self, forKey: .map) - self = .map(value) - - case .number: - let value = try container.decode(String.self, forKey: .number) - self = .number(value) - - case .numberSet: - let values = try container.decode([String].self, forKey: .numberSet) - self = .numberSet(values) - - case .null: - self = .null - - case .string: - let value = try container.decode(String.self, forKey: .string) - self = .string(value) - - case .stringSet: - let values = try container.decode([String].self, forKey: .stringSet) - self = .stringSet(values) - } - } -} - -extension DynamoDB.AttributeValue: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case (.boolean(let lhs), .boolean(let rhs)): - return lhs == rhs - case (.binary(let lhs), .binary(let rhs)): - return lhs == rhs - case (.binarySet(let lhs), .binarySet(let rhs)): - return lhs == rhs - case (.string(let lhs), .string(let rhs)): - return lhs == rhs - case (.stringSet(let lhs), .stringSet(let rhs)): - return lhs == rhs - case (.null, .null): - return true - case (.number(let lhs), .number(let rhs)): - return lhs == rhs - case (.numberSet(let lhs), .numberSet(let rhs)): - return lhs == rhs - case (.list(let lhs), .list(let rhs)): - return lhs == rhs - case (.map(let lhs), .map(let rhs)): - return lhs == rhs - default: - return false - } - } -} - -// MARK: DynamoDB AttributeValue Decoding - -extension DynamoDB { - public struct Decoder { - @usableFromInline var userInfo: [CodingUserInfoKey: Any] = [:] - - public init() {} - - @inlinable public func decode(_ type: T.Type, from image: [String: AttributeValue]) - throws -> T - { - try self.decode(type, from: .map(image)) - } - - @inlinable public func decode(_ type: T.Type, from value: AttributeValue) - throws -> T - { - let decoder = _DecoderImpl(userInfo: userInfo, from: value, codingPath: []) - return try decoder.decode(T.self) - } - } - - @usableFromInline internal struct _DecoderImpl: Swift.Decoder { - @usableFromInline let codingPath: [CodingKey] - @usableFromInline let userInfo: [CodingUserInfoKey: Any] - - @usableFromInline let value: AttributeValue - - @inlinable init(userInfo: [CodingUserInfoKey: Any], from value: AttributeValue, codingPath: [CodingKey]) { - self.userInfo = userInfo - self.codingPath = codingPath - self.value = value - } - - @inlinable public func decode(_: T.Type) throws -> T { - try T(from: self) - } - - @usableFromInline func container(keyedBy type: Key.Type) throws -> - KeyedDecodingContainer where Key: CodingKey { - guard case .map(let dictionary) = self.value else { - throw DecodingError.typeMismatch([String: AttributeValue].self, DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Expected to decode \([String: AttributeValue].self) but found \(self.value.debugDataTypeDescription) instead." - )) - } - - let container = _KeyedDecodingContainer( - impl: self, - codingPath: self.codingPath, - dictionary: dictionary - ) - return KeyedDecodingContainer(container) - } - - @usableFromInline func unkeyedContainer() throws -> UnkeyedDecodingContainer { - guard case .list(let array) = self.value else { - throw DecodingError.typeMismatch([AttributeValue].self, DecodingError.Context( - codingPath: self.codingPath, - debugDescription: "Expected to decode \([AttributeValue].self) but found \(self.value.debugDataTypeDescription) instead." - )) - } - - return _UnkeyedDecodingContainer( - impl: self, - codingPath: self.codingPath, - array: array - ) - } - - @usableFromInline func singleValueContainer() throws -> SingleValueDecodingContainer { - _SingleValueDecodingContainter( - impl: self, - codingPath: self.codingPath, - value: self.value - ) - } - } - - struct ArrayKey: CodingKey, Equatable { - init(index: Int) { - self.intValue = index - } - - init?(stringValue _: String) { - preconditionFailure("Did not expect to be initialized with a string") - } - - init?(intValue: Int) { - self.intValue = intValue - } - - var intValue: Int? - - var stringValue: String { - "Index \(self.intValue!)" - } - - static func == (lhs: ArrayKey, rhs: ArrayKey) -> Bool { - precondition(lhs.intValue != nil) - precondition(rhs.intValue != nil) - return lhs.intValue == rhs.intValue - } - } - - struct _KeyedDecodingContainer: KeyedDecodingContainerProtocol { - typealias Key = K - - let impl: _DecoderImpl - let codingPath: [CodingKey] - let dictionary: [String: AttributeValue] - - init(impl: _DecoderImpl, codingPath: [CodingKey], dictionary: [String: AttributeValue]) { - self.impl = impl - self.codingPath = codingPath - self.dictionary = dictionary - } - - var allKeys: [K] { - self.dictionary.keys.compactMap { K(stringValue: $0) } - } - - func contains(_ key: K) -> Bool { - if let _ = self.dictionary[key.stringValue] { - return true - } - return false - } - - func decodeNil(forKey key: K) throws -> Bool { - let value = try getValue(forKey: key) - return value == .null - } - - func decode(_ type: Bool.Type, forKey key: K) throws -> Bool { - let value = try getValue(forKey: key) - - guard case .boolean(let bool) = value else { - throw self.createTypeMismatchError(type: type, forKey: key, value: value) - } - - return bool - } - - func decode(_ type: String.Type, forKey key: K) throws -> String { - let value = try getValue(forKey: key) - - guard case .string(let string) = value else { - throw self.createTypeMismatchError(type: type, forKey: key, value: value) - } - - return string - } - - func decode(_ type: Double.Type, forKey key: K) throws -> Double { - try self.decodeLosslessStringConvertible(key: key) - } - - func decode(_ type: Float.Type, forKey key: K) throws -> Float { - try self.decodeLosslessStringConvertible(key: key) - } - - func decode(_ type: Int.Type, forKey key: K) throws -> Int { - try self.decodeFixedWidthInteger(key: key) - } - - func decode(_ type: Int8.Type, forKey key: K) throws -> Int8 { - try self.decodeFixedWidthInteger(key: key) - } - - func decode(_ type: Int16.Type, forKey key: K) throws -> Int16 { - try self.decodeFixedWidthInteger(key: key) - } - - func decode(_ type: Int32.Type, forKey key: K) throws -> Int32 { - try self.decodeFixedWidthInteger(key: key) - } - - func decode(_ type: Int64.Type, forKey key: K) throws -> Int64 { - try self.decodeFixedWidthInteger(key: key) - } - - func decode(_ type: UInt.Type, forKey key: K) throws -> UInt { - try self.decodeFixedWidthInteger(key: key) - } - - func decode(_ type: UInt8.Type, forKey key: K) throws -> UInt8 { - try self.decodeFixedWidthInteger(key: key) - } - - func decode(_ type: UInt16.Type, forKey key: K) throws -> UInt16 { - try self.decodeFixedWidthInteger(key: key) - } - - func decode(_ type: UInt32.Type, forKey key: K) throws -> UInt32 { - try self.decodeFixedWidthInteger(key: key) - } - - func decode(_ type: UInt64.Type, forKey key: K) throws -> UInt64 { - try self.decodeFixedWidthInteger(key: key) - } - - func decode(_ type: T.Type, forKey key: K) throws -> T where T: Decodable { - let decoder = try self.decoderForKey(key) - return try T(from: decoder) - } - - func nestedContainer(keyedBy type: NestedKey.Type, forKey key: K) throws - -> KeyedDecodingContainer where NestedKey: CodingKey { - return try self.decoderForKey(key).container(keyedBy: type) - } - - func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer { - try self.decoderForKey(key).unkeyedContainer() - } - - func superDecoder() throws -> Swift.Decoder { - self.impl - } - - func superDecoder(forKey key: K) throws -> Swift.Decoder { - self.impl - } - - private func decoderForKey(_ key: K) throws -> _DecoderImpl { - let value = try getValue(forKey: key) - var newPath = self.codingPath - newPath.append(key) - - return _DecoderImpl( - userInfo: self.impl.userInfo, - from: value, - codingPath: newPath - ) - } - - @inline(__always) private func getValue(forKey key: K) throws -> AttributeValue { - guard let value = self.dictionary[key.stringValue] else { - throw DecodingError.keyNotFound(key, .init( - codingPath: self.codingPath, - debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\")." - )) - } - - return value - } - - @inline(__always) private func createTypeMismatchError(type: Any.Type, forKey key: K, value: AttributeValue) -> DecodingError { - let codingPath = self.codingPath + [key] - return DecodingError.typeMismatch(type, .init( - codingPath: codingPath, debugDescription: "Expected to decode \(type) but found \(value.debugDataTypeDescription) instead." - )) - } - - @inline(__always) private func decodeFixedWidthInteger(key: Self.Key) - throws -> T - { - let value = try getValue(forKey: key) - - guard case .number(let number) = value else { - throw self.createTypeMismatchError(type: T.self, forKey: key, value: value) - } - - guard let integer = T(number) else { - throw DecodingError.dataCorruptedError( - forKey: key, - in: self, - debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)." - ) - } - - return integer - } - - @inline(__always) private func decodeLosslessStringConvertible( - key: Self.Key) throws -> T - { - let value = try getValue(forKey: key) - - guard case .number(let number) = value else { - throw self.createTypeMismatchError(type: T.self, forKey: key, value: value) - } - - guard let floatingPoint = T(number) else { - throw DecodingError.dataCorruptedError( - forKey: key, - in: self, - debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)." - ) - } - - return floatingPoint - } - } - - struct _SingleValueDecodingContainter: SingleValueDecodingContainer { - let impl: _DecoderImpl - let value: AttributeValue - let codingPath: [CodingKey] - - init(impl: _DecoderImpl, codingPath: [CodingKey], value: AttributeValue) { - self.impl = impl - self.codingPath = codingPath - self.value = value - } - - func decodeNil() -> Bool { - self.value == .null - } - - func decode(_: Bool.Type) throws -> Bool { - guard case .boolean(let bool) = self.value else { - throw self.createTypeMismatchError(type: Bool.self, value: self.value) - } - - return bool - } - - func decode(_: String.Type) throws -> String { - guard case .string(let string) = self.value else { - throw self.createTypeMismatchError(type: String.self, value: self.value) - } - - return string - } - - func decode(_: Double.Type) throws -> Double { - try self.decodeLosslessStringConvertible() - } - - func decode(_: Float.Type) throws -> Float { - try self.decodeLosslessStringConvertible() - } - - func decode(_: Int.Type) throws -> Int { - try self.decodeFixedWidthInteger() - } - - func decode(_: Int8.Type) throws -> Int8 { - try self.decodeFixedWidthInteger() - } - - func decode(_: Int16.Type) throws -> Int16 { - try self.decodeFixedWidthInteger() - } - - func decode(_: Int32.Type) throws -> Int32 { - try self.decodeFixedWidthInteger() - } - - func decode(_: Int64.Type) throws -> Int64 { - try self.decodeFixedWidthInteger() - } - - func decode(_: UInt.Type) throws -> UInt { - try self.decodeFixedWidthInteger() - } - - func decode(_: UInt8.Type) throws -> UInt8 { - try self.decodeFixedWidthInteger() - } - - func decode(_: UInt16.Type) throws -> UInt16 { - try self.decodeFixedWidthInteger() - } - - func decode(_: UInt32.Type) throws -> UInt32 { - try self.decodeFixedWidthInteger() - } - - func decode(_: UInt64.Type) throws -> UInt64 { - try self.decodeFixedWidthInteger() - } - - func decode(_: T.Type) throws -> T where T: Decodable { - return try T(from: self.impl) - } - - @inline(__always) private func createTypeMismatchError(type: Any.Type, value: AttributeValue) -> DecodingError { - DecodingError.typeMismatch(type, .init( - codingPath: self.codingPath, - debugDescription: "Expected to decode \(type) but found \(value.debugDataTypeDescription) instead." - )) - } - - @inline(__always) private func decodeFixedWidthInteger() throws - -> T - { - guard case .number(let number) = self.value else { - throw self.createTypeMismatchError(type: T.self, value: self.value) - } - - guard let integer = T(number) else { - throw DecodingError.dataCorruptedError( - in: self, - debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)." - ) - } - - return integer - } - - @inline(__always) private func decodeLosslessStringConvertible() - throws -> T - { - guard case .number(let number) = self.value else { - throw self.createTypeMismatchError(type: T.self, value: self.value) - } - - guard let floatingPoint = T(number) else { - throw DecodingError.dataCorruptedError( - in: self, - debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)." - ) - } - - return floatingPoint - } - } - - struct _UnkeyedDecodingContainer: UnkeyedDecodingContainer { - let impl: _DecoderImpl - let codingPath: [CodingKey] - let array: [AttributeValue] - - let count: Int? // protocol requirement to be optional - var isAtEnd = false - var currentIndex = 0 - - init(impl: _DecoderImpl, codingPath: [CodingKey], array: [AttributeValue]) { - self.impl = impl - self.codingPath = codingPath - self.array = array - self.count = array.count - } - - mutating func decodeNil() throws -> Bool { - if self.array[self.currentIndex] == .null { - defer { - currentIndex += 1 - if currentIndex == count { - isAtEnd = true - } - } - return true - } - - // The protocol states: - // If the value is not null, does not increment currentIndex. - return false - } - - mutating func decode(_ type: Bool.Type) throws -> Bool { - defer { - currentIndex += 1 - if currentIndex == count { - isAtEnd = true - } - } - - guard case .boolean(let bool) = self.array[self.currentIndex] else { - throw self.createTypeMismatchError(type: type, value: self.array[self.currentIndex]) - } - - return bool - } - - mutating func decode(_ type: String.Type) throws -> String { - defer { - currentIndex += 1 - if currentIndex == count { - isAtEnd = true - } - } - - guard case .string(let string) = self.array[self.currentIndex] else { - throw self.createTypeMismatchError(type: type, value: self.array[self.currentIndex]) - } - - return string - } - - mutating func decode(_: Double.Type) throws -> Double { - try self.decodeLosslessStringConvertible() - } - - mutating func decode(_: Float.Type) throws -> Float { - try self.decodeLosslessStringConvertible() - } - - mutating func decode(_: Int.Type) throws -> Int { - try self.decodeFixedWidthInteger() - } - - mutating func decode(_: Int8.Type) throws -> Int8 { - try self.decodeFixedWidthInteger() - } - - mutating func decode(_: Int16.Type) throws -> Int16 { - try self.decodeFixedWidthInteger() - } - - mutating func decode(_: Int32.Type) throws -> Int32 { - try self.decodeFixedWidthInteger() - } - - mutating func decode(_: Int64.Type) throws -> Int64 { - try self.decodeFixedWidthInteger() - } - - mutating func decode(_: UInt.Type) throws -> UInt { - try self.decodeFixedWidthInteger() - } - - mutating func decode(_: UInt8.Type) throws -> UInt8 { - try self.decodeFixedWidthInteger() - } - - mutating func decode(_: UInt16.Type) throws -> UInt16 { - try self.decodeFixedWidthInteger() - } - - mutating func decode(_: UInt32.Type) throws -> UInt32 { - try self.decodeFixedWidthInteger() - } - - mutating func decode(_: UInt64.Type) throws -> UInt64 { - try self.decodeFixedWidthInteger() - } - - mutating func decode(_: T.Type) throws -> T where T: Decodable { - defer { - currentIndex += 1 - if currentIndex == count { - isAtEnd = true - } - } - - let json = self.array[self.currentIndex] - var newPath = self.codingPath - newPath.append(ArrayKey(index: self.currentIndex)) - let decoder = _DecoderImpl(userInfo: impl.userInfo, from: json, codingPath: newPath) - - return try T(from: decoder) - } - - mutating func nestedContainer(keyedBy type: NestedKey.Type) throws - -> KeyedDecodingContainer where NestedKey: CodingKey { - return try self.impl.container(keyedBy: type) - } - - mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { - try self.impl.unkeyedContainer() - } - - mutating func superDecoder() throws -> Swift.Decoder { - self.impl - } - - @inline(__always) private func createTypeMismatchError(type: Any.Type, value: AttributeValue) -> DecodingError { - let codingPath = self.codingPath + [ArrayKey(index: self.currentIndex)] - return DecodingError.typeMismatch(type, .init( - codingPath: codingPath, debugDescription: "Expected to decode \(type) but found \(value.debugDataTypeDescription) instead." - )) - } - - @inline(__always) private mutating func decodeFixedWidthInteger() throws - -> T - { - defer { - currentIndex += 1 - if currentIndex == count { - isAtEnd = true - } - } - - guard case .number(let number) = self.array[self.currentIndex] else { - throw self.createTypeMismatchError(type: T.self, value: self.array[self.currentIndex]) - } - - guard let integer = T(number) else { - throw DecodingError.dataCorruptedError(in: self, - debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self).") - } - - return integer - } - - @inline(__always) private mutating func decodeLosslessStringConvertible() - throws -> T - { - defer { - currentIndex += 1 - if currentIndex == count { - isAtEnd = true - } - } - - guard case .number(let number) = self.array[self.currentIndex] else { - throw self.createTypeMismatchError(type: T.self, value: self.array[self.currentIndex]) - } - - guard let float = T(number) else { - throw DecodingError.dataCorruptedError(in: self, - debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self).") - } - - return float - } - } -} - -extension DynamoDB.AttributeValue { - fileprivate var debugDataTypeDescription: String { - switch self { - case .list: - return "a list" - case .boolean: - return "boolean" - case .number: - return "a number" - case .string: - return "a string" - case .map: - return "a map" - case .null: - return "null" - case .binary: - return "bytes" - case .binarySet: - return "a set of bytes" - case .stringSet: - return "a set of strings" - case .numberSet: - return "a set of numbers" - } - } -} diff --git a/Sources/AWSLambdaEvents/S3.swift b/Sources/AWSLambdaEvents/S3.swift deleted file mode 100644 index 0e7add28..00000000 --- a/Sources/AWSLambdaEvents/S3.swift +++ /dev/null @@ -1,78 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import struct Foundation.Date - -// https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html - -public enum S3 { - public struct Event: Decodable { - public struct Record: Decodable { - public let eventVersion: String - public let eventSource: String - public let awsRegion: AWSRegion - - @ISO8601WithFractionalSecondsCoding - public var eventTime: Date - public let eventName: String - public let userIdentity: UserIdentity - public let requestParameters: RequestParameters - public let responseElements: [String: String] - public let s3: Entity - } - - public let records: [Record] - - public enum CodingKeys: String, CodingKey { - case records = "Records" - } - } - - public struct RequestParameters: Codable, Equatable { - public let sourceIPAddress: String - } - - public struct UserIdentity: Codable, Equatable { - public let principalId: String - } - - public struct Entity: Codable { - public let configurationId: String - public let schemaVersion: String - public let bucket: Bucket - public let object: Object - - enum CodingKeys: String, CodingKey { - case configurationId - case schemaVersion = "s3SchemaVersion" - case bucket - case object - } - } - - public struct Bucket: Codable { - public let name: String - public let ownerIdentity: UserIdentity - public let arn: String - } - - public struct Object: Codable { - public let key: String - public let size: UInt64 - public let urlDecodedKey: String? - public let versionId: String? - public let eTag: String - public let sequencer: String - } -} diff --git a/Sources/AWSLambdaEvents/SES.swift b/Sources/AWSLambdaEvents/SES.swift deleted file mode 100644 index 4c5b3719..00000000 --- a/Sources/AWSLambdaEvents/SES.swift +++ /dev/null @@ -1,100 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import struct Foundation.Date - -// https://docs.aws.amazon.com/lambda/latest/dg/services-ses.html - -public enum SES { - public struct Event: Decodable { - public struct Record: Decodable { - public let eventSource: String - public let eventVersion: String - public let ses: Message - } - - public let records: [Record] - - public enum CodingKeys: String, CodingKey { - case records = "Records" - } - } - - public struct Message: Decodable { - public let mail: Mail - public let receipt: Receipt - } - - public struct Mail: Decodable { - public let commonHeaders: CommonHeaders - public let destination: [String] - public let headers: [Header] - public let headersTruncated: Bool - public let messageId: String - public let source: String - @ISO8601WithFractionalSecondsCoding public var timestamp: Date - } - - public struct CommonHeaders: Decodable { - public let bcc: [String]? - public let cc: [String]? - @RFC5322DateTimeCoding public var date: Date - public let from: [String] - public let messageId: String - public let returnPath: String? - public let subject: String? - public let to: [String]? - } - - public struct Header: Decodable { - public let name: String - public let value: String - } - - public struct Receipt: Decodable { - public let action: Action - public let dmarcPolicy: DMARCPolicy? - public let dmarcVerdict: Verdict? - public let dkimVerdict: Verdict - public let processingTimeMillis: Int - public let recipients: [String] - public let spamVerdict: Verdict - public let spfVerdict: Verdict - @ISO8601WithFractionalSecondsCoding public var timestamp: Date - public let virusVerdict: Verdict - } - - public struct Action: Decodable { - public let functionArn: String - public let invocationType: String - public let type: String - } - - public struct Verdict: Decodable { - public let status: Status - } - - public enum DMARCPolicy: String, Decodable { - case none - case quarantine - case reject - } - - public enum Status: String, Decodable { - case pass = "PASS" - case fail = "FAIL" - case gray = "GRAY" - case processingFailed = "PROCESSING_FAILED" - } -} diff --git a/Sources/AWSLambdaEvents/SNS.swift b/Sources/AWSLambdaEvents/SNS.swift deleted file mode 100644 index 9db4c8f4..00000000 --- a/Sources/AWSLambdaEvents/SNS.swift +++ /dev/null @@ -1,108 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import struct Foundation.Date - -// https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html - -public enum SNS { - public struct Event: Decodable { - public struct Record: Decodable { - public let eventVersion: String - public let eventSubscriptionArn: String - public let eventSource: String - public let sns: Message - - public enum CodingKeys: String, CodingKey { - case eventVersion = "EventVersion" - case eventSubscriptionArn = "EventSubscriptionArn" - case eventSource = "EventSource" - case sns = "Sns" - } - } - - public let records: [Record] - - public enum CodingKeys: String, CodingKey { - case records = "Records" - } - } - - public struct Message { - public enum Attribute { - case string(String) - case binary([UInt8]) - } - - public let signature: String - public let messageId: String - public let type: String - public let topicArn: String - public let messageAttributes: [String: Attribute] - public let signatureVersion: String - - @ISO8601WithFractionalSecondsCoding - public var timestamp: Date - public let signingCertURL: String - public let message: String - public let unsubscribeUrl: String - public let subject: String? - } -} - -extension SNS.Message: Decodable { - enum CodingKeys: String, CodingKey { - case signature = "Signature" - case messageId = "MessageId" - case type = "Type" - case topicArn = "TopicArn" - case messageAttributes = "MessageAttributes" - case signatureVersion = "SignatureVersion" - case timestamp = "Timestamp" - case signingCertURL = "SigningCertUrl" - case message = "Message" - case unsubscribeUrl = "UnsubscribeUrl" - case subject = "Subject" - } -} - -extension SNS.Message.Attribute: Equatable {} - -extension SNS.Message.Attribute: Decodable { - enum CodingKeys: String, CodingKey { - case dataType = "Type" - case dataValue = "Value" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let dataType = try container.decode(String.self, forKey: .dataType) - // https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html#SNSMessageAttributes.DataTypes - switch dataType { - case "String": - let value = try container.decode(String.self, forKey: .dataValue) - self = .string(value) - case "Binary": - let base64encoded = try container.decode(String.self, forKey: .dataValue) - let bytes = try base64encoded.base64decoded() - self = .binary(bytes) - default: - throw DecodingError.dataCorruptedError(forKey: .dataType, in: container, debugDescription: """ - Unexpected value \"\(dataType)\" for key \(CodingKeys.dataType). - Expected `String` or `Binary`. - """) - } - } -} diff --git a/Sources/AWSLambdaEvents/SQS.swift b/Sources/AWSLambdaEvents/SQS.swift deleted file mode 100644 index f2aca1fd..00000000 --- a/Sources/AWSLambdaEvents/SQS.swift +++ /dev/null @@ -1,97 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -// https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html - -public enum SQS { - public struct Event: Decodable { - public let records: [Message] - - enum CodingKeys: String, CodingKey { - case records = "Records" - } - } - - public struct Message { - /// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html - public enum Attribute { - case string(String) - case binary([UInt8]) - case number(String) - } - - public let messageId: String - public let receiptHandle: String - public var body: String - public let md5OfBody: String - public let md5OfMessageAttributes: String? - public let attributes: [String: String] - public let messageAttributes: [String: Attribute] - public let eventSourceArn: String - public let eventSource: String - public let awsRegion: AWSRegion - } -} - -extension SQS.Message: Decodable { - enum CodingKeys: String, CodingKey { - case messageId - case receiptHandle - case body - case md5OfBody - case md5OfMessageAttributes - case attributes - case messageAttributes - case eventSourceArn = "eventSourceARN" - case eventSource - case awsRegion - } -} - -extension SQS.Message.Attribute: Equatable {} - -extension SQS.Message.Attribute: Decodable { - enum CodingKeys: String, CodingKey { - case dataType - case stringValue - case binaryValue - - // BinaryListValue and StringListValue are unimplemented since - // they are not implemented as discussed here: - // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let dataType = try container.decode(String.self, forKey: .dataType) - switch dataType { - case "String": - let value = try container.decode(String.self, forKey: .stringValue) - self = .string(value) - case "Number": - let value = try container.decode(String.self, forKey: .stringValue) - self = .number(value) - case "Binary": - let base64encoded = try container.decode(String.self, forKey: .binaryValue) - let bytes = try base64encoded.base64decoded() - self = .binary(bytes) - default: - throw DecodingError.dataCorruptedError(forKey: .dataType, in: container, debugDescription: """ - Unexpected value \"\(dataType)\" for key \(CodingKeys.dataType). - Expected `String`, `Binary` or `Number`. - """) - } - } -} diff --git a/Sources/AWSLambdaEvents/Utils/Base64.swift b/Sources/AWSLambdaEvents/Utils/Base64.swift deleted file mode 100644 index 310a6aa5..00000000 --- a/Sources/AWSLambdaEvents/Utils/Base64.swift +++ /dev/null @@ -1,219 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -//===----------------------------------------------------------------------===// -// This is a vendored version from: -// https://github.com/fabianfett/swift-base64-kit - -struct Base64 {} - -// MARK: - Decode - - -extension Base64 { - struct DecodingOptions: OptionSet { - let rawValue: UInt - init(rawValue: UInt) { self.rawValue = rawValue } - - static let base64UrlAlphabet = DecodingOptions(rawValue: UInt(1 << 0)) - } - - enum DecodingError: Error, Equatable { - case invalidLength - case invalidCharacter(UInt8) - case unexpectedPaddingCharacter - case unexpectedEnd - } - - @inlinable - static func decode(encoded: Buffer, options: DecodingOptions = []) - throws -> [UInt8] where Buffer.Element == UInt8 { - let alphabet = options.contains(.base64UrlAlphabet) - ? Base64.decodeBase64Url - : Base64.decodeBase64 - - // In Base64 4 encoded bytes, become 3 decoded bytes. We pad to the - // nearest multiple of three. - let inputLength = encoded.count - guard inputLength > 0 else { return [] } - guard inputLength % 4 == 0 else { - throw DecodingError.invalidLength - } - - let inputBlocks = (inputLength + 3) / 4 - let fullQualified = inputBlocks - 1 - let outputLength = ((encoded.count + 3) / 4) * 3 - var iterator = encoded.makeIterator() - var outputBytes = [UInt8]() - outputBytes.reserveCapacity(outputLength) - - // fast loop. we don't expect any padding in here. - for _ in 0 ..< fullQualified { - let firstValue: UInt8 = try iterator.nextValue(alphabet: alphabet) - let secondValue: UInt8 = try iterator.nextValue(alphabet: alphabet) - let thirdValue: UInt8 = try iterator.nextValue(alphabet: alphabet) - let forthValue: UInt8 = try iterator.nextValue(alphabet: alphabet) - - outputBytes.append((firstValue << 2) | (secondValue >> 4)) - outputBytes.append((secondValue << 4) | (thirdValue >> 2)) - outputBytes.append((thirdValue << 6) | forthValue) - } - - // last 4 bytes. we expect padding characters in three and four - let firstValue: UInt8 = try iterator.nextValue(alphabet: alphabet) - let secondValue: UInt8 = try iterator.nextValue(alphabet: alphabet) - let thirdValue: UInt8? = try iterator.nextValueOrEmpty(alphabet: alphabet) - let forthValue: UInt8? = try iterator.nextValueOrEmpty(alphabet: alphabet) - - outputBytes.append((firstValue << 2) | (secondValue >> 4)) - if let thirdValue = thirdValue { - outputBytes.append((secondValue << 4) | (thirdValue >> 2)) - - if let forthValue = forthValue { - outputBytes.append((thirdValue << 6) | forthValue) - } - } - - return outputBytes - } - - @inlinable - static func decode(encoded: String, options: DecodingOptions = []) throws -> [UInt8] { - // A string can be backed by a contiguous storage (pure swift string) - // or a nsstring (bridged string from objc). We only get a pointer - // to the contiguous storage, if the input string is a swift string. - // Therefore to transform the nsstring backed input into a swift - // string we concat the input with nothing, causing a copy on write - // into a swift string. - let decoded = try encoded.utf8.withContiguousStorageIfAvailable { pointer in - try self.decode(encoded: pointer, options: options) - } - - if decoded != nil { - return decoded! - } - - return try self.decode(encoded: encoded + "", options: options) - } - - // MARK: Internal - - @usableFromInline - static let decodeBase64: [UInt8] = [ - // 0 1 2 3 4 5 6 7 8 9 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 0 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 1 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 2 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 3 - 255, 255, 255, 62, 255, 255, 255, 63, 52, 53, // 4 - 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, // 5 - 255, 254, 255, 255, 255, 0, 1, 2, 3, 4, // 6 - 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 7 - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 8 - 25, 255, 255, 255, 255, 255, 255, 26, 27, 28, // 9 - 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // 10 - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, // 11 - 49, 50, 51, 255, 255, 255, 255, 255, 255, 255, // 12 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 13 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 14 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 15 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 16 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 17 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 18 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 19 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 20 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 21 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 22 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 23 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 24 - 255, 255, 255, 255, 255, // 25 - ] - - @usableFromInline - static let decodeBase64Url: [UInt8] = [ - // 0 1 2 3 4 5 6 7 8 9 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 0 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 1 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 2 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 3 - 255, 255, 255, 255, 255, 62, 255, 255, 52, 53, // 4 - 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, // 5 - 255, 254, 255, 255, 255, 0, 1, 2, 3, 4, // 6 - 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 7 - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 8 - 25, 255, 255, 255, 255, 63, 255, 26, 27, 28, // 9 - 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // 10 - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, // 11 - 49, 50, 51, 255, 255, 255, 255, 255, 255, 255, // 12 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 13 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 14 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 15 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 16 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 17 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 18 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 19 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 20 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 21 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 22 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 23 - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 24 - 255, 255, 255, 255, 255, // 25 - ] - - @usableFromInline - static let paddingCharacter: UInt8 = 254 -} - -extension IteratorProtocol where Self.Element == UInt8 { - mutating func nextValue(alphabet: [UInt8]) throws -> UInt8 { - let ascii = self.next()! - - let value = alphabet[Int(ascii)] - - if value < 64 { - return value - } - - if value == Base64.paddingCharacter { - throw Base64.DecodingError.unexpectedPaddingCharacter - } - - throw Base64.DecodingError.invalidCharacter(ascii) - } - - mutating func nextValueOrEmpty(alphabet: [UInt8]) throws -> UInt8? { - let ascii = self.next()! - - let value = alphabet[Int(ascii)] - - if value < 64 { - return value - } - - if value == Base64.paddingCharacter { - return nil - } - - throw Base64.DecodingError.invalidCharacter(ascii) - } -} - -// MARK: - Extensions - - -extension String { - func base64decoded(options: Base64.DecodingOptions = []) throws -> [UInt8] { - // In Base64, 3 bytes become 4 output characters, and we pad to the nearest multiple - // of four. - try Base64.decode(encoded: self, options: options) - } -} diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift deleted file mode 100644 index 3b7fc481..00000000 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ /dev/null @@ -1,108 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import struct Foundation.Date -import class Foundation.DateFormatter -import struct Foundation.Locale -import struct Foundation.TimeZone - -@propertyWrapper -public struct ISO8601Coding: Decodable { - public let wrappedValue: Date - - public init(wrappedValue: Date) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let dateString = try container.decode(String.self) - guard let date = Self.dateFormatter.date(from: dateString) else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: - "Expected date to be in ISO8601 date format, but `\(dateString)` is not in the correct format") - } - self.wrappedValue = date - } - - private static let dateFormatter: DateFormatter = Self.createDateFormatter() - - private static func createDateFormatter() -> DateFormatter { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - return formatter - } -} - -@propertyWrapper -public struct ISO8601WithFractionalSecondsCoding: Decodable { - public let wrappedValue: Date - - public init(wrappedValue: Date) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let dateString = try container.decode(String.self) - guard let date = Self.dateFormatter.date(from: dateString) else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: - "Expected date to be in ISO8601 date format with fractional seconds, but `\(dateString)` is not in the correct format") - } - self.wrappedValue = date - } - - private static let dateFormatter: DateFormatter = Self.createDateFormatter() - - private static func createDateFormatter() -> DateFormatter { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" - return formatter - } -} - -@propertyWrapper -public struct RFC5322DateTimeCoding: Decodable { - public let wrappedValue: Date - - public init(wrappedValue: Date) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - var string = try container.decode(String.self) - // RFC5322 dates sometimes have the alphabetic version of the timezone in brackets after the numeric version. The date formatter - // fails to parse this so we need to remove this before parsing. - if let bracket = string.firstIndex(of: "(") { - string = String(string[string.startIndex ..< bracket].trimmingCharacters(in: .whitespaces)) - } - guard let date = Self.dateFormatter.date(from: string) else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: - "Expected date to be in RFC5322 date-time format with fractional seconds, but `\(string)` is not in the correct format") - } - self.wrappedValue = date - } - - private static let dateFormatter: DateFormatter = Self.createDateFormatter() - private static func createDateFormatter() -> DateFormatter { - let formatter = DateFormatter() - formatter.dateFormat = "EEE, d MMM yyy HH:mm:ss z" - formatter.locale = Locale(identifier: "en_US_POSIX") - return formatter - } -} diff --git a/Sources/AWSLambdaEvents/Utils/HTTP.swift b/Sources/AWSLambdaEvents/Utils/HTTP.swift deleted file mode 100644 index 9e0d8f2d..00000000 --- a/Sources/AWSLambdaEvents/Utils/HTTP.swift +++ /dev/null @@ -1,187 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -// MARK: HTTPMethod - -public typealias HTTPHeaders = [String: String] -public typealias HTTPMultiValueHeaders = [String: [String]] - -public struct HTTPMethod: RawRepresentable, Equatable { - public var rawValue: String - - public init?(rawValue: String) { - guard rawValue.isValidHTTPToken else { - return nil - } - self.rawValue = rawValue - } - - public static var GET: HTTPMethod { HTTPMethod(rawValue: "GET")! } - public static var POST: HTTPMethod { HTTPMethod(rawValue: "POST")! } - public static var PUT: HTTPMethod { HTTPMethod(rawValue: "PUT")! } - public static var PATCH: HTTPMethod { HTTPMethod(rawValue: "PATCH")! } - public static var DELETE: HTTPMethod { HTTPMethod(rawValue: "DELETE")! } - public static var OPTIONS: HTTPMethod { HTTPMethod(rawValue: "OPTIONS")! } - public static var HEAD: HTTPMethod { HTTPMethod(rawValue: "HEAD")! } - - public static func RAW(value: String) -> HTTPMethod? { HTTPMethod(rawValue: value) } -} - -extension HTTPMethod: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let rawMethod = try container.decode(String.self) - - guard let method = HTTPMethod(rawValue: rawMethod) else { - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: #"Method "\#(rawMethod)" does not conform to allowed http method syntax defined in RFC 7230 Section 3.2.6"# - ) - } - - self = method - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.rawValue) - } -} - -// MARK: HTTPResponseStatus - -public struct HTTPResponseStatus { - public let code: UInt - public let reasonPhrase: String? - - public init(code: UInt, reasonPhrase: String? = nil) { - self.code = code - self.reasonPhrase = reasonPhrase - } - - public static var `continue`: HTTPResponseStatus { HTTPResponseStatus(code: 100) } - public static var switchingProtocols: HTTPResponseStatus { HTTPResponseStatus(code: 101) } - public static var processing: HTTPResponseStatus { HTTPResponseStatus(code: 102) } - public static var earlyHints: HTTPResponseStatus { HTTPResponseStatus(code: 103) } - - public static var ok: HTTPResponseStatus { HTTPResponseStatus(code: 200) } - public static var created: HTTPResponseStatus { HTTPResponseStatus(code: 201) } - public static var accepted: HTTPResponseStatus { HTTPResponseStatus(code: 202) } - public static var nonAuthoritativeInformation: HTTPResponseStatus { HTTPResponseStatus(code: 203) } - public static var noContent: HTTPResponseStatus { HTTPResponseStatus(code: 204) } - public static var resetContent: HTTPResponseStatus { HTTPResponseStatus(code: 205) } - public static var partialContent: HTTPResponseStatus { HTTPResponseStatus(code: 206) } - public static var multiStatus: HTTPResponseStatus { HTTPResponseStatus(code: 207) } - public static var alreadyReported: HTTPResponseStatus { HTTPResponseStatus(code: 208) } - public static var imUsed: HTTPResponseStatus { HTTPResponseStatus(code: 226) } - - public static var multipleChoices: HTTPResponseStatus { HTTPResponseStatus(code: 300) } - public static var movedPermanently: HTTPResponseStatus { HTTPResponseStatus(code: 301) } - public static var found: HTTPResponseStatus { HTTPResponseStatus(code: 302) } - public static var seeOther: HTTPResponseStatus { HTTPResponseStatus(code: 303) } - public static var notModified: HTTPResponseStatus { HTTPResponseStatus(code: 304) } - public static var useProxy: HTTPResponseStatus { HTTPResponseStatus(code: 305) } - public static var temporaryRedirect: HTTPResponseStatus { HTTPResponseStatus(code: 307) } - public static var permanentRedirect: HTTPResponseStatus { HTTPResponseStatus(code: 308) } - - public static var badRequest: HTTPResponseStatus { HTTPResponseStatus(code: 400) } - public static var unauthorized: HTTPResponseStatus { HTTPResponseStatus(code: 401) } - public static var paymentRequired: HTTPResponseStatus { HTTPResponseStatus(code: 402) } - public static var forbidden: HTTPResponseStatus { HTTPResponseStatus(code: 403) } - public static var notFound: HTTPResponseStatus { HTTPResponseStatus(code: 404) } - public static var methodNotAllowed: HTTPResponseStatus { HTTPResponseStatus(code: 405) } - public static var notAcceptable: HTTPResponseStatus { HTTPResponseStatus(code: 406) } - public static var proxyAuthenticationRequired: HTTPResponseStatus { HTTPResponseStatus(code: 407) } - public static var requestTimeout: HTTPResponseStatus { HTTPResponseStatus(code: 408) } - public static var conflict: HTTPResponseStatus { HTTPResponseStatus(code: 409) } - public static var gone: HTTPResponseStatus { HTTPResponseStatus(code: 410) } - public static var lengthRequired: HTTPResponseStatus { HTTPResponseStatus(code: 411) } - public static var preconditionFailed: HTTPResponseStatus { HTTPResponseStatus(code: 412) } - public static var payloadTooLarge: HTTPResponseStatus { HTTPResponseStatus(code: 413) } - public static var uriTooLong: HTTPResponseStatus { HTTPResponseStatus(code: 414) } - public static var unsupportedMediaType: HTTPResponseStatus { HTTPResponseStatus(code: 415) } - public static var rangeNotSatisfiable: HTTPResponseStatus { HTTPResponseStatus(code: 416) } - public static var expectationFailed: HTTPResponseStatus { HTTPResponseStatus(code: 417) } - public static var imATeapot: HTTPResponseStatus { HTTPResponseStatus(code: 418) } - public static var misdirectedRequest: HTTPResponseStatus { HTTPResponseStatus(code: 421) } - public static var unprocessableEntity: HTTPResponseStatus { HTTPResponseStatus(code: 422) } - public static var locked: HTTPResponseStatus { HTTPResponseStatus(code: 423) } - public static var failedDependency: HTTPResponseStatus { HTTPResponseStatus(code: 424) } - public static var upgradeRequired: HTTPResponseStatus { HTTPResponseStatus(code: 426) } - public static var preconditionRequired: HTTPResponseStatus { HTTPResponseStatus(code: 428) } - public static var tooManyRequests: HTTPResponseStatus { HTTPResponseStatus(code: 429) } - public static var requestHeaderFieldsTooLarge: HTTPResponseStatus { HTTPResponseStatus(code: 431) } - public static var unavailableForLegalReasons: HTTPResponseStatus { HTTPResponseStatus(code: 451) } - - public static var internalServerError: HTTPResponseStatus { HTTPResponseStatus(code: 500) } - public static var notImplemented: HTTPResponseStatus { HTTPResponseStatus(code: 501) } - public static var badGateway: HTTPResponseStatus { HTTPResponseStatus(code: 502) } - public static var serviceUnavailable: HTTPResponseStatus { HTTPResponseStatus(code: 503) } - public static var gatewayTimeout: HTTPResponseStatus { HTTPResponseStatus(code: 504) } - public static var httpVersionNotSupported: HTTPResponseStatus { HTTPResponseStatus(code: 505) } - public static var variantAlsoNegotiates: HTTPResponseStatus { HTTPResponseStatus(code: 506) } - public static var insufficientStorage: HTTPResponseStatus { HTTPResponseStatus(code: 507) } - public static var loopDetected: HTTPResponseStatus { HTTPResponseStatus(code: 508) } - public static var notExtended: HTTPResponseStatus { HTTPResponseStatus(code: 510) } - public static var networkAuthenticationRequired: HTTPResponseStatus { HTTPResponseStatus(code: 511) } -} - -extension HTTPResponseStatus: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.code == rhs.code - } -} - -extension HTTPResponseStatus: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - self.code = try container.decode(UInt.self) - self.reasonPhrase = nil - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.code) - } -} - -extension String { - internal var isValidHTTPToken: Bool { - self.utf8.allSatisfy { (char) -> Bool in - switch char { - case UInt8(ascii: "a") ... UInt8(ascii: "z"), - UInt8(ascii: "A") ... UInt8(ascii: "Z"), - UInt8(ascii: "0") ... UInt8(ascii: "9"), - UInt8(ascii: "!"), - UInt8(ascii: "#"), - UInt8(ascii: "$"), - UInt8(ascii: "%"), - UInt8(ascii: "&"), - UInt8(ascii: "'"), - UInt8(ascii: "*"), - UInt8(ascii: "+"), - UInt8(ascii: "-"), - UInt8(ascii: "."), - UInt8(ascii: "^"), - UInt8(ascii: "_"), - UInt8(ascii: "`"), - UInt8(ascii: "|"), - UInt8(ascii: "~"): - return true - default: - return false - } - } - } -} diff --git a/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift new file mode 100644 index 00000000..233d7aef --- /dev/null +++ b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOHTTP1 + +enum ControlPlaneRequest: Hashable { + case next + case invocationResponse(String, ByteBuffer?) + case invocationError(String, ErrorResponse) + case initializationError(ErrorResponse) +} + +enum ControlPlaneResponse: Hashable { + case next(InvocationMetadata, ByteBuffer) + case accepted + case error(ErrorResponse) +} + +@usableFromInline +package struct InvocationMetadata: Hashable, Sendable { + @usableFromInline + package let requestID: String + @usableFromInline + package let deadlineInMillisSinceEpoch: Int64 + @usableFromInline + package let invokedFunctionARN: String + @usableFromInline + package let traceID: String + @usableFromInline + package let clientContext: String? + @usableFromInline + package let cognitoIdentity: String? + + package init(headers: HTTPHeaders) throws(LambdaRuntimeError) { + guard let requestID = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else { + throw LambdaRuntimeError(code: .nextInvocationMissingHeaderRequestID) + } + + guard let deadline = headers.first(name: AmazonHeaders.deadline), + let unixTimeInMilliseconds = Int64(deadline) + else { + throw LambdaRuntimeError(code: .nextInvocationMissingHeaderDeadline) + } + + guard let invokedFunctionARN = headers.first(name: AmazonHeaders.invokedFunctionARN) else { + throw LambdaRuntimeError(code: .nextInvocationMissingHeaderInvokeFuctionARN) + } + + self.requestID = requestID + self.deadlineInMillisSinceEpoch = unixTimeInMilliseconds + self.invokedFunctionARN = invokedFunctionARN + self.traceID = + headers.first(name: AmazonHeaders.traceID) ?? "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=0" + self.clientContext = headers["Lambda-Runtime-Client-Context"].first + self.cognitoIdentity = headers["Lambda-Runtime-Cognito-Identity"].first + } +} + +struct ErrorResponse: Hashable, Codable { + var errorType: String + var errorMessage: String +} + +extension ErrorResponse { + func toJSONBytes() -> [UInt8] { + var bytes = [UInt8]() + bytes.append(UInt8(ascii: "{")) + bytes.append(contentsOf: #""errorType":"#.utf8) + self.errorType.encodeAsJSONString(into: &bytes) + bytes.append(contentsOf: #","errorMessage":"#.utf8) + self.errorMessage.encodeAsJSONString(into: &bytes) + bytes.append(UInt8(ascii: "}")) + return bytes + } +} diff --git a/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift new file mode 100644 index 00000000..5848ec76 --- /dev/null +++ b/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift @@ -0,0 +1,131 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +struct ControlPlaneRequestEncoder: _EmittingChannelHandler { + typealias OutboundOut = ByteBuffer + + private var host: String + private var byteBuffer: ByteBuffer! + + init(host: String) { + self.host = host + } + + mutating func writeRequest( + _ request: ControlPlaneRequest, + context: ChannelHandlerContext, + promise: EventLoopPromise? + ) { + self.byteBuffer.clear(minimumCapacity: self.byteBuffer.storageCapacity) + + switch request { + case .next: + self.byteBuffer.writeString(.nextInvocationRequestLine) + self.byteBuffer.writeHostHeader(host: self.host) + self.byteBuffer.writeString(.userAgentHeader) + self.byteBuffer.writeString(.CRLF) // end of head + context.write(self.wrapOutboundOut(self.byteBuffer), promise: promise) + context.flush() + + case .invocationResponse(let requestID, let payload): + let contentLength = payload?.readableBytes ?? 0 + self.byteBuffer.writeInvocationResultRequestLine(requestID) + self.byteBuffer.writeHostHeader(host: self.host) + self.byteBuffer.writeString(.userAgentHeader) + self.byteBuffer.writeContentLengthHeader(length: contentLength) + self.byteBuffer.writeString(.CRLF) // end of head + if let payload = payload, contentLength > 0 { + context.write(self.wrapOutboundOut(self.byteBuffer), promise: nil) + context.write(self.wrapOutboundOut(payload), promise: promise) + } else { + context.write(self.wrapOutboundOut(self.byteBuffer), promise: promise) + } + context.flush() + + case .invocationError(let requestID, let errorMessage): + let payload = errorMessage.toJSONBytes() + self.byteBuffer.writeInvocationErrorRequestLine(requestID) + self.byteBuffer.writeContentLengthHeader(length: payload.count) + self.byteBuffer.writeHostHeader(host: self.host) + self.byteBuffer.writeString(.userAgentHeader) + self.byteBuffer.writeString(.unhandledErrorHeader) + self.byteBuffer.writeString(.CRLF) // end of head + self.byteBuffer.writeBytes(payload) + context.write(self.wrapOutboundOut(self.byteBuffer), promise: promise) + context.flush() + + case .initializationError(let errorMessage): + let payload = errorMessage.toJSONBytes() + self.byteBuffer.writeString(.runtimeInitErrorRequestLine) + self.byteBuffer.writeContentLengthHeader(length: payload.count) + self.byteBuffer.writeHostHeader(host: self.host) + self.byteBuffer.writeString(.userAgentHeader) + self.byteBuffer.writeString(.unhandledErrorHeader) + self.byteBuffer.writeString(.CRLF) // end of head + self.byteBuffer.writeBytes(payload) + context.write(self.wrapOutboundOut(self.byteBuffer), promise: promise) + context.flush() + } + } + + mutating func writerAdded(context: ChannelHandlerContext) { + self.byteBuffer = context.channel.allocator.buffer(capacity: 256) + } + + mutating func writerRemoved(context: ChannelHandlerContext) { + self.byteBuffer = nil + } +} + +extension String { + static let CRLF: String = "\r\n" + + static let userAgent = "Swift-Lambda/Unknown" + static let userAgentHeader: String = "user-agent: \(userAgent)\r\n" + static let unhandledErrorHeader: String = "lambda-runtime-function-error-type: Unhandled\r\n" + + static let nextInvocationRequestLine: String = + "GET /2018-06-01/runtime/invocation/next HTTP/1.1\r\n" + + static let runtimeInitErrorRequestLine: String = + "POST /2018-06-01/runtime/init/error HTTP/1.1\r\n" +} + +extension ByteBuffer { + fileprivate mutating func writeInvocationResultRequestLine(_ requestID: String) { + self.writeString("POST /2018-06-01/runtime/invocation/") + self.writeString(requestID) + self.writeString("/response HTTP/1.1\r\n") + } + + fileprivate mutating func writeInvocationErrorRequestLine(_ requestID: String) { + self.writeString("POST /2018-06-01/runtime/invocation/") + self.writeString(requestID) + self.writeString("/error HTTP/1.1\r\n") + } + + fileprivate mutating func writeHostHeader(host: String) { + self.writeString("host: ") + self.writeString(host) + self.writeString(.CRLF) + } + + fileprivate mutating func writeContentLengthHeader(length: Int) { + self.writeString("content-length: ") + self.writeString("\(length)") + self.writeString(.CRLF) + } +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md b/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md new file mode 100644 index 00000000..7bfd8cce --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md @@ -0,0 +1,627 @@ +# Deploying your Swift Lambda functions + +Learn how to deploy your Swift Lambda functions to AWS. + +There are multiple ways to deploy your Swift code to AWS Lambda. The very first time, you'll probably use the AWS Console to create a new Lambda function and upload your code as a zip file. However, as you iterate on your code, you'll want to automate the deployment process. + +To take full advantage of the cloud, we recommend using Infrastructure as Code (IaC) tools like the [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) or [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/). These tools allow you to define your infrastructure and deployment process as code, which can be version-controlled and automated. + +In this section, we show you how to deploy your Swift Lambda functions using different AWS Tools. Alternatively, you might also consider using popular third-party tools like [Serverless Framework](https://www.serverless.com/), [Terraform](https://www.terraform.io/), or [Pulumi](https://www.pulumi.com/) to deploy Lambda functions and create and manage AWS infrastructure. + +Here is the content of this guide: + + * [Prerequisites](#prerequisites) + * [Choosing the AWS Region where to deploy](#choosing-the-aws-region-where-to-deploy) + * [The Lambda execution IAM role](#the-lambda-execution-iam-role) + * [Deploy your Lambda function with the AWS Console](#deploy-your-lambda-function-with-the-aws-console) + * [Deploy your Lambda function with the AWS Command Line Interface (CLI)](#deploy-your-lambda-function-with-the-aws-command-line-interface-cli) + * [Deploy your Lambda function with AWS Serverless Application Model (SAM)](#deploy-your-lambda-function-with-aws-serverless-application-model-sam) + * [Deploy your Lambda function with AWS Cloud Development Kit (CDK)](#deploy-your-lambda-function-with-aws-cloud-development-kit-cdk) + * [Third-party tools](#third-party-tools) + +## Prerequisites + +1. Your AWS Account + + To deploy a Lambda function on AWS, you need an AWS account. If you don't have one yet, you can create a new account at [aws.amazon.com](https://signin.aws.amazon.com/signup?request_type=register). It takes a few minutes to register. A credit card is required. + + We do not recommend using the root credentials you entered at account creation time for day-to-day work. Instead, create an [Identity and Access Manager (IAM) user](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html) with the necessary permissions and use its credentials. + + Follow the steps in [Create an IAM User in your AWS account](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html). + + We suggest to attach the `AdministratorAccess` policy to the user for the initial setup. For production workloads, you should follow the principle of least privilege and grant only the permissions required for your users. The ['AdministratorAccess' gives the user permission](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html#aws-managed-policies) to manage all resources on the AWS account. + +2. AWS Security Credentials + + [AWS Security Credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/security-creds.html) are required to access the AWS console, AWS APIs, or to let tools access your AWS account. + + AWS Security Credentials can be **long-term credentials** (for example, an Access Key ID and a Secret Access Key attached to your IAM user) or **temporary credentials** obtained via other AWS API, such as when accessing AWS through single sign-on (SSO) or when assuming an IAM role. + + To follow the steps in this guide, you need to know your AWS Access Key ID and Secret Access Key. If you don't have them, you can create them in the AWS Management Console. Follow the steps in [Creating access keys for an IAM user](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey). + + When you use SSO with your enterprise identity tools (such as Microsoft entra ID –formerly Active Directory–, Okta, and others) or when you write scripts or code assuming an IAM role, you receive temporary credentials. These credentials are valid for a limited time, have a limited scope, and are rotated automatically. You can use them in the same way as long-term credentials. In addition to an AWS Access Key and Secret Access Key, temporary credentials include a session token. + + Here is a typical set of temporary credentials (redacted for security). + + ```json + { + "Credentials": { + "AccessKeyId": "ASIA...FFSD", + "SecretAccessKey": "Xn...NL", + "SessionToken": "IQ...pV", + "Expiration": "2024-11-23T11:32:30+00:00" + } + } + ``` + +3. A Swift Lambda function to deploy. + + You need a Swift Lambda function to deploy. If you don't have one yet, you can use one of the examples in the [Examples](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples) directory. + + Compile and package the function using the following command + + ```sh + swift package archive --allow-network-connections docker + ``` + + This command creates a ZIP file with the compiled Swift code. The ZIP file is located in the `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip` folder. + + The name of the ZIP file depends on the target name you entered in the `Package.swift` file. + + >[!NOTE] + > When building on Linux, your current user must have permission to use docker. On most Linux distributions, you can do so by adding your user to the `docker` group with the following command: `sudo usermod -aG docker $USER`. You must log out and log back in for the changes to take effect. + +## Choosing the AWS Region where to deploy + +[AWS Global infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/) spans over 34 geographic Regions (and continuously expanding). When you create a resource on AWS, such as a Lambda function, you have to select a geographic region where the resource will be created. The two main factors to consider to select a Region are the physical proximity with your users and geographical compliance. + +Physical proximity helps you reduce the network latency between the Lambda function and your customers. For example, when the majority of your users are located in South-East Asia, you might consider deploying in the Singapore, the Malaysia, or Jakarta Region. + +Geographical compliance, also known as data residency compliance, involves following location-specific regulations about how and where data can be stored and processed. + +## The Lambda execution IAM role + +A Lambda execution role is an AWS Identity and Access Management (IAM) role that grants your Lambda function the necessary permissions to interact with other AWS services and resources. Think of it as a security passport that determines what your function is allowed to do within AWS. For example, if your Lambda function needs to read files from Amazon S3, write logs to Amazon CloudWatch, or access an Amazon DynamoDB table, the execution role must include the appropriate permissions for these actions. + +When you create a Lambda function, you must specify an execution role. This role contains two main components: a trust policy that allows the Lambda service itself to assume the role, and permission policies that determine what AWS resources the function can access. By default, Lambda functions get basic permissions to write logs to CloudWatch Logs, but any additional permissions (like accessing S3 buckets or sending messages to SQS queues) must be explicitly added to the role's policies. Following the principle of least privilege, it's recommended to grant only the minimum permissions necessary for your function to operate, helping maintain the security of your serverless applications. + +## Deploy your Lambda function with the AWS Console + +In this section, we deploy the HelloWorld example function using the AWS Console. The HelloWorld function is a simple function that takes a `String` as input and returns a `String`. + +Authenticate on the AWS console using your IAM username and password. On the top right side, select the AWS Region where you want to deploy, then navigate to the Lambda section. + +![Console - Select AWS Region](console-10-regions) + +### Create the function + +Select **Create a function** to create a function. + +![Console - Lambda dashboard when there is no function](console-20-dashboard) + +Select **Author function from scratch**. Enter a **Function name** (`HelloWorld`) and select `Amazon Linux 2` as **Runtime**. +Select the architecture. When you compile your Swift code on a x84_64 machine, such as an Intel Mac, select `x86_64`. When you compile your Swift code on an Arm machine, such as the Apple Silicon M1 or more recent, select `arm64`. + +Select **Create function** + +![Console - create function](console-30-create-function) + +On the right side, select **Upload from** and select **.zip file**. + +![Console - select zip file](console-40-select-zip-file) + +Select the zip file created with the `swift package archive --allow-network-connections docker` command as described in the [Prerequisites](#prerequisites) section. + +Select **Save** + +![Console - select zip file](console-50-upload-zip) + +You're now ready to test your function. + +### Invoke the function + +Select the **Test** tab in the console and prepare a payload to send to your Lambda function. In this example, you've deployed the [HelloWorld](Exmaples.HelloWorld/README.md) example function. As explained, the function takes a `String` as input and returns a `String`. we will therefore create a test event with a JSON payload that contains a `String`. + +Select **Create new event**. Enter an **Event name**. Enter `"Swift on Lambda"` as **Event JSON**. Note that the payload must be a valid JSON document, hence we use surrounding double quotes (`"`). + +Select **Test** on the upper right side of the screen. + +![Console - prepare test event](console-60-prepare-test-event) + +The response of the invocation and additional meta data appear in the green section of the page. + +You can see the response from the Swift code: `Hello Swift on Lambda`. + +The function consumed 109.60ms of execution time, out of this 83.72ms where spent to initialize this new runtime. This initialization time is known as Lambda cold start time. + +> Lambda cold start time refers to the initial delay that occurs when a Lambda function is invoked for the first time or after being idle for a while. Cold starts happen because AWS needs to provision and initialize a new container, load your code, and start your runtime environment (in this case, the Swift runtime). This delay is particularly noticeable for the first invocation, but subsequent invocations (known as "warm starts") are typically much faster because the container and runtime are already initialized and ready to process requests. Cold starts are an important consideration when architecting serverless applications, especially for latency-sensitive workloads. Usually, compiled languages, such as Swift, Go, and Rust, have shorter cold start times compared to interpreted languages, such as Python, Java, Ruby, and Node.js. + +```text + +![Console - view invocation result](console-70-view-invocation-response) + +Select **Test** to invoke the function again with the same payload. + +Observe the results. No initialization time is reported because the Lambda execution environment was ready after the first invocation. The runtime duration of the second invocation is 1.12ms. + +```text +REPORT RequestId: f789fbb6-10d9-4ba3-8a84-27aa283369a2 Duration: 1.12 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 26 MB +``` + +AWS lambda charges usage per number of invocations and the CPU time, rounded to the next millisecond. AWS Lambda offers a generous free-tier of 1 million invocation each month and 400,000 GB-seconds of compute time per month. See [Lambda pricing](https://aws.amazon.com/lambda/pricing/) for the details. + +### Delete the function + +When you're finished with testing, you can delete the Lambda function and the IAM execution role that the console created automatically. + +While you are on the `HelloWorld` function page in the AWS console, select **Actions**, then **Delete function** in the menu on the top-right part of the page. + +![Console - delete function](console-80-delete-function) + +Then, navigate to the IAM section of the AWS console. Select **Roles** on the right-side menu and search for `HelloWorld`. The console appended some random characters to role name. The name you see on your console is different that the one on the screenshot. + +Select the `HelloWorld-role-xxxx` role and select **Delete**. Confirm the deletion by entering the role name again, and select **Delete** on the confirmation box. + +![Console - delete IAM role](console-80-delete-role) + +## Deploy your Lambda function with the AWS Command Line Interface (CLI) + +You can deploy your Lambda function using the AWS Command Line Interface (CLI). The CLI is a unified tool to manage your AWS services from the command line and automate your operations through scripts. The CLI is available for Windows, macOS, and Linux. Follow the [installation](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [configuration](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) instructions in the AWS CLI User Guide. + +In this example, we're building the HelloWorld example from the [Examples](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples) directory. + +### Create the function + +To create a function, you must first create the function execution role and define the permission. Then, you create the function with the `create-function` command. + +The command assumes you've already created the ZIP file with the `swift package archive --allow-network-connections docker` command, as described in the [Prerequisites](#prerequisites) section. + +```sh +# enter your AWS Account ID +export AWS_ACCOUNT_ID=123456789012 + +# Allow the Lambda service to assume the execution role +cat < assume-role-policy.json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +EOF + +# Create the execution role +aws iam create-role \ +--role-name lambda_basic_execution \ +--assume-role-policy-document file://assume-role-policy.json + +# create permissions to associate with the role +cat < permissions.json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "arn:aws:logs:*:*:*" + } + ] +} +EOF + +# Attach the permissions to the role +aws iam put-role-policy \ +--role-name lambda_basic_execution \ +--policy-name lambda_basic_execution_policy \ +--policy-document file://permissions.json + +# Create the Lambda function +aws lambda create-function \ +--function-name MyLambda \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution +``` + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +To update the function, use the `update-function-code` command after you've recompiled and archived your code again with the `swift package archive` command. + +```sh +aws lambda update-function-code \ +--function-name MyLambda \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip +``` + +### Invoke the function + +Use the `invoke-function` command to invoke the function. You can pass a well-formed JSON payload as input to the function. The payload must be encoded in base64. The CLI returns the status code and stores the response in a file. + +```sh +# invoke the function +aws lambda invoke \ +--function-name MyLambda \ +--payload $(echo \"Swift Lambda function\" | base64) \ +out.txt + +# show the response +cat out.txt + +# delete the response file +rm out.txt +``` + +### Delete the function + +To cleanup, first delete the Lambda funtion, then delete the IAM role. + +```sh +# delete the Lambda function +aws lambda delete-function --function-name MyLambda + +# delete the IAM policy attached to the role +aws iam delete-role-policy --role-name lambda_basic_execution --policy-name lambda_basic_execution_policy + +# delete the IAM role +aws iam delete-role --role-name lambda_basic_execution +``` + +## Deploy your Lambda function with AWS Serverless Application Model (SAM) + +AWS Serverless Application Model (SAM) is an open-source framework for building serverless applications. It provides a simplified way to define the Amazon API Gateway APIs, AWS Lambda functions, and Amazon DynamoDB tables needed by your serverless application. You can define your serverless application in a single file, and SAM will use it to deploy your function and all its dependencies. + +To use SAM, you need to [install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) on your machine. The SAM CLI provides a set of commands to package, deploy, and manage your serverless applications. + +Use SAM when you want to deploy more than a Lambda function. SAM helps you to create additional resources like an API Gateway, an S3 bucket, or a DynamoDB table, and manage the permissions between them. + +### Create the function + +We assume your Swift function is compiled and packaged, as described in the [Prerequisites](#prerequisites) section. + +When using SAM, you describe the infrastructure you want to deploy in a YAML file. The file contains the definition of the Lambda function, the IAM role, and the permissions needed by the function. The SAM CLI uses this file to package and deploy your function. + +You can create a SAM template to define a REST API implemented by AWS API Gateway and a Lambda function with the following command + +```sh +cat < template.yaml +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for APIGateway Lambda Example + +Resources: + # Lambda function + APIGatewayLambda: + Type: AWS::Serverless::Function + Properties: + # the directory name and ZIP file names depends on the Swift executable target name + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 512 + Architectures: + - arm64 + # The events that will trigger this function + Events: + HttpApiEvent: + Type: HttpApi # AWS API Gateway v2 + +Outputs: + # display API Gateway endpoint + APIGatewayEndpoint: + Description: "API Gateway endpoint URI" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" +EOF +``` + +In this example, the Lambda function must accept an APIGateway v2 JSON payload as input parameter and return a valid APIGAteway v2 JSON response. See the example code in the [APIGateway example README file](https://github.com/swift-server/swift-aws-lambda-runtime/blob/main/Examples/APIGateway/README.md). + +To deploy the function with SAM, use the `sam deploy` command. The very first time you deploy a function, you should use the `--guided` flag to configure the deployment. The command will ask you a series of questions to configure the deployment. + +Here is the command to deploy the function with SAM: + +```sh +# start the first deployment +sam deploy --guided + +Configuring SAM deploy +====================== + + Looking for config file [samconfig.toml] : Not found + + Setting default arguments for 'sam deploy' + ========================================= + Stack Name [sam-app]: APIGatewayLambda + AWS Region [us-east-1]: + #Shows you resources changes to be deployed and require a 'Y' to initiate deploy + Confirm changes before deploy [y/N]: n + #SAM needs permission to be able to create roles to connect to the resources in your template + Allow SAM CLI IAM role creation [Y/n]: y + #Preserves the state of previously provisioned resources when an operation fails + Disable rollback [y/N]: n + APIGatewayLambda has no authentication. Is this okay? [y/N]: y + Save arguments to configuration file [Y/n]: y + SAM configuration file [samconfig.toml]: + SAM configuration environment [default]: + + Looking for resources needed for deployment: + +(redacted for brevity) + +CloudFormation outputs from deployed stack +-------------------------------------------------------------------------------- +Outputs +-------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URI" +Value https://59i4uwbuj2.execute-api.us-east-1.amazonaws.com +-------------------------------------------------------------------------------- + + +Successfully created/updated stack - APIGAtewayLambda in us-east-1 +``` + +To update your function or any other AWS service defined in your YAML file, you can use the `sam deploy` command without the `--guided` flag. + +### Invoke the function + +SAM allows you to invoke the function locally and remotely. + +Local invocations allows you to test your code before uploading it. It requires docker to run. + +```sh +# First, generate a sample event +sam local generate-event apigateway http-api-proxy > event.json + +# Next, invoke the function locally +sam local invoke -e ./event.json + +START RequestId: 3f5096c6-0fd3-4605-b03e-d46658e6b141 Version: $LATEST +END RequestId: 3134f067-9396-4f4f-bebb-3c63ef745803 +REPORT RequestId: 3134f067-9396-4f4f-bebb-3c63ef745803 Init Duration: 0.04 ms Duration: 38.38 msBilled Duration: 39 ms Memory Size: 512 MB Max Memory Used: 512 MB +{"body": "{\"version\":\"2.0\",\"routeKey\":\"$default\",\"rawPath\":\"\\/path\\/to\\/resource\",... REDACTED FOR BREVITY ...., "statusCode": 200, "headers": {"content-type": "application/json"}} +``` + +> If you've previously authenticated to Amazon ECR Public and your auth token has expired, you may receive an authentication error when attempting to do unauthenticated docker pulls from Amazon ECR Public. To resolve this issue, it may be necessary to run `docker logout public.ecr.aws` to avoid the error. This will result in an unauthenticated pull. For more information, see [Authentication issues](https://docs.aws.amazon.com/AmazonECR/latest/public/public-troubleshooting.html#public-troubleshooting-authentication). + +Remote invocations are done with the `sam remote invoke` command. + +```sh +sam remote invoke \ + --stack-name APIGatewayLambda \ + --event-file ./event.json + +Invoking Lambda Function APIGatewayLambda +START RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 Version: $LATEST +END RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 +REPORT RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 Duration: 6.01 ms Billed Duration: 7 ms Memory Size: 512 MB Max Memory Used: 35 MB +{"body":"{\"stageVariables\":{\"stageVariable1\":\"value1\",\"stageVariable2\":\"value2\"},\"rawPath\":\"\\\/path\\\/to\\\/resource\",\"routeKey\":\"$default\",\"cookies\":[\"cookie1\",\"cookie2\"] ... REDACTED FOR BREVITY ... \"statusCode\":200,"headers":{"content-type":"application/json"}} +``` + +SAM allows you to access the function logs from Amazon Cloudwatch. + +```sh +sam logs --stack-name APIGatewayLambda + +Access logging is disabled for HTTP API ID (g9m53sn7xa) +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:16:25.593000 INIT_START Runtime Version: provided:al2.v75 Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:4f3438ed7de2250cc00ea1260c3dc3cd430fad27835d935a02573b6cf07ceed8 +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:16:25.715000 START RequestId: d8afa647-8361-4bce-a817-c57b92a060af Version: $LATEST +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:16:25.758000 END RequestId: d8afa647-8361-4bce-a817-c57b92a060af +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:16:25.758000 REPORT RequestId: d8afa647-8361-4bce-a817-c57b92a060af Duration: 40.74 ms Billed Duration: 162 ms Memory Size: 512 MB Max Memory Used: 34 MB Init Duration: 120.64 ms +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:17:10.343000 START RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 Version: $LATEST +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:17:10.350000 END RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 +2024/12/19/[$LATEST]4dd42d66282145a2964ff13dfcd5dc65 2024-12-19T10:17:10.350000 REPORT RequestId: ec8082c5-933b-4176-9c63-4c8fb41ca259 Duration: 6.01 ms Billed Duration: 7 ms Memory Size: 512 MB Max Memory Used: 35 MB +``` + +You can also tail the logs with the `-t, --tail` flag. + +### Delete the function + +SAM allows you to delete your function and all infrastructure that is defined in the YAML template with just one command. + +```sh +sam delete + +Are you sure you want to delete the stack APIGatewayLambda in the region us-east-1 ? [y/N]: y +Are you sure you want to delete the folder APIGatewayLambda in S3 which contains the artifacts? [y/N]: y +- Deleting S3 object with key APIGatewayLambda/1b5a27c048549382462bd8ea589f7cfe +- Deleting S3 object with key APIGatewayLambda/396d2c434ecc24aaddb670bd5cca5fe8.template +- Deleting Cloudformation stack APIGatewayLambda + +Deleted successfully +``` + +## Deploy your Lambda function with the AWS Cloud Development Kit (CDK) + +The AWS Cloud Development Kit is an open-source software development framework to define cloud infrastructure in code and provision it through AWS CloudFormation. The CDK provides high-level constructs that preconfigure AWS resources with best practices, and you can use familiar programming languages like TypeScript, Javascript, Python, Java, C#, and Go to define your infrastructure. + +To use the CDK, you need to [install the CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) on your machine. The CDK CLI provides a set of commands to manage your CDK projects. + +Use the CDK when you want to define your infrastructure in code and manage the deployment of your Lambda function and other AWS services. + +This example deploys the [APIGateway]((https://github.com/swift-server/swift-aws-lambda-runtime/blob/main/Examples/APIGateway/) example code. It comprises a Lambda function that implements a REST API and an API Gateway to expose the function over HTTPS. + +### Create a CDK project + +To create a new CDK project, use the `cdk init` command. The command creates a new directory with the project structure and the necessary files to define your infrastructure. + +```sh +# In your Swift Lambda project folder +mkdir infra && cd infra +cdk init app --language typescript +``` + +In this example, the code to create a Swift Lambda function with the CDK is written in TypeScript. The following code creates a new Lambda function with the `swift` runtime. + +It requires the `@aws-cdk/aws-lambda` package to define the Lambda function. You can install the dependency with the following command: + +```sh +npm install aws-cdk-lib constructs +``` + +Then, in the lib folder, create a new file named `swift-lambda-stack.ts` with the following content: + +```typescript +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +export class LambdaApiStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Create the Lambda function + const lambdaFunction = new lambda.Function(this, 'SwiftLambdaFunction', { + runtime: lambda.Runtime.PROVIDED_AL2, + architecture: lambda.Architecture.ARM_64, + handler: 'bootstrap', + code: lambda.Code.fromAsset('../.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/APIGatewayLambda/APIGatewayLambda.zip'), + memorySize: 512, + timeout: cdk.Duration.seconds(30), + environment: { + LOG_LEVEL: 'debug', + }, + }); + } +} +``` +The code assumes you already built and packaged the APIGateway Lambda function with the `swift package archive --allow-network-connections docker` command, as described in the [Prerequisites](#prerequisites) section. + +You can write code to add an API Gateway to invoke your Lambda function. The following code creates an HTTP API Gateway that triggers the Lambda function. + +```typescript +// in the import section at the top +import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2'; +import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; + +// in the constructor, after having created the Lambda function +// ... + + // Create the API Gateway + const httpApi = new apigateway.HttpApi(this, 'HttpApi', { + defaultIntegration: new HttpLambdaIntegration({ + handler: lambdaFunction, + }), + }); + + // Output the API Gateway endpoint + new cdk.CfnOutput(this, 'APIGatewayEndpoint', { + value: httpApi.url!, + }); + +// ... +``` + +### Deploy the infrastructure + +To deploy the infrastructure, type the following commands. + +```sh +# Change to the infra directory +cd infra + +# Install the dependencies (only before the first deployment) +npm install + +# Deploy the infrastructure +cdk deploy + +✨ Synthesis time: 2.88s +... redacted for brevity ... +Do you wish to deploy these changes (y/n)? y +... redacted for brevity ... + ✅ LambdaApiStack + +✨ Deployment time: 42.96s + +Outputs: +LambdaApiStack.ApiUrl = https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com/ +Stack ARN: +arn:aws:cloudformation:eu-central-1:012345678901:stack/LambdaApiStack/e0054390-be05-11ef-9504-065628de4b89 + +✨ Total time: 45.84s +``` + +### Invoke your Lambda function + +To invoke the Lambda function, use this `curl` command line. + +```bash +curl https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com +``` + +Be sure to replace the URL with the API Gateway endpoint returned in the previous step. + +This should print a JSON similar to + +```bash +{"version":"2.0","rawPath":"\/","isBase64Encoded":false,"rawQueryString":"","headers":{"user-agent":"curl\/8.7.1","accept":"*\/*","host":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","content-length":"0","x-amzn-trace-id":"Root=1-66fb0388-691f744d4bd3c99c7436a78d","x-forwarded-port":"443","x-forwarded-for":"81.0.0.43","x-forwarded-proto":"https"},"requestContext":{"requestId":"e719cgNpoAMEcwA=","http":{"sourceIp":"81.0.0.43","path":"\/","protocol":"HTTP\/1.1","userAgent":"curl\/8.7.1","method":"GET"},"stage":"$default","apiId":"a5q74es3k2","time":"30\/Sep\/2024:20:01:12 +0000","timeEpoch":1727726472922,"domainPrefix":"a5q74es3k2","domainName":"a5q74es3k2.execute-api.us-east-1.amazonaws.com","accountId":"012345678901"} +``` + +If you have `jq` installed, you can use it to pretty print the output. + +```bash +curl -s https://tyqnjcawh0.execute-api.eu-central-1.amazonaws.com | jq +{ + "version": "2.0", + "rawPath": "/", + "requestContext": { + "domainPrefix": "a5q74es3k2", + "stage": "$default", + "timeEpoch": 1727726558220, + "http": { + "protocol": "HTTP/1.1", + "method": "GET", + "userAgent": "curl/8.7.1", + "path": "/", + "sourceIp": "81.0.0.43" + }, + "apiId": "a5q74es3k2", + "accountId": "012345678901", + "requestId": "e72KxgsRoAMEMSA=", + "domainName": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "time": "30/Sep/2024:20:02:38 +0000" + }, + "rawQueryString": "", + "routeKey": "$default", + "headers": { + "x-forwarded-for": "81.0.0.43", + "user-agent": "curl/8.7.1", + "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", + "accept": "*/*", + "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", + "content-length": "0", + "x-forwarded-proto": "https", + "x-forwarded-port": "443" + }, + "isBase64Encoded": false +} +``` + +### Delete the infrastructure + +When done testing, you can delete the infrastructure with this command. + +```bash +cdk destroy + +Are you sure you want to delete: LambdaApiStack (y/n)? y +LambdaApiStack: destroying... [1/1] +... redacted for brevity ... + ✅ LambdaApiStack: destroyed +``` + +## Third-party tools + +We welcome contributions to this section. If you have experience deploying Swift Lambda functions with third-party tools like Serverless Framework, Terraform, or Pulumi, please share your knowledge with the community. \ No newline at end of file diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Documentation.md b/Sources/AWSLambdaRuntime/Docs.docc/Documentation.md new file mode 100644 index 00000000..e820ce26 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Documentation.md @@ -0,0 +1,23 @@ +# ``AWSLambdaRuntime`` + +An AWS Lambda runtime for the Swift programming language + +## Overview + +Many modern systems have client components like iOS, macOS or watchOS applications as well as server components that those clients interact with. Serverless functions are often the easiest and most efficient way for client application developers to extend their applications into the cloud. + +Serverless functions are increasingly becoming a popular choice for running event-driven or otherwise ad-hoc compute tasks in the cloud. They power mission critical microservices and data intensive workloads. In many cases, serverless functions allow developers to more easily scale and control compute costs given their on-demand nature. + +When using serverless functions, attention must be given to resource utilization as it directly impacts the costs of the system. This is where Swift shines! With its low memory footprint, deterministic performance, and quick start time, Swift is a fantastic match for the serverless functions architecture. + +Combine this with Swift's developer friendliness, expressiveness, and emphasis on safety, and we have a solution that is great for developers at all skill levels, scalable, and cost effective. + +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-api.html) and uses an embedded asynchronous HTTP client based on [SwiftNIO](https://github.com/apple/swift-nio) that is fine-tuned for performance in the AWS Lambda 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. + +## Topics + +### Essentials + +- +- +- diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md new file mode 100644 index 00000000..0396d48d --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md @@ -0,0 +1,905 @@ +# v2 API proposal for swift-aws-lambda-runtime + +`swift-aws-lambda-runtime` is an important library for the Swift on Server ecosystem. The initial API was written before +async/await was introduced to Swift. When async/await was introduced, shims were added to bridge between the underlying +SwiftNIO `EventLoop` interfaces and async/await. However, just like `gRPC-swift` and `postgres-nio`, we now want to +shift to solely using async/await instead of `EventLoop` interfaces. For this, large parts of the current API have to be +reconsidered. + +## Overview + +Versions: + +- v1 (2024-08-07): Initial version +- v1.1: + - Remove the `reportError(_:)` method from `LambdaResponseStreamWriter` and instead make the `handle(...)` method of + `StreamingLambdaHandler` throwing. + - Remove the `addBackgroundTask(_:)` method from `LambdaContext` due to structured concurrency concerns and introduce + the `LambdaWithBackgroundProcessingHandler` protocol as a solution. + - Introduce `LambdaHandlerAdapter`, which adapts handlers conforming to `LambdaHandler` with + `LambdaWithBackgroundProcessingHandler`. + - Update `LambdaCodableAdapter` to now be generic over any handler conforming to + `LambdaWithBackgroundProcessingHandler` instead of `LambdaHandler`. +- v1.2: + - Remove `~Copyable` from `LambdaResponseStreamWriter` and `LambdaResponseWriter`. Instead throw an error when + `finish()` is called multiple times or when `write`/`writeAndFinish` is called after `finish()`. + +## Motivation + +### Current Limitations + +#### EventLoop interfaces + +The current API extensively uses the `EventLoop` family of interfaces from SwiftNIO in many areas. To use these +interfaces correctly though, it requires developers to exercise great care and understand the various transform methods +that are used to work with `EventLoop`s and `EventLoopFuture`s. This results in a lot of cognitive complexity and makes +the code in the current API hard to reason about and maintain. For these reasons, the overarching trend in the Swift on +Server ecosystem is to shift to newer, more readable, Swift concurrency constructs and de-couple from SwiftNIO's +`EventLoop` interfaces. + +#### No ownership of the main() function + +A Lambda function can currently be implemented through conformance to the various handler protocols defined in +``AWSLambdaRuntime/LambdaHandler``. Each of these protocols have an extension which implements a `static func main()`. +This allows users to annotate their `LambdaHandler` conforming object with `@main`. The `static func main()` calls the +internal `Lambda.run()` function, which starts the Lambda function. Since the `Lambda.run()` method is internal, users +cannot override the default implementation. This has proven challenging for users who want to +[set up global properties before the Lambda starts-up](https://github.com/swift-server/swift-aws-lambda-runtime/issues/265). +Setting up global properties is required to customize the Swift Logging, Metric and Tracing backend. + +#### Non-trivial transition from SimpleLambdaHandler to LambdaHandler + +The `SimpleLambdaHandler` protocol provides a quick and easy way to implement a basic Lambda function. It only requires +an implementation of the `handle` function where the business logic of the Lambda function can be written. +`SimpleLambdaHandler` is perfectly sufficient for small use-cases as the user does not need to spend much time looking +into the library. + +However, `SimpleLambdaHandler` cannot be used when services such as a database client need to be initialized before the +Lambda runtime starts and then also gracefully shutdown prior to the runtime terminating. This is because the only way +to register termination logic is through the `LambdaInitializationContext` (containing a field +`terminator: LambdaTerminator`) which is created and used _internally_ within `LambdaRuntime` and never exposed through +`SimpleLambdaHandler`. For such use-cases, other handler protocols like `LambdaHandler` must be used. `LambdaHandler` +exposes a `context` argument of type `LambdaInitializationContext` through its initializer. Within the initializer, +required services can be initialized and their graceful shutdown logic can be registered with the +`context.terminator.register` function. + +Yet, `LambdaHandler` is quite cumbersome to use in such use-cases as users have to deviate from the established norms of +the Swift on Server ecosystem in order to cleanly manage the lifecycle of the services intended to be used. This is +because the convenient `swift-service-lifecycle` v2 library — which is commonly used for cleanly managing the lifecycles +of required services and widely supported by many libraries — cannot be used in a structured concurrency manner. + +#### Does not integrate well with swift-service-lifecycle in a structured concurrency manner + +The Lambda runtime can only be started using the **internal** `Lambda.run()` function. This function is called by the +`main()` function defined by the `LambdaHandler` protocol, preventing users from injecting initialized services into the +runtime _prior_ to it starting. As shown below, this forces users to use an **unstructured concurrency** approach and +manually initialize services, leading to the issue of the user then perhaps forgetting to gracefully shutdown the +initialized services: + +```swift +struct MyLambda: LambdaHandler { + let pgClient: PostgresClient + + init(context: AWSLambdaRuntime.LambdaInitializationContext) async throws { + /// Instantiate service + let client = PostgresClient(configuration: ...) + + /// Unstructured concurrency to initialize the service + let pgTask = Task { + await client.run() + } + + /// Store the client in `self` so that it can be used in `handle(...)` + self.pgClient = client + + /// !!! Must remember to explicitly register termination logic for PostgresClient !!! + context.terminator.register( + name: "PostgreSQL Client", + handler: { eventLoop in + pgTask.cancel() + return eventLoop.makeFutureWithTask { + await pgTask.value + } + } + ) + } + + func handle(_ event: Event, context: LambdaContext) async throws -> Output { + /// Use the initialized service stored in `self.pgClient` + try await self.pgClient.query(...) + } +} +``` + +#### Verbose Codable support + +In the current API, there are extensions and Codable wrapper classes for decoding events and encoding computed responses +for _each_ different handler protocol and for both `String` and `JSON` formats. This has resulted in a lot of +boilerplate code which can very easily be made generic and simplified in v2. + +### New features + +#### Support response streaming + +In April 2023 +[AWS introduced support for response streaming](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/) +in Lambda. The current API does not support streaming. For v2 we want to change this. + +#### Scheduling background work + +In May +[AWS described in a blog post that you can run background tasks in Lambda](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/) +until the runtime asks for more work from the control plane. We want to support this by adding new API that allows +background processing, even after the response has been returned. + +## Proposed Solution + +### async/await-first API + +Large parts of `Lambda`, `LambdaHandler`, and `LambdaRuntime` will be re-written to use async/await constructs in place +of the `EventLoop` family of interfaces. + +### Providing ownership of main() and support for swift-service-lifecycle + +- Instead of conforming to a handler protocol, users can now create a `LambdaRuntime` by passing in a handler closure. +- `LambdaRuntime` conforms to `ServiceLifecycle.Service` by implementing a `run()` method that contains initialization + and graceful shutdown logic. +- This allows the lifecycle of the `LambdaRuntime` to be managed with `swift-service-lifecycle` _alongside_ and in the + same way the lifecycles of the required services are managed, e.g. + `try await ServiceGroup(services: [postgresClient, ..., lambdaRuntime], ...).run()`. +- Dependencies can now be injected into `LambdaRuntime`. With `swift-service-lifecycle`, services will be initialized + together with `LambdaRuntime`. +- The required services can then be used within the handler in a structured concurrency manner. + `swift-service-lifecycle` takes care of listening for termination signals and terminating the services as well as the + `LambdaRuntime` in correct order. +- `LambdaTerminator` can now be eliminated because its role is replaced with `swift-service-lifecycle`. The termination + logic of the Lambda function will be implemented in the conforming `run()` function of `LambdaRuntime`. + +With this, the earlier code snippet can be replaced with something much easier to read, maintain, and debug: + +```swift +/// Instantiate services +let postgresClient = PostgresClient() + +/// Instantiate LambdaRuntime with a closure handler implementing the business logic of the Lambda function +let runtime = LambdaRuntime { (event: Input, context: LambdaContext) in + /// Use initialized service within the handler + try await postgresClient.query(...) +} + +/// Use ServiceLifecycle to manage the initialization and termination +/// of the services as well as the LambdaRuntime +let serviceGroup = ServiceGroup( + services: [postgresClient, runtime], + configuration: .init(gracefulShutdownSignals: [.sigterm]), + logger: logger +) +try await serviceGroup.run() +``` + +### Simplifying Codable support + +A detailed explanation is provided in the **Codable Support** section. In short, much of the boilerplate code defined +for each handler protocol in `Lambda+Codable` and `Lambda+String` will be replaced with a single `LambdaCodableAdapter` +struct. + +This adapter struct is generic over (1) any handler conforming to a new handler protocol +`LambdaWithBackgroundProcessingHandler`, (2) the user-specified input and output types, and (3) any decoder and encoder +conforming to protocols `LambdaEventDecoder` and `LambdaOutputDecoder`. The adapter will wrap the underlying handler +with encoding/decoding logic. + +## Detailed Solution + +Below are explanations for all types that we want to use in AWS Lambda Runtime v2. + +### LambdaResponseStreamWriter + +We will introduce a new `LambdaResponseStreamWriter` protocol. It is used in the new `StreamingLambdaHandler` (defined +below), which is the new base protocol for the `LambdaRuntime` (defined below as well). + +```swift +/// A writer object to write the Lambda response stream into +public protocol LambdaResponseStreamWriter { + /// Write a response part into the stream. The HTTP response is started lazily before the first call to `write(_:)`. + /// Bytes written to the writer are streamed continually. + func write(_ buffer: ByteBuffer) async throws + /// End the response stream and the underlying HTTP response. + func finish() async throws + /// Write a response part into the stream and end the response stream as well as the underlying HTTP response. + func writeAndFinish(_ buffer: ByteBuffer) async throws +} +``` + +If the user does not call `finish()`, the library will automatically finish the stream after the last `write`. +Appropriate errors will be thrown if `finish()` is called multiple times, or if `write`/`writeAndFinish` is called after +`finish()`. + +### LambdaContext + +`LambdaContext` will be largely unchanged, but the `eventLoop` property will be removed. The `allocator` property of +type `ByteBufferAllocator` will also be removed because (1), we generally want to reduce the number of SwiftNIO types +exposed in the API, and (2), `ByteBufferAllocator` does not optimize the allocation strategies. The common pattern +observed across many libraries is to re-use existing `ByteBuffer`s as much as possible. This is also what we do for the +`LambdaCodableAdapter` (explained in the **Codable Support** section) implementation. + +```swift +/// A context object passed as part of an invocation in LambdaHandler handle functions. +public struct LambdaContext: Sendable { + /// The request ID, which identifies the request that triggered the function invocation. + public var requestID: String { get } + + /// The AWS X-Ray tracing header. + public var traceID: String { get } + + /// The ARN of the Lambda function, version, or alias that's specified in the invocation. + public var invokedFunctionARN: String { get } + + /// The timestamp that the function times out. + public var deadline: DispatchWallTime { get } + + /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. + public var cognitoIdentity: String? { get } + + /// For invocations from the AWS Mobile SDK, data about the client application and device. + public var clientContext: String? { get } + + /// `Logger` to log with. + /// + /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. + public var logger: Logger { get } +} +``` + +### Handlers + +We introduce three handler protocols: `StreamingLambdaHandler`, `LambdaHandler`, and +`LambdaWithBackgroundProcessingHandler`. + +#### StreamingLambdaHandler + +The new `StreamingLambdaHandler` protocol is the base protocol to implement a Lambda function. Most users will not use +this protocol and instead use the `LambdaHandler` protocol defined below. + +```swift +/// The base StreamingLambdaHandler protocol +public protocol StreamingLambdaHandler { + /// The business logic of the Lambda function + /// - Parameters: + /// - event: The invocation's input data + /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. + /// If no response or error is written to the `responseWriter` it will + /// report an error to the invoker. + /// - context: The LambdaContext containing the invocation's metadata + /// - Throws: + /// How the thrown error will be handled by the runtime: + /// - An invocation error will be reported if the error is thrown before the first call to + /// ``LambdaResponseStreamWriter.write(_:)``. + /// - If the error is thrown after call(s) to ``LambdaResponseStreamWriter.write(_:)`` but before + /// a call to ``LambdaResponseStreamWriter.finish()``, the response stream will be closed and trailing + /// headers will be sent. + /// - If ``LambdaResponseStreamWriter.finish()`` has already been called before the error is thrown, the + /// error will be logged. + mutating func handle(_ event: ByteBuffer, responseWriter: some LambdaResponseStreamWriter, context: LambdaContext) async throws +} +``` + +Using this protocol requires the `handle` method to receive the incoming event as a `ByteBuffer` and return the output +as a `ByteBuffer` too. + +Through the `LambdaResponseStreamWriter`, which is passed as an argument in the `handle` function, the **response can be +streamed** by calling the `write(_:)` function of the `LambdaResponseStreamWriter` with partial data repeatedly before +finally closing the response stream by calling `finish()`. Users can also choose to return the entire output and not +stream the response by calling `writeAndFinish(_:)`. + +This protocol also allows for background tasks to be run after a result has been reported to the AWS Lambda control +plane, since the `handle(...)` function is free to implement any background work after the call to +`responseWriter.finish()`. + +The protocol is defined in a way that supports a broad range of use-cases. The handle method is marked as `mutating` to +allow handlers to be implemented with a `struct`. + +An implementation that sends the number 1 to 10 every 500ms could look like this: + +```swift +struct SendNumbersWithPause: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + for i in 1...10 { + // Send partial data + responseWriter.write(ByteBuffer(string: #"\#(i)\n\r"#)) + // Perform some long asynchronous work + try await Task.sleep(for: .milliseconds(500)) + } + // All data has been sent. Close off the response stream. + responseWriter.finish() + } +} +``` + +#### LambdaHandler: + +This handler protocol will be the go-to choice for most use-cases because it is completely agnostic to any +encoding/decoding logic -- conforming objects simply have to implement the `handle` function where the input and return +types are Swift objects. + +Note that the `handle` function does not receive a `LambdaResponseStreamWriter` as an argument. Response streaming is +not viable for `LambdaHandler` because the output has to be encoded prior to it being sent, e.g. it is not possible to +encode a partial/incomplete JSON string. + +```swift +public protocol LambdaHandler { + /// Generic input type + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume + associatedtype Event + /// Generic output type + /// This is the return type of the handle() function. + associatedtype Output + + /// The business logic of the Lambda function. Receives a generic input type and returns a generic output type. + /// Agnostic to encoding/decoding + mutating func handle(_ event: Event, context: LambdaContext) async throws -> Output +} +``` + +#### LambdaWithBackgroundProcessingHandler: + +This protocol is exactly like `LambdaHandler`, with the only difference being the added support for executing background +work after the result has been sent to the AWS Lambda control plane. + +This is achieved by not having a return type in the `handle` function. The output is instead written into a +`LambdaResponseWriter` that is passed in as an argument, meaning that the `handle` function is then free to implement +any background work after the result has been sent to the AWS Lambda control plane. + +`LambdaResponseWriter` has different semantics to the `LambdaResponseStreamWriter`. Where the `write(_:)` function of +`LambdaResponseStreamWriter` means writing into a response stream, the `write(_:)` function of `LambdaResponseWriter` +simply serves as a mechanism to return the output without explicitly returning from the `handle` function. + +```swift +public protocol LambdaResponseWriter { + associatedtype Output + + /// Sends the generic Output object (representing the computed result of the handler) + /// to the AWS Lambda response endpoint. + /// An error will be thrown if this function is called more than once. + func write(_: Output) async throws +} + +public protocol LambdaWithBackgroundProcessingHandler { + /// Generic input type + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume + associatedtype Event + /// Generic output type + /// This is the type that the handle() function will send through the ``LambdaResponseWriter``. + associatedtype Output + + /// The business logic of the Lambda function. Receives a generic input type and returns a generic output type. + /// Agnostic to JSON encoding/decoding + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws +} +``` + +##### Example Usage: + +```swift +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + try await outputWriter.write(result: Greeting(echoedMessage: event.messageToEcho)) + + // Perform some background work, e.g: + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + return + } +} +``` + +#### Handler Adapters + +Since the `StreamingLambdaHandler` protocol is the base protocol the `LambdaRuntime` works with, there are adapters to +make both `LambdaHandler` and `LambdaWithBackgroundProcessingHandler` compatible with `StreamingLambdaHandler`. + +1. `LambdaHandlerAdapter` accepts a `LambdaHandler` and conforms it to `LambdaWithBackgroundProcessingHandler`. This is + achieved by taking the generic `Output` object returned from the `handle` function of `LambdaHandler` and passing it + to the `write(_:)` function of the `LambdaResponseWriter`. + +2. `LambdaCodableAdapter` accepts a `LambdaWithBackgroundProcessingHandler` and conforms it to `StreamingLambdaHandler`. + This is achieved by wrapping the `LambdaResponseWriter` with the `LambdaResponseStreamWriter` provided by + `StreamingLambdaHandler`. A call to the `write(_:)` function of `LambdaResponseWriter` is translated into a call to + the `writeAndFinish(_:)` function of `LambdaResponseStreamWriter`. + +Both `LambdaHandlerAdapter` and `LambdaCodableAdapter` are described in greater detail in the **Codable Support** +section. + +To summarize, `LambdaHandler` can be used with the `LambdaRuntime` by first going through `LambdaHandlerAdapter` and +then through `LambdaCodableAdapter`. `LambdaWithBackgroundHandler` just requires `LambdaCodableAdapter`. + +For the common JSON-in and JSON-out use-case, there is an extension on `LambdaRuntime` that abstracts away this wrapping +from the user. + +### LambdaRuntime + +`LambdaRuntime` is the class that communicates with the Lambda control plane as defined in +[Building a custom runtime for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) and +forward the invocations to the provided `StreamingLambdaHandler`. It will conform to `ServiceLifecycle.Service` to +provide support for `swift-service-lifecycle`. + +```swift +/// The LambdaRuntime object. This object communicates with the Lambda control plane +/// to fetch work and report errors. +public final class LambdaRuntime: ServiceLifecycle.Service, Sendable + where Handler: StreamingLambdaHandler +{ + + /// Create a LambdaRuntime by passing a handler, an eventLoop and a logger. + /// - Parameter handler: A ``StreamingLambdaHandler`` that will be invoked + /// - Parameter eventLoop: An ``EventLoop`` on which the LambdaRuntime will be + /// executed. Defaults to an EventLoop from + /// ``NIOSingletons.posixEventLoopGroup``. + /// - Parameter logger: A logger + public init( + handler: sending Handler, + eventLoop: EventLoop = Lambda.defaultEventLoop, + logger: Logger = Logger(label: "Lambda") + ) + + /// Create a LambdaRuntime by passing a ``StreamingLambdaHandler``. + public convenience init(handler: sending Handler) + + /// Starts the LambdaRuntime by connecting to the Lambda control plane to ask + /// for events to process. If the environment variable AWS_LAMBDA_RUNTIME_API is + /// set, the LambdaRuntime will connect to the Lambda control plane. Otherwise + /// it will start a mock server that can be used for testing at port 8080 + /// locally. + /// Cancel the task that runs this function to close the communication with + /// the Lambda control plane or close the local mock server. This function + /// only returns once cancelled. + public func run() async throws +} +``` + +The current API allows for a Lambda function to be tested locally through a mock server by requiring an environment +variable named `LOCAL_LAMBDA_SERVER_ENABLED` to be set to `true`. If this environment variable is not set, the program +immediately crashes as the user will not have the `AWS_LAMBDA_RUNTIME_API` environment variable on their local machine +(set automatically when deployed to AWS Lambda). However, making the user set the `LOCAL_LAMBDA_SERVER_ENABLED` +environment variable is an unnecessary step that can be avoided. In the v2 API, the `run()` function will automatically +start the mock server when the `AWS_LAMBDA_RUNTIME_API` environment variable cannot be found. + +### Lambda + +We also add an enum to store a static function and a property on. We put this on the static `Lambda` because +`LambdaRuntime` is generic and thus has bad ergonomics for static properties and functions. + +```swift +enum Lambda { + /// This returns the default EventLoop that a LambdaRuntime is scheduled on. + /// It uses `NIOSingletons.posixEventLoopGroup.next()` under the hood. + public static var defaultEventLoop: any EventLoop { get } + + /// Report a startup error to the Lambda Control Plane API + public static func reportStartupError(any Error) async +} +``` + +Since the library now provides ownership of the `main()` function and allows users to initialize services before the +`LambdaRuntime` is initialized, the library cannot implicitly report +[errors that occur during initialization to the dedicated endpoint AWS exposes](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-initerror) +like it currently does through the `initialize()` function of `LambdaRunner` which wraps the handler's `init(...)` and +handles any errors thrown by reporting it to the dedicated AWS endpoint. + +To retain support for initialization error reporting, the `Lambda.reportStartupError(any Error)` function gives users +the option to manually report initialization errors in their closure handler. Although this should ideally happen +implicitly like it currently does in v1, we believe this is a small compromise in comparison to the benefits gained in +now being able to cleanly manage the lifecycles of required services in a structured concurrency manner. + +> Use-case: +> +> Assume we want to load a secret for the Lambda function from a secret vault first. If this fails, we want to report +> the error to the control plane: +> +> ```swift +> let secretVault = SecretVault() +> +> do { +> /// !!! Error thrown: secret "foo" does not exist !!! +> let secret = try await secretVault.getSecret("foo") +> +> let runtime = LambdaRuntime { (event: Input, context: LambdaContext) in +> /// Lambda business logic +> } +> +> let serviceGroup = ServiceGroup( +> services: [postgresClient, runtime], +> configuration: .init(gracefulShutdownSignals: [.sigterm]), +> logger: logger +> ) +> try await serviceGroup.run() +> } catch { +> /// Report startup error straight away to the dedicated initialization error endpoint +> try await Lambda.reportStartupError(error) +> } +> ``` + +### Codable support + +The `LambdaHandler` and `LambdaWithBackgroundProcessingHandler` protocols abstract away encoding/decoding logic from the +conformers as they are generic over custom `Event` and `Output` types. We introduce two adapters `LambdaHandlerAdapter` +and `CodableLambdaAdapter` that implement the encoding/decoding logic and in turn allow the respective handlers to +conform to `StreamingLambdaHandler`. + +#### LambdaHandlerAdapter + +Any handler conforming to `LambdaHandler` can be conformed to `LambdaWithBackgroundProcessingHandler` through +`LambdaHandlerAdapter`. + +```swift +/// Wraps an underlying handler conforming to ``LambdaHandler`` +/// with ``LambdaWithBackgroundProcessingHandler``. +public struct LambdaHandlerAdapter< + Event: Decodable, + Output, + Handler: LambdaHandler +>: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { + let handler: Handler + + /// Register the concrete handler. + public init(handler: Handler) + + /// 1. Call the `self.handler.handle(...)` with `event` and `context`. + /// 2. Pass the generic `Output` object returned from `self.handler.handle(...)` to `outputWriter.write(_:)` + public func handle(_ event: Event, outputWriter: some LambdaResponseWriter, context: LambdaContext) async throws +} +``` + +#### LambdaCodableAdapter + +`LambdaCodableAdapter` accepts any generic underlying handler conforming to `LambdaWithBackgroundProcessingHandler`. It +also accepts _any_ encoder and decoder object conforming to the `LambdaEventDecoder` and `LambdaOutputEncoder` +protocols: + +##### LambdaEventDecoder and LambdaOutputEncoder protocols + +```swift +public protocol LambdaEventDecoder { + /// Decode the ByteBuffer representing the received event into the generic type Event + /// the handler will receive + func decode(_ type: Event.Type, from buffer: ByteBuffer) throws -> Event +} + +public protocol LambdaOutputEncoder { + /// Encode the generic type Output the handler has produced into a ByteBuffer + func encode(_ value: Output, into buffer: inout ByteBuffer) throws +} +``` + +We provide conformances for Foundation's `JSONDecoder` to `LambdaEventDecoder` and `JSONEncoder` to +`LambdaOutputEncoder`. + +`LambdaCodableAdapter` implements its `handle()` method by: + +1. Decoding the `ByteBuffer` event into the generic `Event` type. +2. Wrapping the `LambdaResponseStreamWriter` with a concrete `LambdaResponseWriter` such that calls to + `LambdaResponseWriter`s `write(_:)` are mapped to `LambdaResponseStreamWriter`s `writeAndFinish(_:)`. + - Note that the argument to `LambdaResponseWriter`s `write(_:)` is a generic `Output` object whereas + `LambdaResponseStreamWriter`s `writeAndFinish(_:)` requires a `ByteBuffer`. + - Therefore, the concrete implementation of `LambdaResponseWriter` also accepts an encoder. Its `write(_:)` function + first encodes the generic `Output` object and then passes it to the underlying `LambdaResponseStreamWriter`. +3. Passing the generic `Event` instance, the concrete `LambdaResponseWriter`, as well as the `LambdaContext` to the + underlying handler's `handle()` method. + +`LambdaCodableAdapter` can implement encoding/decoding for _any_ handler conforming to +`LambdaWithBackgroundProcessingHandler` if `Event` is `Decodable` and the `Output` is `Encodable` or `Void`, meaning +that the encoding/decoding stubs do not need to be implemented by the user. + +```swift +/// Wraps an underlying handler conforming to `LambdaWithBackgroundProcessingHandler` +/// with encoding/decoding logic +public struct LambdaCodableAdapter< + Handler: LambdaWithBackgroundProcessingHandler, + Event: Decodable, + Output, + Decoder: LambdaEventDecoder, + Encoder: LambdaOutputEncoder +>: StreamingLambdaHandler where Handler.Output == Output, Handler.Event == Event { + + /// Register the concrete handler, encoder, and decoder. + public init( + handler: Handler, + encoder: Encoder, + decoder: Decoder + ) where Output: Encodable + + /// For handler with a void output -- the user doesn't specify an encoder. + public init( + handler: Handler, + decoder: Decoder + ) where Output == Void, Encoder == VoidEncoder + + /// 1. Decode the invocation event using `self.decoder` + /// 2. Create a concrete `LambdaResponseWriter` that maps calls to `write(_:)` with the `responseWriter`s `writeAndFinish(_:)` + /// 2. Call the underlying `self.handler.handle()` method with the decoded event data, the concrete `LambdaResponseWriter`, + /// and the `LambdaContext`. + public mutating func handle( + _ request: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws +} +``` + +### Handler as a Closure + +To create a Lambda function using the current API, a user first has to create an object and conform it to one of the +handler protocols by implementing the initializer and the `handle(...)` function. Now that `LambdaRuntime` is public, +this verbosity can very easily be simplified. + +#### ClosureHandler + +This handler is generic over any `Event` type conforming to `Decodable` and any `Output` type conforming to `Encodable` +or `Void`. + +```swift +public struct ClosureHandler: LambdaHandler { + /// Initialize with a closure handler over generic Input and Output types + public init(body: @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable + /// Initialize with a closure handler over a generic Input type (Void Output). + public init(body: @escaping (Event, LambdaContext) async throws -> Void) where Output == Void + /// The business logic of the Lambda function. + public func handle(_ event: Event, context: LambdaContext) async throws -> Output +} +``` + +Given that `ClosureHandler` conforms to `LambdaHandler`: + +1. We can extend the `LambdaRuntime` initializer such that it accepts a closure as an argument. +2. Within the initializer, the closure handler is wrapped with `LambdaCodableAdapter`. + +```swift +extension LambdaRuntime { + /// Initialize a LambdaRuntime with a closure handler over generic Event and Output types. + /// This initializer bolts on encoding/decoding logic by wrapping the closure handler with + /// LambdaCodableAdapter. + public init( + body: @escaping (Event, LambdaContext) async throws -> Output + ) where Handler == LambdaCodableAdapter, Event, Output, JSONDecoder, JSONEncoder> + + /// Same as above but for handlers with a void output + public init( + body: @escaping (Event, LambdaContext) async throws -> Void + ) where Handler == LambdaCodableAdapter, Event, Void, JSONDecoder, VoidEncoder> +} +``` + +We can now significantly reduce the verbosity and leverage Swift's trailing closure syntax to cleanly create and run a +Lambda function, abstracting away the decoding and encoding logic from the user: + +```swift +/// The type the handler will use as input +struct Input: Decodable { + var message: String +} + +/// The type the handler will output +struct Greeting: Encodable { + var echoedMessage: String +} + +/// A simple Lambda function that echoes the input +let runtime = LambdaRuntime { (event: Input, context: LambdaContext) in + Greeting(echoedMessage: event.message) +} + +try await runtime.run() +``` + +We also add a `StreamingClosureHandler` conforming to `StreamingLambdaHandler` for use-cases where the user wants to +handle encoding/decoding themselves: + +```swift +public struct StreamingClosureHandler: StreamingLambdaHandler { + + public init( + body: @escaping sending (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> () + ) + + public func handle( + _ request: ByteBuffer, + responseWriter: LambdaResponseStreamWriter, + context: LambdaContext + ) async throws +} + +extension LambdaRuntime { + public init( + body: @escaping sending (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> () + ) +} +``` + +## Alternatives considered + +### [UInt8] instead of ByteBuffer + +We considered using `[UInt8]` instead of `ByteBuffer` in the base `LambdaHandler` API. We decided to use `ByteBuffer` +for two reasons. + +1. 99% of use-cases will use the JSON codable API and will not directly get in touch with ByteBuffer anyway. For those + users it does not matter if the base API uses `ByteBuffer` or `[UInt8]`. +2. The incoming and outgoing data must be in the `ByteBuffer` format anyway, as Lambda uses SwiftNIO under the hood and + SwiftNIO uses `ByteBuffer` in its APIs. By using `ByteBuffer` we can save a copies to and from `[UInt8]`. This will + reduce the invocation time for all users. +3. The base `LambdaHandler` API is most likely mainly being used by developers that want to integrate their web + framework with Lambda (examples: Vapor, Hummingbird, ...). Those developers will most likely prefer to get the data + in the `ByteBuffer` format anyway, as their lower level networking stack also depends on SwiftNIO. + +### Users create a LambdaResponse, that supports streaming instead of being passed a LambdaResponseStreamWriter + +Instead of passing the `LambdaResponseStreamWriter` in the invocation we considered a new type `LambdaResponse`, that +users must return in the `StreamingLambdaHandler`. + +Its API would look like this: + +```swift +/// A response returned from a ``LambdaHandler``. +/// The response can be empty, a single ByteBuffer or a response stream. +public struct LambdaResponse { + /// A writer to be used when creating a streamed response. + public struct Writer { + /// Writes data to the response stream + public func write(_ byteBuffer: ByteBuffer) async throws + /// Closes off the response stream + public func finish() async throws + /// Writes the `byteBuffer` to the response stream and subsequently closes the stream + public func writeAndFinish(_ byteBuffer: ByteBuffer) async throws + } + + /// Creates an empty lambda response + public init() + + /// Creates a LambdaResponse with a fixed ByteBuffer. + public init(_ byteBuffer: ByteBuffer) + + /// Creates a streamed lambda response. Use the ``Writer`` to send + /// response chunks on the stream. + public init(_ stream: @escaping sending (Writer) async throws -> ()) +} +``` + +The `StreamingLambdaHandler` would look like this: + +```swift +/// The base LambdaHandler protocol +public protocol StreamingLambdaHandler { + /// The business logic of the Lambda function + /// - Parameters: + /// - event: The invocation's input data + /// - context: The LambdaContext containing the invocation's metadata + /// - Returns: A LambdaResponse, that can be streamed + mutating func handle( + _ event: ByteBuffer, + context: LambdaContext + ) async throws -> LambdaResponse +} +``` + +There are pros and cons for the API that returns the `LambdaResponses` and there are pros and cons for the API that +receives a `LambdaResponseStreamWriter` as a parameter. + +Concerning following structured concurrency principles the approach that receives a `LambdaResponseStreamWriter` as a +parameter has benefits as the lifetime of the handle function is tied to the invocation runtime. The approach that +returns a `LambdaResponse` splits the invocation into two separate function calls. First the handle method is invoked, +second the `LambdaResponse` writer closure is invoked. This means that it is impossible to use Swift APIs that use +`with` style lifecycle management patterns from before creating the response until sending the full response stream off. +For example, users instrumenting their lambdas with Swift tracing likely can not use the `withSpan` API for the full +lifetime of the request, if they return a streamed response. + +However, if it comes to consistency with the larger Swift on server ecosystem, the API that returns a `LambdaResponse` +is likely the better choice. Hummingbird v2, OpenAPI and the new Swift gRPC v2 implementation all use this approach. +This might be due to the fact that writing middleware becomes easier, if a Response is explicitly returned. + +We decided to implement the approach in which a `LambdaResponseStreamWriter` is passed to the function, since the +approach in which a `LambdaResponse` is returned can trivially be built on top of it. This is not true vice versa. + +We welcome the discussion on this topic and are open to change our minds and API here. + +### Adding a function `addBackgroundTask(_ body: sending @escaping () async -> ())` in `LambdaContext` + +Initially we proposed an explicit `addBackgroundTask(_:)` function in `LambdaContext` that users could call from their +handler object to schedule a background task to be run after the result is reported to AWS. We received feedback that +this approach for supporting background tasks does not exhibit structured concurrency, as code could still be in +execution after leaving the scope of the `handle(...)` function. + +For handlers conforming to the `StreamingLambdaHandler`, `addBackgroundTask(_:)` was anyways unnecessary as background +work could be executed in a structured concurrency manner within the `handle(...)` function after the call to +`LambdaResponseStreamWriter.finish()`. + +For handlers conforming to the `LambdaHandler` protocol, we considered extending `LambdaHandler` with a +`performPostHandleWork(...)` function that will be called after the `handle` function by the library. Users wishing to +add background work can override this function in their `LambdaHandler` conforming object. + +```swift +public protocol LambdaHandler { + associatedtype Event + associatedtype Output + + func handle(_ event: Event, context: LambdaContext) async throws -> Output + + func performPostHandleWork(...) async throws -> Void +} + +extension LambdaHandler { + // User's can override this function if they wish to perform background work + // after returning a response from ``handle``. + func performPostHandleWork(...) async throws -> Void { + // nothing to do + } +} +``` + +Yet this poses difficulties when the user wishes to use any state created in the `handle(...)` function as part of the +background work. + +In general, the most common use-case for this library will be to implement simple Lambda functions that do not have +requirements for response streaming, nor to perform any background work after returning the output. To keep things easy +for the common use-case, and with Swift's principle of progressive disclosure of complexity in mind, we settled on three +handler protocols: + +1. `LambdaHandler`: Most common use-case. JSON-in, JSON-out. Does not support background work execution. An intuitive + `handle(event: Event, context: LambdaContext) -> Output` API that is simple to understand, i.e. users are not exposed + to the concept of sending their response through a writer. `LambdaHandler` can be very cleanly implemented and used + with `LambdaRuntime`, especially with `ClosureHandler`. +2. `LambdaWithBackgroundProcessingHandler`: If users wish to augment their `LambdaHandler` with the ability to run + background tasks, they can easily migrate. A user simply has to: + 1. Change the conformance to `LambdaWithBackgroundProcessingHandler`. + 2. Add an additional `outputWriter: some LambdaResponseWriter` argument to the `handle` function. + 3. Replace the `return ...` with `outputWriter.write(...)`. + 4. Implement any background work after `outputWriter.write(...)`. +3. `StreamingLambdaHandler`: This is the base handler protocol which is intended to be used directly only for advanced + use-cases. Users are provided the invocation event as a `ByteBuffer` and a `LambdaResponseStreamWriter` where the + computed result (as `ByteBuffer`) can either be streamed (with repeated calls to `write(_:)`) or sent all at once + (with a single call to `writeAndFinish(_:)`). After closing the `LambdaResponseStreamWriter`, any background work can + be implemented. + +### Making LambdaResponseStreamWriter and LambdaResponseWriter ~Copyable + +We initially proposed to make the `LambdaResponseStreamWriter` and `LambdaResponseWriter` protocols `~Copyable`, with +the functions that close the response having the `consuming` ownership keyword. This was so that the compiler could +enforce the restriction of not being able to interact with the writer after the response stream has closed. + +However, non-copyable types do not compose nicely and add complexity for users. Further, for the compiler to actually +enforce the `consuming` restrictions, user's have to explicitly mark the writer argument as `consuming` in the `handle` +function. + +Therefore, throwing appropriate errors to prevent abnormal interaction with the writers seems to be the simplest +approach. + +## A word about versioning + +We are aware that AWS Lambda Runtime has not reached a proper 1.0. We intend to keep the current implementation around +at 1.0-alpha. We don't want to change the current API without releasing a new major. We think there are lots of adopters +out there that depend on the API in v1. Because of this we intend to release the proposed API here as AWS Lambda Runtime +v2. diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/.shellcheckrc b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/.shellcheckrc new file mode 100644 index 00000000..95ac895f --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/.shellcheckrc @@ -0,0 +1 @@ +disable=all \ No newline at end of file diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-01-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-01-package-init.sh new file mode 100644 index 00000000..600d8880 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-01-package-init.sh @@ -0,0 +1,2 @@ +# Create a project directory +mkdir Palindrome && cd Palindrome diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-02-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-02-package-init.sh new file mode 100644 index 00000000..dfa52a08 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-02-package-init.sh @@ -0,0 +1,5 @@ +# Create a project directory +mkdir Palindrome && cd Palindrome + +# create a skeleton project +swift package init --type executable diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-03-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-03-package-init.sh new file mode 100644 index 00000000..54b1c96b --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-03-package-init.sh @@ -0,0 +1,8 @@ +# Create a project directory +mkdir Palindrome && cd Palindrome + +# create a skeleton project +swift package init --type executable + +# open Xcode in the current directory +xed . diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-04-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-04-package-init.sh new file mode 100644 index 00000000..a8fcbf5d --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-04-package-init.sh @@ -0,0 +1,11 @@ +# Create a project directory +mkdir Palindrome && cd Palindrome + +# create a skeleton project +swift package init --type executable + +# open Xcode in the current directory +xed . + +# alternatively, you may open VSCode +code . \ No newline at end of file diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-01-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-01-package.swift new file mode 100644 index 00000000..5f1d9b8d --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-01-package.swift @@ -0,0 +1,8 @@ +// swift-tools-version:6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Palindrome" +) diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-02-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-02-package.swift new file mode 100644 index 00000000..4fd09eba --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-02-package.swift @@ -0,0 +1,11 @@ +// swift-tools-version:6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Palindrome", + platforms: [ + .macOS(.v15) + ] +) diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift new file mode 100644 index 00000000..d1c46993 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift @@ -0,0 +1,14 @@ +// swift-tools-version:6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Palindrome", + platforms: [ + .macOS(.v15) + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ] +) diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift new file mode 100644 index 00000000..89a81437 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift @@ -0,0 +1,17 @@ +// swift-tools-version:6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Palindrome", + platforms: [ + .macOS(.v15) + ], + products: [ + .executable(name: "PalindromeLambda", targets: ["PalindromeLambda"]) + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ] +) diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift new file mode 100644 index 00000000..35801e7c --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift @@ -0,0 +1,26 @@ +// swift-tools-version:6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Palindrome", + platforms: [ + .macOS(.v15) + ], + products: [ + .executable(name: "PalindromeLambda", targets: ["PalindromeLambda"]) + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "PalindromeLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources" + ) + ] +) diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-01-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-01-main.swift new file mode 100644 index 00000000..72751f48 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-01-main.swift @@ -0,0 +1,4 @@ +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-02-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-02-main.swift new file mode 100644 index 00000000..5b0d8e2b --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-02-main.swift @@ -0,0 +1,11 @@ +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-03-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-03-main.swift new file mode 100644 index 00000000..da6de3de --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-03-main.swift @@ -0,0 +1,17 @@ +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-04-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-04-main.swift new file mode 100644 index 00000000..4b14c43b --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-04-main.swift @@ -0,0 +1,19 @@ +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-05-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-05-main.swift new file mode 100644 index 00000000..f308e04b --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-05-main.swift @@ -0,0 +1,25 @@ +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} + +// the lambda handler function +let runtime = LambdaRuntime { + (event: Request, context: LambdaContext) -> Response in + +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-06-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-06-main.swift new file mode 100644 index 00000000..0a382bc1 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-06-main.swift @@ -0,0 +1,32 @@ +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} + +// the lambda handler function +let runtime = LambdaRuntime { + (event: Request, context: LambdaContext) -> Response in + + // call the business function and return a response + let result = isPalindrome(event.text) + return Response( + text: event.text, + isPalindrome: result, + message: "Your text is \(result ? "a" : "not a") palindrome" + ) +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-07-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-07-main.swift new file mode 100644 index 00000000..c777c23b --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-07-main.swift @@ -0,0 +1,35 @@ +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct Request: Decodable { + let text: String +} + +// the data structure to represent the response parameter +struct Response: Encodable { + let text: String + let isPalindrome: Bool + let message: String +} + +// the business function +func isPalindrome(_ text: String) -> Bool { + let cleanedText = text.lowercased().filter { $0.isLetter } + return cleanedText == String(cleanedText.reversed()) +} + +// the lambda handler function +let runtime = LambdaRuntime { + (event: Request, context: LambdaContext) -> Response in + + // call the business function and return a response + let result = isPalindrome(event.text) + return Response( + text: event.text, + isPalindrome: result, + message: "Your text is \(result ? "a" : "not a") palindrome" + ) +} + +// start the runtime +try await runtime.run() diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh new file mode 100644 index 00000000..11c59fb9 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh @@ -0,0 +1,2 @@ +2025-01-02T14:59:29+0100 info LocalLambdaServer : [AWSLambdaRuntime] +LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-03-curl.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-03-curl.sh new file mode 100644 index 00000000..c8f991a2 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-03-curl.sh @@ -0,0 +1,5 @@ +curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"text": "Was it a car or a cat I saw?"}' \ + http://127.0.0.1:7000/invoke + diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-04-curl.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-04-curl.sh new file mode 100644 index 00000000..7928ac36 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-04-curl.sh @@ -0,0 +1,7 @@ +curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"text": "Was it a car or a cat I saw?"}' \ + http://127.0.0.1:7000/invoke + +{"message":"Your text is a palindrome","isPalindrome":true,"text":"Was it a car or a cat I saw?"} + diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-06-terminal.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-06-terminal.sh new file mode 100644 index 00000000..59fc671e --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-06-terminal.sh @@ -0,0 +1 @@ +swift run diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh new file mode 100644 index 00000000..1348bddc --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh @@ -0,0 +1,6 @@ +swift run + +Building for debugging... +[1/1] Write swift-version--58304C5D6DBC2206.txt +Build of product 'PalindromeLambda' complete! (0.11s) +2025-01-02T15:12:49+0100 info LocalLambdaServer : [AWSLambdaRuntime] LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh new file mode 100644 index 00000000..1f5ca9d8 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh @@ -0,0 +1 @@ +swift package archive --allow-network-connections docker diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh new file mode 100644 index 00000000..c760c981 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh @@ -0,0 +1,20 @@ +swift package archive --allow-network-connections docker + +------------------------------------------------------------------------- +building "palindrome" in docker +------------------------------------------------------------------------- +updating "swift:amazonlinux2" docker image + amazonlinux2: Pulling from library/swift + Digest: sha256:df06a50f70e2e87f237bd904d2fc48195742ebda9f40b4a821c4d39766434009 +Status: Image is up to date for swift:amazonlinux2 + docker.io/library/swift:amazonlinux2 +building "PalindromeLambda" + [0/1] Planning build + Building for production... + [0/2] Write swift-version-24593BA9C3E375BF.txt + Build of product 'PalindromeLambda' complete! (1.91s) +------------------------------------------------------------------------- +archiving "PalindromeLambda" +------------------------------------------------------------------------- +1 archive created + * PalindromeLambda at /Users/sst/Palindrome/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/PalindromeLambda/PalindromeLambda.zip diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh new file mode 100644 index 00000000..c347694e --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh @@ -0,0 +1,22 @@ +swift package archive --allow-network-connections docker + +------------------------------------------------------------------------- +building "palindrome" in docker +------------------------------------------------------------------------- +updating "swift:amazonlinux2" docker image + amazonlinux2: Pulling from library/swift + Digest: sha256:df06a50f70e2e87f237bd904d2fc48195742ebda9f40b4a821c4d39766434009 +Status: Image is up to date for swift:amazonlinux2 + docker.io/library/swift:amazonlinux2 +building "PalindromeLambda" + [0/1] Planning build + Building for production... + [0/2] Write swift-version-24593BA9C3E375BF.txt + Build of product 'PalindromeLambda' complete! (1.91s) +------------------------------------------------------------------------- +archiving "PalindromeLambda" +------------------------------------------------------------------------- +1 archive created + * PalindromeLambda at /Users/sst/Palindrome/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/PalindromeLambda/PalindromeLambda.zip + +cp .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/PalindromeLambda/PalindromeLambda.zip ~/Desktop diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-01-aws-cli.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-01-aws-cli.sh new file mode 100644 index 00000000..7a33b49d --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-01-aws-cli.sh @@ -0,0 +1 @@ +aws --version diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh new file mode 100644 index 00000000..6c9513cd --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh @@ -0,0 +1,5 @@ +# --region the AWS Region to send the command +# --function-name the name of your function +# --cli-binary-format tells the cli to use raw data as input (default is base64) +# --payload the payload to pass to your function code +# result.json the name of the file to store the response from the function diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke.sh new file mode 100644 index 00000000..00c96fcb --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke.sh @@ -0,0 +1,13 @@ +# --region the AWS Region to send the command +# --function-name the name of your function +# --cli-binary-format tells the cli to use raw data as input (default is base64) +# --payload the payload to pass to your function code +# result.json the name of the file to store the response from the function + +aws lambda invoke \ + --region us-west-2 \ + --function-name PalindromeLambda \ + --cli-binary-format raw-in-base64-out \ + --payload '{"text": "Was it a car or a cat I saw?"}' \ + result.json + diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-03-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-03-lambda-invoke.sh new file mode 100644 index 00000000..032c722d --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-03-lambda-invoke.sh @@ -0,0 +1,17 @@ +# --region the AWS Region to send the command +# --function-name the name of your function +# --cli-binary-format tells the cli to use raw data as input (default is base64) +# --payload the payload to pass to your function code +# result.json the name of the file to store the response from the function + +aws lambda invoke \ + --region us-west-2 \ + --function-name PalindromeLambda \ + --cli-binary-format raw-in-base64-out \ + --payload '{"text": "Was it a car or a cat I saw?"}' \ + result.json + +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-04-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-04-lambda-invoke.sh new file mode 100644 index 00000000..52b17573 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-04-lambda-invoke.sh @@ -0,0 +1,19 @@ +# --region the AWS Region to send the command +# --function-name the name of your function +# --cli-binary-format tells the cli to use raw data as input (default is base64) +# --payload the payload to pass to your function code +# result.json the name of the file to store the response from the function + +aws lambda invoke \ + --region us-west-2 \ + --function-name PalindromeLambda \ + --cli-binary-format raw-in-base64-out \ + --payload '{"text": "Was it a car or a cat I saw?"}' \ + result.json + +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} + +cat result.json diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-05-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-05-lambda-invoke.sh new file mode 100644 index 00000000..bb4889b6 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-05-lambda-invoke.sh @@ -0,0 +1,21 @@ +# --region the AWS Region to send the command +# --function-name the name of your function +# --cli-binary-format tells the cli to use raw data as input (default is base64) +# --payload the payload to pass to your function code +# result.json the name of the file to store the response from the function + +aws lambda invoke \ + --region us-west-2 \ + --function-name PalindromeLambda \ + --cli-binary-format raw-in-base64-out \ + --payload '{"text": "Was it a car or a cat I saw?"}' \ + result.json + +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} + +cat result.json +{"text":"Was it a car or a cat I saw?","isPalindrome":true,"message":"Your text is a palindrome"} + diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-10-regions.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-10-regions.png new file mode 100644 index 00000000..afe97b53 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-10-regions.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-20-dashboard.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-20-dashboard.png new file mode 100644 index 00000000..b48ea591 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-20-dashboard.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-30-create-function.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-30-create-function.png new file mode 100644 index 00000000..e3bd131f Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-30-create-function.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-40-select-zip-file.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-40-select-zip-file.png new file mode 100644 index 00000000..da4ff924 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-40-select-zip-file.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-50-upload-zip.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-50-upload-zip.png new file mode 100644 index 00000000..89eedab9 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-50-upload-zip.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-60-prepare-test-event.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-60-prepare-test-event.png new file mode 100644 index 00000000..2f7b15ed Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-60-prepare-test-event.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-70-view-invocation-response.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-70-view-invocation-response.png new file mode 100644 index 00000000..f4f712bc Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-70-view-invocation-response.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-function.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-function.png new file mode 100644 index 00000000..f205f47b Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-function.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-role.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-role.png new file mode 100644 index 00000000..2a335d5d Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-role.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/00-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/00-swift_on_lambda.png new file mode 100644 index 00000000..dcbeb6e0 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/00-swift_on_lambda.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/01-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/01-swift_on_lambda.png new file mode 100644 index 00000000..90c5ba74 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/01-swift_on_lambda.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-terminal-package-init.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-terminal-package-init.png new file mode 100644 index 00000000..d12e5f9d Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-terminal-package-init.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode@2x.png new file mode 100644 index 00000000..96347ebf Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode~dark@2x.png new file mode 100644 index 00000000..bdb286e6 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-02-swift-package-manager.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-02-swift-package-manager.png new file mode 100644 index 00000000..62f41427 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-02-swift-package-manager.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-03-swift-code-xcode.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-03-swift-code-xcode.png new file mode 100644 index 00000000..90c5ba74 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-03-swift-code-xcode.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run@2x.png new file mode 100644 index 00000000..7dfc7f64 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png new file mode 100644 index 00000000..3315a90c Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-test-locally.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-test-locally.png new file mode 100644 index 00000000..dba46581 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-test-locally.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-swift_on_lambda.png new file mode 100644 index 00000000..ac9d2006 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-swift_on_lambda.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-01-docker-started@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-01-docker-started@2x.png new file mode 100644 index 00000000..e65aba1f Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-01-docker-started@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-compile-for-linux.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-compile-for-linux.png new file mode 100644 index 00000000..16ec0b5d Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-compile-for-linux.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-01-console-login@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-01-console-login@2x.png new file mode 100644 index 00000000..e51f6d81 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-01-console-login@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-02-console-login@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-02-console-login@2x.png new file mode 100644 index 00000000..e4cf04b5 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-02-console-login@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-03-select-region@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-03-select-region@2x.png new file mode 100644 index 00000000..8460945b Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-03-select-region@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda@2x.png new file mode 100644 index 00000000..c79c495f Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png new file mode 100644 index 00000000..2abd5056 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function@2x.png new file mode 100644 index 00000000..1c123d5a Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png new file mode 100644 index 00000000..c23355a9 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function@2x.png new file mode 100644 index 00000000..b8048d19 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png new file mode 100644 index 00000000..2fbdf277 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip@2x.png new file mode 100644 index 00000000..0e4ba22c Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png new file mode 100644 index 00000000..cf73d7f9 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip@2x.png new file mode 100644 index 00000000..ef3d8160 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png new file mode 100644 index 00000000..4273f810 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda@2x.png new file mode 100644 index 00000000..e9e7a309 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png new file mode 100644 index 00000000..61cf6147 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png new file mode 100644 index 00000000..e962e7c2 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png new file mode 100644 index 00000000..54ca5198 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-create-lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-create-lambda.png new file mode 100644 index 00000000..70152ba5 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-create-lambda.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-03-invoke-lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-03-invoke-lambda.png new file mode 100644 index 00000000..71167f24 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-03-invoke-lambda.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-swift_on_lambda.png new file mode 100644 index 00000000..386bdef7 Binary files /dev/null and b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-swift_on_lambda.png differ diff --git a/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md new file mode 100644 index 00000000..bf2f6db4 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md @@ -0,0 +1,147 @@ +# Getting Started Quickly + +Learn how to create your first project in 3 minutes. + +Follow these instructions to get a high-level overview of the steps to write, test, and deploy your first Lambda function written in Swift. + +For a detailed step-by-step instruction, follow the tutorial instead. + + + +For the impatient, keep reading. + +## High-level instructions + +Follow these 6 steps to write, test, and deploy a Lambda function in Swift. + +1. Create a Swift project for an executable target + +```sh +swift package init --type executable +``` + +2. Add dependencies on `AWSLambdaRuntime` library + +```swift +// swift-tools-version:6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "YourProjetName", + platforms: [ + .macOS(.v15), + ], + products: [ + .executable(name: "MyFirstLambdaFunction", targets: ["MyFirstLambdaFunction"]), + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "main"), + ], + targets: [ + .executableTarget( + name: "MyFirstLambdaFunction", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ], + path: "Sources" + ), + ] +) +``` + +3. Write your function code. + +Create an instance of `LambdaRuntime` and pass a function as a closure. The function has this signature: `(_: Event, context: LambdaContext) async throws -> Output` (as defined in the `LambdaHandler` protocol). `Event` must be `Decodable`. `Output` must be `Encodable`. + +If your Lambda function is invoked by another AWS service, use the `AWSLambdaEvent` library at [https://github.com/swift-server/swift-aws-lambda-events](https://github.com/swift-server/swift-aws-lambda-events) to represent the input event. + +Finally, call `runtime.run()` to start the event loop. + +```swift +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// the Lambda runtime +let runtime = LambdaRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} + +// start the loop +try await runtime.run() +``` + +4. Test your code locally + +```sh +swift run # this starts a local server on port 7000 + +# Switch to another Terminal tab + +curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"name": "Seb", "age": 50}' \ + http://localhost:7000/invoke + +{"greetings":"Hello Seb. You look younger than your age."} +``` + +5. Build and package your code for AWS Lambda + +AWS Lambda runtime runs on Amazon Linux. You must compile your code for Amazon Linux. + +> Be sure to have [Docker](https://docs.docker.com/desktop/install/mac-install/) installed for this step. + +```sh +swift package --allow-network-connections docker archive + +------------------------------------------------------------------------- +building "MyFirstLambdaFunction" in docker +------------------------------------------------------------------------- +updating "swift:amazonlinux2" docker image + amazonlinux2: Pulling from library/swift + Digest: sha256:5b0cbe56e35210fa90365ba3a4db9cd2b284a5b74d959fc1ee56a13e9c35b378 + Status: Image is up to date for swift:amazonlinux2 + docker.io/library/swift:amazonlinux2 +building "MyFirstLambdaFunction" + Building for production... +... +------------------------------------------------------------------------- +archiving "MyFirstLambdaFunction" +------------------------------------------------------------------------- +1 archive created + * MyFirstLambdaFunction at /Users/YourUserName/MyFirstLambdaFunction/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyFirstLambdaFunction/MyFirstLambdaFunction.zip + + +cp .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyFirstLambdaFunction/MyFirstLambdaFunction.zip ~/Desktop +``` + +6. Deploy on AWS Lambda + +> Be sure [to have an AWS Account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) to follow these steps. + +- Connect to the [AWS Console](https://console.aws.amazon.com) +- Navigate to Lambda +- Create a function +- Select **Provide your own bootstrap on Amazon Linux 2** as **Runtime** +- Select an **Architecture** that matches the one of the machine where you build the code. Select **x86_64** when you build on Intel-based Macs or **arm64** for Apple Silicon-based Macs. +- Upload the ZIP create during step 5 +- Select the **Test** tab, enter a test event such as `{"name": "Seb", "age": 50}` and select **Test** + +If the test succeeds, you will see the result: `{"greetings":"Hello Seb. You look younger than your age."}`. + + +Congratulations 🎉! You just wrote, test, build, and deployed a Lambda function written in Swift. diff --git a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/01-overview.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/01-overview.tutorial new file mode 100644 index 00000000..5ca896db --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/01-overview.tutorial @@ -0,0 +1,18 @@ +@Article(time: 1) { + + @Intro(title: "Overview") + +This tutorial helps you to get started writing your first AWS Lambda function in Swift. You will use the `AWSLambdaRuntime` package to write code that can be deployed on AWS Lambda. + +You will learn three things: + +1. How to implement simple Lambda function and test it locally +2. How to build a Lambda function for deployment on AWS +3. How to deploy your Lambda function on AWS and invoke it + +It's a beginners' tutorial. The business logic of the function is very simple, it computes the square of a number passed as input parameter. This simplicity allows you to focus on the project setup and the deployment. You will deploy your code using the AWS Management Console. It is the easiest way to get started with AWS Lambda. + +If you have any questions or recommendations, please [leave your feedback on GitHub](https://github.com/swift-server/swift-aws-lambda-runtime/issues) so that you can get your question answered and this tutorial can be improved. + +*The following instructions were recorded on January 2025 and the AWS Management Console may have changed since then. Feel free to raise an issue if you spot differences with our screenshots* +} \ No newline at end of file diff --git a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/02-what-is-lambda.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/02-what-is-lambda.tutorial new file mode 100644 index 00000000..da86703b --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/02-what-is-lambda.tutorial @@ -0,0 +1,28 @@ +@Article(time: 3) { + + @Intro(title: "What is AWS Lambda") + + + AWS Lambda is a compute service that lets you run code without provisioning or managing servers. Lambda runs your code on a high-availability compute infrastructure and performs all of the administration of the compute resources, including server and operating system maintenance, capacity provisioning and automatic scaling, and logging. With Lambda, you can run code for virtually any type of application or backend service. All you need to do is supply your code in [one of the languages that Lambda supports](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html), including Swift. + + You organize your code into [Lambda functions](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-concepts.html#gettingstarted-concepts-function). Lambda runs your function only when needed and scales automatically, from a few requests per day to thousands per second. You pay only for the compute time that you consume—there is no charge when your code is not running. For more information, see [AWS Lambda Pricing](http://aws.amazon.com/lambda/pricing/). + + Lambda is a highly available service. For more information, see the [AWS Lambda Service Level Agreement](https://aws.amazon.com/lambda/sla/). + + ### When to use AWS Lambda + + Lambda is an ideal compute service for many application scenarios, as long as you can run your application code using the Lambda [standard runtime environment](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html) and within the resources that Lambda provides. For example, you can use Lambda for: + + - Mobile backends: Build backends using Lambda and Amazon API Gateway to authenticate and process API requests. Use AWS Amplify to easily integrate your backend with your iOS, Android, Web, and React Native front ends. + + - Web applications: Combine Lambda with other AWS services to build powerful web applications that automatically scale up and down and run in a highly available configuration across multiple data centers. + + - File processing: Use Amazon Simple Storage Service (Amazon S3) to trigger Lambda data processing in real time after an upload. + + - Stream processing: Use Lambda and Amazon Kinesis to process real-time streaming data for application activity tracking, transaction order processing, click stream analysis, data cleansing, log filtering, indexing, social media analysis, Internet of Things (IoT) device data telemetry, and metering. + + - IoT backends: Build serverless backends using Lambda to handle web, mobile, IoT, and third-party API requests. + + When using Lambda, you are responsible only for your code. Lambda manages the compute fleet that offers a balance of memory, CPU, network, and other resources to run your code. Because Lambda manages these resources, you cannot log in to compute instances or customize the operating system on [provided runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html). Lambda performs operational and administrative activities on your behalf, including managing capacity, monitoring, and logging your Lambda functions. + +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-prerequisites.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-prerequisites.tutorial new file mode 100644 index 00000000..48b1b7c3 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-prerequisites.tutorial @@ -0,0 +1,18 @@ +@Article(time: 3) { + + @Intro(title: "Prerequisites") + + +This tutorial has been tested on macOS, since this is what most Swift developers work on. It should also work on Linux. + +To follow the instructions provided by this tutorial you'll need to meet a couple of prerequisites. + +- We expect you to have a basic understanding of the Swift programming language and be somewhat familiar with the terminal/console. You can follow this [guided tour to have an overview of Swift](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/guidedtour). [You can follow this short tutorial to learn about macOS commands and the Terminal](https://support.apple.com/guide/terminal/open-or-quit-terminal-apd5265185d-f365-44cb-8b09-71a064a42125/mac). + +- You'll need to have a text editor and the Swift toolchain installed. On macOS, you can use [Xcode](https://developer.apple.com/xcode/). On Linux, you must install [the Swift runtime and toolchain](https://www.swift.org/download/). On macOS and Linux, you may also use [VSCode](https://code.visualstudio.com/download) and the [Swift extension for VSCode](https://www.swift.org/blog/vscode-extension/). + +- To compile your Lambda function to run on AWS Lambda, you will need to install [Docker](https://docs.docker.com/desktop/install/mac-install/). This tutorial doesn't go into much detail what Docker is and what it does. Just remember that AWS Lambda functions run on Linux. Therefore, you have to compile your Swift Lambda function code for Linux. Docker allows you to start a Linux virtual machine where you will compile your Swift code before to deploy it on AWS Lambda. This tutorial contains all the commands you will have to type to interact with Docker. Follow [the instructions provided by Docker](https://docs.docker.com/desktop/install/mac-install/) to install Docker on your machine. + +- To deploy your Lambda function on AWS you need an [Amazon Web Service (AWS)](https://aws.amazon.com/what-is-aws/) account. [Follow these instructions to create an AWS account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html). + +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial new file mode 100644 index 00000000..d062e224 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial @@ -0,0 +1,224 @@ +@Tutorial(time: 15) { + @Intro(title: "Write your first Lambda function") { + Learn how to create your project, add dependencies, and create and test your first Lambda function in Swift. + + In this example, we will create a Lambda function that receives a text and checks if this text is a palindrome or not. + + A palindrome is a word or phrase that reads the same forward and backward. + } + + @Section(title: "Initialize a new project") { + @ContentAndMedia() { + Create a new swift project and open Xcode. + @Image(source: "03-01-terminal-package-init", alt: "A Terminal with package init command") + } + + @Steps { + + Start in your development folder. + + @Step { + Open a Terminal and create a directory for your Lambda function. + @Code(name: "Commands to type in the Terminal", file: 03-01-01-package-init.sh) + } + + @Step { + Initialize a new Swift package for an executable target. + @Code(name: "Commands to type in the Terminal", file: 03-01-02-package-init.sh) + } + + @Step { + Open Xcode in this newly created directory. + @Code(name: "Commands to type in the Terminal", file: 03-01-03-package-init.sh) + } + + @Step { + Alternatively, if you use VSCode, use the `code` command to open VSCode in your project repository. + @Code(name: "Commands to type in the Terminal", file: 03-01-04-package-init.sh) + } + + @Step { + In your development environment, expand the project if necessary and open the file `Package.swift`. + + If you are an iOS developer, you might wonder what is a `Package.swift`. In simple terms, your `Package.swift` defines the dependencies your code has and what products (libraries and/or executables) your code offers. + @Image(source: 03-01-xcode.png, alt: "project open in Xcode") + } + } + } + + @Section(title: "Add the project dependencies") { + @ContentAndMedia() { + Prepare `Package.swift` to define the project targets and dependencies. + @Image(source: "03-02-swift-package-manager.png", alt: "Swift Package Manager icon as a box") + } + @Steps { + @Step { + In the Xcode editor, replace the content of `Package.swift` with the file on the right side of the screen. + + It defines a package for a project named `Palindrome`. The package name only matters when you build a library that is used by other Swift packages. + + > Comments are important here, do not skip them. They define the minimum version of Swift to use. + @Code(name: "Package.swift", file: 03-02-01-package.swift) + } + @Step { + Add the `platform` section. + + It defines on which Apple platforms the code can be executed. Since Lambda functions are supposed to be run on Linux servers with Amazon Linux 2, it is reasonable to make them run only on macOS, for debugging for example. It does not make sense to run this code on iOS, iPadOS, tvOS, and watchOS. + @Code(name: "Package.swift", file: 03-02-02-package.swift) + } + @Step { + Add the `dependencies` section. + + It defines what external libraries your code depends on. To run code within AWS Lambda you'll need a runtime that handles the communication with the [Lambda Runtime Interface](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). This is what the `AWSLambdaRuntime` is for. You import it by specifying its GitHub url: `https://github.com/swift-server/swift-aws-lambda-runtime.git`. + + @Code(name: "Package.swift", file: 03-02-03-package.swift) + } + @Step { + Add the `target` section. + + In the `targets` section you specify your own targets. They are pretty comparable to targets you specify within an Xcode project (that's probably why they share the name 😎). In our example we only want to create an executable that is called `PalindromeLambda`. An executable must have an entrypoint. This can be either a `main.swift` or an object that is marked with `@main`. For Lambda we will use the `@main` approach. + + @Code(name: "Package.swift", file: 03-02-04-package.swift) + } + @Step { + Add the `product` section. + + To advertise our `executableTarget` as a product of our package, we add it to the `products` section. + @Code(name: "Package.swift", file: 03-02-05-package.swift) + } + + } + } + + @Section(title: "Write the function code") { + @ContentAndMedia() { + Now that our project structure is ready, let's write the code of your Lambda function. Perform the following steps in Xcode or another IDE of your choice. + + @Image(source: 03-03-swift-code-xcode, alt: "Swift code in Xcode") + } + + @Steps { + + @Step { + Open the `main.swift` file, remove the code generated and write the code to represent the request sent to your Lambda function. + + Input parameters must conform to the `Decodable` protocol. This ensures that your Lambda function accepts any JSON input. + + > When your function is triggered by another AWS service, we modeled most of the input and output data format for you. You can add the dependency on [https://github.com/swift-server/swift-aws-lambda-events](https://github.com/swift-server/swift-aws-lambda-events) and import `AWSLambdaEvents` in your code. + + @Code(name: "main.swift", file: 03-03-01-main.swift) + } + + @Step { + Write the code to represent the response returned by your Lambda function. + + Output parameters must conform to the `Encodable` protocol. This ensures that your Lambda function returns a valid JSON output. Your function might also return `Void` if it does not return any value. + + > You can also write function that stream a response back to the caller. This is useful when you have a large amount of data to return. See the [Lambda Streaming example](https://github.com/swift-server/swift-aws-lambda-runtime/tree/main/Examples/Streaming) for more information. + + @Code(name: "main.swift", file: 03-03-02-main.swift) + } + + @Step { + Write your business logic. + + In real life project, this will be the most complex part of your code. It will live in spearate files or libraries. For this example, we will keep it simple and just return `true` if a `String` is a palindrome. + + @Code(name: "main.swift", file: 03-03-03-main.swift) + } + + @Step { + Add an `import` statement to import the `AWSLambdaRuntime` library. + + @Code(name: "main.swift", file: 03-03-04-main.swift) + } + + @Step { + Create a `LambdaRuntime` struct and add a handler function that will be called by the Lambda runtime. + + This function is passed as a closure to the initializer of the `LambdaRuntime` struct. It accepts two parameters: the input event and the context. The input event is the JSON payload sent to your Lambda function. The context provides information about the function, such as the function name, memory limit, and log group name. The function returns the output event, which is the JSON payload returned by your Lambda function or Void if your function does not return any value. + + @Code(name: "main.swift", file: 03-03-05-main.swift) + } + + @Step { + Add the business logic to the handler function and return the response. + + In this example, we call the `isPalindrome(_:)` function to check if the input string is a palindrome. Then, we create a response with the result of the check. + + @Code(name: "main.swift", file: 03-03-06-main.swift) + } + + @Step { + Start the runtime by calling the `run()` function. + + This function starts the Lambda runtime and listens for incoming requests. When a request is received, it calls the handler function with the input event and context. The handler function processes the request and returns the output event. The runtime sends the output event back to the caller. This function might `throw` an error if the runtime fails to process an event or if the handler function throws an error. This function is asynchronous and does not return until the runtime is stopped. + + @Code(name: "main.swift", file: 03-03-07-main.swift) + } + + } + } + + @Section(title: "Test Your Code Locally") { + @ContentAndMedia() { + Before to deploy your Lambda to AWS, you want to ensure that it works on your local machine. + + The `AWSLambdaRuntime` embeds a simple web server you can start and use to send your requests to your Lambda function. + + @Image(source: 03-04-test-locally.png, alt: "Icons of succeeded and failed tests") + } + + @Steps { + + The embedded web server starts only when compiling in `DEBUG` mode and when the code is not run inside a Lambda function environment. You will start the test server directly from Xcode. + + @Step { + Compile and run your project. Click on the `Run` button (▶️) in Xcode. + + @Image(source: 03-04-01-compile-run.png, alt: "Compile and run the project") + } + + @Step { + Verify the server is correctlys started. You should see the following output in the console. + + @Code(name: "Console output", file: 03-04-02-console-output.sh) + } + + @Step { + Now that the local server started, open a Terminal and use `curl` or any other HTTP client to POST your input payload to `127.0.0.1:7000`. + + @Code(name: "curl command in a terminal", file: 03-04-03-curl.sh) + } + + @Step { + When you pass `'{"text": "Was it a car or a cat I saw?"}'`, you should receive the response `{"message":"Your text is a palindrome","isPalindrome":true,"text":"Was it a car or a cat I saw?"}` + + > Do not forget to stop the running scheme in Xcode (⏹️) when you're done. + + @Code(name: "curl command in a terminal", file: 03-04-04-curl.sh) + } + + Alternatively, you can use the command line from the Terminal. + + @Step { + Use the command `swift run` to start the local embedded web server. + @Code(name: "curl command in a terminal", file: 03-04-06-terminal.sh) + } + + @Step { + You should see the following output in the console. + @Code(name: "curl command in a terminal", file: 03-04-07-terminal.sh) + } + + @Step { + Now that the local server started, open a second tab in the Terminal and use `curl` or any other HTTP client to POST your input payload to `127.0.0.1:7000`. + + > Do not forget to stop the local server with `CTRL-C` when you're done. + @Code(name: "curl command in a terminal", file: 03-04-03-curl.sh) + } + + } + } + +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial new file mode 100644 index 00000000..70213532 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial @@ -0,0 +1,178 @@ +@Tutorial(time: 15) { + @Intro(title: "Deploy your function to AWS Lambda") { + Learn how to package your code for AWS Lambda and to deploy it using the AWS Management Console + } + + @Section(title: "Compile for Amazon Linux") { + @ContentAndMedia() { + Learn how to compile your code to run it on Amazon Linux. + + @Image(source: 04-01-compile-for-linux, alt: "Compile for Amazon Linux") + } + + @Steps { + + AWS Lambda runs on top of [Amazon Linux 2](https://aws.amazon.com/amazon-linux-2/). You must therefore compile your code for Linux. The AWS Lambda Runtime for Swift uses Docker to do so. Once the code is compiled, it must be assembled in a ZIP file before being deployed in the cloud. + The AWS Lambda Runtime for Swift provides a [Swift Package Manager plugin](https://github.com/apple/swift-package-manager/blob/main/Documentation/Plugins.md) to compile and zip your Lambda function in one simple step. + + @Step { + Be sure Docker is started on your machine. On macOS, you can check the Docker icon in the menu bar. + + @Image(source: 04-01-01-docker-started.png, alt: "Docker icon and menu on macOS") + } + + @Step { + In a terminal, invoke the `archive` command to build and zip your Lambda function. + + @Code(name: "Commands in a Terminal", file: 04-01-02-plugin-archive.sh) + } + + @Step { + The plugin starts a Docker container running Amazon Linux 2 and compile your Lambda function code. It then creates a zip file. When everything goes well, you should see an output similar to this one. + + @Code(name: "Commands in a Terminal", file: 04-01-03-plugin-archive.sh) + } + + @Step { + Copy the generated zip files to your Desktop or Download directory for easy access. I choose the Desktop. + + @Code(name: "Commands in a Terminal", file: 04-01-04-plugin-archive.sh) + } + } + } + + @Section(title: "Create an Lambda Function") { + @ContentAndMedia() { + Learn how to create a Lambda function using the AWS Management Console and to deploy your zip file + + @Image(source: 04-02-create-lambda.png, alt: "Create a Lambda function") + } + + @Steps { + + You will now deploy your code to AWS Lambda. To complete the remaining steps in this tutorial, you must have an AWS Account. You can [create an AWS Account by following these instructions](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html). + + @Step { + Open a web browser and navigate to [https://console.aws.amazon.com](https://console.aws.amazon.com). + + @Image(source: 04-02-01-console-login.png, alt: "AWS console login") + } + + @Step { + If you have an IAM user ID and password, select **IAM User**, otherwise proceed by entering your **Root user** email address and password. For this tutorial, I sign in using my IAM User ID. + + @Image(source: 04-02-02-console-login.png, alt: "AWS console login with IAM user") + } + + @Step { + On the top right side of the console, select the AWS Region where you want to deploy your Lambda function. + + You typically choose a Region close to your customers to minimize the network latency. For this demo, I selected **Oregon (us-west-2)** + + > AWS has multiple Regions across all continents. You can learn more about [AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/) here. + + @Image(source: 04-02-03-select-region.png, alt: "AWS console - Select Region") + } + + @Step { + On the top left side of the console, select the Lambda service to navigate to the Lambda section of the console. + + @Image(source: 04-02-04-select-lambda.png, alt: "AWS console - Select Lambda") + } + + @Step { + On the top right side of the Lambda page, select **Create function**. + + @Image(source: 04-02-05-create-function.png, alt: "Create function") + } + + @Step { + Enter a **Function name**. I choose `PalindromeLambda`. Select `Provide your own bootstrap on Amazon Linux 2` as **Runtime**. And select `arm64` as **Architecture** when you build on a Mac with Apple Silicon. Leave all other parameter as default, and select **Create function** on the bottom right part. + + > The runtime architecture for Lambda (`arm64` or `x86_64`) must match the one of the machine where you compiled the code. When you compiled on an Intel-based Mac, use `x86_64`. When compiling on an Apple Silicon-based Mac select `arm64`. + + @Image(source: 04-02-06-create-function.png, alt: "Create function details") + } + + @Step { + On the next screen, select **.zip file** from the **Upload from** selection box on the middle right part of the screen. + + @Image(source: 04-02-07-upload-zip.png, alt: "Upload ") + } + + @Step { + Select the zip file that was generated earlier and select **Save**. + + @Image(source: 04-02-08-upload-zip.png, alt: "Create function") + } + + @Step { + To verify everything works well, create a test event and invoke the function from the **Test** tab in the console. Enter `MyTestEvent` as **Event name**. Enter `{"text": "Was it a car or a cat I saw?"}` as **Event JSON**. Then, select **Test**. + + @Image(source: 04-02-09-test-lambda.png, alt: "Create function") + } + + @Step { + When the invocation succeeds, you can see the execution details and the result: `{ "message": "Your text is a palindrome","isPalindrome": true, "text": "Was it a car or a cat I saw?"}`. + + > The execution result also shares the execution duration, the actual memory consumed and the logs generated by the function. These are important data to help you to fine-tune your function. Providing the function with more memory will also give it more compute power, resulting in lower execution time. + + @Image(source: 04-02-10-test-lambda-result.png, alt: "Create function") + } + + } + } + + @Section(title: "Invoke your Lambda function") { + @ContentAndMedia() { + Learn how to invoke the Lambda function using the AWS Lambda API and the AWS command line. + + @Image(source: 04-03-invoke-lambda.png, alt: "Invoke a Lambda function") + } + + @Steps { + + Typically you will [associate an URL to your Lambda function](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html), or [you will expose the Lambda function through a REST API](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-with-lambda-integration.html). You might use the [Serverless Application Model (SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) to do so. We'll leave tasks for another tutorial. + + In the remaining section of this tutorial, you will learn how to invoke your Lambda function from the AWS command-line tool. + + @Step { + First, check that you have the `aws` command line tool installed and configured. + + > You can install the `aws` CLI with the command `brew awscli`. You need to configure the `aws` CLI with your AWS credentials. You may use the command `aws configure` to configure the CLI. [The AWS CLI documentation has more details](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html). + + @Code(name: "Command to type in the Terminal", file: 04-03-01-aws-cli.sh) + } + + @Step { + Enter the following command to invoke your Lambda function. + + @Code(name: "Command to type in the Terminal", file: 04-03-02-lambda-invoke.sh, previousFile: 04-03-02-lambda-invoke-hidden.sh) + + } + + @Step { + The command returns with the invocation status. + + @Code(name: "Command to type in the Terminal", file: 04-03-03-lambda-invoke.sh) + + } + + @Step { + Type `cat result.json` to see the value returned by your function. + + @Code(name: "Command to type in the Terminal", file: 04-03-04-lambda-invoke.sh) + + } + @Step { + When everything goes well, you will see `{"text":"Was it a car or a cat I saw?","isPalindrome":true,"message":"Your text is a palindrome"}`. + + Congratulation 🎉 ! + + @Code(name: "Command to type in the Terminal", file: 04-03-05-lambda-invoke.sh) + + } + } + } + +} diff --git a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/table-of-content.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/table-of-content.tutorial new file mode 100644 index 00000000..aa1e26b3 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/table-of-content.tutorial @@ -0,0 +1,28 @@ +@Tutorials(name: "AWS Lambda functions in Swift") { + @Intro(title: "Your First Lambda Function Written in Swift") { + A step-by-step tutorial to learn how to implement, build, test, and deploy your first Lambda function written in Swift. + @Image(source: "00-swift_on_lambda.png", alt: "Swift on AWS Lambda icons") + } + + @Chapter(name: "Before getting started") { + + @Image(source: "01-swift_on_lambda.png", alt: "A Swift project open in Xcode") + An overview of what to expect and what you need before getting started. + @TutorialReference(tutorial: "doc:01-overview") + @TutorialReference(tutorial: "doc:02-what-is-lambda") + @TutorialReference(tutorial: "doc:03-prerequisites") + } + @Chapter(name: "Your first Lambda function in Swift") { + + @Image(source: "03-swift_on_lambda.png", alt: "Swift Icon") + Create your first function and test it locally + @TutorialReference(tutorial: "doc:03-write-function") + } + @Chapter(name: "Deploy your code to the cloud") { + + @Image(source: "04-swift_on_lambda.png", alt: "Deploying Swift into AWS Lambda") + Build, package, upload, and invoke your code on AWS Lambda. + @TutorialReference(tutorial: "doc:04-deploy-function") + } + +} diff --git a/Sources/AWSLambdaRuntime/Context+Foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift similarity index 81% rename from Sources/AWSLambdaRuntime/Context+Foundation.swift rename to Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift index 0aa1c019..72b7a65d 100644 --- a/Sources/AWSLambdaRuntime/Context+Foundation.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift @@ -12,12 +12,17 @@ // //===----------------------------------------------------------------------===// -import AWSLambdaRuntimeCore +#if FoundationJSONSupport +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import struct Foundation.Date +#endif -extension Lambda.Context { +extension LambdaContext { var deadlineDate: Date { let secondsSinceEpoch = Double(Int64(bitPattern: self.deadline.rawValue)) / -1_000_000_000 return Date(timeIntervalSince1970: secondsSinceEpoch) } } +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift new file mode 100644 index 00000000..9bd4d30f --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if FoundationJSONSupport +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import struct Foundation.Data +import class Foundation.JSONDecoder +import class Foundation.JSONEncoder +#endif + +public struct LambdaJSONEventDecoder: LambdaEventDecoder { + @usableFromInline let jsonDecoder: JSONDecoder + + @inlinable + public init(_ jsonDecoder: JSONDecoder) { + self.jsonDecoder = jsonDecoder + } + + @inlinable + public func decode(_ type: Event.Type, from buffer: NIOCore.ByteBuffer) throws -> Event + where Event: Decodable { + try buffer.getJSONDecodable( + Event.self, + decoder: self.jsonDecoder, + at: buffer.readerIndex, + length: buffer.readableBytes + )! // must work, enough readable bytes + } +} + +public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { + @usableFromInline let jsonEncoder: JSONEncoder + + @inlinable + public init(_ jsonEncoder: JSONEncoder) { + self.jsonEncoder = jsonEncoder + } + + @inlinable + public func encode(_ value: Output, into buffer: inout ByteBuffer) throws { + try buffer.writeJSONEncodable(value, encoder: self.jsonEncoder) + } +} + +extension LambdaCodableAdapter { + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. + /// - handler: The handler object. + public init( + encoder: JSONEncoder = JSONEncoder(), + decoder: JSONDecoder = JSONDecoder(), + handler: sending Handler + ) + where + Output: Encodable, + Output == Handler.Output, + Encoder == LambdaJSONOutputEncoder, + Decoder == LambdaJSONEventDecoder + { + self.init( + encoder: LambdaJSONOutputEncoder(encoder), + decoder: LambdaJSONEventDecoder(decoder), + handler: handler + ) + } +} + +extension LambdaRuntime { + /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. + /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. `JSONEncoder()` used as default. + /// - body: The handler in the form of a closure. + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder(), + body: sending @escaping (Event, LambdaContext) async throws -> Output + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Output, + LambdaJSONEventDecoder, + LambdaJSONOutputEncoder + > + { + let handler = LambdaCodableAdapter( + encoder: encoder, + decoder: decoder, + handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + ) + + self.init(handler: handler) + } + + /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a `Void` return type**. + /// - Parameter body: The handler in the form of a closure. + /// - Parameter decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + body: sending @escaping (Event, LambdaContext) async throws -> Void + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Void, + LambdaJSONEventDecoder, + VoidEncoder + > + { + let handler = LambdaCodableAdapter( + decoder: LambdaJSONEventDecoder(decoder), + handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + ) + + self.init(handler: handler) + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift new file mode 100644 index 00000000..482e020f --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift @@ -0,0 +1,108 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if FoundationJSONSupport +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// This is NIO's `NIOFoundationCompat` module which at the moment only adds `ByteBuffer` utility methods +// for Foundation's `Data` type. +// +// The reason that it's not in the `NIO` module is that we don't want to have any direct Foundation dependencies +// in `NIO` as Foundation is problematic for a few reasons: +// +// - its implementation is different on Linux and on macOS which means our macOS tests might be inaccurate +// - on macOS Foundation is mostly written in ObjC which means the autorelease pool might get populated +// - `swift-corelibs-foundation` (the OSS Foundation used on Linux) links the world which will prevent anyone from +// having static binaries. It can also cause problems in the choice of an SSL library as Foundation already brings +// the platforms OpenSSL in which might cause problems. + +extension ByteBuffer { + /// Controls how bytes are transferred between `ByteBuffer` and other storage types. + @usableFromInline + enum ByteTransferStrategy: Sendable { + /// Force a copy of the bytes. + case copy + + /// Do not copy the bytes if at all possible. + case noCopy + + /// Use a heuristic to decide whether to copy the bytes or not. + case automatic + } + + // MARK: - Data APIs + + /// Return `length` bytes starting at `index` and return the result as `Data`. This will not change the reader index. + /// The selected bytes must be readable or else `nil` will be returned. + /// + /// - parameters: + /// - index: The starting index of the bytes of interest into the `ByteBuffer` + /// - length: The number of bytes of interest + /// - byteTransferStrategy: Controls how to transfer the bytes. See `ByteTransferStrategy` for an explanation + /// of the options. + /// - returns: A `Data` value containing the bytes of interest or `nil` if the selected bytes are not readable. + @usableFromInline + func getData(at index0: Int, length: Int, byteTransferStrategy: ByteTransferStrategy) -> Data? { + let index = index0 - self.readerIndex + guard index >= 0 && length >= 0 && index <= self.readableBytes - length else { + return nil + } + let doCopy: Bool + switch byteTransferStrategy { + case .copy: + doCopy = true + case .noCopy: + doCopy = false + case .automatic: + doCopy = length <= 256 * 1024 + } + + return self.withUnsafeReadableBytesWithStorageManagement { ptr, storageRef in + if doCopy { + return Data( + bytes: UnsafeMutableRawPointer(mutating: ptr.baseAddress!.advanced(by: index)), + count: Int(length) + ) + } else { + _ = storageRef.retain() + return Data( + bytesNoCopy: UnsafeMutableRawPointer(mutating: ptr.baseAddress!.advanced(by: index)), + count: Int(length), + deallocator: .custom { _, _ in storageRef.release() } + ) + } + } + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift new file mode 100644 index 00000000..89ce9b87 --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift @@ -0,0 +1,151 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2019-2021 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if FoundationJSONSupport +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension ByteBuffer { + /// Attempts to decode the `length` bytes from `index` using the `JSONDecoder` `decoder` as `T`. + /// + /// - parameters: + /// - type: The type type that is attempted to be decoded. + /// - decoder: The `JSONDecoder` that is used for the decoding. + /// - index: The index of the first byte to decode. + /// - length: The number of bytes to decode. + /// - returns: The decoded value if successful or `nil` if there are not enough readable bytes available. + @inlinable + func getJSONDecodable( + _ type: T.Type, + decoder: JSONDecoder = JSONDecoder(), + at index: Int, + length: Int + ) throws -> T? { + guard let data = self.getData(at: index, length: length, byteTransferStrategy: .noCopy) else { + return nil + } + return try decoder.decode(T.self, from: data) + } + + /// Encodes `value` using the `JSONEncoder` `encoder` and set the resulting bytes into this `ByteBuffer` at the + /// given `index`. + /// + /// - note: The `writerIndex` remains unchanged. + /// + /// - parameters: + /// - value: An `Encodable` value to encode. + /// - encoder: The `JSONEncoder` to encode `value` with. + /// - returns: The number of bytes written. + @inlinable + @discardableResult + mutating func setJSONEncodable( + _ value: T, + encoder: JSONEncoder = JSONEncoder(), + at index: Int + ) throws -> Int { + let data = try encoder.encode(value) + return self.setBytes(data, at: index) + } + + /// Encodes `value` using the `JSONEncoder` `encoder` and writes the resulting bytes into this `ByteBuffer`. + /// + /// If successful, this will move the writer index forward by the number of bytes written. + /// + /// - parameters: + /// - value: An `Encodable` value to encode. + /// - encoder: The `JSONEncoder` to encode `value` with. + /// - returns: The number of bytes written. + @inlinable + @discardableResult + mutating func writeJSONEncodable( + _ value: T, + encoder: JSONEncoder = JSONEncoder() + ) throws -> Int { + let result = try self.setJSONEncodable(value, encoder: encoder, at: self.writerIndex) + self.moveWriterIndex(forwardBy: result) + return result + } +} + +extension JSONDecoder { + /// Returns a value of the type you specify, decoded from a JSON object inside the readable bytes of a `ByteBuffer`. + /// + /// If the `ByteBuffer` does not contain valid JSON, this method throws the + /// `DecodingError.dataCorrupted(_:)` error. If a value within the JSON + /// fails to decode, this method throws the corresponding error. + /// + /// - note: The provided `ByteBuffer` remains unchanged, neither the `readerIndex` nor the `writerIndex` will move. + /// If you would like the `readerIndex` to move, consider using `ByteBuffer.readJSONDecodable(_:length:)`. + /// + /// - parameters: + /// - type: The type of the value to decode from the supplied JSON object. + /// - buffer: The `ByteBuffer` that contains JSON object to decode. + /// - returns: The decoded object. + func decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T { + try buffer.getJSONDecodable( + T.self, + decoder: self, + at: buffer.readerIndex, + length: buffer.readableBytes + )! // must work, enough readable bytes// must work, enough readable bytes + } +} + +extension JSONEncoder { + /// Writes a JSON-encoded representation of the value you supply into the supplied `ByteBuffer`. + /// + /// - parameters: + /// - value: The value to encode as JSON. + /// - buffer: The `ByteBuffer` to encode into. + @inlinable + func encode( + _ value: T, + into buffer: inout ByteBuffer + ) throws { + try buffer.writeJSONEncodable(value, encoder: self) + } + + /// Writes a JSON-encoded representation of the value you supply into a `ByteBuffer` that is freshly allocated. + /// + /// - parameters: + /// - value: The value to encode as JSON. + /// - allocator: The `ByteBufferAllocator` which is used to allocate the `ByteBuffer` to be returned. + /// - returns: The `ByteBuffer` containing the encoded JSON. + func encodeAsByteBuffer(_ value: T, allocator: ByteBufferAllocator) throws -> ByteBuffer { + let data = try self.encode(value) + var buffer = allocator.buffer(capacity: data.count) + buffer.writeBytes(data) + return buffer + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index 495e5a69..abc8728b 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,136 +12,151 @@ // //===----------------------------------------------------------------------===// -@_exported import AWSLambdaRuntimeCore -import struct Foundation.Data -import class Foundation.JSONDecoder -import class Foundation.JSONEncoder -import NIO -import NIOFoundationCompat - -/// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `Codable` events. -extension Lambda { - /// An asynchronous Lambda Closure that takes a `In: Decodable` and returns a `Result` via a completion handler. - public typealias CodableClosure = (Lambda.Context, In, @escaping (Result) -> Void) -> Void - - /// Run a Lambda defined by implementing the `CodableClosure` function. - /// - /// - parameters: - /// - closure: `CodableClosure` based Lambda. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ closure: @escaping CodableClosure) { - self.run(CodableClosureWrapper(closure)) - } - - /// An asynchronous Lambda Closure that takes a `In: Decodable` and returns a `Result` via a completion handler. - public typealias CodableVoidClosure = (Lambda.Context, In, @escaping (Result) -> Void) -> Void - - /// Run a Lambda defined by implementing the `CodableVoidClosure` function. - /// - /// - parameters: - /// - closure: `CodableVoidClosure` based Lambda. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ closure: @escaping CodableVoidClosure) { - self.run(CodableVoidClosureWrapper(closure)) - } +import NIOCore + +/// The protocol a decoder must conform to so that it can be used with ``LambdaCodableAdapter`` to decode incoming +/// `ByteBuffer` events. +public protocol LambdaEventDecoder { + /// Decode the `ByteBuffer` representing the received event into the generic `Event` type + /// the handler will receive. + /// - Parameters: + /// - type: The type of the object to decode the buffer into. + /// - buffer: The buffer to be decoded. + /// - Returns: An object containing the decoded data. + func decode(_ type: Event.Type, from buffer: ByteBuffer) throws -> Event } -internal struct CodableClosureWrapper: LambdaHandler { - typealias In = In - typealias Out = Out - - private let closure: Lambda.CodableClosure - - init(_ closure: @escaping Lambda.CodableClosure) { - self.closure = closure - } +/// The protocol an encoder must conform to so that it can be used with ``LambdaCodableAdapter`` to encode the generic +/// ``LambdaOutputEncoder/Output`` object into a `ByteBuffer`. +public protocol LambdaOutputEncoder { + associatedtype Output - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - self.closure(context, event, callback) - } + /// Encode the generic type `Output` the handler has returned into a `ByteBuffer`. + /// - Parameters: + /// - value: The object to encode into a `ByteBuffer`. + /// - buffer: The `ByteBuffer` where the encoded value will be written to. + func encode(_ value: Output, into buffer: inout ByteBuffer) throws } -internal struct CodableVoidClosureWrapper: LambdaHandler { - typealias In = In - typealias Out = Void - - private let closure: Lambda.CodableVoidClosure +public struct VoidEncoder: LambdaOutputEncoder { + public typealias Output = Void - init(_ closure: @escaping Lambda.CodableVoidClosure) { - self.closure = closure - } + public init() {} - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - self.closure(context, event, callback) - } + @inlinable + public func encode(_ value: Void, into buffer: inout NIOCore.ByteBuffer) throws {} } -/// Implementation of a`ByteBuffer` to `In` decoding -public extension EventLoopLambdaHandler where In: Decodable { - func decode(buffer: ByteBuffer) throws -> In { - try self.decoder.decode(In.self, from: buffer) +/// Adapts a ``LambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``. +public struct LambdaHandlerAdapter< + Event: Decodable, + Output, + Handler: LambdaHandler +>: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { + @usableFromInline let handler: Handler + + /// Initializes an instance given a concrete handler. + /// - Parameter handler: The ``LambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. + @inlinable + public init(handler: sending Handler) { + self.handler = handler } -} -/// Implementation of `Out` to `ByteBuffer` encoding -public extension EventLoopLambdaHandler where Out: Encodable { - func encode(allocator: ByteBufferAllocator, value: Out) throws -> ByteBuffer? { - try self.encoder.encode(value, using: allocator) + /// Passes the generic `Event` object to the ``LambdaHandler/handle(_:context:)`` function, and + /// the resulting output is then written to ``LambdaWithBackgroundProcessingHandler``'s `outputWriter`. + /// - Parameters: + /// - event: The received event. + /// - outputWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + let output = try await self.handler.handle(event, context: context) + try await outputWriter.write(output) } } -/// Default `ByteBuffer` to `In` decoder using Foundation's JSONDecoder -/// Advanced users that want to inject their own codec can do it by overriding these functions. -public extension EventLoopLambdaHandler where In: Decodable { - var decoder: LambdaCodableDecoder { - Lambda.defaultJSONDecoder +/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``. +public struct LambdaCodableAdapter< + Handler: LambdaWithBackgroundProcessingHandler, + Event: Decodable, + Output, + Decoder: LambdaEventDecoder, + Encoder: LambdaOutputEncoder +>: StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output { + @usableFromInline let handler: Handler + @usableFromInline let encoder: Encoder + @usableFromInline let decoder: Decoder + @usableFromInline var byteBuffer: ByteBuffer = .init() + + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(encoder: sending Encoder, decoder: sending Decoder, handler: sending Handler) where Output: Encodable { + self.encoder = encoder + self.decoder = decoder + self.handler = handler } -} -/// Default `Out` to `ByteBuffer` encoder using Foundation's JSONEncoder -/// Advanced users that want to inject their own codec can do it by overriding these functions. -public extension EventLoopLambdaHandler where Out: Encodable { - var encoder: LambdaCodableEncoder { - Lambda.defaultJSONEncoder + /// Initializes an instance given a decoder, and a handler with a `Void` output. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(decoder: sending Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { + self.encoder = VoidEncoder() + self.decoder = decoder + self.handler = handler } -} - -public protocol LambdaCodableDecoder { - func decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T -} - -public protocol LambdaCodableEncoder { - func encode(_ value: T, using allocator: ByteBufferAllocator) throws -> ByteBuffer -} - -private extension Lambda { - static let defaultJSONDecoder = JSONDecoder() - static let defaultJSONEncoder = JSONEncoder() -} -extension JSONDecoder: LambdaCodableDecoder {} - -extension JSONEncoder: LambdaCodableEncoder { - public func encode(_ value: T, using allocator: ByteBufferAllocator) throws -> ByteBuffer where T: Encodable { - // nio will resize the buffer if necessary - var buffer = allocator.buffer(capacity: 1024) - try self.encode(value, into: &buffer) - return buffer + /// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper. + /// - Parameters: + /// - request: The received event. + /// - responseWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public mutating func handle( + _ request: ByteBuffer, + responseWriter: Writer, + context: LambdaContext + ) async throws { + let event = try self.decoder.decode(Event.self, from: request) + + let writer = LambdaCodableResponseWriter( + encoder: self.encoder, + streamWriter: responseWriter + ) + try await self.handler.handle(event, outputWriter: writer, context: context) } } -extension JSONEncoder { - /// Convenience method to allow encoding json directly into a `String`. It can be used to encode a payload into an `APIGateway.V2.Response`'s body. - public func encodeAsString(_ value: T) throws -> String { - try String(decoding: self.encode(value), as: Unicode.UTF8.self) +/// A ``LambdaResponseStreamWriter`` wrapper that conforms to ``LambdaResponseWriter``. +public struct LambdaCodableResponseWriter: + LambdaResponseWriter +where Output == Encoder.Output { + @usableFromInline let underlyingStreamWriter: Base + @usableFromInline let encoder: Encoder + + /// Initializes an instance given an encoder and an underlying ``LambdaResponseStreamWriter``. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`, which will then be passed to `streamWriter`. + /// - streamWriter: The underlying ``LambdaResponseStreamWriter`` that will be wrapped. + @inlinable + public init(encoder: Encoder, streamWriter: Base) { + self.encoder = encoder + self.underlyingStreamWriter = streamWriter } -} -extension JSONDecoder { - /// Convenience method to allow decoding json directly from a `String`. It can be used to decode a payload from an `APIGateway.V2.Request`'s body. - public func decode(_ type: T.Type, from string: String) throws -> T { - try self.decode(type, from: Data(string.utf8)) + @inlinable + public func write(_ output: Output) async throws { + var outputBuffer = ByteBuffer() + try self.encoder.encode(output, into: &outputBuffer) + try await self.underlyingStreamWriter.writeAndFinish(outputBuffer) } } diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift new file mode 100644 index 00000000..baa08a44 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -0,0 +1,539 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if LocalServerSupport +import DequeModule +import Dispatch +import Logging +import NIOCore +import NIOHTTP1 +import NIOPosix +import Synchronization + +// This functionality is designed for local testing hence being a #if DEBUG flag. + +// For example: +// try Lambda.withLocalServer { +// try await LambdaRuntimeClient.withRuntimeClient( +// configuration: .init(ip: "127.0.0.1", port: 7000), +// eventLoop: self.eventLoop, +// logger: self.logger +// ) { runtimeClient in +// try await Lambda.runLoop( +// runtimeClient: runtimeClient, +// handler: handler, +// logger: self.logger +// ) +// } +// } +extension Lambda { + /// Execute code in the context of a mock Lambda server. + /// + /// - parameters: + /// - invocationEndpoint: The endpoint to post events to. + /// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call. + /// + /// - note: This API is designed strictly for local testing and is behind a DEBUG flag + @usableFromInline + static func withLocalServer( + invocationEndpoint: String? = nil, + _ body: sending @escaping () async throws -> Void + ) async throws { + var logger = Logger(label: "LocalServer") + logger.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info + + try await LambdaHTTPServer.withLocalServer( + invocationEndpoint: invocationEndpoint, + logger: logger + ) { + try await body() + } + } +} + +// MARK: - Local HTTP Server + +/// An HTTP server that behaves like the AWS Lambda service for local testing. +/// This server is used to simulate the AWS Lambda service for local testing but also to accept invocation requests from the lambda client. +/// +/// It accepts three types of requests from the Lambda function (through the LambdaRuntimeClient): +/// 1. GET /next - the lambda function polls this endpoint to get the next invocation request +/// 2. POST /:requestID/response - the lambda function posts the response to the invocation request +/// 3. POST /:requestID/error - the lambda function posts an error response to the invocation request +/// +/// It also accepts one type of request from the client invoking the lambda function: +/// 1. POST /invoke - the client posts the event to the lambda function +/// +/// This server passes the data received from /invoke POST request to the lambda function (GET /next) and then forwards the response back to the client. +private struct LambdaHTTPServer { + private let invocationEndpoint: String + + private let invocationPool = Pool() + private let responsePool = Pool() + + private init( + invocationEndpoint: String? + ) { + self.invocationEndpoint = invocationEndpoint ?? "/invoke" + } + + private enum TaskResult: Sendable { + case closureResult(Swift.Result) + case serverReturned(Swift.Result) + } + + struct UnsafeTransferBox: @unchecked Sendable { + let value: Value + + init(value: sending Value) { + self.value = value + } + } + + static func withLocalServer( + invocationEndpoint: String?, + host: String = "127.0.0.1", + port: Int = 7000, + eventLoopGroup: MultiThreadedEventLoopGroup = .singleton, + logger: Logger, + _ closure: sending @escaping () async throws -> Result + ) async throws -> Result { + let channel = try await ServerBootstrap(group: eventLoopGroup) + .serverChannelOption(.backlog, value: 256) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + .childChannelOption(.maxMessagesPerRead, value: 1) + .bind( + host: host, + port: port + ) { channel in + channel.eventLoop.makeCompletedFuture { + + try channel.pipeline.syncOperations.configureHTTPServerPipeline( + withErrorHandling: true + ) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: NIOAsyncChannel.Configuration( + inboundType: HTTPServerRequestPart.self, + outboundType: HTTPServerResponsePart.self + ) + ) + } + } + + logger.info( + "Server started and listening", + metadata: [ + "host": "\(channel.channel.localAddress?.ipAddress?.debugDescription ?? "")", + "port": "\(channel.channel.localAddress?.port ?? 0)", + ] + ) + + let server = LambdaHTTPServer(invocationEndpoint: invocationEndpoint) + + // Sadly the Swift compiler does not understand that the passed in closure will only be + // invoked once. Because of this we need an unsafe transfer box here. Buuuh! + let closureBox = UnsafeTransferBox(value: closure) + let result = await withTaskGroup(of: TaskResult.self, returning: Swift.Result.self) { + group in + group.addTask { + let c = closureBox.value + do { + let result = try await c() + return .closureResult(.success(result)) + } catch { + return .closureResult(.failure(error)) + } + } + + group.addTask { + do { + // We are handling each incoming connection in a separate child task. It is important + // to use a discarding task group here which automatically discards finished child tasks. + // A normal task group retains all child tasks and their outputs in memory until they are + // consumed by iterating the group or by exiting the group. Since, we are never consuming + // the results of the group we need the group to automatically discard them; otherwise, this + // would result in a memory leak over time. + try await withThrowingDiscardingTaskGroup { taskGroup in + try await channel.executeThenClose { inbound in + for try await connectionChannel in inbound { + + taskGroup.addTask { + logger.trace("Handling a new connection") + await server.handleConnection(channel: connectionChannel, logger: logger) + logger.trace("Done handling the connection") + } + } + } + } + return .serverReturned(.success(())) + } catch { + return .serverReturned(.failure(error)) + } + } + + // Now that the local HTTP server and LambdaHandler tasks are started, wait for the + // first of the two that will terminate. + // When the first task terminates, cancel the group and collect the result of the + // second task. + + // collect and return the result of the LambdaHandler + let serverOrHandlerResult1 = await group.next()! + group.cancelAll() + + switch serverOrHandlerResult1 { + case .closureResult(let result): + return result + + case .serverReturned(let result): + logger.error( + "Server shutdown before closure completed", + metadata: [ + "error": "\(result.maybeError != nil ? "\(result.maybeError!)" : "none")" + ] + ) + switch await group.next()! { + case .closureResult(let result): + return result + + case .serverReturned: + fatalError("Only one task is a server, and only one can return `serverReturned`") + } + } + } + + logger.info("Server shutting down") + return try result.get() + } + + /// This method handles individual TCP connections + private func handleConnection( + channel: NIOAsyncChannel, + logger: Logger + ) async { + + var requestHead: HTTPRequestHead! + var requestBody: ByteBuffer? + + // Note that this method is non-throwing and we are catching any error. + // We do this since we don't want to tear down the whole server when a single connection + // encounters an error. + do { + try await channel.executeThenClose { inbound, outbound in + for try await inboundData in inbound { + switch inboundData { + case .head(let head): + requestHead = head + + case .body(let body): + requestBody = body + + case .end: + precondition(requestHead != nil, "Received .end without .head") + // process the request + let response = try await self.processRequest( + head: requestHead, + body: requestBody, + logger: logger + ) + // send the responses + try await self.sendResponse( + response: response, + outbound: outbound, + logger: logger + ) + + requestHead = nil + requestBody = nil + } + } + } + } catch { + logger.error("Hit error: \(error)") + } + } + + /// This function process the URI request sent by the client and by the Lambda function + /// + /// It enqueues the client invocation and iterate over the invocation queue when the Lambda function sends /next request + /// It answers the /:requestID/response and /:requestID/error requests sent by the Lambda function but do not process the body + /// + /// - Parameters: + /// - head: the HTTP request head + /// - body: the HTTP request body + /// - Throws: + /// - Returns: the response to send back to the client or the Lambda function + private func processRequest( + head: HTTPRequestHead, + body: ByteBuffer?, + logger: Logger + ) async throws -> LocalServerResponse { + + if let body { + logger.trace( + "Processing request", + metadata: ["URI": "\(head.method) \(head.uri)", "Body": "\(String(buffer: body))"] + ) + } else { + logger.trace("Processing request", metadata: ["URI": "\(head.method) \(head.uri)"]) + } + + switch (head.method, head.uri) { + + // + // client invocations + // + // client POST /invoke + case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): + guard let body else { + return .init(status: .badRequest, headers: [], body: nil) + } + // we always accept the /invoke request and push them to the pool + let requestId = "\(DispatchTime.now().uptimeNanoseconds)" + var logger = logger + logger[metadataKey: "requestID"] = "\(requestId)" + logger.trace("/invoke received invocation") + await self.invocationPool.push(LocalServerInvocation(requestId: requestId, request: body)) + + // wait for the lambda function to process the request + for try await response in self.responsePool { + logger.trace( + "Received response to return to client", + metadata: ["requestId": "\(response.requestId ?? "")"] + ) + if response.requestId == requestId { + return response + } else { + logger.error( + "Received response for a different request id", + metadata: ["response requestId": "\(response.requestId ?? "")", "requestId": "\(requestId)"] + ) + // should we return an error here ? Or crash as this is probably a programming error? + } + } + // What todo when there is no more responses to process? + // This should not happen as the async iterator blocks until there is a response to process + fatalError("No more responses to process - the async for loop should not return") + + // client uses incorrect HTTP method + case (_, let url) where url.hasSuffix(self.invocationEndpoint): + return .init(status: .methodNotAllowed) + + // + // lambda invocations + // + + // /next endpoint is called by the lambda polling for work + // this call only returns when there is a task to give to the lambda function + case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): + + // pop the tasks from the queue + logger.trace("/next waiting for /invoke") + for try await invocation in self.invocationPool { + logger.trace("/next retrieved invocation", metadata: ["requestId": "\(invocation.requestId)"]) + // this call also stores the invocation requestId into the response + return invocation.makeResponse(status: .accepted) + } + // What todo when there is no more tasks to process? + // This should not happen as the async iterator blocks until there is a task to process + fatalError("No more invocations to process - the async for loop should not return") + + // :requestID/response endpoint is called by the lambda posting the response + case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): + let parts = head.uri.split(separator: "/") + guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { + // the request is malformed, since we were expecting a requestId in the path + return .init(status: .badRequest) + } + // enqueue the lambda function response to be served as response to the client /invoke + logger.trace("/:requestID/response received response", metadata: ["requestId": "\(requestID)"]) + await self.responsePool.push( + LocalServerResponse( + id: requestID, + status: .ok, + headers: [("Content-Type", "application/json")], + body: body + ) + ) + + // tell the Lambda function we accepted the response + return .init(id: requestID, status: .accepted) + + // :requestID/error endpoint is called by the lambda posting an error response + // we accept all requestID and we do not handle the body, we just acknowledge the request + case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): + let parts = head.uri.split(separator: "/") + guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { + // the request is malformed, since we were expecting a requestId in the path + return .init(status: .badRequest) + } + // enqueue the lambda function response to be served as response to the client /invoke + logger.trace("/:requestID/response received response", metadata: ["requestId": "\(requestID)"]) + await self.responsePool.push( + LocalServerResponse( + id: requestID, + status: .internalServerError, + headers: [("Content-Type", "application/json")], + body: body + ) + ) + + return .init(status: .accepted) + + // unknown call + default: + return .init(status: .notFound) + } + } + + private func sendResponse( + response: LocalServerResponse, + outbound: NIOAsyncChannelOutboundWriter, + logger: Logger + ) async throws { + var headers = HTTPHeaders(response.headers ?? []) + headers.add(name: "Content-Length", value: "\(response.body?.readableBytes ?? 0)") + + logger.trace("Writing response", metadata: ["requestId": "\(response.requestId ?? "")"]) + try await outbound.write( + HTTPServerResponsePart.head( + HTTPResponseHead( + version: .init(major: 1, minor: 1), + status: response.status, + headers: headers + ) + ) + ) + if let body = response.body { + try await outbound.write(HTTPServerResponsePart.body(.byteBuffer(body))) + } + + try await outbound.write(HTTPServerResponsePart.end(nil)) + } + + /// A shared data structure to store the current invocation or response requests and the continuation objects. + /// This data structure is shared between instances of the HTTPHandler + /// (one instance to serve requests from the Lambda function and one instance to serve requests from the client invoking the lambda function). + private final class Pool: AsyncSequence, AsyncIteratorProtocol, Sendable where T: Sendable { + typealias Element = T + + enum State: ~Copyable { + case buffer(Deque) + case continuation(CheckedContinuation?) + } + + private let lock = Mutex(.buffer([])) + + /// enqueue an element, or give it back immediately to the iterator if it is waiting for an element + public func push(_ invocation: T) async { + // if the iterator is waiting for an element, give it to it + // otherwise, enqueue the element + let maybeContinuation = self.lock.withLock { state -> CheckedContinuation? in + switch consume state { + case .continuation(let continuation): + state = .buffer([]) + return continuation + + case .buffer(var buffer): + buffer.append(invocation) + state = .buffer(buffer) + return nil + } + } + + maybeContinuation?.resume(returning: invocation) + } + + func next() async throws -> T? { + // exit the async for loop if the task is cancelled + guard !Task.isCancelled else { + return nil + } + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let nextAction = self.lock.withLock { state -> T? in + switch consume state { + case .buffer(var buffer): + if let first = buffer.popFirst() { + state = .buffer(buffer) + return first + } else { + state = .continuation(continuation) + return nil + } + + case .continuation: + fatalError("Concurrent invocations to next(). This is illegal.") + } + } + + guard let nextAction else { return } + + continuation.resume(returning: nextAction) + } + } + + func makeAsyncIterator() -> Pool { + self + } + } + + private struct LocalServerResponse: Sendable { + let requestId: String? + let status: HTTPResponseStatus + let headers: [(String, String)]? + let body: ByteBuffer? + init(id: String? = nil, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: ByteBuffer? = nil) + { + self.requestId = id + self.status = status + self.headers = headers + self.body = body + } + } + + private struct LocalServerInvocation: Sendable { + let requestId: String + let request: ByteBuffer + + func makeResponse(status: HTTPResponseStatus) -> LocalServerResponse { + + // required headers + let headers = [ + (AmazonHeaders.requestID, self.requestId), + ( + AmazonHeaders.invokedFunctionARN, + "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" + ), + (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), + (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), + ] + + return LocalServerResponse(id: self.requestId, status: status, headers: headers, body: self.request) + } + } +} + +extension Result { + var maybeError: Failure? { + switch self { + case .success: + return nil + case .failure(let error): + return error + } + } +} +#endif diff --git a/Sources/AWSLambdaRuntime/Lambda.swift b/Sources/AWSLambdaRuntime/Lambda.swift new file mode 100644 index 00000000..e38851c0 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Lambda.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Dispatch +import Logging +import NIOCore +import NIOPosix + +#if os(macOS) +import Darwin.C +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif os(Windows) +import ucrt +#else +#error("Unsupported platform") +#endif + +public enum Lambda { + @inlinable + package static func runLoop( + runtimeClient: RuntimeClient, + handler: Handler, + logger: Logger + ) async throws where Handler: StreamingLambdaHandler { + var handler = handler + + var logger = logger + do { + while !Task.isCancelled { + let (invocation, writer) = try await runtimeClient.nextInvocation() + logger[metadataKey: "aws-request-id"] = "\(invocation.metadata.requestID)" + + do { + try await handler.handle( + invocation.event, + responseWriter: writer, + context: LambdaContext( + requestID: invocation.metadata.requestID, + traceID: invocation.metadata.traceID, + invokedFunctionARN: invocation.metadata.invokedFunctionARN, + deadline: DispatchWallTime( + millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch + ), + logger: logger + ) + ) + } catch { + try await writer.reportError(error) + continue + } + } + } catch is CancellationError { + // don't allow cancellation error to propagate further + } + } + + /// The default EventLoop the Lambda is scheduled on. + public static let defaultEventLoop: any EventLoop = NIOSingletons.posixEventLoopGroup.next() +} + +// MARK: - Public API + +extension Lambda { + /// Utility to access/read environment variables + public static func env(_ name: String) -> String? { + guard let value = getenv(name) else { + return nil + } + return String(cString: value) + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift new file mode 100644 index 00000000..fbf84158 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Dispatch +import Logging +import NIOCore + +// MARK: - Context + +/// Lambda runtime context. +/// The Lambda runtime generates and passes the `LambdaContext` to the Lambda handler as an argument. +public struct LambdaContext: CustomDebugStringConvertible, Sendable { + final class _Storage: Sendable { + let requestID: String + let traceID: String + let invokedFunctionARN: String + let deadline: DispatchWallTime + let cognitoIdentity: String? + let clientContext: String? + let logger: Logger + + init( + requestID: String, + traceID: String, + invokedFunctionARN: String, + deadline: DispatchWallTime, + cognitoIdentity: String?, + clientContext: String?, + logger: Logger + ) { + self.requestID = requestID + self.traceID = traceID + self.invokedFunctionARN = invokedFunctionARN + self.deadline = deadline + self.cognitoIdentity = cognitoIdentity + self.clientContext = clientContext + self.logger = logger + } + } + + private var storage: _Storage + + /// The request ID, which identifies the request that triggered the function invocation. + public var requestID: String { + self.storage.requestID + } + + /// The AWS X-Ray tracing header. + public var traceID: String { + self.storage.traceID + } + + /// The ARN of the Lambda function, version, or alias that's specified in the invocation. + public var invokedFunctionARN: String { + self.storage.invokedFunctionARN + } + + /// The timestamp that the function times out. + public var deadline: DispatchWallTime { + self.storage.deadline + } + + /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. + public var cognitoIdentity: String? { + self.storage.cognitoIdentity + } + + /// For invocations from the AWS Mobile SDK, data about the client application and device. + public var clientContext: String? { + self.storage.clientContext + } + + /// `Logger` to log with. + /// + /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. + public var logger: Logger { + self.storage.logger + } + + public init( + requestID: String, + traceID: String, + invokedFunctionARN: String, + deadline: DispatchWallTime, + cognitoIdentity: String? = nil, + clientContext: String? = nil, + logger: Logger + ) { + self.storage = _Storage( + requestID: requestID, + traceID: traceID, + invokedFunctionARN: invokedFunctionARN, + deadline: deadline, + cognitoIdentity: cognitoIdentity, + clientContext: clientContext, + logger: logger + ) + } + + public func getRemainingTime() -> Duration { + let deadline = self.deadline.millisSinceEpoch + let now = DispatchWallTime.now().millisSinceEpoch + + let remaining = deadline - now + return .milliseconds(remaining) + } + + public var debugDescription: String { + "\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(self.clientContext ?? "nil"), deadline: \(self.deadline))" + } + + /// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning. + package static func __forTestsOnly( + requestID: String, + traceID: String, + invokedFunctionARN: String, + timeout: DispatchTimeInterval, + logger: Logger + ) -> LambdaContext { + LambdaContext( + requestID: requestID, + traceID: traceID, + invokedFunctionARN: invokedFunctionARN, + deadline: .now() + timeout, + logger: logger + ) + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaHandlers.swift b/Sources/AWSLambdaRuntime/LambdaHandlers.swift new file mode 100644 index 00000000..d6e0b373 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaHandlers.swift @@ -0,0 +1,244 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +/// The base handler protocol that receives a `ByteBuffer` representing the incoming event and returns the response as a `ByteBuffer` too. +/// This handler protocol supports response streaming. Bytes can be streamed outwards through the ``LambdaResponseStreamWriter`` +/// passed as an argument in the ``handle(_:responseWriter:context:)`` function. +/// Background work can also be executed after returning the response. After closing the response stream by calling +/// ``LambdaResponseStreamWriter/finish()`` or ``LambdaResponseStreamWriter/writeAndFinish(_:)``, +/// the ``handle(_:responseWriter:context:)`` function is free to execute any background work. +public protocol StreamingLambdaHandler { + /// The handler function -- implement the business logic of the Lambda function here. + /// - Parameters: + /// - event: The invocation's input data. + /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. + /// If no response or error is written to `responseWriter` an error will be reported to the invoker. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + /// - Throws: + /// How the thrown error will be handled by the runtime: + /// - An invocation error will be reported if the error is thrown before the first call to + /// ``LambdaResponseStreamWriter/write(_:)``. + /// - If the error is thrown after call(s) to ``LambdaResponseStreamWriter/write(_:)`` but before + /// a call to ``LambdaResponseStreamWriter/finish()``, the response stream will be closed and trailing + /// headers will be sent. + /// - If ``LambdaResponseStreamWriter/finish()`` has already been called before the error is thrown, the + /// error will be logged. + mutating func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws +} + +/// A writer object to write the Lambda response stream into. The HTTP response is started lazily. +/// before the first call to ``write(_:)`` or ``writeAndFinish(_:)``. +public protocol LambdaResponseStreamWriter { + /// Write a response part into the stream. Bytes written are streamed continually. + /// - Parameter buffer: The buffer to write. + func write(_ buffer: ByteBuffer) async throws + + /// End the response stream and the underlying HTTP response. + func finish() async throws + + /// Write a response part into the stream and then end the stream as well as the underlying HTTP response. + /// - Parameter buffer: The buffer to write. + func writeAndFinish(_ buffer: ByteBuffer) async throws +} + +/// This handler protocol is intended to serve the most common use-cases. +/// This protocol is completely agnostic to any encoding/decoding -- decoding the received event invocation into an ``Event`` object and encoding the returned ``Output`` object is handled by the library. +/// The``handle(_:context:)`` function simply receives the generic ``Event`` object as input and returns the generic ``Output`` object. +/// +/// - note: This handler protocol does not support response streaming because the output has to be encoded prior to it being sent, e.g. it is not possible to encode a partial/incomplete JSON string. +/// This protocol also does not support the execution of background work after the response has been returned -- the ``LambdaWithBackgroundProcessingHandler`` protocol caters for such use-cases. +public protocol LambdaHandler { + /// Generic input type. + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. + associatedtype Event + /// Generic output type. + /// This is the return type of the ``LambdaHandler/handle(_:context:)`` function. + associatedtype Output + + /// Implement the business logic of the Lambda function here. + /// - Parameters: + /// - event: The generic ``LambdaHandler/Event`` object representing the invocation's input data. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + /// - Returns: A generic ``Output`` object representing the computed result. + func handle(_ event: Event, context: LambdaContext) async throws -> Output +} + +/// This protocol is exactly like ``LambdaHandler``, with the only difference being the added support for executing background +/// work after the result has been sent to the AWS Lambda control plane. +/// This is achieved by not having a return type in the `handle` function. The output is instead written into a +/// ``LambdaResponseWriter``that is passed in as an argument, meaning that the +/// ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` function is then +/// free to implement any background work after the result has been sent to the AWS Lambda control plane. +public protocol LambdaWithBackgroundProcessingHandler { + /// Generic input type. + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. + associatedtype Event + /// Generic output type. + /// This is the type that the `handle` function will send through the ``LambdaResponseWriter``. + associatedtype Output + + /// Implement the business logic of the Lambda function here. + /// - Parameters: + /// - event: The generic ``LambdaWithBackgroundProcessingHandler/Event`` object representing the invocation's input data. + /// - outputWriter: The writer to send the computed response to. A call to `outputWriter.write(_:)` will return the response to the AWS Lambda response endpoint. + /// Any background work can then be executed before returning. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws +} + +/// Used with ``LambdaWithBackgroundProcessingHandler``. +/// A mechanism to "return" an output from ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` without the function needing to +/// have a return type and exit at that point. This allows for background work to be executed _after_ a response has been sent to the AWS Lambda response endpoint. +public protocol LambdaResponseWriter { + associatedtype Output + /// Sends the generic ``LambdaResponseWriter/Output`` object (representing the computed result of the handler) + /// to the AWS Lambda response endpoint. + /// This function simply serves as a mechanism to return the computed result from a handler function + /// without an explicit `return`. + func write(_ output: Output) async throws +} + +/// A ``StreamingLambdaHandler`` conforming handler object that can be constructed with a closure. +/// Allows for a handler to be defined in a clean manner, leveraging Swift's trailing closure syntax. +public struct StreamingClosureHandler: StreamingLambdaHandler { + let body: @Sendable (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void + + /// Initialize an instance from a handler function in the form of a closure. + /// - Parameter body: The handler function written as a closure. + public init( + body: @Sendable @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void + ) { + self.body = body + } + + /// Calls the provided `self.body` closure with the `ByteBuffer` invocation event, the ``LambdaResponseStreamWriter``, and the ``LambdaContext`` + /// - Parameters: + /// - event: The invocation's input data. + /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. + /// If no response or error is written to `responseWriter` an error will be reported to the invoker. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + public func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + try await self.body(event, responseWriter, context) + } +} + +/// A ``LambdaHandler`` conforming handler object that can be constructed with a closure. +/// Allows for a handler to be defined in a clean manner, leveraging Swift's trailing closure syntax. +public struct ClosureHandler: LambdaHandler { + let body: (Event, LambdaContext) async throws -> Output + + /// Initialize with a closure handler over generic `Input` and `Output` types. + /// - Parameter body: The handler function written as a closure. + public init(body: sending @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable { + self.body = body + } + + /// Initialize with a closure handler over a generic `Input` type, and a `Void` `Output`. + /// - Parameter body: The handler function written as a closure. + public init(body: @escaping (Event, LambdaContext) async throws -> Void) where Output == Void { + self.body = body + } + + /// Calls the provided `self.body` closure with the generic `Event` object representing the incoming event, and the ``LambdaContext`` + /// - Parameters: + /// - event: The generic `Event` object representing the invocation's input data. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + public func handle(_ event: Event, context: LambdaContext) async throws -> Output { + try await self.body(event, context) + } +} + +extension LambdaRuntime { + /// Initialize an instance with a ``StreamingLambdaHandler`` in the form of a closure. + /// - Parameter body: The handler in the form of a closure. + public convenience init( + body: @Sendable @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void + ) where Handler == StreamingClosureHandler { + self.init(handler: StreamingClosureHandler(body: body)) + } + + /// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a non-`Void` return type**, an encoder, and a decoder. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. + /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. + /// - body: The handler in the form of a closure. + public convenience init< + Event: Decodable, + Output: Encodable, + Encoder: LambdaOutputEncoder, + Decoder: LambdaEventDecoder + >( + encoder: sending Encoder, + decoder: sending Decoder, + body: sending @escaping (Event, LambdaContext) async throws -> Output + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Output, + Decoder, + Encoder + > + { + let closureHandler = ClosureHandler(body: body) + let streamingAdapter = LambdaHandlerAdapter(handler: closureHandler) + let codableWrapper = LambdaCodableAdapter( + encoder: encoder, + decoder: decoder, + handler: streamingAdapter + ) + + self.init(handler: codableWrapper) + } + + /// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a `Void` return type**, an encoder, and a decoder. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. + /// - body: The handler in the form of a closure. + public convenience init( + decoder: sending Decoder, + body: sending @escaping (Event, LambdaContext) async throws -> Void + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Void, + Decoder, + VoidEncoder + > + { + let handler = LambdaCodableAdapter( + decoder: decoder, + handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + ) + + self.init(handler: handler) + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaRequestID.swift b/Sources/AWSLambdaRuntime/LambdaRequestID.swift new file mode 100644 index 00000000..df576947 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRequestID.swift @@ -0,0 +1,377 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +// This is heavily inspired by: +// https://github.com/swift-extras/swift-extras-uuid + +struct LambdaRequestID { + typealias uuid_t = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8 + ) + + var uuid: uuid_t { + self._uuid + } + + static let null: uuid_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + + /// Creates a random [v4](https://tools.ietf.org/html/rfc4122#section-4.1.3) UUID. + init() { + self = Self.generateRandom() + } + + init?(uuidString: String) { + guard uuidString.utf8.count == 36 else { + return nil + } + + if let requestID = uuidString.utf8.withContiguousStorageIfAvailable({ ptr -> LambdaRequestID? in + let rawBufferPointer = UnsafeRawBufferPointer(ptr) + let requestID = Self.fromPointer(rawBufferPointer) + return requestID + }) { + if let requestID = requestID { + self = requestID + } else { + return nil + } + } else { + var newSwiftCopy = uuidString + newSwiftCopy.makeContiguousUTF8() + if let value = Self(uuidString: newSwiftCopy) { + self = value + } else { + return nil + } + } + } + + /// Creates a UUID from a `uuid_t`. + init(uuid: uuid_t) { + self._uuid = uuid + } + + private let _uuid: uuid_t + + /// Returns a string representation for the `LambdaRequestID`, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + var uuidString: String { + self.uppercased + } + + /// Returns a lowercase string representation for the `LambdaRequestID`, such as "e621e1f8-c36c-495a-93fc-0c247a3e6e5f" + var lowercased: String { + var bytes = self.toAsciiBytesOnStack(characters: Self.lowercaseLookup) + return withUnsafeBytes(of: &bytes) { + String(decoding: $0, as: Unicode.UTF8.self) + } + } + + /// Returns an uppercase string representation for the `LambdaRequestID`, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + var uppercased: String { + var bytes = self.toAsciiBytesOnStack(characters: Self.uppercaseLookup) + return withUnsafeBytes(of: &bytes) { + String(decoding: $0, as: Unicode.UTF8.self) + } + } + + /// thread safe secure random number generator. + private static func generateRandom() -> Self { + var generator = SystemRandomNumberGenerator() + + var _uuid: uuid_t = LambdaRequestID.null + // https://tools.ietf.org/html/rfc4122#page-14 + // o Set all the other bits to randomly (or pseudo-randomly) chosen + // values. + withUnsafeMutableBytes(of: &_uuid) { ptr in + ptr.storeBytes(of: generator.next(), toByteOffset: 0, as: UInt64.self) + ptr.storeBytes(of: generator.next(), toByteOffset: 8, as: UInt64.self) + } + + // o Set the four most significant bits (bits 12 through 15) of the + // time_hi_and_version field to the 4-bit version number from + // Section 4.1.3. + _uuid.6 = (_uuid.6 & 0x0F) | 0x40 + + // o Set the two most significant bits (bits 6 and 7) of the + // clock_seq_hi_and_reserved to zero and one, respectively. + _uuid.8 = (_uuid.8 & 0x3F) | 0x80 + return LambdaRequestID(uuid: _uuid) + } +} + +// MARK: - Protocol extensions - + +extension LambdaRequestID: Equatable { + // sadly no auto conformance from the compiler + static func == (lhs: Self, rhs: Self) -> Bool { + lhs._uuid.0 == rhs._uuid.0 && lhs._uuid.1 == rhs._uuid.1 && lhs._uuid.2 == rhs._uuid.2 + && lhs._uuid.3 == rhs._uuid.3 && lhs._uuid.4 == rhs._uuid.4 && lhs._uuid.5 == rhs._uuid.5 + && lhs._uuid.6 == rhs._uuid.6 && lhs._uuid.7 == rhs._uuid.7 && lhs._uuid.8 == rhs._uuid.8 + && lhs._uuid.9 == rhs._uuid.9 && lhs._uuid.10 == rhs._uuid.10 && lhs._uuid.11 == rhs._uuid.11 + && lhs._uuid.12 == rhs._uuid.12 && lhs._uuid.13 == rhs._uuid.13 && lhs._uuid.14 == rhs._uuid.14 + && lhs._uuid.15 == rhs._uuid.15 + } +} + +extension LambdaRequestID: Hashable { + func hash(into hasher: inout Hasher) { + var value = self._uuid + withUnsafeBytes(of: &value) { ptr in + hasher.combine(bytes: ptr) + } + } +} + +extension LambdaRequestID: CustomStringConvertible { + var description: String { + self.uuidString + } +} + +extension LambdaRequestID: CustomDebugStringConvertible { + var debugDescription: String { + self.uuidString + } +} + +extension LambdaRequestID: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let uuidString = try container.decode(String.self) + + guard let uuid = LambdaRequestID.fromString(uuidString) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Attempted to decode UUID from invalid UUID string." + ) + } + + self = uuid + } +} + +extension LambdaRequestID: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.uuidString) + } +} + +// MARK: - Implementation details - + +extension LambdaRequestID { + fileprivate static let lowercaseLookup: [UInt8] = [ + UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), + UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), + UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "a"), UInt8(ascii: "b"), + UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"), + ] + + fileprivate static let uppercaseLookup: [UInt8] = [ + UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), + UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), + UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "A"), UInt8(ascii: "B"), + UInt8(ascii: "C"), UInt8(ascii: "D"), UInt8(ascii: "E"), UInt8(ascii: "F"), + ] + + /// Use this type to create a backing store for a 8-4-4-4-12 UUID String on stack. + /// + /// Using this type we ensure to only have one allocation for creating a String even before Swift 5.3 and it can + /// also be used as an intermediary before copying the string bytes into a NIO `ByteBuffer`. + fileprivate typealias uuid_string_t = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8 + ) + + fileprivate static let nullString: uuid_string_t = ( + 0, 0, 0, 0, 0, 0, 0, 0, UInt8(ascii: "-"), + 0, 0, 0, 0, UInt8(ascii: "-"), + 0, 0, 0, 0, UInt8(ascii: "-"), + 0, 0, 0, 0, UInt8(ascii: "-"), + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ) + + fileprivate func toAsciiBytesOnStack(characters: [UInt8]) -> uuid_string_t { + var string: uuid_string_t = Self.nullString + // to get the best performance we access the lookup table's unsafe buffer pointer + // since the lookup table has 16 elements and we shift the byte values in such a way + // that the max value is 15 (last 4 bytes = 16 values). For this reason the lookups + // are safe and we don't need Swifts safety guards. + + characters.withUnsafeBufferPointer { lookup in + string.0 = lookup[Int(self.uuid.0 >> 4)] + string.1 = lookup[Int(self.uuid.0 & 0x0F)] + string.2 = lookup[Int(self.uuid.1 >> 4)] + string.3 = lookup[Int(self.uuid.1 & 0x0F)] + string.4 = lookup[Int(self.uuid.2 >> 4)] + string.5 = lookup[Int(self.uuid.2 & 0x0F)] + string.6 = lookup[Int(self.uuid.3 >> 4)] + string.7 = lookup[Int(self.uuid.3 & 0x0F)] + string.9 = lookup[Int(self.uuid.4 >> 4)] + string.10 = lookup[Int(self.uuid.4 & 0x0F)] + string.11 = lookup[Int(self.uuid.5 >> 4)] + string.12 = lookup[Int(self.uuid.5 & 0x0F)] + string.14 = lookup[Int(self.uuid.6 >> 4)] + string.15 = lookup[Int(self.uuid.6 & 0x0F)] + string.16 = lookup[Int(self.uuid.7 >> 4)] + string.17 = lookup[Int(self.uuid.7 & 0x0F)] + string.19 = lookup[Int(self.uuid.8 >> 4)] + string.20 = lookup[Int(self.uuid.8 & 0x0F)] + string.21 = lookup[Int(self.uuid.9 >> 4)] + string.22 = lookup[Int(self.uuid.9 & 0x0F)] + string.24 = lookup[Int(self.uuid.10 >> 4)] + string.25 = lookup[Int(self.uuid.10 & 0x0F)] + string.26 = lookup[Int(self.uuid.11 >> 4)] + string.27 = lookup[Int(self.uuid.11 & 0x0F)] + string.28 = lookup[Int(self.uuid.12 >> 4)] + string.29 = lookup[Int(self.uuid.12 & 0x0F)] + string.30 = lookup[Int(self.uuid.13 >> 4)] + string.31 = lookup[Int(self.uuid.13 & 0x0F)] + string.32 = lookup[Int(self.uuid.14 >> 4)] + string.33 = lookup[Int(self.uuid.14 & 0x0F)] + string.34 = lookup[Int(self.uuid.15 >> 4)] + string.35 = lookup[Int(self.uuid.15 & 0x0F)] + } + + return string + } + + static func fromString(_ string: String) -> LambdaRequestID? { + guard string.utf8.count == 36 else { + // invalid length + return nil + } + var string = string + return string.withUTF8 { + LambdaRequestID.fromPointer(.init($0)) + } + } +} + +extension LambdaRequestID { + static func fromPointer(_ ptr: UnsafeRawBufferPointer) -> LambdaRequestID? { + func uint4Value(from value: UInt8, valid: inout Bool) -> UInt8 { + switch value { + case UInt8(ascii: "0")...UInt8(ascii: "9"): + return value &- UInt8(ascii: "0") + case UInt8(ascii: "a")...UInt8(ascii: "f"): + return value &- UInt8(ascii: "a") &+ 10 + case UInt8(ascii: "A")...UInt8(ascii: "F"): + return value &- UInt8(ascii: "A") &+ 10 + default: + valid = false + return 0 + } + } + + func dashCheck(from value: UInt8, valid: inout Bool) { + if value != UInt8(ascii: "-") { + valid = false + } + } + + precondition(ptr.count == 36) + var uuid = Self.null + var valid = true + uuid.0 = uint4Value(from: ptr[0], valid: &valid) &<< 4 &+ uint4Value(from: ptr[1], valid: &valid) + uuid.1 = uint4Value(from: ptr[2], valid: &valid) &<< 4 &+ uint4Value(from: ptr[3], valid: &valid) + uuid.2 = uint4Value(from: ptr[4], valid: &valid) &<< 4 &+ uint4Value(from: ptr[5], valid: &valid) + uuid.3 = uint4Value(from: ptr[6], valid: &valid) &<< 4 &+ uint4Value(from: ptr[7], valid: &valid) + dashCheck(from: ptr[8], valid: &valid) + uuid.4 = uint4Value(from: ptr[9], valid: &valid) &<< 4 &+ uint4Value(from: ptr[10], valid: &valid) + uuid.5 = uint4Value(from: ptr[11], valid: &valid) &<< 4 &+ uint4Value(from: ptr[12], valid: &valid) + dashCheck(from: ptr[13], valid: &valid) + uuid.6 = uint4Value(from: ptr[14], valid: &valid) &<< 4 &+ uint4Value(from: ptr[15], valid: &valid) + uuid.7 = uint4Value(from: ptr[16], valid: &valid) &<< 4 &+ uint4Value(from: ptr[17], valid: &valid) + dashCheck(from: ptr[18], valid: &valid) + uuid.8 = uint4Value(from: ptr[19], valid: &valid) &<< 4 &+ uint4Value(from: ptr[20], valid: &valid) + uuid.9 = uint4Value(from: ptr[21], valid: &valid) &<< 4 &+ uint4Value(from: ptr[22], valid: &valid) + dashCheck(from: ptr[23], valid: &valid) + uuid.10 = uint4Value(from: ptr[24], valid: &valid) &<< 4 &+ uint4Value(from: ptr[25], valid: &valid) + uuid.11 = uint4Value(from: ptr[26], valid: &valid) &<< 4 &+ uint4Value(from: ptr[27], valid: &valid) + uuid.12 = uint4Value(from: ptr[28], valid: &valid) &<< 4 &+ uint4Value(from: ptr[29], valid: &valid) + uuid.13 = uint4Value(from: ptr[30], valid: &valid) &<< 4 &+ uint4Value(from: ptr[31], valid: &valid) + uuid.14 = uint4Value(from: ptr[32], valid: &valid) &<< 4 &+ uint4Value(from: ptr[33], valid: &valid) + uuid.15 = uint4Value(from: ptr[34], valid: &valid) &<< 4 &+ uint4Value(from: ptr[35], valid: &valid) + + if valid { + return LambdaRequestID(uuid: uuid) + } + + return nil + } +} + +extension ByteBuffer { + func getRequestID(at index: Int) -> LambdaRequestID? { + guard let range = self.rangeWithinReadableBytes(index: index, length: 36) else { + return nil + } + return self.withUnsafeReadableBytes { ptr in + LambdaRequestID.fromPointer(UnsafeRawBufferPointer(fastRebase: ptr[range])) + } + } + + mutating func readRequestID() -> LambdaRequestID? { + guard let requestID = self.getRequestID(at: self.readerIndex) else { + return nil + } + self.moveReaderIndex(forwardBy: 36) + return requestID + } + + @discardableResult + mutating func setRequestID(_ requestID: LambdaRequestID, at index: Int) -> Int { + var localBytes = requestID.toAsciiBytesOnStack(characters: LambdaRequestID.lowercaseLookup) + return withUnsafeBytes(of: &localBytes) { + self.setBytes($0, at: index) + } + } + + mutating func writeRequestID(_ requestID: LambdaRequestID) -> Int { + let length = self.setRequestID(requestID, at: self.writerIndex) + self.moveWriterIndex(forwardBy: length) + return length + } + + // copy and pasted from NIOCore + func rangeWithinReadableBytes(index: Int, length: Int) -> Range? { + guard index >= self.readerIndex && length >= 0 else { + return nil + } + + // both these &-s are safe, they can't underflow because both left & right side are >= 0 (and index >= readerIndex) + let indexFromReaderIndex = index &- self.readerIndex + assert(indexFromReaderIndex >= 0) + guard indexFromReaderIndex <= self.readableBytes &- length else { + return nil + } + + let upperBound = indexFromReaderIndex &+ length // safe, can't overflow, we checked it above. + + // uncheckedBounds is safe because `length` is >= 0, so the lower bound will always be lower/equal to upper + return Range(uncheckedBounds: (lower: indexFromReaderIndex, upper: upperBound)) + } +} + +// copy and pasted from NIOCore +extension UnsafeRawBufferPointer { + init(fastRebase slice: Slice) { + let base = slice.base.baseAddress?.advanced(by: slice.startIndex) + self.init(start: base, count: slice.endIndex &- slice.startIndex) + } +} diff --git a/Examples/LambdaFunctions/Tests/LinuxMain.swift b/Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift similarity index 74% rename from Examples/LambdaFunctions/Tests/LinuxMain.swift rename to Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift index c46de763..54ecb537 100644 --- a/Examples/LambdaFunctions/Tests/LinuxMain.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,4 +12,8 @@ // //===----------------------------------------------------------------------===// -preconditionFailure("use `swift test --enable-test-discovery`") +#if ServiceLifecycleSupport +import ServiceLifecycle + +extension LambdaRuntime: Service {} +#endif diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift new file mode 100644 index 00000000..5ff0daff --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOConcurrencyHelpers +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// We need `@unchecked` Sendable here, as `NIOLockedValueBox` does not understand `sending` today. +// We don't want to use `NIOLockedValueBox` here anyway. We would love to use Mutex here, but this +// sadly crashes the compiler today. +public final class LambdaRuntime: @unchecked Sendable where Handler: StreamingLambdaHandler { + // TODO: We want to change this to Mutex as soon as this doesn't crash the Swift compiler on Linux anymore + @usableFromInline + let handlerMutex: NIOLockedValueBox + @usableFromInline + let logger: Logger + @usableFromInline + let eventLoop: EventLoop + + public init( + handler: sending Handler, + eventLoop: EventLoop = Lambda.defaultEventLoop, + logger: Logger = Logger(label: "LambdaRuntime") + ) { + self.handlerMutex = NIOLockedValueBox(handler) + self.eventLoop = eventLoop + + // by setting the log level here, we understand it can not be changed dynamically at runtime + // developers have to wait for AWS Lambda to dispose and recreate a runtime environment to pickup a change + // this approach is less flexible but more performant than reading the value of the environment variable at each invocation + var log = logger + log.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info + self.logger = log + self.logger.debug("LambdaRuntime initialized") + } + + @inlinable + public func run() async throws { + let handler = self.handlerMutex.withLockedValue { handler in + let result = handler + handler = nil + return result + } + + guard let handler else { + throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) + } + + // are we running inside an AWS Lambda runtime environment ? + // AWS_LAMBDA_RUNTIME_API is set when running on Lambda + // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html + if let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") { + + let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1) + let ip = String(ipAndPort[0]) + guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: ip, port: port), + eventLoop: self.eventLoop, + logger: self.logger + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: self.logger + ) + } + + } else { + + #if LocalServerSupport + // we're not running on Lambda and we're compiled in DEBUG mode, + // let's start a local server for testing + try await Lambda.withLocalServer(invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT")) + { + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: "127.0.0.1", port: 7000), + eventLoop: self.eventLoop, + logger: self.logger + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: self.logger + ) + } + } + #else + // in release mode, we can't start a local server because the local server code is not compiled. + throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable) + #endif + } + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift new file mode 100644 index 00000000..657127d5 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift @@ -0,0 +1,857 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOCore +import NIOHTTP1 +import NIOPosix + +@usableFromInline +final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { + @usableFromInline + nonisolated let unownedExecutor: UnownedSerialExecutor + + @usableFromInline + struct Configuration: Sendable { + var ip: String + var port: Int + + @usableFromInline + init(ip: String, port: Int) { + self.ip = ip + self.port = port + } + } + + @usableFromInline + struct Writer: LambdaRuntimeClientResponseStreamWriter, Sendable { + private var runtimeClient: LambdaRuntimeClient + + fileprivate init(runtimeClient: LambdaRuntimeClient) { + self.runtimeClient = runtimeClient + } + + @usableFromInline + func write(_ buffer: NIOCore.ByteBuffer) async throws { + try await self.runtimeClient.write(buffer) + } + + @usableFromInline + func finish() async throws { + try await self.runtimeClient.writeAndFinish(nil) + } + + @usableFromInline + func writeAndFinish(_ buffer: NIOCore.ByteBuffer) async throws { + try await self.runtimeClient.writeAndFinish(buffer) + } + + @usableFromInline + func reportError(_ error: any Error) async throws { + try await self.runtimeClient.reportError(error) + } + } + + private typealias ConnectionContinuation = CheckedContinuation< + NIOLoopBound>, any Error + > + + private enum ConnectionState { + case disconnected + case connecting([ConnectionContinuation]) + case connected(Channel, LambdaChannelHandler) + } + + enum LambdaState { + /// this is the "normal" state. Transitions to `waitingForNextInvocation` + case idle(previousRequestID: String?) + /// this is the state while we wait for an invocation. A next call is running. + /// Transitions to `waitingForResponse` + case waitingForNextInvocation + /// The invocation was forwarded to the handler and we wait for a response. + /// Transitions to `sendingResponse` or `sentResponse`. + case waitingForResponse(requestID: String) + case sendingResponse(requestID: String) + case sentResponse(requestID: String) + } + + enum ClosingState { + case notClosing + case closing(CheckedContinuation) + case closed + } + + private let eventLoop: any EventLoop + private let logger: Logger + private let configuration: Configuration + + private var connectionState: ConnectionState = .disconnected + private var lambdaState: LambdaState = .idle(previousRequestID: nil) + private var closingState: ClosingState = .notClosing + + // connections that are currently being closed. In the `run` method we must await all of them + // being fully closed before we can return from it. + private var closingConnections: [any Channel] = [] + + @inlinable + static func withRuntimeClient( + configuration: Configuration, + eventLoop: any EventLoop, + logger: Logger, + _ body: (LambdaRuntimeClient) async throws -> Result + ) async throws -> Result { + let runtime = LambdaRuntimeClient(configuration: configuration, eventLoop: eventLoop, logger: logger) + let result: Swift.Result + do { + result = .success(try await body(runtime)) + } catch { + result = .failure(error) + } + + await runtime.close() + + //try? await runtime.close() + return try result.get() + } + + @usableFromInline + init(configuration: Configuration, eventLoop: any EventLoop, logger: Logger) { + self.unownedExecutor = eventLoop.executor.asUnownedSerialExecutor() + self.configuration = configuration + self.eventLoop = eventLoop + self.logger = logger + } + + @usableFromInline + func close() async { + self.logger.trace("Close lambda runtime client") + + guard case .notClosing = self.closingState else { + return + } + await withCheckedContinuation { continuation in + self.closingState = .closing(continuation) + + switch self.connectionState { + case .disconnected: + if self.closingConnections.isEmpty { + return continuation.resume() + } + + case .connecting(let continuations): + for continuation in continuations { + continuation.resume(throwing: LambdaRuntimeError(code: .closingRuntimeClient)) + } + self.connectionState = .connecting([]) + + case .connected(let channel, _): + channel.close(mode: .all, promise: nil) + } + } + } + + @usableFromInline + func nextInvocation() async throws -> (Invocation, Writer) { + try await withTaskCancellationHandler { + switch self.lambdaState { + case .idle: + self.lambdaState = .waitingForNextInvocation + let handler = try await self.makeOrGetConnection() + let invocation = try await handler.nextInvocation() + guard case .waitingForNextInvocation = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + self.lambdaState = .waitingForResponse(requestID: invocation.metadata.requestID) + return (invocation, Writer(runtimeClient: self)) + + case .waitingForNextInvocation, + .waitingForResponse, + .sendingResponse, + .sentResponse: + fatalError("Invalid state: \(self.lambdaState)") + } + } onCancel: { + Task { + await self.close() + } + } + } + + private func write(_ buffer: NIOCore.ByteBuffer) async throws { + switch self.lambdaState { + case .idle, .sentResponse: + throw LambdaRuntimeError(code: .writeAfterFinishHasBeenSent) + + case .waitingForNextInvocation: + fatalError("Invalid state: \(self.lambdaState)") + + case .waitingForResponse(let requestID): + self.lambdaState = .sendingResponse(requestID: requestID) + fallthrough + + case .sendingResponse(let requestID): + let handler = try await self.makeOrGetConnection() + guard case .sendingResponse(requestID) = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + return try await handler.writeResponseBodyPart(buffer, requestID: requestID) + } + } + + private func writeAndFinish(_ buffer: NIOCore.ByteBuffer?) async throws { + switch self.lambdaState { + case .idle, .sentResponse: + throw LambdaRuntimeError(code: .finishAfterFinishHasBeenSent) + + case .waitingForNextInvocation: + fatalError("Invalid state: \(self.lambdaState)") + + case .waitingForResponse(let requestID): + fallthrough + + case .sendingResponse(let requestID): + self.lambdaState = .sentResponse(requestID: requestID) + let handler = try await self.makeOrGetConnection() + guard case .sentResponse(requestID) = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + try await handler.finishResponseRequest(finalData: buffer, requestID: requestID) + guard case .sentResponse(requestID) = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + self.lambdaState = .idle(previousRequestID: requestID) + } + } + + private func reportError(_ error: any Error) async throws { + switch self.lambdaState { + case .idle, .waitingForNextInvocation, .sentResponse: + fatalError("Invalid state: \(self.lambdaState)") + + case .waitingForResponse(let requestID): + fallthrough + + case .sendingResponse(let requestID): + self.lambdaState = .sentResponse(requestID: requestID) + let handler = try await self.makeOrGetConnection() + guard case .sentResponse(requestID) = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + try await handler.reportError(error, requestID: requestID) + guard case .sentResponse(requestID) = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + self.lambdaState = .idle(previousRequestID: requestID) + } + } + + private func channelClosed(_ channel: any Channel) { + switch (self.connectionState, self.closingState) { + case (_, .closed): + fatalError("Invalid state: \(self.connectionState), \(self.closingState)") + + case (.disconnected, .notClosing): + if let index = self.closingConnections.firstIndex(where: { $0 === channel }) { + self.closingConnections.remove(at: index) + } + + case (.disconnected, .closing(let continuation)): + if let index = self.closingConnections.firstIndex(where: { $0 === channel }) { + self.closingConnections.remove(at: index) + } + + if self.closingConnections.isEmpty { + self.closingState = .closed + continuation.resume() + } + + case (.connecting(let array), .notClosing): + self.connectionState = .disconnected + for continuation in array { + continuation.resume(throwing: LambdaRuntimeError(code: .lostConnectionToControlPlane)) + } + + case (.connecting(let array), .closing(let continuation)): + self.connectionState = .disconnected + precondition(array.isEmpty, "If we are closing we should have failed all connection attempts already") + if self.closingConnections.isEmpty { + self.closingState = .closed + continuation.resume() + } + + case (.connected, .notClosing): + self.connectionState = .disconnected + + case (.connected, .closing(let continuation)): + self.connectionState = .disconnected + + if self.closingConnections.isEmpty { + self.closingState = .closed + continuation.resume() + } + } + } + + private func makeOrGetConnection() async throws -> LambdaChannelHandler { + switch self.connectionState { + case .disconnected: + self.connectionState = .connecting([]) + break + case .connecting(var array): + // Since we do get sequential invocations this case normally should never be hit. + // We'll support it anyway. + let loopBound = try await withCheckedThrowingContinuation { (continuation: ConnectionContinuation) in + array.append(continuation) + self.connectionState = .connecting(array) + } + return loopBound.value + case .connected(_, let handler): + return handler + } + + let bootstrap = ClientBootstrap(group: self.eventLoop) + .channelInitializer { channel in + do { + try channel.pipeline.syncOperations.addHTTPClientHandlers() + // Lambda quotas... An invocation payload is maximal 6MB in size: + // https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html + try channel.pipeline.syncOperations.addHandler( + NIOHTTPClientResponseAggregator(maxContentLength: 6 * 1024 * 1024) + ) + try channel.pipeline.syncOperations.addHandler( + LambdaChannelHandler(delegate: self, logger: self.logger, configuration: self.configuration) + ) + return channel.eventLoop.makeSucceededFuture(()) + } catch { + return channel.eventLoop.makeFailedFuture(error) + } + } + .connectTimeout(.seconds(2)) + + do { + // connect directly via socket address to avoid happy eyeballs (perf) + let address = try SocketAddress(ipAddress: self.configuration.ip, port: self.configuration.port) + let channel = try await bootstrap.connect(to: address).get() + let handler = try channel.pipeline.syncOperations.handler( + type: LambdaChannelHandler.self + ) + self.logger.trace( + "Connection to control plane created", + metadata: [ + "lambda_port": "\(self.configuration.port)", + "lambda_ip": "\(self.configuration.ip)", + ] + ) + channel.closeFuture.whenComplete { result in + self.assumeIsolated { runtimeClient in + runtimeClient.channelClosed(channel) + } + } + + switch self.connectionState { + case .disconnected, .connected: + fatalError("Unexpected state: \(self.connectionState)") + + case .connecting(let array): + self.connectionState = .connected(channel, handler) + defer { + let loopBound = NIOLoopBound(handler, eventLoop: self.eventLoop) + for continuation in array { + continuation.resume(returning: loopBound) + } + } + return handler + } + } catch { + switch self.connectionState { + case .disconnected, .connected: + fatalError("Unexpected state: \(self.connectionState)") + + case .connecting(let array): + self.connectionState = .disconnected + defer { + for continuation in array { + continuation.resume(throwing: error) + } + } + throw error + } + } + } +} + +extension LambdaRuntimeClient: LambdaChannelHandlerDelegate { + nonisolated func connectionErrorHappened(_ error: any Error, channel: any Channel) { + + } + + nonisolated func connectionWillClose(channel: any Channel) { + self.assumeIsolated { isolated in + switch isolated.connectionState { + case .disconnected: + // this case should never happen. But whatever + if channel.isActive { + isolated.closingConnections.append(channel) + } + + case .connecting(let continuations): + // this case should never happen. But whatever + if channel.isActive { + isolated.closingConnections.append(channel) + } + + for continuation in continuations { + continuation.resume(throwing: LambdaRuntimeError(code: .connectionToControlPlaneLost)) + } + + case .connected(let stateChannel, _): + guard channel === stateChannel else { + isolated.closingConnections.append(channel) + return + } + + isolated.connectionState = .disconnected + + } + } + + } +} + +private protocol LambdaChannelHandlerDelegate { + func connectionWillClose(channel: any Channel) + func connectionErrorHappened(_ error: any Error, channel: any Channel) +} + +private final class LambdaChannelHandler { + let nextInvocationPath = Consts.invocationURLPrefix + Consts.getNextInvocationURLSuffix + + enum State { + case disconnected + case connected(ChannelHandlerContext, LambdaState) + case closing + + enum LambdaState { + /// this is the "normal" state. Transitions to `waitingForNextInvocation` + case idle + /// this is the state while we wait for an invocation. A next call is running. + /// Transitions to `waitingForResponse` + case waitingForNextInvocation(CheckedContinuation) + /// The invocation was forwarded to the handler and we wait for a response. + /// Transitions to `sendingResponse` or `sentResponse`. + case waitingForResponse + case sendingResponse + case sentResponse(CheckedContinuation) + } + } + + private var state: State = .disconnected + private var lastError: Error? + private var reusableErrorBuffer: ByteBuffer? + private let logger: Logger + private let delegate: Delegate + private let configuration: LambdaRuntimeClient.Configuration + + /// These are the default headers that must be sent along an invocation + let defaultHeaders: HTTPHeaders + /// These headers must be sent along an invocation or initialization error report + let errorHeaders: HTTPHeaders + /// These headers must be sent when streaming a response + let streamingHeaders: HTTPHeaders + + init(delegate: Delegate, logger: Logger, configuration: LambdaRuntimeClient.Configuration) { + self.delegate = delegate + self.logger = logger + self.configuration = configuration + self.defaultHeaders = [ + "host": "\(self.configuration.ip):\(self.configuration.port)", + "user-agent": .userAgent, + ] + self.errorHeaders = [ + "host": "\(self.configuration.ip):\(self.configuration.port)", + "user-agent": .userAgent, + "lambda-runtime-function-error-type": "Unhandled", + ] + self.streamingHeaders = [ + "host": "\(self.configuration.ip):\(self.configuration.port)", + "user-agent": .userAgent, + "transfer-encoding": "chunked", + ] + } + + func nextInvocation(isolation: isolated (any Actor)? = #isolation) async throws -> Invocation { + switch self.state { + case .connected(let context, .idle): + return try await withCheckedThrowingContinuation { + (continuation: CheckedContinuation) in + self.state = .connected(context, .waitingForNextInvocation(continuation)) + self.sendNextRequest(context: context) + } + + case .connected(_, .sendingResponse), + .connected(_, .sentResponse), + .connected(_, .waitingForNextInvocation), + .connected(_, .waitingForResponse), + .closing: + fatalError("Invalid state: \(self.state)") + + case .disconnected: + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + } + } + + func reportError( + isolation: isolated (any Actor)? = #isolation, + _ error: any Error, + requestID: String + ) async throws { + switch self.state { + case .connected(_, .waitingForNextInvocation): + fatalError("Invalid state: \(self.state)") + + case .connected(let context, .waitingForResponse): + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.state = .connected(context, .sentResponse(continuation)) + self.sendReportErrorRequest(requestID: requestID, error: error, context: context) + } + + case .connected(let context, .sendingResponse): + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.state = .connected(context, .sentResponse(continuation)) + self.sendResponseStreamingFailure(error: error, context: context) + } + + case .connected(_, .idle), + .connected(_, .sentResponse): + // The final response has already been sent. The only way to report the unhandled error + // now is to log it. Normally this library never logs higher than debug, we make an + // exception here, as there is no other way of reporting the error otherwise. + self.logger.error( + "Unhandled error after stream has finished", + metadata: [ + "lambda_request_id": "\(requestID)", + "lambda_error": "\(String(describing: error))", + ] + ) + + case .disconnected: + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + + case .closing: + throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) + } + } + + func writeResponseBodyPart( + isolation: isolated (any Actor)? = #isolation, + _ byteBuffer: ByteBuffer, + requestID: String + ) async throws { + switch self.state { + case .connected(_, .waitingForNextInvocation): + fatalError("Invalid state: \(self.state)") + + case .connected(let context, .waitingForResponse): + self.state = .connected(context, .sendingResponse) + try await self.sendResponseBodyPart(byteBuffer, sendHeadWithRequestID: requestID, context: context) + + case .connected(let context, .sendingResponse): + try await self.sendResponseBodyPart(byteBuffer, sendHeadWithRequestID: nil, context: context) + + case .connected(_, .idle), + .connected(_, .sentResponse): + throw LambdaRuntimeError(code: .writeAfterFinishHasBeenSent) + + case .disconnected: + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + + case .closing: + throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) + } + } + + func finishResponseRequest( + isolation: isolated (any Actor)? = #isolation, + finalData: ByteBuffer?, + requestID: String + ) async throws { + switch self.state { + case .connected(_, .idle), + .connected(_, .waitingForNextInvocation): + fatalError("Invalid state: \(self.state)") + + case .connected(let context, .waitingForResponse): + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.state = .connected(context, .sentResponse(continuation)) + self.sendResponseFinish(finalData, sendHeadWithRequestID: requestID, context: context) + } + + case .connected(let context, .sendingResponse): + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.state = .connected(context, .sentResponse(continuation)) + self.sendResponseFinish(finalData, sendHeadWithRequestID: nil, context: context) + } + + case .connected(_, .sentResponse): + throw LambdaRuntimeError(code: .finishAfterFinishHasBeenSent) + + case .disconnected: + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + + case .closing: + throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) + } + } + + private func sendResponseBodyPart( + isolation: isolated (any Actor)? = #isolation, + _ byteBuffer: ByteBuffer, + sendHeadWithRequestID: String?, + context: ChannelHandlerContext + ) async throws { + + if let requestID = sendHeadWithRequestID { + // TODO: This feels super expensive. We should be able to make this cheaper. requestIDs are fixed length + let url = Consts.invocationURLPrefix + "/" + requestID + Consts.postResponseURLSuffix + + let httpRequest = HTTPRequestHead( + version: .http1_1, + method: .POST, + uri: url, + headers: self.streamingHeaders + ) + + context.write(self.wrapOutboundOut(.head(httpRequest)), promise: nil) + } + + let future = context.write(self.wrapOutboundOut(.body(.byteBuffer(byteBuffer)))) + context.flush() + try await future.get() + } + + private func sendResponseFinish( + isolation: isolated (any Actor)? = #isolation, + _ byteBuffer: ByteBuffer?, + sendHeadWithRequestID: String?, + context: ChannelHandlerContext + ) { + if let requestID = sendHeadWithRequestID { + // TODO: This feels quite expensive. We should be able to make this cheaper. requestIDs are fixed length + let url = "\(Consts.invocationURLPrefix)/\(requestID)\(Consts.postResponseURLSuffix)" + + // If we have less than 6MB, we don't want to use the streaming API. If we have more + // than 6MB we must use the streaming mode. + let headers: HTTPHeaders = + if byteBuffer?.readableBytes ?? 0 < 6_000_000 { + [ + "host": "\(self.configuration.ip):\(self.configuration.port)", + "user-agent": .userAgent, + "content-length": "\(byteBuffer?.readableBytes ?? 0)", + ] + } else { + self.streamingHeaders + } + + let httpRequest = HTTPRequestHead( + version: .http1_1, + method: .POST, + uri: url, + headers: headers + ) + + context.write(self.wrapOutboundOut(.head(httpRequest)), promise: nil) + } + + if let byteBuffer { + context.write(self.wrapOutboundOut(.body(.byteBuffer(byteBuffer))), promise: nil) + } + + context.write(self.wrapOutboundOut(.end(nil)), promise: nil) + context.flush() + } + + private func sendNextRequest(context: ChannelHandlerContext) { + let httpRequest = HTTPRequestHead( + version: .http1_1, + method: .GET, + uri: self.nextInvocationPath, + headers: self.defaultHeaders + ) + + context.write(self.wrapOutboundOut(.head(httpRequest)), promise: nil) + context.write(self.wrapOutboundOut(.end(nil)), promise: nil) + context.flush() + } + + private func sendReportErrorRequest(requestID: String, error: any Error, context: ChannelHandlerContext) { + // TODO: This feels quite expensive. We should be able to make this cheaper. requestIDs are fixed length + let url = "\(Consts.invocationURLPrefix)/\(requestID)\(Consts.postErrorURLSuffix)" + + let httpRequest = HTTPRequestHead( + version: .http1_1, + method: .POST, + uri: url, + headers: self.errorHeaders + ) + + if self.reusableErrorBuffer == nil { + self.reusableErrorBuffer = context.channel.allocator.buffer(capacity: 1024) + } else { + self.reusableErrorBuffer!.clear() + } + + let errorResponse = ErrorResponse(errorType: Consts.functionError, errorMessage: "\(error)") + // TODO: Write this directly into our ByteBuffer + let bytes = errorResponse.toJSONBytes() + self.reusableErrorBuffer!.writeBytes(bytes) + + context.write(self.wrapOutboundOut(.head(httpRequest)), promise: nil) + context.write(self.wrapOutboundOut(.body(.byteBuffer(self.reusableErrorBuffer!))), promise: nil) + context.write(self.wrapOutboundOut(.end(nil)), promise: nil) + context.flush() + } + + private func sendResponseStreamingFailure(error: any Error, context: ChannelHandlerContext) { + // TODO: Use base64 here + let trailers: HTTPHeaders = [ + "Lambda-Runtime-Function-Error-Type": "Unhandled", + "Lambda-Runtime-Function-Error-Body": "Requires base64", + ] + + context.write(self.wrapOutboundOut(.end(trailers)), promise: nil) + context.flush() + } + + func cancelCurrentRequestAndCloseConnection() { + fatalError("Unimplemented") + } +} + +extension LambdaChannelHandler: ChannelInboundHandler { + typealias OutboundIn = Never + typealias InboundIn = NIOHTTPClientResponseFull + typealias OutboundOut = HTTPClientRequestPart + + func handlerAdded(context: ChannelHandlerContext) { + if context.channel.isActive { + self.state = .connected(context, .idle) + } + } + + func channelActive(context: ChannelHandlerContext) { + switch self.state { + case .disconnected: + self.state = .connected(context, .idle) + case .connected: + break + case .closing: + fatalError("Invalid state: \(self.state)") + } + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let response = unwrapInboundIn(data) + + // handle response content + + switch self.state { + case .connected(let context, .waitingForNextInvocation(let continuation)): + do { + let metadata = try InvocationMetadata(headers: response.head.headers) + self.state = .connected(context, .waitingForResponse) + continuation.resume(returning: Invocation(metadata: metadata, event: response.body ?? ByteBuffer())) + } catch { + self.state = .closing + + self.delegate.connectionWillClose(channel: context.channel) + context.close(promise: nil) + continuation.resume( + throwing: LambdaRuntimeError(code: .invocationMissingMetadata, underlying: error) + ) + } + + case .connected(let context, .sentResponse(let continuation)): + if response.head.status == .accepted { + self.state = .connected(context, .idle) + continuation.resume() + } else { + self.state = .connected(context, .idle) + continuation.resume(throwing: LambdaRuntimeError(code: .unexpectedStatusCodeForRequest)) + } + + case .disconnected, .closing, .connected(_, _): + break + } + + // As defined in RFC 7230 Section 6.3: + // HTTP/1.1 defaults to the use of "persistent connections", allowing + // multiple requests and responses to be carried over a single + // connection. The "close" connection option is used to signal that a + // connection will not persist after the current request/response. HTTP + // implementations SHOULD support persistent connections. + // + // That's why we only assume the connection shall be closed if we receive + // a "connection = close" header. + let serverCloseConnection = + response.head.headers["connection"].contains(where: { $0.lowercased() == "close" }) + + let closeConnection = serverCloseConnection || response.head.version != .http1_1 + + if closeConnection { + // If we were succeeding the request promise here directly and closing the connection + // after succeeding the promise we may run into a race condition: + // + // The lambda runtime will ask for the next work item directly after a succeeded post + // response request. The desire for the next work item might be faster than the attempt + // to close the connection. This will lead to a situation where we try to the connection + // but the next request has already been scheduled on the connection that we want to + // close. For this reason we postpone succeeding the promise until the connection has + // been closed. This codepath will only be hit in the very, very unlikely event of the + // Lambda control plane demanding to close connection. (It's more or less only + // implemented to support http1.1 correctly.) This behavior is ensured with the test + // `LambdaTest.testNoKeepAliveServer`. + self.state = .closing + self.delegate.connectionWillClose(channel: context.channel) + context.close(promise: nil) + } + } + + func errorCaught(context: ChannelHandlerContext, error: Error) { + self.logger.trace( + "Channel error caught", + metadata: [ + "error": "\(error)" + ] + ) + // pending responses will fail with lastError in channelInactive since we are calling context.close + self.delegate.connectionErrorHappened(error, channel: context.channel) + + self.lastError = error + context.channel.close(promise: nil) + } + + func channelInactive(context: ChannelHandlerContext) { + // fail any pending responses with last error or assume peer disconnected + switch self.state { + case .connected(_, .waitingForNextInvocation(let continuation)): + continuation.resume(throwing: self.lastError ?? ChannelError.ioOnClosedChannel) + default: + break + } + + // we don't need to forward channelInactive to the delegate, as the delegate observes the + // closeFuture + context.fireChannelInactive() + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClientProtocol.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClientProtocol.swift new file mode 100644 index 00000000..cedd9f35 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClientProtocol.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +@usableFromInline +package protocol LambdaRuntimeClientResponseStreamWriter: LambdaResponseStreamWriter { + func write(_ buffer: ByteBuffer) async throws + func finish() async throws + func writeAndFinish(_ buffer: ByteBuffer) async throws + func reportError(_ error: any Error) async throws +} + +@usableFromInline +package protocol LambdaRuntimeClientProtocol { + associatedtype Writer: LambdaRuntimeClientResponseStreamWriter + + func nextInvocation() async throws -> (Invocation, Writer) +} + +@usableFromInline +package struct Invocation: Sendable { + @usableFromInline + package var metadata: InvocationMetadata + @usableFromInline + package var event: ByteBuffer + + package init(metadata: InvocationMetadata, event: ByteBuffer) { + self.metadata = metadata + self.event = event + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift new file mode 100644 index 00000000..1a52801e --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@usableFromInline +package struct LambdaRuntimeError: Error { + @usableFromInline + package enum Code: Sendable { + case closingRuntimeClient + + case connectionToControlPlaneLost + case connectionToControlPlaneGoingAway + case invocationMissingMetadata + + case writeAfterFinishHasBeenSent + case finishAfterFinishHasBeenSent + case lostConnectionToControlPlane + case unexpectedStatusCodeForRequest + + case nextInvocationMissingHeaderRequestID + case nextInvocationMissingHeaderDeadline + case nextInvocationMissingHeaderInvokeFuctionARN + + case missingLambdaRuntimeAPIEnvironmentVariable + case runtimeCanOnlyBeStartedOnce + case invalidPort + } + + @usableFromInline + package init(code: Code, underlying: (any Error)? = nil) { + self.code = code + self.underlying = underlying + } + + @usableFromInline + package var code: Code + @usableFromInline + package var underlying: (any Error)? + +} diff --git a/Sources/AWSLambdaRuntimeCore/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift similarity index 87% rename from Sources/AWSLambdaRuntimeCore/Utils.swift rename to Sources/AWSLambdaRuntime/Utils.swift index 5e7ffa6e..6ed21ed1 100644 --- a/Sources/AWSLambdaRuntimeCore/Utils.swift +++ b/Sources/AWSLambdaRuntime/Utils.swift @@ -13,9 +13,9 @@ //===----------------------------------------------------------------------===// import Dispatch -import NIO +import NIOPosix -internal enum Consts { +enum Consts { static let apiPrefix = "/2018-06-01" static let invocationURLPrefix = "\(apiPrefix)/runtime/invocation" static let getNextInvocationURLSuffix = "/next" @@ -27,7 +27,7 @@ internal enum Consts { } /// AWS Lambda HTTP Headers, used to populate the `LambdaContext` object. -internal enum AmazonHeaders { +enum AmazonHeaders { static let requestID = "Lambda-Runtime-Aws-Request-Id" static let traceID = "Lambda-Runtime-Trace-Id" static let clientContext = "X-Amz-Client-Context" @@ -37,7 +37,7 @@ internal enum AmazonHeaders { } /// Helper function to trap signals -internal func trap(signal sig: Signal, handler: @escaping (Signal) -> Void) -> DispatchSourceSignal { +func trap(signal sig: Signal, handler: @escaping (Signal) -> Void) -> DispatchSourceSignal { let signalSource = DispatchSource.makeSignalSource(signal: sig.rawValue, queue: DispatchQueue.global()) signal(sig.rawValue, SIG_IGN) signalSource.setEventHandler(handler: { @@ -48,17 +48,18 @@ internal func trap(signal sig: Signal, handler: @escaping (Signal) -> Void) -> D return signalSource } -internal enum Signal: Int32 { +enum Signal: Int32 { case HUP = 1 case INT = 2 case QUIT = 3 case ABRT = 6 - case KILL = 9 + case KILL = 9 // ignore-unacceptable-language case ALRM = 14 case TERM = 15 } -internal extension DispatchWallTime { +extension DispatchWallTime { + @usableFromInline init(millisSinceEpoch: Int64) { let nanoSinceEpoch = UInt64(millisSinceEpoch) * 1_000_000 let seconds = UInt64(nanoSinceEpoch / 1_000_000_000) @@ -80,7 +81,7 @@ extension String { while nextIndex != stringBytes.endIndex { switch stringBytes[nextIndex] { - case 0 ..< 32, UInt8(ascii: "\""), UInt8(ascii: "\\"): + case 0..<32, UInt8(ascii: "\""), UInt8(ascii: "\\"): // All Unicode characters may be placed within the // quotation marks, except for the characters that MUST be escaped: // quotation mark, reverse solidus, and the control characters (U+0000 @@ -88,7 +89,7 @@ extension String { // https://tools.ietf.org/html/rfc7159#section-7 // copy the current range over - bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex]) + bytes.append(contentsOf: stringBytes[startCopyIndex.. String { + static func generateXRayTraceID() -> String { // The version number, that is, 1. let version: UInt = 1 // The time of the original request, in Unix epoch time, in 8 hexadecimal digits. @@ -125,8 +126,9 @@ extension AmazonHeaders { let dateValue = String(now, radix: 16, uppercase: false) let datePadding = String(repeating: "0", count: max(0, 8 - dateValue.count)) // A 96-bit identifier for the trace, globally unique, in 24 hexadecimal digits. - let identifier = String(UInt64.random(in: UInt64.min ... UInt64.max) | 1 << 63, radix: 16, uppercase: false) - + String(UInt32.random(in: UInt32.min ... UInt32.max) | 1 << 31, radix: 16, uppercase: false) + let identifier = + String(UInt64.random(in: UInt64.min...UInt64.max) | 1 << 63, radix: 16, uppercase: false) + + String(UInt32.random(in: UInt32.min...UInt32.max) | 1 << 31, radix: 16, uppercase: false) return "\(version)-\(datePadding)\(dateValue)-\(identifier)" } } diff --git a/Sources/AWSLambdaRuntimeCore/HTTPClient.swift b/Sources/AWSLambdaRuntimeCore/HTTPClient.swift deleted file mode 100644 index fcd2a450..00000000 --- a/Sources/AWSLambdaRuntimeCore/HTTPClient.swift +++ /dev/null @@ -1,343 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIO -import NIOConcurrencyHelpers -import NIOHTTP1 - -/// A barebone HTTP client to interact with AWS Runtime Engine which is an HTTP server. -/// Note that Lambda Runtime API dictate that only one requests runs at a time. -/// This means we can avoid locks and other concurrency concern we would otherwise need to build into the client -internal final class HTTPClient { - private let eventLoop: EventLoop - private let configuration: Lambda.Configuration.RuntimeEngine - private let targetHost: String - - private var state = State.disconnected - private var executing = false - - init(eventLoop: EventLoop, configuration: Lambda.Configuration.RuntimeEngine) { - self.eventLoop = eventLoop - self.configuration = configuration - self.targetHost = "\(self.configuration.ip):\(self.configuration.port)" - } - - func get(url: String, headers: HTTPHeaders, timeout: TimeAmount? = nil) -> EventLoopFuture { - self.execute(Request(targetHost: self.targetHost, - url: url, - method: .GET, - headers: headers, - timeout: timeout ?? self.configuration.requestTimeout)) - } - - func post(url: String, headers: HTTPHeaders, body: ByteBuffer?, timeout: TimeAmount? = nil) -> EventLoopFuture { - self.execute(Request(targetHost: self.targetHost, - url: url, - method: .POST, - headers: headers, - body: body, - timeout: timeout ?? self.configuration.requestTimeout)) - } - - /// cancels the current request if there is one - func cancel() { - guard self.executing else { - // there is no request running. nothing to cancel - return - } - - guard case .connected(let channel) = self.state else { - preconditionFailure("if we are executing, we expect to have an open channel") - } - - channel.triggerUserOutboundEvent(RequestCancelEvent(), promise: nil) - } - - // TODO: cap reconnect attempt - private func execute(_ request: Request, validate: Bool = true) -> EventLoopFuture { - if validate { - precondition(self.executing == false, "expecting single request at a time") - self.executing = true - } - - switch self.state { - case .disconnected: - return self.connect().flatMap { channel -> EventLoopFuture in - self.state = .connected(channel) - return self.execute(request, validate: false) - } - case .connected(let channel): - guard channel.isActive else { - self.state = .disconnected - return self.execute(request, validate: false) - } - - let promise = channel.eventLoop.makePromise(of: Response.self) - promise.futureResult.whenComplete { _ in - precondition(self.executing == true, "invalid execution state") - self.executing = false - } - let wrapper = HTTPRequestWrapper(request: request, promise: promise) - channel.writeAndFlush(wrapper).cascadeFailure(to: promise) - return promise.futureResult - } - } - - private func connect() -> EventLoopFuture { - let bootstrap = ClientBootstrap(group: self.eventLoop) - .channelInitializer { channel in - channel.pipeline.addHTTPClientHandlers().flatMap { - channel.pipeline.addHandlers([HTTPHandler(keepAlive: self.configuration.keepAlive), - UnaryHandler(keepAlive: self.configuration.keepAlive)]) - } - } - - do { - // connect directly via socket address to avoid happy eyeballs (perf) - let address = try SocketAddress(ipAddress: self.configuration.ip, port: self.configuration.port) - return bootstrap.connect(to: address) - } catch { - return self.eventLoop.makeFailedFuture(error) - } - } - - internal struct Request: Equatable { - let url: String - let method: HTTPMethod - let targetHost: String - let headers: HTTPHeaders - let body: ByteBuffer? - let timeout: TimeAmount? - - init(targetHost: String, url: String, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: ByteBuffer? = nil, timeout: TimeAmount?) { - self.targetHost = targetHost - self.url = url - self.method = method - self.headers = headers - self.body = body - self.timeout = timeout - } - } - - internal struct Response: Equatable { - public var version: HTTPVersion - public var status: HTTPResponseStatus - public var headers: HTTPHeaders - public var body: ByteBuffer? - } - - internal enum Errors: Error { - case connectionResetByPeer - case timeout - case cancelled - } - - private enum State { - case disconnected - case connected(Channel) - } -} - -private final class HTTPHandler: ChannelDuplexHandler { - typealias OutboundIn = HTTPClient.Request - typealias InboundOut = HTTPClient.Response - typealias InboundIn = HTTPClientResponsePart - typealias OutboundOut = HTTPClientRequestPart - - private let keepAlive: Bool - private var readState: ReadState = .idle - - init(keepAlive: Bool) { - self.keepAlive = keepAlive - } - - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { - let request = unwrapOutboundIn(data) - - var head = HTTPRequestHead(version: .init(major: 1, minor: 1), method: request.method, uri: request.url, headers: request.headers) - head.headers.add(name: "host", value: request.targetHost) - switch request.method { - case .POST, .PUT: - head.headers.add(name: "content-length", value: String(request.body?.readableBytes ?? 0)) - default: - break - } - - // We don't add a "Connection" header here if we want to keep the connection open, - // HTTP/1.1 defines specifies the following in RFC 2616, Section 8.1.2.1: - // - // An HTTP/1.1 server MAY assume that a HTTP/1.1 client intends to - // maintain a persistent connection unless a Connection header including - // the connection-token "close" was sent in the request. If the server - // chooses to close the connection immediately after sending the - // response, it SHOULD send a Connection header including the - // connection-token close. - // - // See also UnaryHandler.channelRead below. - if !self.keepAlive { - head.headers.add(name: "connection", value: "close") - } - - context.write(self.wrapOutboundOut(HTTPClientRequestPart.head(head))).flatMap { _ -> EventLoopFuture in - if let body = request.body { - return context.writeAndFlush(self.wrapOutboundOut(HTTPClientRequestPart.body(.byteBuffer(body)))) - } else { - context.flush() - return context.eventLoop.makeSucceededFuture(()) - } - }.cascade(to: promise) - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let response = unwrapInboundIn(data) - - switch response { - case .head(let head): - guard case .idle = self.readState else { - preconditionFailure("invalid read state \(self.readState)") - } - self.readState = .head(head) - case .body(var bodyPart): - switch self.readState { - case .head(let head): - self.readState = .body(head, bodyPart) - case .body(let head, var body): - body.writeBuffer(&bodyPart) - self.readState = .body(head, body) - default: - preconditionFailure("invalid read state \(self.readState)") - } - case .end: - switch self.readState { - case .head(let head): - context.fireChannelRead(wrapInboundOut(HTTPClient.Response(version: head.version, status: head.status, headers: head.headers, body: nil))) - self.readState = .idle - case .body(let head, let body): - context.fireChannelRead(wrapInboundOut(HTTPClient.Response(version: head.version, status: head.status, headers: head.headers, body: body))) - self.readState = .idle - default: - preconditionFailure("invalid read state \(self.readState)") - } - } - } - - private enum ReadState { - case idle - case head(HTTPResponseHead) - case body(HTTPResponseHead, ByteBuffer) - } -} - -// no need in locks since we validate only one request can run at a time -private final class UnaryHandler: ChannelDuplexHandler { - typealias OutboundIn = HTTPRequestWrapper - typealias InboundIn = HTTPClient.Response - typealias OutboundOut = HTTPClient.Request - - private let keepAlive: Bool - - private var pending: (promise: EventLoopPromise, timeout: Scheduled?)? - private var lastError: Error? - - init(keepAlive: Bool) { - self.keepAlive = keepAlive - } - - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { - guard self.pending == nil else { - preconditionFailure("invalid state, outstanding request") - } - let wrapper = unwrapOutboundIn(data) - let timeoutTask = wrapper.request.timeout.map { - context.eventLoop.scheduleTask(in: $0) { - if self.pending != nil { - context.pipeline.fireErrorCaught(HTTPClient.Errors.timeout) - } - } - } - self.pending = (promise: wrapper.promise, timeout: timeoutTask) - context.writeAndFlush(wrapOutboundOut(wrapper.request), promise: promise) - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let response = unwrapInboundIn(data) - guard let pending = self.pending else { - preconditionFailure("invalid state, no pending request") - } - - // As defined in RFC 7230 Section 6.3: - // HTTP/1.1 defaults to the use of "persistent connections", allowing - // multiple requests and responses to be carried over a single - // connection. The "close" connection option is used to signal that a - // connection will not persist after the current request/response. HTTP - // implementations SHOULD support persistent connections. - // - // That's why we only assume the connection shall be closed if we receive - // a "connection = close" header. - let serverCloseConnection = response.headers.first(name: "connection")?.lowercased() == "close" - - if !self.keepAlive || serverCloseConnection || response.version != .init(major: 1, minor: 1) { - pending.promise.futureResult.whenComplete { _ in - _ = context.channel.close() - } - } - self.completeWith(.success(response)) - } - - func errorCaught(context: ChannelHandlerContext, error: Error) { - // pending responses will fail with lastError in channelInactive since we are calling context.close - self.lastError = error - context.channel.close(promise: nil) - } - - func channelInactive(context: ChannelHandlerContext) { - // fail any pending responses with last error or assume peer disconnected - if self.pending != nil { - let error = self.lastError ?? HTTPClient.Errors.connectionResetByPeer - self.completeWith(.failure(error)) - } - context.fireChannelInactive() - } - - func triggerUserOutboundEvent(context: ChannelHandlerContext, event: Any, promise: EventLoopPromise?) { - switch event { - case is RequestCancelEvent: - if self.pending != nil { - self.completeWith(.failure(HTTPClient.Errors.cancelled)) - // after the cancel error has been send, we want to close the connection so - // that no more packets can be read on this connection. - _ = context.channel.close() - } - default: - context.triggerUserOutboundEvent(event, promise: promise) - } - } - - private func completeWith(_ result: Result) { - guard let pending = self.pending else { - preconditionFailure("invalid state, no pending request") - } - self.pending = nil - self.lastError = nil - pending.timeout?.cancel() - pending.promise.completeWith(result) - } -} - -private struct HTTPRequestWrapper { - let request: HTTPClient.Request - let promise: EventLoopPromise -} - -private struct RequestCancelEvent {} diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift deleted file mode 100644 index 48cea94e..00000000 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ /dev/null @@ -1,302 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if DEBUG -import Dispatch -import Logging -import NIO -import NIOConcurrencyHelpers -import NIOHTTP1 - -// This functionality is designed for local testing hence beind a #if DEBUG flag. -// For example: -// -// try Lambda.withLocalServer { -// Lambda.run { (context: Lambda.Context, event: String, callback: @escaping (Result) -> Void) in -// callback(.success("Hello, \(event)!")) -// } -// } -extension Lambda { - /// Execute code in the context of a mock Lambda server. - /// - /// - parameters: - /// - invocationEndpoint: The endpoint to post events to. - /// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call. - /// - /// - note: This API is designed stricly for local testing and is behind a DEBUG flag - internal static func withLocalServer(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value { - let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) - try server.start().wait() - defer { try! server.stop() } - return body() - } -} - -// MARK: - Local Mock Server - -private enum LocalLambda { - struct Server { - private let logger: Logger - private let group: EventLoopGroup - private let host: String - private let port: Int - private let invocationEndpoint: String - - public init(invocationEndpoint: String?) { - let configuration = Lambda.Configuration() - var logger = Logger(label: "LocalLambdaServer") - logger.logLevel = configuration.general.logLevel - self.logger = logger - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.host = configuration.runtimeEngine.ip - self.port = configuration.runtimeEngine.port - self.invocationEndpoint = invocationEndpoint ?? "/invoke" - } - - func start() -> EventLoopFuture { - let bootstrap = ServerBootstrap(group: group) - .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) - .childChannelInitializer { channel in - channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in - channel.pipeline.addHandler(HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint)) - } - } - return bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in - guard channel.localAddress != nil else { - return channel.eventLoop.makeFailedFuture(ServerError.cantBind) - } - self.logger.info("LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)") - return channel.eventLoop.makeSucceededFuture(()) - } - } - - func stop() throws { - try self.group.syncShutdownGracefully() - } - } - - final class HTTPHandler: ChannelInboundHandler { - public typealias InboundIn = HTTPServerRequestPart - public typealias OutboundOut = HTTPServerResponsePart - - private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - - private static var invocations = CircularBuffer() - private static var invocationState = InvocationState.waitingForLambdaRequest - - private let logger: Logger - private let invocationEndpoint: String - - init(logger: Logger, invocationEndpoint: String) { - self.logger = logger - self.invocationEndpoint = invocationEndpoint - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let requestPart = unwrapInboundIn(data) - - switch requestPart { - case .head(let head): - self.pending.append((head: head, body: nil)) - case .body(var buffer): - var request = self.pending.removeFirst() - if request.body == nil { - request.body = buffer - } else { - request.body!.writeBuffer(&buffer) - } - self.pending.prepend(request) - case .end: - let request = self.pending.removeFirst() - self.processRequest(context: context, request: request) - } - } - - func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { - switch (request.head.method, request.head.uri) { - // this endpoint is called by the client invoking the lambda - case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): - guard let work = request.body else { - return self.writeResponse(context: context, response: .init(status: .badRequest)) - } - let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: - let promise = context.eventLoop.makePromise(of: Response.self) - promise.futureResult.whenComplete { result in - switch result { - case .failure(let error): - self.logger.error("invocation error: \(error)") - self.writeResponse(context: context, response: .init(status: .internalServerError)) - case .success(let response): - self.writeResponse(context: context, response: response) - } - } - let invocation = Invocation(requestID: requestID, request: work, responsePromise: promise) - switch Self.invocationState { - case .waitingForInvocation(let promise): - promise.succeed(invocation) - case .waitingForLambdaRequest, .waitingForLambdaResponse: - Self.invocations.append(invocation) - } - - // lambda invocation using the wrong http method - case (_, let url) where url.hasSuffix(self.invocationEndpoint): - self.writeResponse(context: context, status: .methodNotAllowed) - - // /next endpoint is called by the lambda polling for work - case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): - // check if our server is in the correct state - guard case .waitingForLambdaRequest = Self.invocationState else { - self.logger.error("invalid invocation state \(Self.invocationState)") - self.writeResponse(context: context, response: .init(status: .unprocessableEntity)) - return - } - - // pop the first task from the queue - switch Self.invocations.popFirst() { - case .none: - // if there is nothing in the queue, - // create a promise that we can fullfill when we get a new task - let promise = context.eventLoop.makePromise(of: Invocation.self) - promise.futureResult.whenComplete { result in - switch result { - case .failure(let error): - self.logger.error("invocation error: \(error)") - self.writeResponse(context: context, status: .internalServerError) - case .success(let invocation): - Self.invocationState = .waitingForLambdaResponse(invocation) - self.writeResponse(context: context, response: invocation.makeResponse()) - } - } - Self.invocationState = .waitingForInvocation(promise) - case .some(let invocation): - // if there is a task pending, we can immediatly respond with it. - Self.invocationState = .waitingForLambdaResponse(invocation) - self.writeResponse(context: context, response: invocation.makeResponse()) - } - - // :requestID/response endpoint is called by the lambda posting the response - case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): - let parts = request.head.uri.split(separator: "/") - guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { - // the request is malformed, since we were expecting a requestId in the path - return self.writeResponse(context: context, status: .badRequest) - } - guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { - // a response was send, but we did not expect to receive one - self.logger.error("invalid invocation state \(Self.invocationState)") - return self.writeResponse(context: context, status: .unprocessableEntity) - } - guard requestID == invocation.requestID else { - // the request's requestId is not matching the one we are expecting - self.logger.error("invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)") - return self.writeResponse(context: context, status: .badRequest) - } - - invocation.responsePromise.succeed(.init(status: .ok, body: request.body)) - self.writeResponse(context: context, status: .accepted) - Self.invocationState = .waitingForLambdaRequest - - // :requestID/error endpoint is called by the lambda posting an error response - case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): - let parts = request.head.uri.split(separator: "/") - guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { - // the request is malformed, since we were expecting a requestId in the path - return self.writeResponse(context: context, status: .badRequest) - } - guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { - // a response was send, but we did not expect to receive one - self.logger.error("invalid invocation state \(Self.invocationState)") - return self.writeResponse(context: context, status: .unprocessableEntity) - } - guard requestID == invocation.requestID else { - // the request's requestId is not matching the one we are expecting - self.logger.error("invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)") - return self.writeResponse(context: context, status: .badRequest) - } - - invocation.responsePromise.succeed(.init(status: .internalServerError, body: request.body)) - self.writeResponse(context: context, status: .accepted) - Self.invocationState = .waitingForLambdaRequest - - // unknown call - default: - self.writeResponse(context: context, status: .notFound) - } - } - - func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus) { - self.writeResponse(context: context, response: .init(status: status)) - } - - func writeResponse(context: ChannelHandlerContext, response: Response) { - var headers = HTTPHeaders(response.headers ?? []) - headers.add(name: "content-length", value: "\(response.body?.readableBytes ?? 0)") - let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: response.status, headers: headers) - - context.write(wrapOutboundOut(.head(head))).whenFailure { error in - self.logger.error("\(self) write error \(error)") - } - - if let buffer = response.body { - context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in - self.logger.error("\(self) write error \(error)") - } - } - - context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in - if case .failure(let error) = result { - self.logger.error("\(self) write error \(error)") - } - } - } - - struct Response { - var status: HTTPResponseStatus = .ok - var headers: [(String, String)]? - var body: ByteBuffer? - } - - struct Invocation { - let requestID: String - let request: ByteBuffer - let responsePromise: EventLoopPromise - - func makeResponse() -> Response { - var response = Response() - response.body = self.request - // required headers - response.headers = [ - (AmazonHeaders.requestID, self.requestID), - (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime"), - (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), - ] - return response - } - } - - enum InvocationState { - case waitingForInvocation(EventLoopPromise) - case waitingForLambdaRequest - case waitingForLambdaResponse(Invocation) - } - } - - enum ServerError: Error { - case notReady - case cantBind - } -} - -#endif diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+String.swift b/Sources/AWSLambdaRuntimeCore/Lambda+String.swift deleted file mode 100644 index 9d37356a..00000000 --- a/Sources/AWSLambdaRuntimeCore/Lambda+String.swift +++ /dev/null @@ -1,108 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import NIO - -/// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `String` events. -extension Lambda { - /// An asynchronous Lambda Closure that takes a `String` and returns a `Result` via a completion handler. - public typealias StringClosure = (Lambda.Context, String, @escaping (Result) -> Void) -> Void - - /// Run a Lambda defined by implementing the `StringClosure` function. - /// - /// - parameters: - /// - closure: `StringClosure` based Lambda. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ closure: @escaping StringClosure) { - if case .failure(let error) = self.run(closure: closure) { - fatalError("\(error)") - } - } - - /// An asynchronous Lambda Closure that takes a `String` and returns a `Result` via a completion handler. - public typealias StringVoidClosure = (Lambda.Context, String, @escaping (Result) -> Void) -> Void - - /// Run a Lambda defined by implementing the `StringVoidClosure` function. - /// - /// - parameters: - /// - closure: `StringVoidClosure` based Lambda. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ closure: @escaping StringVoidClosure) { - if case .failure(let error) = self.run(closure: closure) { - fatalError("\(error)") - } - } - - // for testing - internal static func run(configuration: Configuration = .init(), closure: @escaping StringClosure) -> Result { - self.run(configuration: configuration, handler: StringClosureWrapper(closure)) - } - - // for testing - internal static func run(configuration: Configuration = .init(), closure: @escaping StringVoidClosure) -> Result { - self.run(configuration: configuration, handler: StringVoidClosureWrapper(closure)) - } -} - -internal struct StringClosureWrapper: LambdaHandler { - typealias In = String - typealias Out = String - - private let closure: Lambda.StringClosure - - init(_ closure: @escaping Lambda.StringClosure) { - self.closure = closure - } - - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - self.closure(context, event, callback) - } -} - -internal struct StringVoidClosureWrapper: LambdaHandler { - typealias In = String - typealias Out = Void - - private let closure: Lambda.StringVoidClosure - - init(_ closure: @escaping Lambda.StringVoidClosure) { - self.closure = closure - } - - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - self.closure(context, event, callback) - } -} - -public extension EventLoopLambdaHandler where In == String { - /// Implementation of a `ByteBuffer` to `String` decoding - func decode(buffer: ByteBuffer) throws -> String { - var buffer = buffer - guard let string = buffer.readString(length: buffer.readableBytes) else { - fatalError("buffer.readString(length: buffer.readableBytes) failed") - } - return string - } -} - -public extension EventLoopLambdaHandler where Out == String { - /// Implementation of `String` to `ByteBuffer` encoding - func encode(allocator: ByteBufferAllocator, value: String) throws -> ByteBuffer? { - // FIXME: reusable buffer - var buffer = allocator.buffer(capacity: value.utf8.count) - buffer.writeString(value) - return buffer - } -} diff --git a/Sources/AWSLambdaRuntimeCore/Lambda.swift b/Sources/AWSLambdaRuntimeCore/Lambda.swift deleted file mode 100644 index 5dc27648..00000000 --- a/Sources/AWSLambdaRuntimeCore/Lambda.swift +++ /dev/null @@ -1,154 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if os(Linux) -import Glibc -#else -import Darwin.C -#endif - -import Backtrace -import Logging -import NIO - -public enum Lambda { - public typealias Handler = ByteBufferLambdaHandler - - /// `ByteBufferLambdaHandler` factory. - /// - /// A function that takes a `InitializationContext` and returns an `EventLoopFuture` of a `ByteBufferLambdaHandler` - public typealias HandlerFactory = (InitializationContext) -> EventLoopFuture - - /// Run a Lambda defined by implementing the `LambdaHandler` protocol. - /// - /// - parameters: - /// - handler: `ByteBufferLambdaHandler` based Lambda. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ handler: Handler) { - if case .failure(let error) = self.run(handler: handler) { - fatalError("\(error)") - } - } - - /// Run a Lambda defined by implementing the `LambdaHandler` protocol provided via a `LambdaHandlerFactory`. - /// Use this to initialize all your resources that you want to cache between invocations. This could be database connections and HTTP clients for example. - /// It is encouraged to use the given `EventLoop`'s conformance to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance. - /// - /// - parameters: - /// - factory: A `ByteBufferLambdaHandler` factory. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ factory: @escaping HandlerFactory) { - if case .failure(let error) = self.run(factory: factory) { - fatalError("\(error)") - } - } - - /// Run a Lambda defined by implementing the `LambdaHandler` protocol provided via a factory, typically a constructor. - /// - /// - parameters: - /// - factory: A `ByteBufferLambdaHandler` factory. - /// - /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - public static func run(_ factory: @escaping (InitializationContext) throws -> Handler) { - if case .failure(let error) = self.run(factory: factory) { - fatalError("\(error)") - } - } - - /// Utility to access/read environment variables - public static func env(_ name: String) -> String? { - guard let value = getenv(name) else { - return nil - } - return String(cString: value) - } - - // for testing and internal use - internal static func run(configuration: Configuration = .init(), handler: Handler) -> Result { - self.run(configuration: configuration, factory: { $0.eventLoop.makeSucceededFuture(handler) }) - } - - // for testing and internal use - internal static func run(configuration: Configuration = .init(), factory: @escaping (InitializationContext) throws -> Handler) -> Result { - self.run(configuration: configuration, factory: { context -> EventLoopFuture in - let promise = context.eventLoop.makePromise(of: Handler.self) - // if we have a callback based handler factory, we offload the creation of the handler - // onto the default offload queue, to ensure that the eventloop is never blocked. - Lambda.defaultOffloadQueue.async { - do { - promise.succeed(try factory(context)) - } catch { - promise.fail(error) - } - } - return promise.futureResult - }) - } - - // for testing and internal use - internal static func run(configuration: Configuration = .init(), factory: @escaping HandlerFactory) -> Result { - let _run = { (configuration: Configuration, factory: @escaping HandlerFactory) -> Result in - Backtrace.install() - var logger = Logger(label: "Lambda") - logger.logLevel = configuration.general.logLevel - - var result: Result! - MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in - let lifecycle = Lifecycle(eventLoop: eventLoop, logger: logger, configuration: configuration, factory: factory) - #if DEBUG - let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in - logger.info("intercepted signal: \(signal)") - lifecycle.shutdown() - } - #endif - - lifecycle.start().flatMap { - lifecycle.shutdownFuture - }.whenComplete { lifecycleResult in - #if DEBUG - signalSource.cancel() - #endif - eventLoop.shutdownGracefully { error in - if let error = error { - preconditionFailure("Failed to shutdown eventloop: \(error)") - } - } - result = lifecycleResult - } - } - - logger.info("shutdown completed") - return result - } - - // start local server for debugging in DEBUG mode only - #if DEBUG - if Lambda.env("LOCAL_LAMBDA_SERVER_ENABLED").flatMap(Bool.init) ?? false { - do { - return try Lambda.withLocalServer { - _run(configuration, factory) - } - } catch { - return .failure(error) - } - } else { - return _run(configuration, factory) - } - #else - return _run(configuration, factory) - #endif - } -} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift b/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift deleted file mode 100644 index 9b9ec8fb..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift +++ /dev/null @@ -1,90 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Dispatch -import Logging -import NIO - -extension Lambda { - internal struct Configuration: CustomStringConvertible { - let general: General - let lifecycle: Lifecycle - let runtimeEngine: RuntimeEngine - - init() { - self.init(general: .init(), lifecycle: .init(), runtimeEngine: .init()) - } - - init(general: General? = nil, lifecycle: Lifecycle? = nil, runtimeEngine: RuntimeEngine? = nil) { - self.general = general ?? General() - self.lifecycle = lifecycle ?? Lifecycle() - self.runtimeEngine = runtimeEngine ?? RuntimeEngine() - } - - struct General: CustomStringConvertible { - let logLevel: Logger.Level - - init(logLevel: Logger.Level? = nil) { - self.logLevel = logLevel ?? env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info - } - - var description: String { - "\(General.self)(logLevel: \(self.logLevel))" - } - } - - struct Lifecycle: CustomStringConvertible { - let id: String - let maxTimes: Int - let stopSignal: Signal - - init(id: String? = nil, maxTimes: Int? = nil, stopSignal: Signal? = nil) { - self.id = id ?? "\(DispatchTime.now().uptimeNanoseconds)" - self.maxTimes = maxTimes ?? env("MAX_REQUESTS").flatMap(Int.init) ?? 0 - self.stopSignal = stopSignal ?? env("STOP_SIGNAL").flatMap(Int32.init).flatMap(Signal.init) ?? Signal.TERM - precondition(self.maxTimes >= 0, "maxTimes must be equal or larger than 0") - } - - var description: String { - "\(Lifecycle.self)(id: \(self.id), maxTimes: \(self.maxTimes), stopSignal: \(self.stopSignal))" - } - } - - struct RuntimeEngine: CustomStringConvertible { - let ip: String - let port: Int - let keepAlive: Bool - let requestTimeout: TimeAmount? - - init(address: String? = nil, keepAlive: Bool? = nil, requestTimeout: TimeAmount? = nil) { - let ipPort = (address ?? env("AWS_LAMBDA_RUNTIME_API"))?.split(separator: ":") ?? ["127.0.0.1", "7000"] - guard ipPort.count == 2, let port = Int(ipPort[1]) else { - preconditionFailure("invalid ip+port configuration \(ipPort)") - } - self.ip = String(ipPort[0]) - self.port = port - self.keepAlive = keepAlive ?? env("KEEP_ALIVE").flatMap(Bool.init) ?? true - self.requestTimeout = requestTimeout ?? env("REQUEST_TIMEOUT").flatMap(Int64.init).flatMap { .milliseconds($0) } - } - - var description: String { - "\(RuntimeEngine.self)(ip: \(self.ip), port: \(self.port), keepAlive: \(self.keepAlive), requestTimeout: \(String(describing: self.requestTimeout))" - } - } - - var description: String { - "\(Configuration.self)\n \(self.general))\n \(self.lifecycle)\n \(self.runtimeEngine)" - } - } -} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift deleted file mode 100644 index ab30dd7b..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift +++ /dev/null @@ -1,148 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Dispatch -import Logging -import NIO - -// MARK: - InitializationContext - -extension Lambda { - /// Lambda runtime initialization context. - /// The Lambda runtime generates and passes the `InitializationContext` to the Lambda factory as an argument. - public final class InitializationContext { - /// `Logger` to log with - /// - /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. - public let logger: Logger - - /// The `EventLoop` the Lambda is executed on. Use this to schedule work with. - /// - /// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care. - /// Most importantly the `EventLoop` must never be blocked. - public let eventLoop: EventLoop - - /// `ByteBufferAllocator` to allocate `ByteBuffer` - public let allocator: ByteBufferAllocator - - internal init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator) { - self.eventLoop = eventLoop - self.logger = logger - self.allocator = allocator - } - } -} - -// MARK: - Context - -extension Lambda { - /// Lambda runtime context. - /// The Lambda runtime generates and passes the `Context` to the Lambda handler as an argument. - public final class Context: CustomDebugStringConvertible { - /// The request ID, which identifies the request that triggered the function invocation. - public let requestID: String - - /// The AWS X-Ray tracing header. - public let traceID: String - - /// The ARN of the Lambda function, version, or alias that's specified in the invocation. - public let invokedFunctionARN: String - - /// The timestamp that the function times out - public let deadline: DispatchWallTime - - /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. - public let cognitoIdentity: String? - - /// For invocations from the AWS Mobile SDK, data about the client application and device. - public let clientContext: String? - - /// `Logger` to log with - /// - /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. - public let logger: Logger - - /// The `EventLoop` the Lambda is executed on. Use this to schedule work with. - /// This is useful when implementing the `EventLoopLambdaHandler` protocol. - /// - /// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care. - /// Most importantly the `EventLoop` must never be blocked. - public let eventLoop: EventLoop - - /// `ByteBufferAllocator` to allocate `ByteBuffer` - /// This is useful when implementing `EventLoopLambdaHandler` - public let allocator: ByteBufferAllocator - - internal init(requestID: String, - traceID: String, - invokedFunctionARN: String, - deadline: DispatchWallTime, - cognitoIdentity: String? = nil, - clientContext: String? = nil, - logger: Logger, - eventLoop: EventLoop, - allocator: ByteBufferAllocator) { - self.requestID = requestID - self.traceID = traceID - self.invokedFunctionARN = invokedFunctionARN - self.cognitoIdentity = cognitoIdentity - self.clientContext = clientContext - self.deadline = deadline - // utility - self.eventLoop = eventLoop - self.allocator = allocator - // mutate logger with context - var logger = logger - logger[metadataKey: "awsRequestID"] = .string(requestID) - logger[metadataKey: "awsTraceID"] = .string(traceID) - self.logger = logger - } - - public func getRemainingTime() -> TimeAmount { - let deadline = self.deadline.millisSinceEpoch - let now = DispatchWallTime.now().millisSinceEpoch - - let remaining = deadline - now - return .milliseconds(remaining) - } - - public var debugDescription: String { - "\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(self.clientContext ?? "nil"), deadline: \(self.deadline))" - } - } -} - -// MARK: - ShutdownContext - -extension Lambda { - /// Lambda runtime shutdown context. - /// The Lambda runtime generates and passes the `ShutdownContext` to the Lambda handler as an argument. - public final class ShutdownContext { - /// `Logger` to log with - /// - /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. - public let logger: Logger - - /// The `EventLoop` the Lambda is executed on. Use this to schedule work with. - /// - /// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care. - /// Most importantly the `EventLoop` must never be blocked. - public let eventLoop: EventLoop - - internal init(logger: Logger, eventLoop: EventLoop) { - self.eventLoop = eventLoop - self.logger = logger - } - } -} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift deleted file mode 100644 index 16eba1cb..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift +++ /dev/null @@ -1,206 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Dispatch -import NIO - -// MARK: - LambdaHandler - -/// Strongly typed, callback based processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out` asynchronously. -/// `LambdaHandler` implements `EventLoopLambdaHandler`, performing callback to `EventLoopFuture` mapping, over a `DispatchQueue` for safety. -/// -/// - note: To implement a Lambda, implement either `LambdaHandler` or the `EventLoopLambdaHandler` protocol. -/// The `LambdaHandler` will offload the Lambda execution to a `DispatchQueue` making processing safer but slower. -/// The `EventLoopLambdaHandler` will execute the Lambda on the same `EventLoop` as the core runtime engine, making the processing faster but requires -/// more care from the implementation to never block the `EventLoop`. -public protocol LambdaHandler: EventLoopLambdaHandler { - /// Defines to which `DispatchQueue` the Lambda execution is offloaded to. - var offloadQueue: DispatchQueue { get } - - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. - /// - callback: Completion handler to report the result of the Lambda back to the runtime engine. - /// The completion handler expects a `Result` with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) -} - -internal extension Lambda { - static let defaultOffloadQueue = DispatchQueue(label: "LambdaHandler.offload") -} - -public extension LambdaHandler { - /// The queue on which `handle` is invoked on. - var offloadQueue: DispatchQueue { - Lambda.defaultOffloadQueue - } - - /// `LambdaHandler` is offloading the processing to a `DispatchQueue` - /// This is slower but safer, in case the implementation blocks the `EventLoop` - /// Performance sensitive Lambdas should be based on `EventLoopLambdaHandler` which does not offload. - func handle(context: Lambda.Context, event: In) -> EventLoopFuture { - let promise = context.eventLoop.makePromise(of: Out.self) - // FIXME: reusable DispatchQueue - self.offloadQueue.async { - self.handle(context: context, event: event, callback: promise.completeWith) - } - return promise.futureResult - } -} - -public extension LambdaHandler { - func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture { - let promise = context.eventLoop.makePromise(of: Void.self) - self.offloadQueue.async { - do { - try self.syncShutdown(context: context) - promise.succeed(()) - } catch { - promise.fail(error) - } - } - return promise.futureResult - } - - /// Clean up the Lambda resources synchronously. - /// Concrete Lambda handlers implement this method to shutdown resources like `HTTPClient`s and database connections. - func syncShutdown(context: Lambda.ShutdownContext) throws { - // noop - } -} - -// MARK: - EventLoopLambdaHandler - -/// Strongly typed, `EventLoopFuture` based processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out` asynchronously. -/// `EventLoopLambdaHandler` extends `ByteBufferLambdaHandler`, performing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding. -/// -/// - note: To implement a Lambda, implement either `LambdaHandler` or the `EventLoopLambdaHandler` protocol. -/// The `LambdaHandler` will offload the Lambda execution to a `DispatchQueue` making processing safer but slower -/// The `EventLoopLambdaHandler` will execute the Lambda on the same `EventLoop` as the core runtime engine, making the processing faster but requires -/// more care from the implementation to never block the `EventLoop`. -public protocol EventLoopLambdaHandler: ByteBufferLambdaHandler { - associatedtype In - associatedtype Out - - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. - /// - /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. - /// The `EventLoopFuture` should be completed with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In) -> EventLoopFuture - - /// Encode a response of type `Out` to `ByteBuffer` - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - parameters: - /// - allocator: A `ByteBufferAllocator` to help allocate the `ByteBuffer`. - /// - value: Response of type `Out`. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(allocator: ByteBufferAllocator, value: Out) throws -> ByteBuffer? - - /// Decode a`ByteBuffer` to a request or event of type `In` - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - /// - parameters: - /// - buffer: The `ByteBuffer` to decode. - /// - /// - Returns: A request or event of type `In`. - func decode(buffer: ByteBuffer) throws -> In -} - -public extension EventLoopLambdaHandler { - /// Driver for `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding - func handle(context: Lambda.Context, event: ByteBuffer) -> EventLoopFuture { - switch self.decodeIn(buffer: event) { - case .failure(let error): - return context.eventLoop.makeFailedFuture(CodecError.requestDecoding(error)) - case .success(let `in`): - return self.handle(context: context, event: `in`).flatMapThrowing { out in - switch self.encodeOut(allocator: context.allocator, value: out) { - case .failure(let error): - throw CodecError.responseEncoding(error) - case .success(let buffer): - return buffer - } - } - } - } - - private func decodeIn(buffer: ByteBuffer) -> Result { - do { - return .success(try self.decode(buffer: buffer)) - } catch { - return .failure(error) - } - } - - private func encodeOut(allocator: ByteBufferAllocator, value: Out) -> Result { - do { - return .success(try self.encode(allocator: allocator, value: value)) - } catch { - return .failure(error) - } - } -} - -/// Implementation of `ByteBuffer` to `Void` decoding -public extension EventLoopLambdaHandler where Out == Void { - func encode(allocator: ByteBufferAllocator, value: Void) throws -> ByteBuffer? { - nil - } -} - -// MARK: - ByteBufferLambdaHandler - -/// An `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns a `ByteBuffer?` asynchronously. -/// -/// - note: This is a low level protocol designed to power the higher level `EventLoopLambdaHandler` and `LambdaHandler` based APIs. -/// Most users are not expected to use this protocol. -public protocol ByteBufferLambdaHandler { - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: The event or input payload encoded as `ByteBuffer`. - /// - /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. - /// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error` - func handle(context: Lambda.Context, event: ByteBuffer) -> EventLoopFuture - - /// Clean up the Lambda resources asynchronously. - /// Concrete Lambda handlers implement this method to shutdown resources like `HTTPClient`s and database connections. - /// - /// - Note: In case your Lambda fails while creating your LambdaHandler in the `HandlerFactory`, this method - /// **is not invoked**. In this case you must cleanup the created resources immediately in the `HandlerFactory`. - func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture -} - -public extension ByteBufferLambdaHandler { - func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(()) - } -} - -private enum CodecError: Error { - case requestDecoding(Error) - case responseEncoding(Error) -} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaLifecycle.swift b/Sources/AWSLambdaRuntimeCore/LambdaLifecycle.swift deleted file mode 100644 index ec609901..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaLifecycle.swift +++ /dev/null @@ -1,197 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Logging -import NIO -import NIOConcurrencyHelpers - -extension Lambda { - /// `Lifecycle` manages the Lambda process lifecycle. - /// - /// - note: It is intended to be used within a single `EventLoop`. For this reason this class is not thread safe. - public final class Lifecycle { - private let eventLoop: EventLoop - private let shutdownPromise: EventLoopPromise - private let logger: Logger - private let configuration: Configuration - private let factory: HandlerFactory - - private var state = State.idle { - willSet { - self.eventLoop.assertInEventLoop() - precondition(newValue.order > self.state.order, "invalid state \(newValue) after \(self.state.order)") - } - } - - /// Create a new `Lifecycle`. - /// - /// - parameters: - /// - eventLoop: An `EventLoop` to run the Lambda on. - /// - logger: A `Logger` to log the Lambda events. - /// - factory: A `LambdaHandlerFactory` to create the concrete Lambda handler. - public convenience init(eventLoop: EventLoop, logger: Logger, factory: @escaping HandlerFactory) { - self.init(eventLoop: eventLoop, logger: logger, configuration: .init(), factory: factory) - } - - init(eventLoop: EventLoop, logger: Logger, configuration: Configuration, factory: @escaping HandlerFactory) { - self.eventLoop = eventLoop - self.shutdownPromise = eventLoop.makePromise(of: Int.self) - self.logger = logger - self.configuration = configuration - self.factory = factory - } - - deinit { - guard case .shutdown = self.state else { - preconditionFailure("invalid state \(self.state)") - } - } - - /// The `Lifecycle` shutdown future. - /// - /// - Returns: An `EventLoopFuture` that is fulfilled after the Lambda lifecycle has fully shutdown. - public var shutdownFuture: EventLoopFuture { - self.shutdownPromise.futureResult - } - - /// Start the `Lifecycle`. - /// - /// - Returns: An `EventLoopFuture` that is fulfilled after the Lambda hander has been created and initiliazed, and a first run has been scheduled. - /// - /// - note: This method must be called on the `EventLoop` the `Lifecycle` has been initialized with. - public func start() -> EventLoopFuture { - self.eventLoop.assertInEventLoop() - - logger.info("lambda lifecycle starting with \(self.configuration)") - self.state = .initializing - - var logger = self.logger - logger[metadataKey: "lifecycleId"] = .string(self.configuration.lifecycle.id) - let runner = Runner(eventLoop: self.eventLoop, configuration: self.configuration) - - let startupFuture = runner.initialize(logger: logger, factory: self.factory) - startupFuture.flatMap { handler -> EventLoopFuture<(ByteBufferLambdaHandler, Result)> in - // after the startup future has succeeded, we have a handler that we can use - // to `run` the lambda. - let finishedPromise = self.eventLoop.makePromise(of: Int.self) - self.state = .active(runner, handler) - self.run(promise: finishedPromise) - return finishedPromise.futureResult.mapResult { (handler, $0) } - } - .flatMap { (handler, runnerResult) -> EventLoopFuture in - // after the lambda finishPromise has succeeded or failed we need to - // shutdown the handler - let shutdownContext = ShutdownContext(logger: logger, eventLoop: self.eventLoop) - return handler.shutdown(context: shutdownContext).flatMapErrorThrowing { error in - // if, we had an error shuting down the lambda, we want to concatenate it with - // the runner result - logger.error("Error shutting down handler: \(error)") - throw RuntimeError.shutdownError(shutdownError: error, runnerResult: runnerResult) - }.flatMapResult { (_) -> Result in - // we had no error shutting down the lambda. let's return the runner's result - runnerResult - } - }.always { _ in - // triggered when the Lambda has finished its last run or has a startup failure. - self.markShutdown() - }.cascade(to: self.shutdownPromise) - - return startupFuture.map { _ in } - } - - // MARK: - Private - - #if DEBUG - /// Begin the `Lifecycle` shutdown. Only needed for debugging purposes, hence behind a `DEBUG` flag. - public func shutdown() { - // make this method thread safe by dispatching onto the eventloop - self.eventLoop.execute { - let oldState = self.state - self.state = .shuttingdown - if case .active(let runner, _) = oldState { - runner.cancelWaitingForNextInvocation() - } - } - } - #endif - - private func markShutdown() { - self.state = .shutdown - } - - @inline(__always) - private func run(promise: EventLoopPromise) { - func _run(_ count: Int) { - switch self.state { - case .active(let runner, let handler): - if self.configuration.lifecycle.maxTimes > 0, count >= self.configuration.lifecycle.maxTimes { - return promise.succeed(count) - } - var logger = self.logger - logger[metadataKey: "lifecycleIteration"] = "\(count)" - runner.run(logger: logger, handler: handler).whenComplete { result in - switch result { - case .success: - logger.log(level: .debug, "lambda invocation sequence completed successfully") - // recursive! per aws lambda runtime spec the polling requests are to be done one at a time - _run(count + 1) - case .failure(HTTPClient.Errors.cancelled): - if case .shuttingdown = self.state { - // if we ware shutting down, we expect to that the get next - // invocation request might have been cancelled. For this reason we - // succeed the promise here. - logger.log(level: .info, "lambda invocation sequence has been cancelled for shutdown") - return promise.succeed(count) - } - logger.log(level: .error, "lambda invocation sequence has been cancelled unexpectedly") - promise.fail(HTTPClient.Errors.cancelled) - case .failure(let error): - logger.log(level: .error, "lambda invocation sequence completed with error: \(error)") - promise.fail(error) - } - } - case .shuttingdown: - promise.succeed(count) - default: - preconditionFailure("invalid run state: \(self.state)") - } - } - - _run(0) - } - - private enum State { - case idle - case initializing - case active(Runner, Handler) - case shuttingdown - case shutdown - - internal var order: Int { - switch self { - case .idle: - return 0 - case .initializing: - return 1 - case .active: - return 2 - case .shuttingdown: - return 3 - case .shutdown: - return 4 - } - } - } - } -} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift b/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift deleted file mode 100644 index 8fc22de3..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift +++ /dev/null @@ -1,156 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Dispatch -import Logging -import NIO - -extension Lambda { - /// LambdaRunner manages the Lambda runtime workflow, or business logic. - internal final class Runner { - private let runtimeClient: RuntimeClient - private let eventLoop: EventLoop - private let allocator: ByteBufferAllocator - - private var isGettingNextInvocation = false - - init(eventLoop: EventLoop, configuration: Configuration) { - self.eventLoop = eventLoop - self.runtimeClient = RuntimeClient(eventLoop: self.eventLoop, configuration: configuration.runtimeEngine) - self.allocator = ByteBufferAllocator() - } - - /// Run the user provided initializer. This *must* only be called once. - /// - /// - Returns: An `EventLoopFuture` fulfilled with the outcome of the initialization. - func initialize(logger: Logger, factory: @escaping HandlerFactory) -> EventLoopFuture { - logger.debug("initializing lambda") - // 1. create the handler from the factory - // 2. report initialization error if one occured - let context = InitializationContext(logger: logger, - eventLoop: self.eventLoop, - allocator: self.allocator) - return factory(context) - // Hopping back to "our" EventLoop is importnant in case the factory returns a future - // that originated from a foreign EventLoop/EventLoopGroup. - // This can happen if the factory uses a library (let's say a database client) that manages its own threads/loops - // for whatever reason and returns a future that originated from that foreign EventLoop. - .hop(to: self.eventLoop) - .peekError { error in - self.runtimeClient.reportInitializationError(logger: logger, error: error).peekError { reportingError in - // We're going to bail out because the init failed, so there's not a lot we can do other than log - // that we couldn't report this error back to the runtime. - logger.error("failed reporting initialization error to lambda runtime engine: \(reportingError)") - } - } - } - - func run(logger: Logger, handler: Handler) -> EventLoopFuture { - logger.debug("lambda invocation sequence starting") - // 1. request invocation from lambda runtime engine - self.isGettingNextInvocation = true - return self.runtimeClient.getNextInvocation(logger: logger).peekError { error in - logger.error("could not fetch work from lambda runtime engine: \(error)") - }.flatMap { invocation, event in - // 2. send invocation to handler - self.isGettingNextInvocation = false - let context = Context(logger: logger, - eventLoop: self.eventLoop, - allocator: self.allocator, - invocation: invocation) - logger.debug("sending invocation to lambda handler \(handler)") - return handler.handle(context: context, event: event) - // Hopping back to "our" EventLoop is importnant in case the handler returns a future that - // originiated from a foreign EventLoop/EventLoopGroup. - // This can happen if the handler uses a library (lets say a DB client) that manages its own threads/loops - // for whatever reason and returns a future that originated from that foreign EventLoop. - .hop(to: self.eventLoop) - .mapResult { result in - if case .failure(let error) = result { - logger.warning("lambda handler returned an error: \(error)") - } - return (invocation, result) - } - }.flatMap { invocation, result in - // 3. report results to runtime engine - self.runtimeClient.reportResults(logger: logger, invocation: invocation, result: result).peekError { error in - logger.error("could not report results to lambda runtime engine: \(error)") - } - } - } - - /// cancels the current run, if we are waiting for next invocation (long poll from Lambda control plane) - /// only needed for debugging purposes. - func cancelWaitingForNextInvocation() { - if self.isGettingNextInvocation { - self.runtimeClient.cancel() - } - } - } -} - -private extension Lambda.Context { - convenience init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator, invocation: Lambda.Invocation) { - self.init(requestID: invocation.requestID, - traceID: invocation.traceID, - invokedFunctionARN: invocation.invokedFunctionARN, - deadline: DispatchWallTime(millisSinceEpoch: invocation.deadlineInMillisSinceEpoch), - cognitoIdentity: invocation.cognitoIdentity, - clientContext: invocation.clientContext, - logger: logger, - eventLoop: eventLoop, - allocator: allocator) - } -} - -// TODO: move to nio? -extension EventLoopFuture { - // callback does not have side effects, failing with original result - func peekError(_ callback: @escaping (Error) -> Void) -> EventLoopFuture { - self.flatMapError { error in - callback(error) - return self - } - } - - // callback does not have side effects, failing with original result - func peekError(_ callback: @escaping (Error) -> EventLoopFuture) -> EventLoopFuture { - self.flatMapError { error in - let promise = self.eventLoop.makePromise(of: Value.self) - callback(error).whenComplete { _ in - promise.completeWith(self) - } - return promise.futureResult - } - } - - func mapResult(_ callback: @escaping (Result) -> NewValue) -> EventLoopFuture { - self.map { value in - callback(.success(value)) - }.flatMapErrorThrowing { error in - callback(.failure(error)) - } - } -} - -private extension Result { - var successful: Bool { - switch self { - case .success: - return true - case .failure: - return false - } - } -} diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift deleted file mode 100644 index 5e9e6aea..00000000 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift +++ /dev/null @@ -1,203 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Logging -import NIO -import NIOHTTP1 - -/// An HTTP based client for AWS Runtime Engine. This encapsulates the RESTful methods exposed by the Runtime Engine: -/// * /runtime/invocation/next -/// * /runtime/invocation/response -/// * /runtime/invocation/error -/// * /runtime/init/error -extension Lambda { - internal struct RuntimeClient { - private let eventLoop: EventLoop - private let allocator = ByteBufferAllocator() - private let httpClient: HTTPClient - - init(eventLoop: EventLoop, configuration: Configuration.RuntimeEngine) { - self.eventLoop = eventLoop - self.httpClient = HTTPClient(eventLoop: eventLoop, configuration: configuration) - } - - /// Requests invocation from the control plane. - func getNextInvocation(logger: Logger) -> EventLoopFuture<(Invocation, ByteBuffer)> { - let url = Consts.invocationURLPrefix + Consts.getNextInvocationURLSuffix - logger.debug("requesting work from lambda runtime engine using \(url)") - return self.httpClient.get(url: url, headers: RuntimeClient.defaultHeaders).flatMapThrowing { response in - guard response.status == .ok else { - throw RuntimeError.badStatusCode(response.status) - } - let invocation = try Invocation(headers: response.headers) - guard let event = response.body else { - throw RuntimeError.noBody - } - return (invocation, event) - }.flatMapErrorThrowing { error in - switch error { - case HTTPClient.Errors.timeout: - throw RuntimeError.upstreamError("timeout") - case HTTPClient.Errors.connectionResetByPeer: - throw RuntimeError.upstreamError("connectionResetByPeer") - default: - throw error - } - } - } - - /// Reports a result to the Runtime Engine. - func reportResults(logger: Logger, invocation: Invocation, result: Result) -> EventLoopFuture { - var url = Consts.invocationURLPrefix + "/" + invocation.requestID - var body: ByteBuffer? - let headers: HTTPHeaders - - switch result { - case .success(let buffer): - url += Consts.postResponseURLSuffix - body = buffer - headers = RuntimeClient.defaultHeaders - case .failure(let error): - url += Consts.postErrorURLSuffix - let errorResponse = ErrorResponse(errorType: Consts.functionError, errorMessage: "\(error)") - let bytes = errorResponse.toJSONBytes() - body = self.allocator.buffer(capacity: bytes.count) - body!.writeBytes(bytes) - headers = RuntimeClient.errorHeaders - } - logger.debug("reporting results to lambda runtime engine using \(url)") - return self.httpClient.post(url: url, headers: headers, body: body).flatMapThrowing { response in - guard response.status == .accepted else { - throw RuntimeError.badStatusCode(response.status) - } - return () - }.flatMapErrorThrowing { error in - switch error { - case HTTPClient.Errors.timeout: - throw RuntimeError.upstreamError("timeout") - case HTTPClient.Errors.connectionResetByPeer: - throw RuntimeError.upstreamError("connectionResetByPeer") - default: - throw error - } - } - } - - /// Reports an initialization error to the Runtime Engine. - func reportInitializationError(logger: Logger, error: Error) -> EventLoopFuture { - let url = Consts.postInitErrorURL - let errorResponse = ErrorResponse(errorType: Consts.initializationError, errorMessage: "\(error)") - let bytes = errorResponse.toJSONBytes() - var body = self.allocator.buffer(capacity: bytes.count) - body.writeBytes(bytes) - logger.warning("reporting initialization error to lambda runtime engine using \(url)") - return self.httpClient.post(url: url, headers: RuntimeClient.errorHeaders, body: body).flatMapThrowing { response in - guard response.status == .accepted else { - throw RuntimeError.badStatusCode(response.status) - } - return () - }.flatMapErrorThrowing { error in - switch error { - case HTTPClient.Errors.timeout: - throw RuntimeError.upstreamError("timeout") - case HTTPClient.Errors.connectionResetByPeer: - throw RuntimeError.upstreamError("connectionResetByPeer") - default: - throw error - } - } - } - - /// Cancels the current request, if one is running. Only needed for debugging purposes - func cancel() { - self.httpClient.cancel() - } - } -} - -internal extension Lambda { - enum RuntimeError: Error { - case badStatusCode(HTTPResponseStatus) - case upstreamError(String) - case invocationMissingHeader(String) - case noBody - case json(Error) - case shutdownError(shutdownError: Error, runnerResult: Result) - } -} - -internal struct ErrorResponse: Codable { - var errorType: String - var errorMessage: String -} - -internal extension ErrorResponse { - func toJSONBytes() -> [UInt8] { - var bytes = [UInt8]() - bytes.append(UInt8(ascii: "{")) - bytes.append(contentsOf: #""errorType":"# .utf8) - self.errorType.encodeAsJSONString(into: &bytes) - bytes.append(contentsOf: #","errorMessage":"# .utf8) - self.errorMessage.encodeAsJSONString(into: &bytes) - bytes.append(UInt8(ascii: "}")) - return bytes - } -} - -extension Lambda { - internal struct Invocation { - let requestID: String - let deadlineInMillisSinceEpoch: Int64 - let invokedFunctionARN: String - let traceID: String - let clientContext: String? - let cognitoIdentity: String? - - init(headers: HTTPHeaders) throws { - guard let requestID = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else { - throw RuntimeError.invocationMissingHeader(AmazonHeaders.requestID) - } - - guard let deadline = headers.first(name: AmazonHeaders.deadline), - let unixTimeInMilliseconds = Int64(deadline) else { - throw RuntimeError.invocationMissingHeader(AmazonHeaders.deadline) - } - - guard let invokedFunctionARN = headers.first(name: AmazonHeaders.invokedFunctionARN) else { - throw RuntimeError.invocationMissingHeader(AmazonHeaders.invokedFunctionARN) - } - - guard let traceID = headers.first(name: AmazonHeaders.traceID) else { - throw RuntimeError.invocationMissingHeader(AmazonHeaders.traceID) - } - - self.requestID = requestID - self.deadlineInMillisSinceEpoch = unixTimeInMilliseconds - self.invokedFunctionARN = invokedFunctionARN - self.traceID = traceID - self.clientContext = headers["Lambda-Runtime-Client-Context"].first - self.cognitoIdentity = headers["Lambda-Runtime-Cognito-Identity"].first - } - } -} - -extension Lambda.RuntimeClient { - internal static let defaultHeaders = HTTPHeaders([("user-agent", "Swift-Lambda/Unknown")]) - - /// These headers must be sent along an invocation or initialization error report - internal static let errorHeaders = HTTPHeaders([ - ("user-agent", "Swift-Lambda/Unknown"), - ("lambda-runtime-function-error-type", "Unhandled"), - ]) -} diff --git a/Sources/AWSLambdaTesting/Lambda+Testing.swift b/Sources/AWSLambdaTesting/Lambda+Testing.swift deleted file mode 100644 index 981ca736..00000000 --- a/Sources/AWSLambdaTesting/Lambda+Testing.swift +++ /dev/null @@ -1,113 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -// This functionality is designed to help with Lambda unit testing with XCTest -// #if filter required for release builds which do not support @testable import -// @testable is used to access of internal functions -// For exmaple: -// -// func test() { -// struct MyLambda: EventLoopLambdaHandler { -// typealias In = String -// typealias Out = String -// -// func handle(context: Lambda.Context, event: String) -> EventLoopFuture { -// return context.eventLoop.makeSucceededFuture("echo" + event) -// } -// } -// -// let input = UUID().uuidString -// var result: String? -// XCTAssertNoThrow(result = try Lambda.test(MyLambda(), with: input)) -// XCTAssertEqual(result, "echo" + input) -// } - -#if DEBUG -@testable import AWSLambdaRuntime -@testable import AWSLambdaRuntimeCore -import Dispatch -import Logging -import NIO - -extension Lambda { - public struct TestConfig { - public var requestID: String - public var traceID: String - public var invokedFunctionARN: String - public var timeout: DispatchTimeInterval - - public init(requestID: String = "\(DispatchTime.now().uptimeNanoseconds)", - traceID: String = "Root=\(DispatchTime.now().uptimeNanoseconds);Parent=\(DispatchTime.now().uptimeNanoseconds);Sampled=1", - invokedFunctionARN: String = "arn:aws:lambda:us-west-1:\(DispatchTime.now().uptimeNanoseconds):function:custom-runtime", - timeout: DispatchTimeInterval = .seconds(5)) { - self.requestID = requestID - self.traceID = traceID - self.invokedFunctionARN = invokedFunctionARN - self.timeout = timeout - } - } - - public static func test(_ closure: @escaping Lambda.StringClosure, - with event: String, - using config: TestConfig = .init()) throws -> String { - try Self.test(StringClosureWrapper(closure), with: event, using: config) - } - - public static func test(_ closure: @escaping Lambda.StringVoidClosure, - with event: String, - using config: TestConfig = .init()) throws { - _ = try Self.test(StringVoidClosureWrapper(closure), with: event, using: config) - } - - public static func test( - _ closure: @escaping Lambda.CodableClosure, - with event: In, - using config: TestConfig = .init() - ) throws -> Out { - try Self.test(CodableClosureWrapper(closure), with: event, using: config) - } - - public static func test( - _ closure: @escaping Lambda.CodableVoidClosure, - with event: In, - using config: TestConfig = .init() - ) throws { - _ = try Self.test(CodableVoidClosureWrapper(closure), with: event, using: config) - } - - public static func test( - _ handler: Handler, - with event: In, - using config: TestConfig = .init() - ) throws -> Out where Handler.In == In, Handler.Out == Out { - let logger = Logger(label: "test") - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! eventLoopGroup.syncShutdownGracefully() - } - let eventLoop = eventLoopGroup.next() - let context = Context(requestID: config.requestID, - traceID: config.traceID, - invokedFunctionARN: config.invokedFunctionARN, - deadline: .now() + config.timeout, - logger: logger, - eventLoop: eventLoop, - allocator: ByteBufferAllocator()) - - return try eventLoop.flatSubmit { - handler.handle(context: context, event: event) - }.wait() - } -} -#endif diff --git a/Sources/CodableSample/main.swift b/Sources/CodableSample/main.swift deleted file mode 100644 index 59cc8dee..00000000 --- a/Sources/CodableSample/main.swift +++ /dev/null @@ -1,46 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntime -import NIO - -struct Request: Codable { - let body: String -} - -struct Response: Codable { - let body: String -} - -// in this example we are receiving and responding with codables. Request and Response above are examples of how to use -// codables to model your reqeuest and response objects -struct Handler: EventLoopLambdaHandler { - typealias In = Request - typealias Out = Response - - func handle(context: Lambda.Context, event: Request) -> EventLoopFuture { - // as an example, respond with the input event's reversed body - context.eventLoop.makeSucceededFuture(Response(body: String(event.body.reversed()))) - } -} - -Lambda.run(Handler()) - -// MARK: - this can also be expressed as a closure: - -/* - Lambda.run { (_, request: Request, callback) in - callback(.success(Response(body: String(request.body.reversed())))) - } - */ diff --git a/Sources/MockServer/MockHTTPServer.swift b/Sources/MockServer/MockHTTPServer.swift new file mode 100644 index 00000000..0849e325 --- /dev/null +++ b/Sources/MockServer/MockHTTPServer.swift @@ -0,0 +1,298 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOCore +import NIOHTTP1 +import NIOPosix +import Synchronization + +// for UUID and Date +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@main +struct HttpServer { + /// The server's host. (default: 127.0.0.1) + private let host: String + /// The server's port. (default: 7000) + private let port: Int + /// The server's event loop group. (default: MultiThreadedEventLoopGroup.singleton) + private let eventLoopGroup: MultiThreadedEventLoopGroup + /// the mode. Are we mocking a server for a Lambda function that expects a String or a JSON document? (default: string) + private let mode: Mode + /// the number of connections this server must accept before shutting down (default: 1) + private let maxInvocations: Int + /// the logger (control verbosity with LOG_LEVEL environment variable) + private let logger: Logger + + static func main() async throws { + var log = Logger(label: "MockServer") + log.logLevel = env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info + + let server = HttpServer( + host: env("HOST") ?? "127.0.0.1", + port: env("PORT").flatMap(Int.init) ?? 7000, + eventLoopGroup: .singleton, + mode: env("MODE").flatMap(Mode.init) ?? .string, + maxInvocations: env("MAX_INVOCATIONS").flatMap(Int.init) ?? 1, + logger: log + ) + try await server.run() + } + + /// This method starts the server and handles one unique incoming connections + /// The Lambda function will send two HTTP requests over this connection: one for the next invocation and one for the response. + private func run() async throws { + let channel = try await ServerBootstrap(group: self.eventLoopGroup) + .serverChannelOption(.backlog, value: 256) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + .childChannelOption(.maxMessagesPerRead, value: 1) + .bind( + host: self.host, + port: self.port + ) { channel in + channel.eventLoop.makeCompletedFuture { + + try channel.pipeline.syncOperations.configureHTTPServerPipeline( + withErrorHandling: true + ) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: NIOAsyncChannel.Configuration( + inboundType: HTTPServerRequestPart.self, + outboundType: HTTPServerResponsePart.self + ) + ) + } + } + + logger.info( + "Server started and listening", + metadata: [ + "host": "\(channel.channel.localAddress?.ipAddress?.debugDescription ?? "")", + "port": "\(channel.channel.localAddress?.port ?? 0)", + "maxInvocations": "\(self.maxInvocations)", + ] + ) + + // This counter is used to track the number of incoming connections. + // This mock servers accepts n TCP connection then shutdowns + let connectionCounter = SharedCounter(maxValue: self.maxInvocations) + + // We are handling each incoming connection in a separate child task. It is important + // to use a discarding task group here which automatically discards finished child tasks. + // A normal task group retains all child tasks and their outputs in memory until they are + // consumed by iterating the group or by exiting the group. Since, we are never consuming + // the results of the group we need the group to automatically discard them; otherwise, this + // would result in a memory leak over time. + try await withThrowingDiscardingTaskGroup { group in + try await channel.executeThenClose { inbound in + for try await connectionChannel in inbound { + + let counter = connectionCounter.current() + logger.trace("Handling new connection", metadata: ["connectionNumber": "\(counter)"]) + + group.addTask { + await self.handleConnection(channel: connectionChannel) + logger.trace("Done handling connection", metadata: ["connectionNumber": "\(counter)"]) + } + + if connectionCounter.increment() { + logger.info( + "Maximum number of connections reached, shutting down after current connection", + metadata: ["maxConnections": "\(self.maxInvocations)"] + ) + break // this causes the server to shutdown after handling the connection + } + } + } + } + logger.info("Server shutting down") + } + + /// This method handles a single connection by responsing hard coded value to a Lambda function request. + /// It handles two requests: one for the next invocation and one for the response. + /// when the maximum number of requests is reached, it closes the connection. + private func handleConnection( + channel: NIOAsyncChannel + ) async { + + var requestHead: HTTPRequestHead! + var requestBody: ByteBuffer? + + // each Lambda invocation results in TWO HTTP requests (next and response) + let requestCount = SharedCounter(maxValue: 2) + + // Note that this method is non-throwing and we are catching any error. + // We do this since we don't want to tear down the whole server when a single connection + // encounters an error. + do { + try await channel.executeThenClose { inbound, outbound in + for try await inboundData in inbound { + let requestNumber = requestCount.current() + logger.trace("Handling request", metadata: ["requestNumber": "\(requestNumber)"]) + + if case .head(let head) = inboundData { + logger.trace("Received request head", metadata: ["head": "\(head)"]) + requestHead = head + } + if case .body(let body) = inboundData { + logger.trace("Received request body", metadata: ["body": "\(body)"]) + requestBody = body + } + if case .end(let end) = inboundData { + logger.trace("Received request end", metadata: ["end": "\(String(describing: end))"]) + + precondition(requestHead != nil, "Received .end without .head") + let (responseStatus, responseHeaders, responseBody) = self.processRequest( + requestHead: requestHead, + requestBody: requestBody + ) + + try await self.sendResponse( + responseStatus: responseStatus, + responseHeaders: responseHeaders, + responseBody: responseBody, + outbound: outbound + ) + + requestHead = nil + + if requestCount.increment() { + logger.info( + "Maximum number of requests reached, closing this connection", + metadata: ["maxRequest": "2"] + ) + break // this finishes handiling request on this connection + } + } + } + } + } catch { + logger.error("Hit error: \(error)") + } + } + /// This function process the requests and return an hard-coded response (string or JSON depending on the mode). + /// We ignore the requestBody. + private func processRequest( + requestHead: HTTPRequestHead, + requestBody: ByteBuffer? + ) -> (HTTPResponseStatus, [(String, String)], String) { + var responseStatus: HTTPResponseStatus = .ok + var responseBody: String = "" + var responseHeaders: [(String, String)] = [] + + logger.trace( + "Processing request", + metadata: ["VERB": "\(requestHead.method)", "URI": "\(requestHead.uri)"] + ) + + if requestHead.uri.hasSuffix("/next") { + responseStatus = .accepted + + let requestId = UUID().uuidString + switch self.mode { + case .string: + responseBody = "\"Seb\"" // must be a valid JSON document + case .json: + responseBody = "{ \"name\": \"Seb\", \"age\" : 52 }" + } + let deadline = Int64(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) + responseHeaders = [ + (AmazonHeaders.requestID, requestId), + (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), + (AmazonHeaders.traceID, "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1"), + (AmazonHeaders.deadline, String(deadline)), + ] + } else if requestHead.uri.hasSuffix("/response") { + responseStatus = .accepted + } else if requestHead.uri.hasSuffix("/error") { + responseStatus = .ok + } else { + responseStatus = .notFound + } + logger.trace("Returning response: \(responseStatus), \(responseHeaders), \(responseBody)") + return (responseStatus, responseHeaders, responseBody) + } + + private func sendResponse( + responseStatus: HTTPResponseStatus, + responseHeaders: [(String, String)], + responseBody: String, + outbound: NIOAsyncChannelOutboundWriter + ) async throws { + var headers = HTTPHeaders(responseHeaders) + headers.add(name: "Content-Length", value: "\(responseBody.utf8.count)") + headers.add(name: "KeepAlive", value: "timeout=1, max=2") + + logger.trace("Writing response head") + try await outbound.write( + HTTPServerResponsePart.head( + HTTPResponseHead( + version: .init(major: 1, minor: 1), // use HTTP 1.1 it keeps connection alive between requests + status: responseStatus, + headers: headers + ) + ) + ) + logger.trace("Writing response body") + try await outbound.write(HTTPServerResponsePart.body(.byteBuffer(ByteBuffer(string: responseBody)))) + logger.trace("Writing response end") + try await outbound.write(HTTPServerResponsePart.end(nil)) + } + + private enum Mode: String { + case string + case json + } + + private static func env(_ name: String) -> String? { + guard let value = getenv(name) else { + return nil + } + return String(cString: value) + } + + private enum AmazonHeaders { + static let requestID = "Lambda-Runtime-Aws-Request-Id" + static let traceID = "Lambda-Runtime-Trace-Id" + static let clientContext = "X-Amz-Client-Context" + static let cognitoIdentity = "X-Amz-Cognito-Identity" + static let deadline = "Lambda-Runtime-Deadline-Ms" + static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn" + } + + private final class SharedCounter: Sendable { + private let counterMutex = Mutex(0) + private let maxValue: Int + + init(maxValue: Int) { + self.maxValue = maxValue + } + func current() -> Int { + counterMutex.withLock { $0 } + } + func increment() -> Bool { + counterMutex.withLock { + $0 += 1 + return $0 >= maxValue + } + } + } +} diff --git a/Sources/MockServer/main.swift b/Sources/MockServer/main.swift deleted file mode 100644 index 72b37ce5..00000000 --- a/Sources/MockServer/main.swift +++ /dev/null @@ -1,165 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation -import NIO -import NIOHTTP1 - -internal struct MockServer { - private let group: EventLoopGroup - private let host: String - private let port: Int - private let mode: Mode - - public init() { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - self.host = env("HOST") ?? "127.0.0.1" - self.port = env("PORT").flatMap(Int.init) ?? 7000 - self.mode = env("MODE").flatMap(Mode.init) ?? .string - } - - func start() throws { - let bootstrap = ServerBootstrap(group: group) - .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) - .childChannelInitializer { channel in - channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in - channel.pipeline.addHandler(HTTPHandler(mode: self.mode)) - } - } - try bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in - guard let localAddress = channel.localAddress else { - return channel.eventLoop.makeFailedFuture(ServerError.cantBind) - } - print("\(self) started and listening on \(localAddress)") - return channel.eventLoop.makeSucceededFuture(()) - }.wait() - } -} - -internal final class HTTPHandler: ChannelInboundHandler { - public typealias InboundIn = HTTPServerRequestPart - public typealias OutboundOut = HTTPServerResponsePart - - private let mode: Mode - - private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - - public init(mode: Mode) { - self.mode = mode - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let requestPart = unwrapInboundIn(data) - - switch requestPart { - case .head(let head): - self.pending.append((head: head, body: nil)) - case .body(var buffer): - var request = self.pending.removeFirst() - if request.body == nil { - request.body = buffer - } else { - request.body!.writeBuffer(&buffer) - } - self.pending.prepend(request) - case .end: - let request = self.pending.removeFirst() - self.processRequest(context: context, request: request) - } - } - - func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { - var responseStatus: HTTPResponseStatus - var responseBody: String? - var responseHeaders: [(String, String)]? - - if request.head.uri.hasSuffix("/next") { - let requestId = UUID().uuidString - responseStatus = .ok - switch self.mode { - case .string: - responseBody = requestId - case .json: - responseBody = "{ \"body\": \"\(requestId)\" }" - } - let deadline = Int64(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) - responseHeaders = [ - (AmazonHeaders.requestID, requestId), - (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), - (AmazonHeaders.traceID, "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1"), - (AmazonHeaders.deadline, String(deadline)), - ] - } else if request.head.uri.hasSuffix("/response") { - responseStatus = .accepted - } else { - responseStatus = .notFound - } - self.writeResponse(context: context, status: responseStatus, headers: responseHeaders, body: responseBody) - } - - func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: String? = nil) { - var headers = HTTPHeaders(headers ?? []) - headers.add(name: "content-length", value: "\(body?.utf8.count ?? 0)") - let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: status, headers: headers) - - context.write(wrapOutboundOut(.head(head))).whenFailure { error in - print("\(self) write error \(error)") - } - - if let b = body { - var buffer = context.channel.allocator.buffer(capacity: b.utf8.count) - buffer.writeString(b) - context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in - print("\(self) write error \(error)") - } - } - - context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in - if case .failure(let error) = result { - print("\(self) write error \(error)") - } - } - } -} - -internal enum ServerError: Error { - case notReady - case cantBind -} - -internal enum AmazonHeaders { - static let requestID = "Lambda-Runtime-Aws-Request-Id" - static let traceID = "Lambda-Runtime-Trace-Id" - static let clientContext = "X-Amz-Client-Context" - static let cognitoIdentity = "X-Amz-Cognito-Identity" - static let deadline = "Lambda-Runtime-Deadline-Ms" - static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn" -} - -internal enum Mode: String { - case string - case json -} - -func env(_ name: String) -> String? { - guard let value = getenv(name) else { - return nil - } - return String(cString: value) -} - -// main -let server = MockServer() -try! server.start() -dispatchMain() diff --git a/Sources/StringSample/main.swift b/Sources/StringSample/main.swift deleted file mode 100644 index 452160f9..00000000 --- a/Sources/StringSample/main.swift +++ /dev/null @@ -1,37 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntimeCore -import NIO - -// in this example we are receiving and responding with strings -struct Handler: EventLoopLambdaHandler { - typealias In = String - typealias Out = String - - func handle(context: Lambda.Context, event: String) -> EventLoopFuture { - // as an example, respond with the event's reversed body - context.eventLoop.makeSucceededFuture(String(event.reversed())) - } -} - -Lambda.run(Handler()) - -// MARK: - this can also be expressed as a closure: - -/* - Lambda.run { (_, event: String, callback) in - callback(.success(String(event.reversed()))) - } - */ diff --git a/Tests/AWSLambdaEventsTests/ALBTests.swift b/Tests/AWSLambdaEventsTests/ALBTests.swift deleted file mode 100644 index b24684c9..00000000 --- a/Tests/AWSLambdaEventsTests/ALBTests.swift +++ /dev/null @@ -1,64 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class ALBTests: XCTestCase { - static let exampleSingleValueHeadersEventBody = """ - { - "requestContext":{ - "elb":{ - "targetGroupArn": "arn:aws:elasticloadbalancing:eu-central-1:079477498937:targetgroup/EinSternDerDeinenNamenTraegt/621febf5a44b2ce5" - } - }, - "httpMethod": "GET", - "path": "/", - "queryStringParameters": {}, - "headers":{ - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "accept-encoding": "gzip, deflate", - "accept-language": "en-us", - "connection": "keep-alive", - "host": "event-testl-1wa3wrvmroilb-358275751.eu-central-1.elb.amazonaws.com", - "upgrade-insecure-requests": "1", - "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", - "x-amzn-trace-id": "Root=1-5e189143-ad18a2b0a7728cd0dac45e10", - "x-forwarded-for": "90.187.8.137", - "x-forwarded-port": "80", - "x-forwarded-proto": "http" - }, - "body":"", - "isBase64Encoded":false - } - """ - - func testRequestWithSingleValueHeadersEvent() { - let data = ALBTests.exampleSingleValueHeadersEventBody.data(using: .utf8)! - do { - let decoder = JSONDecoder() - - let event = try decoder.decode(ALB.TargetGroupRequest.self, from: data) - - XCTAssertEqual(event.httpMethod, .GET) - XCTAssertEqual(event.body, "") - XCTAssertEqual(event.isBase64Encoded, false) - XCTAssertEqual(event.headers?.count, 11) - XCTAssertEqual(event.path, "/") - XCTAssertEqual(event.queryStringParameters, [:]) - } catch { - XCTFail("Unexpected error: \(error)") - } - } -} diff --git a/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift deleted file mode 100644 index 9d682c94..00000000 --- a/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift +++ /dev/null @@ -1,91 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class APIGatewayV2Tests: XCTestCase { - static let exampleGetEventBody = """ - { - "routeKey":"GET /hello", - "version":"2.0", - "rawPath":"/hello", - "stageVariables":{ - "foo":"bar" - }, - "requestContext":{ - "timeEpoch":1587750461466, - "domainPrefix":"hello", - "authorizer":{ - "jwt":{ - "scopes":[ - "hello" - ], - "claims":{ - "aud":"customers", - "iss":"https://hello.test.com/", - "iat":"1587749276", - "exp":"1587756476" - } - } - }, - "accountId":"0123456789", - "stage":"$default", - "domainName":"hello.test.com", - "apiId":"pb5dg6g3rg", - "requestId":"LgLpnibOFiAEPCA=", - "http":{ - "path":"/hello", - "userAgent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", - "method":"GET", - "protocol":"HTTP/1.1", - "sourceIp":"91.64.117.86" - }, - "time":"24/Apr/2020:17:47:41 +0000" - }, - "isBase64Encoded":false, - "rawQueryString":"foo=bar", - "queryStringParameters":{ - "foo":"bar" - }, - "headers":{ - "x-forwarded-proto":"https", - "x-forwarded-for":"91.64.117.86", - "x-forwarded-port":"443", - "authorization":"Bearer abc123", - "host":"hello.test.com", - "x-amzn-trace-id":"Root=1-5ea3263d-07c5d5ddfd0788bed7dad831", - "user-agent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", - "content-length":"0" - } - } - """ - - // MARK: - Request - - - // MARK: Decoding - - func testRequestDecodingExampleGetRequest() { - let data = APIGatewayV2Tests.exampleGetEventBody.data(using: .utf8)! - var req: APIGateway.V2.Request? - XCTAssertNoThrow(req = try JSONDecoder().decode(APIGateway.V2.Request.self, from: data)) - - XCTAssertEqual(req?.rawPath, "/hello") - XCTAssertEqual(req?.context.http.method, .GET) - XCTAssertEqual(req?.queryStringParameters?.count, 1) - XCTAssertEqual(req?.rawQueryString, "foo=bar") - XCTAssertEqual(req?.headers.count, 8) - XCTAssertNil(req?.body) - } -} diff --git a/Tests/AWSLambdaEventsTests/APIGatewayTests.swift b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift deleted file mode 100644 index 37cba98a..00000000 --- a/Tests/AWSLambdaEventsTests/APIGatewayTests.swift +++ /dev/null @@ -1,77 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class APIGatewayTests: XCTestCase { - static let exampleGetEventBody = """ - {"httpMethod": "GET", "body": null, "resource": "/test", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/test", "httpMethod": "GET", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "Prod", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/test"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Cache-Control": "max-age=0", "Dnt": "1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24", "Sec-Fetch-User": "?1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Sec-Fetch-Site": "none", "Sec-Fetch-Mode": "navigate", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Cache-Control": ["max-age=0"], "Dnt": ["1"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24"], "Sec-Fetch-User": ["?1"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"], "Sec-Fetch-Site": ["none"], "Sec-Fetch-Mode": ["navigate"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "/test", "isBase64Encoded": false} - """ - - static let todoPostEventBody = """ - {"httpMethod": "POST", "body": "{\\"title\\":\\"a todo\\"}", "resource": "/todos", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/todos", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "test", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/todos"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Content-Length": "18", "Pragma": "no-cache", "Cache-Control": "no-cache", "Accept": "text/plain, */*; q=0.01", "Origin": "http://todobackend.com", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25", "Dnt": "1", "Content-Type": "application/json", "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Mode": "cors", "Referer": "http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Content-Length": ["18"], "Pragma": ["no-cache"], "Cache-Control": ["no-cache"], "Accept": ["text/plain, */*; q=0.01"], "Origin": ["http://todobackend.com"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25"], "Dnt": ["1"], "Content-Type": ["application/json"], "Sec-Fetch-Site": ["cross-site"], "Sec-Fetch-Mode": ["cors"], "Referer": ["http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "/todos", "isBase64Encoded": false} - """ - - // MARK: - Request - - - // MARK: Decoding - - func testRequestDecodingExampleGetRequest() { - let data = APIGatewayTests.exampleGetEventBody.data(using: .utf8)! - var req: APIGateway.Request? - XCTAssertNoThrow(req = try JSONDecoder().decode(APIGateway.Request.self, from: data)) - - XCTAssertEqual(req?.path, "/test") - XCTAssertEqual(req?.httpMethod, .GET) - } - - func testRequestDecodingTodoPostRequest() { - let data = APIGatewayTests.todoPostEventBody.data(using: .utf8)! - var req: APIGateway.Request? - XCTAssertNoThrow(req = try JSONDecoder().decode(APIGateway.Request.self, from: data)) - - XCTAssertEqual(req?.path, "/todos") - XCTAssertEqual(req?.httpMethod, .POST) - } - - // MARK: - Response - - - // MARK: Encoding - - struct JSONResponse: Codable { - let statusCode: UInt - let headers: [String: String]? - let body: String? - let isBase64Encoded: Bool? - } - - func testResponseEncoding() { - let resp = APIGateway.Response( - statusCode: .ok, - headers: ["Server": "Test"], - body: "abc123" - ) - - var data: Data? - XCTAssertNoThrow(data = try JSONEncoder().encode(resp)) - var json: JSONResponse? - XCTAssertNoThrow(json = try JSONDecoder().decode(JSONResponse.self, from: XCTUnwrap(data))) - - XCTAssertEqual(json?.statusCode, resp.statusCode.code) - XCTAssertEqual(json?.body, resp.body) - XCTAssertEqual(json?.isBase64Encoded, resp.isBase64Encoded) - XCTAssertEqual(json?.headers?["Server"], "Test") - } -} diff --git a/Tests/AWSLambdaEventsTests/CloudwatchTests.swift b/Tests/AWSLambdaEventsTests/CloudwatchTests.swift deleted file mode 100644 index b931e020..00000000 --- a/Tests/AWSLambdaEventsTests/CloudwatchTests.swift +++ /dev/null @@ -1,137 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class CloudwatchTests: XCTestCase { - static func eventBody(type: String, details: String) -> String { - """ - { - "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", - "detail-type": "\(type)", - "source": "aws.events", - "account": "123456789012", - "time": "1970-01-01T00:00:00Z", - "region": "us-east-1", - "resources": [ - "arn:aws:events:us-east-1:123456789012:rule/ExampleRule" - ], - "detail": \(details) - } - """ - } - - func testScheduledEventFromJSON() { - let eventBody = CloudwatchTests.eventBody(type: Cloudwatch.Scheduled.name, details: "{}") - let data = eventBody.data(using: .utf8)! - var maybeEvent: Cloudwatch.ScheduledEvent? - XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data)) - - guard let event = maybeEvent else { - return XCTFail("Expected to have an event") - } - - XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") - XCTAssertEqual(event.source, "aws.events") - XCTAssertEqual(event.accountId, "123456789012") - XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) - XCTAssertEqual(event.region, .us_east_1) - XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) - } - - func testEC2InstanceStateChangeNotificationEventFromJSON() { - let eventBody = CloudwatchTests.eventBody(type: Cloudwatch.EC2.InstanceStateChangeNotification.name, - details: "{ \"instance-id\": \"0\", \"state\": \"stopping\" }") - let data = eventBody.data(using: .utf8)! - var maybeEvent: Cloudwatch.EC2.InstanceStateChangeNotificationEvent? - XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.EC2.InstanceStateChangeNotificationEvent.self, from: data)) - - guard let event = maybeEvent else { - return XCTFail("Expected to have an event") - } - - XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") - XCTAssertEqual(event.source, "aws.events") - XCTAssertEqual(event.accountId, "123456789012") - XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) - XCTAssertEqual(event.region, .us_east_1) - XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) - XCTAssertEqual(event.detail.instanceId, "0") - XCTAssertEqual(event.detail.state, .stopping) - } - - func testEC2SpotInstanceInterruptionNoticeEventFromJSON() { - let eventBody = CloudwatchTests.eventBody(type: Cloudwatch.EC2.SpotInstanceInterruptionNotice.name, - details: "{ \"instance-id\": \"0\", \"instance-action\": \"terminate\" }") - let data = eventBody.data(using: .utf8)! - var maybeEvent: Cloudwatch.EC2.SpotInstanceInterruptionNoticeEvent? - XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.EC2.SpotInstanceInterruptionNoticeEvent.self, from: data)) - - guard let event = maybeEvent else { - return XCTFail("Expected to have an event") - } - - XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") - XCTAssertEqual(event.source, "aws.events") - XCTAssertEqual(event.accountId, "123456789012") - XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) - XCTAssertEqual(event.region, .us_east_1) - XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) - XCTAssertEqual(event.detail.instanceId, "0") - XCTAssertEqual(event.detail.action, .terminate) - } - - func testCustomEventFromJSON() { - struct Custom: CloudwatchDetail { - public static let name = "Custom" - - let name: String - } - - let eventBody = CloudwatchTests.eventBody(type: Custom.name, details: "{ \"name\": \"foo\" }") - let data = eventBody.data(using: .utf8)! - var maybeEvent: Cloudwatch.Event? - XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.Event.self, from: data)) - - guard let event = maybeEvent else { - return XCTFail("Expected to have an event") - } - - XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") - XCTAssertEqual(event.source, "aws.events") - XCTAssertEqual(event.accountId, "123456789012") - XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) - XCTAssertEqual(event.region, .us_east_1) - XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) - XCTAssertEqual(event.detail.name, "foo") - } - - func testUnregistredType() { - let eventBody = CloudwatchTests.eventBody(type: UUID().uuidString, details: "{}") - let data = eventBody.data(using: .utf8)! - XCTAssertThrowsError(try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data)) { error in - XCTAssert(error is Cloudwatch.DetailTypeMismatch, "expected DetailTypeMismatch but received \(error)") - } - } - - func testTypeMismatch() { - let eventBody = CloudwatchTests.eventBody(type: Cloudwatch.EC2.InstanceStateChangeNotification.name, - details: "{ \"instance-id\": \"0\", \"state\": \"stopping\" }") - let data = eventBody.data(using: .utf8)! - XCTAssertThrowsError(try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data)) { error in - XCTAssert(error is Cloudwatch.DetailTypeMismatch, "expected DetailTypeMismatch but received \(error)") - } - } -} diff --git a/Tests/AWSLambdaEventsTests/DynamoDBTests.swift b/Tests/AWSLambdaEventsTests/DynamoDBTests.swift deleted file mode 100644 index 91745ff6..00000000 --- a/Tests/AWSLambdaEventsTests/DynamoDBTests.swift +++ /dev/null @@ -1,232 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class DynamoDBTests: XCTestCase { - static let streamEventBody = """ - { - "Records": [ - { - "eventID": "1", - "eventVersion": "1.0", - "dynamodb": { - "ApproximateCreationDateTime": 1.578648338E9, - "Keys": { - "Id": { - "N": "101" - } - }, - "NewImage": { - "Message": { - "S": "New item!" - }, - "Id": { - "N": "101" - } - }, - "StreamViewType": "NEW_AND_OLD_IMAGES", - "SequenceNumber": "111", - "SizeBytes": 26 - }, - "awsRegion": "eu-central-1", - "eventName": "INSERT", - "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", - "eventSource": "aws:dynamodb" - }, - { - "eventID": "2", - "eventVersion": "1.0", - "dynamodb": { - "ApproximateCreationDateTime": 1.578648338E9, - "OldImage": { - "Message": { - "S": "New item!" - }, - "Id": { - "N": "101" - } - }, - "SequenceNumber": "222", - "Keys": { - "Id": { - "N": "101" - } - }, - "SizeBytes": 59, - "NewImage": { - "Message": { - "S": "This item has changed" - }, - "Id": { - "N": "101" - } - }, - "StreamViewType": "NEW_AND_OLD_IMAGES" - }, - "awsRegion": "eu-central-1", - "eventName": "MODIFY", - "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", - "eventSource": "aws:dynamodb" - }, - { - "eventID": "3", - "eventVersion": "1.0", - "dynamodb": { - "ApproximateCreationDateTime":1.578648338E9, - "Keys": { - "Id": { - "N": "101" - } - }, - "SizeBytes": 38, - "SequenceNumber": "333", - "OldImage": { - "Message": { - "S": "This item has changed" - }, - "Id": { - "N": "101" - } - }, - "StreamViewType": "NEW_AND_OLD_IMAGES" - }, - "awsRegion": "eu-central-1", - "eventName": "REMOVE", - "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", - "eventSource": "aws:dynamodb" - } - ] - } - """ - - func testEventFromJSON() { - let data = DynamoDBTests.streamEventBody.data(using: .utf8)! - var event: DynamoDB.Event? - XCTAssertNoThrow(event = try JSONDecoder().decode(DynamoDB.Event.self, from: data)) - - XCTAssertEqual(event?.records.count, 3) - } - - // MARK: - Parse Attribute Value Tests - - - func testAttributeValueBoolDecoding() { - let json = "{\"BOOL\": true}" - var value: DynamoDB.AttributeValue? - XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(value, .boolean(true)) - } - - func testAttributeValueBinaryDecoding() { - let json = "{\"B\": \"YmFzZTY0\"}" - var value: DynamoDB.AttributeValue? - XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(value, .binary([UInt8]("base64".utf8))) - } - - func testAttributeValueBinarySetDecoding() { - let json = "{\"BS\": [\"YmFzZTY0\", \"YWJjMTIz\"]}" - var value: DynamoDB.AttributeValue? - XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(value, .binarySet([[UInt8]("base64".utf8), [UInt8]("abc123".utf8)])) - } - - func testAttributeValueStringDecoding() { - let json = "{\"S\": \"huhu\"}" - var value: DynamoDB.AttributeValue? - XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(value, .string("huhu")) - } - - func testAttributeValueStringSetDecoding() { - let json = "{\"SS\": [\"huhu\", \"haha\"]}" - var value: DynamoDB.AttributeValue? - XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(value, .stringSet(["huhu", "haha"])) - } - - func testAttributeValueNullDecoding() { - let json = "{\"NULL\": true}" - var value: DynamoDB.AttributeValue? - XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(value, .null) - } - - func testAttributeValueNumberDecoding() { - let json = "{\"N\": \"1.2345\"}" - var value: DynamoDB.AttributeValue? - XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(value, .number("1.2345")) - } - - func testAttributeValueNumberSetDecoding() { - let json = "{\"NS\": [\"1.2345\", \"-19\"]}" - var value: DynamoDB.AttributeValue? - XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(value, .numberSet(["1.2345", "-19"])) - } - - func testAttributeValueListDecoding() { - let json = "{\"L\": [{\"NS\": [\"1.2345\", \"-19\"]}, {\"S\": \"huhu\"}]}" - var value: DynamoDB.AttributeValue? - XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(value, .list([.numberSet(["1.2345", "-19"]), .string("huhu")])) - } - - func testAttributeValueMapDecoding() { - let json = "{\"M\": {\"numbers\": {\"NS\": [\"1.2345\", \"-19\"]}, \"string\": {\"S\": \"huhu\"}}}" - var value: DynamoDB.AttributeValue? - XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(value, .map([ - "numbers": .numberSet(["1.2345", "-19"]), - "string": .string("huhu"), - ])) - } - - func testAttributeValueEmptyDecoding() { - let json = "{\"haha\": 1}" - XCTAssertThrowsError(_ = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) { error in - guard case DecodingError.dataCorrupted = error else { - XCTFail("Unexpected error: \(String(describing: error))") - return - } - } - } - - func testAttributeValueEquatable() { - XCTAssertEqual(DynamoDB.AttributeValue.boolean(true), .boolean(true)) - XCTAssertNotEqual(DynamoDB.AttributeValue.boolean(true), .boolean(false)) - XCTAssertNotEqual(DynamoDB.AttributeValue.boolean(true), .string("haha")) - } - - // MARK: - DynamoDB Decoder Tests - - - func testDecoderSimple() { - let value: [String: DynamoDB.AttributeValue] = [ - "foo": .string("bar"), - "xyz": .number("123"), - ] - - struct Test: Codable { - let foo: String - let xyz: UInt8 - } - - var test: Test? - XCTAssertNoThrow(test = try DynamoDB.Decoder().decode(Test.self, from: value)) - XCTAssertEqual(test?.foo, "bar") - XCTAssertEqual(test?.xyz, 123) - } -} diff --git a/Tests/AWSLambdaEventsTests/S3Tests.swift b/Tests/AWSLambdaEventsTests/S3Tests.swift deleted file mode 100644 index f1655208..00000000 --- a/Tests/AWSLambdaEventsTests/S3Tests.swift +++ /dev/null @@ -1,88 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class S3Tests: XCTestCase { - static let eventBody = """ - { - "Records": [ - { - "eventVersion":"2.1", - "eventSource":"aws:s3", - "awsRegion":"eu-central-1", - "eventTime":"2020-01-13T09:25:40.621Z", - "eventName":"ObjectCreated:Put", - "userIdentity":{ - "principalId":"AWS:AAAAAAAJ2MQ4YFQZ7AULJ" - }, - "requestParameters":{ - "sourceIPAddress":"123.123.123.123" - }, - "responseElements":{ - "x-amz-request-id":"01AFA1430E18C358", - "x-amz-id-2":"JsbNw6sHGFwgzguQjbYcew//bfAeZITyTYLfjuu1U4QYqCq5CPlSyYLtvWQS+gw0RxcroItGwm8=" - }, - "s3":{ - "s3SchemaVersion":"1.0", - "configurationId":"98b55bc4-3c0c-4007-b727-c6b77a259dde", - "bucket":{ - "name":"eventsources", - "ownerIdentity":{ - "principalId":"AAAAAAAAAAAAAA" - }, - "arn":"arn:aws:s3:::eventsources" - }, - "object":{ - "key":"Hi.md", - "size":2880, - "eTag":"91a7f2c3ae81bcc6afef83979b463f0e", - "sequencer":"005E1C37948E783A6E" - } - } - } - ] - } - """ - - func testSimpleEventFromJSON() { - let data = S3Tests.eventBody.data(using: .utf8)! - var event: S3.Event? - XCTAssertNoThrow(event = try JSONDecoder().decode(S3.Event.self, from: data)) - - guard let record = event?.records.first else { - XCTFail("Expected to have one record") - return - } - - XCTAssertEqual(record.eventVersion, "2.1") - XCTAssertEqual(record.eventSource, "aws:s3") - XCTAssertEqual(record.awsRegion, .eu_central_1) - XCTAssertEqual(record.eventName, "ObjectCreated:Put") - XCTAssertEqual(record.eventTime, Date(timeIntervalSince1970: 1_578_907_540.621)) - XCTAssertEqual(record.userIdentity, S3.UserIdentity(principalId: "AWS:AAAAAAAJ2MQ4YFQZ7AULJ")) - XCTAssertEqual(record.requestParameters, S3.RequestParameters(sourceIPAddress: "123.123.123.123")) - XCTAssertEqual(record.responseElements.count, 2) - XCTAssertEqual(record.s3.schemaVersion, "1.0") - XCTAssertEqual(record.s3.configurationId, "98b55bc4-3c0c-4007-b727-c6b77a259dde") - XCTAssertEqual(record.s3.bucket.name, "eventsources") - XCTAssertEqual(record.s3.bucket.ownerIdentity, S3.UserIdentity(principalId: "AAAAAAAAAAAAAA")) - XCTAssertEqual(record.s3.bucket.arn, "arn:aws:s3:::eventsources") - XCTAssertEqual(record.s3.object.key, "Hi.md") - XCTAssertEqual(record.s3.object.size, 2880) - XCTAssertEqual(record.s3.object.eTag, "91a7f2c3ae81bcc6afef83979b463f0e") - XCTAssertEqual(record.s3.object.sequencer, "005E1C37948E783A6E") - } -} diff --git a/Tests/AWSLambdaEventsTests/SESTests.swift b/Tests/AWSLambdaEventsTests/SESTests.swift deleted file mode 100644 index 0f4b417d..00000000 --- a/Tests/AWSLambdaEventsTests/SESTests.swift +++ /dev/null @@ -1,128 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class SESTests: XCTestCase { - static let eventBody = """ - { - "Records": [ - { - "eventSource": "aws:ses", - "eventVersion": "1.0", - "ses": { - "mail": { - "commonHeaders": { - "date": "Wed, 7 Oct 2015 12:34:56 -0700", - "from": [ - "Jane Doe " - ], - "messageId": "<0123456789example.com>", - "returnPath": "janedoe@example.com", - "subject": "Test Subject", - "to": [ - "johndoe@example.com" - ] - }, - "destination": [ - "johndoe@example.com" - ], - "headers": [ - { - "name": "Return-Path", - "value": "" - }, - { - "name": "Received", - "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)" - } - ], - "headersTruncated": true, - "messageId": "5h5auqp1oa1bg49b2q8f8tmli1oju8pcma2haao1", - "source": "janedoe@example.com", - "timestamp": "1970-01-01T00:00:00.000Z" - }, - "receipt": { - "action": { - "functionArn": "arn:aws:lambda:eu-west-1:123456789012:function:Example", - "invocationType": "Event", - "type": "Lambda" - }, - "dkimVerdict": { - "status": "PASS" - }, - "processingTimeMillis": 574, - "recipients": [ - "test@swift-server.com", - "test2@swift-server.com" - ], - "spamVerdict": { - "status": "PASS" - }, - "spfVerdict": { - "status": "PROCESSING_FAILED" - }, - "timestamp": "1970-01-01T00:00:00.000Z", - "virusVerdict": { - "status": "FAIL" - } - } - } - } - ] - } - """ - - func testSimpleEventFromJSON() { - let data = Data(SESTests.eventBody.utf8) - var event: SES.Event? - XCTAssertNoThrow(event = try JSONDecoder().decode(SES.Event.self, from: data)) - - guard let record = event?.records.first else { - XCTFail("Expected to have one record") - return - } - - XCTAssertEqual(record.eventSource, "aws:ses") - XCTAssertEqual(record.eventVersion, "1.0") - XCTAssertEqual(record.ses.mail.commonHeaders.date.description, "2015-10-07 19:34:56 +0000") - XCTAssertEqual(record.ses.mail.commonHeaders.from[0], "Jane Doe ") - XCTAssertEqual(record.ses.mail.commonHeaders.messageId, "<0123456789example.com>") - XCTAssertEqual(record.ses.mail.commonHeaders.returnPath, "janedoe@example.com") - XCTAssertEqual(record.ses.mail.commonHeaders.subject, "Test Subject") - XCTAssertEqual(record.ses.mail.commonHeaders.to?[0], "johndoe@example.com") - XCTAssertEqual(record.ses.mail.destination[0], "johndoe@example.com") - XCTAssertEqual(record.ses.mail.headers[0].name, "Return-Path") - XCTAssertEqual(record.ses.mail.headers[0].value, "") - XCTAssertEqual(record.ses.mail.headers[1].name, "Received") - XCTAssertEqual(record.ses.mail.headers[1].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)") - XCTAssertEqual(record.ses.mail.headersTruncated, true) - XCTAssertEqual(record.ses.mail.messageId, "5h5auqp1oa1bg49b2q8f8tmli1oju8pcma2haao1") - XCTAssertEqual(record.ses.mail.source, "janedoe@example.com") - XCTAssertEqual(record.ses.mail.timestamp.description, "1970-01-01 00:00:00 +0000") - - XCTAssertEqual(record.ses.receipt.action.functionArn, "arn:aws:lambda:eu-west-1:123456789012:function:Example") - XCTAssertEqual(record.ses.receipt.action.invocationType, "Event") - XCTAssertEqual(record.ses.receipt.action.type, "Lambda") - XCTAssertEqual(record.ses.receipt.dkimVerdict.status, .pass) - XCTAssertEqual(record.ses.receipt.processingTimeMillis, 574) - XCTAssertEqual(record.ses.receipt.recipients[0], "test@swift-server.com") - XCTAssertEqual(record.ses.receipt.recipients[1], "test2@swift-server.com") - XCTAssertEqual(record.ses.receipt.spamVerdict.status, .pass) - XCTAssertEqual(record.ses.receipt.spfVerdict.status, .processingFailed) - XCTAssertEqual(record.ses.receipt.timestamp.description, "1970-01-01 00:00:00 +0000") - XCTAssertEqual(record.ses.receipt.virusVerdict.status, .fail) - } -} diff --git a/Tests/AWSLambdaEventsTests/SNSTests.swift b/Tests/AWSLambdaEventsTests/SNSTests.swift deleted file mode 100644 index 3830baed..00000000 --- a/Tests/AWSLambdaEventsTests/SNSTests.swift +++ /dev/null @@ -1,82 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class SNSTests: XCTestCase { - static let eventBody = """ - { - "Records": [ - { - "EventSource": "aws:sns", - "EventVersion": "1.0", - "EventSubscriptionArn": "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c", - "Sns": { - "Type": "Notification", - "MessageId": "bdb6900e-1ae9-5b4b-b7fc-c681fde222e3", - "TopicArn": "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5", - "Subject": null, - "Message": "{\\\"hello\\\": \\\"world\\\"}", - "Timestamp": "2020-01-08T14:18:51.203Z", - "SignatureVersion": "1", - "Signature": "LJMF/xmMH7A1gNy2unLA3hmzyf6Be+zS/Yeiiz9tZbu6OG8fwvWZeNOcEZardhSiIStc0TF7h9I+4Qz3omCntaEfayzTGmWN8itGkn2mfn/hMFmPbGM8gEUz3+jp1n6p+iqP3XTx92R0LBIFrU3ylOxSo8+SCOjA015M93wfZzwj0WPtynji9iAvvtf15d8JxPUu1T05BRitpFd5s6ZXDHtVQ4x/mUoLUN8lOVp+rs281/ZdYNUG/V5CwlyUDTOERdryTkBJ/GO1NNPa+6m04ywJFa5d+BC8mDcUcHhhXXjpTEbt8AHBmswK3nudHrVMRO/G4zmssxU2P7ii5+gCfA==", - "SigningCertUrl": "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem", - "UnsubscribeUrl": "https://sns.eu-central-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c", - "MessageAttributes": { - "binary":{ - "Type": "Binary", - "Value": "YmFzZTY0" - }, - "string":{ - "Type": "String", - "Value": "abc123" - } - } - } - } - ] - } - """ - - func testSimpleEventFromJSON() { - let data = SNSTests.eventBody.data(using: .utf8)! - var event: SNS.Event? - XCTAssertNoThrow(event = try JSONDecoder().decode(SNS.Event.self, from: data)) - - guard let record = event?.records.first else { - XCTFail("Expected to have one record") - return - } - - XCTAssertEqual(record.eventSource, "aws:sns") - XCTAssertEqual(record.eventVersion, "1.0") - XCTAssertEqual(record.eventSubscriptionArn, "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c") - - XCTAssertEqual(record.sns.type, "Notification") - XCTAssertEqual(record.sns.messageId, "bdb6900e-1ae9-5b4b-b7fc-c681fde222e3") - XCTAssertEqual(record.sns.topicArn, "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5") - XCTAssertEqual(record.sns.message, "{\"hello\": \"world\"}") - XCTAssertEqual(record.sns.timestamp, Date(timeIntervalSince1970: 1_578_493_131.203)) - XCTAssertEqual(record.sns.signatureVersion, "1") - XCTAssertEqual(record.sns.signature, "LJMF/xmMH7A1gNy2unLA3hmzyf6Be+zS/Yeiiz9tZbu6OG8fwvWZeNOcEZardhSiIStc0TF7h9I+4Qz3omCntaEfayzTGmWN8itGkn2mfn/hMFmPbGM8gEUz3+jp1n6p+iqP3XTx92R0LBIFrU3ylOxSo8+SCOjA015M93wfZzwj0WPtynji9iAvvtf15d8JxPUu1T05BRitpFd5s6ZXDHtVQ4x/mUoLUN8lOVp+rs281/ZdYNUG/V5CwlyUDTOERdryTkBJ/GO1NNPa+6m04ywJFa5d+BC8mDcUcHhhXXjpTEbt8AHBmswK3nudHrVMRO/G4zmssxU2P7ii5+gCfA==") - XCTAssertEqual(record.sns.signingCertURL, "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem") - XCTAssertEqual(record.sns.unsubscribeUrl, "https://sns.eu-central-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c") - - XCTAssertEqual(record.sns.messageAttributes.count, 2) - - XCTAssertEqual(record.sns.messageAttributes["binary"], .binary([UInt8]("base64".utf8))) - XCTAssertEqual(record.sns.messageAttributes["string"], .string("abc123")) - } -} diff --git a/Tests/AWSLambdaEventsTests/SQSTests.swift b/Tests/AWSLambdaEventsTests/SQSTests.swift deleted file mode 100644 index ca8e3c70..00000000 --- a/Tests/AWSLambdaEventsTests/SQSTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class SQSTests: XCTestCase { - static let eventBody = """ - { - "Records": [ - { - "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", - "receiptHandle": "MessageReceiptHandle", - "body": "Hello from SQS!", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1523232000000", - "SenderId": "123456789012", - "ApproximateFirstReceiveTimestamp": "1523232000001" - }, - "messageAttributes": { - "number":{ - "stringValue":"123", - "stringListValues":[], - "binaryListValues":[], - "dataType":"Number" - }, - "string":{ - "stringValue":"abc123", - "stringListValues":[], - "binaryListValues":[], - "dataType":"String" - }, - "binary":{ - "dataType": "Binary", - "stringListValues":[], - "binaryListValues":[], - "binaryValue":"YmFzZTY0" - }, - - }, - "md5OfBody": "7b270e59b47ff90a553787216d55d91d", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", - "awsRegion": "us-east-1" - } - ] - } - """ - - func testSimpleEventFromJSON() { - let data = SQSTests.eventBody.data(using: .utf8)! - var event: SQS.Event? - XCTAssertNoThrow(event = try JSONDecoder().decode(SQS.Event.self, from: data)) - - guard let message = event?.records.first else { - XCTFail("Expected to have one message in the event") - return - } - - XCTAssertEqual(message.messageId, "19dd0b57-b21e-4ac1-bd88-01bbb068cb78") - XCTAssertEqual(message.receiptHandle, "MessageReceiptHandle") - XCTAssertEqual(message.body, "Hello from SQS!") - XCTAssertEqual(message.attributes.count, 4) - - XCTAssertEqual(message.messageAttributes, [ - "number": .number("123"), - "string": .string("abc123"), - "binary": .binary([UInt8]("base64".utf8)), - ]) - XCTAssertEqual(message.md5OfBody, "7b270e59b47ff90a553787216d55d91d") - XCTAssertEqual(message.eventSource, "aws:sqs") - XCTAssertEqual(message.eventSourceArn, "arn:aws:sqs:us-east-1:123456789012:MyQueue") - XCTAssertEqual(message.awsRegion, .us_east_1) - } -} diff --git a/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift b/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift deleted file mode 100644 index 59e300c1..00000000 --- a/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift +++ /dev/null @@ -1,72 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class Base64Tests: XCTestCase { - // MARK: - Decoding - - - func testDecodeEmptyString() throws { - var decoded: [UInt8]? - XCTAssertNoThrow(decoded = try "".base64decoded()) - XCTAssertEqual(decoded?.count, 0) - } - - func testBase64DecodingArrayOfNulls() throws { - let expected = Array(repeating: UInt8(0), count: 10) - var decoded: [UInt8]? - XCTAssertNoThrow(decoded = try "AAAAAAAAAAAAAA==".base64decoded()) - XCTAssertEqual(decoded, expected) - } - - func testBase64DecodingAllTheBytesSequentially() { - let base64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==" - - let expected = Array(UInt8(0) ... UInt8(255)) - var decoded: [UInt8]? - XCTAssertNoThrow(decoded = try base64.base64decoded()) - - XCTAssertEqual(decoded, expected) - } - - func testBase64UrlDecodingAllTheBytesSequentially() { - let base64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w==" - - let expected = Array(UInt8(0) ... UInt8(255)) - var decoded: [UInt8]? - XCTAssertNoThrow(decoded = try base64.base64decoded(options: .base64UrlAlphabet)) - - XCTAssertEqual(decoded, expected) - } - - func testBase64DecodingWithPoop() { - XCTAssertThrowsError(_ = try "💩".base64decoded()) { error in - XCTAssertEqual(error as? Base64.DecodingError, .invalidCharacter(240)) - } - } - - func testBase64DecodingWithInvalidLength() { - XCTAssertThrowsError(_ = try "AAAAA".base64decoded()) { error in - XCTAssertEqual(error as? Base64.DecodingError, .invalidLength) - } - } - - func testNSStringToDecode() { - let test = "1234567" - let nsstring = test.data(using: .utf8)!.base64EncodedString() - - XCTAssertNoThrow(try nsstring.base64decoded()) - } -} diff --git a/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift b/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift deleted file mode 100644 index 35b1a474..00000000 --- a/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift +++ /dev/null @@ -1,140 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaEvents -import XCTest - -class DateWrapperTests: XCTestCase { - func testISO8601CodingWrapperSuccess() { - struct TestEvent: Decodable { - @ISO8601Coding - var date: Date - } - - let json = #"{"date":"2020-03-26T16:53:05Z"}"# - var event: TestEvent? - XCTAssertNoThrow(event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) - - XCTAssertEqual(event?.date, Date(timeIntervalSince1970: 1_585_241_585)) - } - - func testISO8601CodingWrapperFailure() { - struct TestEvent: Decodable { - @ISO8601Coding - var date: Date - } - - let date = "2020-03-26T16:53:05" // missing Z at end - let json = #"{"date":"\#(date)"}"# - XCTAssertThrowsError(_ = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) { error in - guard case DecodingError.dataCorrupted(let context) = error else { - XCTFail("Unexpected error: \(error)"); return - } - - XCTAssertEqual(context.codingPath.compactMap { $0.stringValue }, ["date"]) - XCTAssertEqual(context.debugDescription, "Expected date to be in ISO8601 date format, but `\(date)` is not in the correct format") - XCTAssertNil(context.underlyingError) - } - } - - func testISO8601WithFractionalSecondsCodingWrapperSuccess() { - struct TestEvent: Decodable { - @ISO8601WithFractionalSecondsCoding - var date: Date - } - - let json = #"{"date":"2020-03-26T16:53:05.123Z"}"# - var event: TestEvent? - XCTAssertNoThrow(event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) - - XCTAssertEqual(event?.date, Date(timeIntervalSince1970: 1_585_241_585.123)) - } - - func testISO8601WithFractionalSecondsCodingWrapperFailure() { - struct TestEvent: Decodable { - @ISO8601WithFractionalSecondsCoding - var date: Date - } - - let date = "2020-03-26T16:53:05Z" // missing fractional seconds - let json = #"{"date":"\#(date)"}"# - XCTAssertThrowsError(_ = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) { error in - guard case DecodingError.dataCorrupted(let context) = error else { - XCTFail("Unexpected error: \(error)"); return - } - - XCTAssertEqual(context.codingPath.compactMap { $0.stringValue }, ["date"]) - XCTAssertEqual(context.debugDescription, "Expected date to be in ISO8601 date format with fractional seconds, but `\(date)` is not in the correct format") - XCTAssertNil(context.underlyingError) - } - } - - func testRFC5322DateTimeCodingWrapperSuccess() { - struct TestEvent: Decodable { - @RFC5322DateTimeCoding - var date: Date - } - - let json = #"{"date":"Thu, 5 Apr 2012 23:47:37 +0200"}"# - var event: TestEvent? - XCTAssertNoThrow(event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) - - XCTAssertEqual(event?.date.description, "2012-04-05 21:47:37 +0000") - } - - func testRFC5322DateTimeCodingWrapperWithExtraTimeZoneSuccess() { - struct TestEvent: Decodable { - @RFC5322DateTimeCoding - var date: Date - } - - let json = #"{"date":"Fri, 26 Jun 2020 03:04:03 -0500 (CDT)"}"# - var event: TestEvent? - XCTAssertNoThrow(event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) - - XCTAssertEqual(event?.date.description, "2020-06-26 08:04:03 +0000") - } - - func testRFC5322DateTimeCodingWrapperWithAlphabeticTimeZoneSuccess() { - struct TestEvent: Decodable { - @RFC5322DateTimeCoding - var date: Date - } - - let json = #"{"date":"Fri, 26 Jun 2020 03:04:03 CDT"}"# - var event: TestEvent? - XCTAssertNoThrow(event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) - - XCTAssertEqual(event?.date.description, "2020-06-26 08:04:03 +0000") - } - - func testRFC5322DateTimeCodingWrapperFailure() { - struct TestEvent: Decodable { - @RFC5322DateTimeCoding - var date: Date - } - - let date = "Thu, 5 Apr 2012 23:47 +0200" // missing seconds - let json = #"{"date":"\#(date)"}"# - XCTAssertThrowsError(_ = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) { error in - guard case DecodingError.dataCorrupted(let context) = error else { - XCTFail("Unexpected error: \(error)"); return - } - - XCTAssertEqual(context.codingPath.compactMap { $0.stringValue }, ["date"]) - XCTAssertEqual(context.debugDescription, "Expected date to be in RFC5322 date-time format with fractional seconds, but `\(date)` is not in the correct format") - XCTAssertNil(context.underlyingError) - } - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/Lambda+StringTest.swift b/Tests/AWSLambdaRuntimeCoreTests/Lambda+StringTest.swift deleted file mode 100644 index 8e880296..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/Lambda+StringTest.swift +++ /dev/null @@ -1,245 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import NIO -import XCTest - -class StringLambdaTest: XCTestCase { - func testCallbackSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = String - typealias Out = String - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.success(event)) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testVoidCallbackSuccess() { - let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = String - typealias Out = Void - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.success(())) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testCallbackFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = String - typealias Out = String - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.failure(TestError("boom"))) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testEventLoopSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: EventLoopLambdaHandler { - typealias In = String - typealias Out = String - - func handle(context: Lambda.Context, event: String) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(event) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testVoidEventLoopSuccess() { - let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: EventLoopLambdaHandler { - typealias In = String - typealias Out = Void - - func handle(context: Lambda.Context, event: String) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(()) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testEventLoopFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: EventLoopLambdaHandler { - typealias In = String - typealias Out = String - - func handle(context: Lambda.Context, event: String) -> EventLoopFuture { - context.eventLoop.makeFailedFuture(TestError("boom")) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testClosureSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration) { (_, event: String, callback) in - callback(.success(event)) - } - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testVoidClosureSuccess() { - let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration) { (_, _: String, callback) in - callback(.success(())) - } - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testClosureFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result: Result = Lambda.run(configuration: configuration) { (_, _: String, callback: (Result) -> Void) in - callback(.failure(TestError("boom"))) - } - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testBootstrapFailure() { - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = String - typealias Out = String - - init(context: Lambda.InitializationContext) throws { - throw TestError("kaboom") - } - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.failure(TestError("should not be called"))) - } - } - - let result = Lambda.run(factory: Handler.init) - assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) - } -} - -private struct Behavior: LambdaServerBehavior { - let requestId: String - let event: String - let result: Result - - init(requestId: String = UUID().uuidString, event: String = "hello", result: Result = .success("hello")) { - self.requestId = requestId - self.event = event - self.result = result - } - - func getInvocation() -> GetInvocationResult { - .success((requestId: self.requestId, event: self.event)) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - switch self.result { - case .success(let expected): - XCTAssertEqual(expected, response, "expecting response to match") - return .success(()) - case .failure: - XCTFail("unexpected to fail, but succeeded with: \(response ?? "undefined")") - return .failure(.internalServerError) - } - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - switch self.result { - case .success: - XCTFail("unexpected to succeed, but failed with: \(error)") - return .failure(.internalServerError) - case .failure(let expected): - XCTAssertEqual(expected.description, error.errorMessage, "expecting error to match") - return .success(()) - } - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaLifecycleTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaLifecycleTest.swift deleted file mode 100644 index a485530d..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaLifecycleTest.swift +++ /dev/null @@ -1,142 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import Logging -import NIO -import NIOHTTP1 -import XCTest - -class LambdaLifecycleTest: XCTestCase { - func testShutdownFutureIsFulfilledWithStartUpError() { - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let eventLoop = eventLoopGroup.next() - let logger = Logger(label: "TestLogger") - let testError = TestError("kaboom") - let lifecycle = Lambda.Lifecycle(eventLoop: eventLoop, logger: logger, factory: { - $0.eventLoop.makeFailedFuture(testError) - }) - - // eventLoop.submit in this case returns an EventLoopFuture> - // which is why we need `wait().wait()` - XCTAssertThrowsError(_ = try eventLoop.flatSubmit { lifecycle.start() }.wait()) { error in - XCTAssertEqual(testError, error as? TestError) - } - - XCTAssertThrowsError(_ = try lifecycle.shutdownFuture.wait()) { error in - XCTAssertEqual(testError, error as? TestError) - } - } - - struct CallbackLambdaHandler: ByteBufferLambdaHandler { - let handler: (Lambda.Context, ByteBuffer) -> (EventLoopFuture) - let shutdown: (Lambda.ShutdownContext) -> EventLoopFuture - - init(_ handler: @escaping (Lambda.Context, ByteBuffer) -> (EventLoopFuture), shutdown: @escaping (Lambda.ShutdownContext) -> EventLoopFuture) { - self.handler = handler - self.shutdown = shutdown - } - - func handle(context: Lambda.Context, event: ByteBuffer) -> EventLoopFuture { - self.handler(context, event) - } - - func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture { - self.shutdown(context) - } - } - - func testShutdownIsCalledWhenLambdaShutsdown() { - let server = MockLambdaServer(behavior: BadBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - var count = 0 - let handler = CallbackLambdaHandler({ XCTFail("Should not be reached"); return $0.eventLoop.makeSucceededFuture($1) }) { context in - count += 1 - return context.eventLoop.makeSucceededFuture(Void()) - } - - let eventLoop = eventLoopGroup.next() - let logger = Logger(label: "TestLogger") - let lifecycle = Lambda.Lifecycle(eventLoop: eventLoop, logger: logger, factory: { - $0.eventLoop.makeSucceededFuture(handler) - }) - - XCTAssertNoThrow(_ = try eventLoop.flatSubmit { lifecycle.start() }.wait()) - XCTAssertThrowsError(_ = try lifecycle.shutdownFuture.wait()) { error in - XCTAssertEqual(.badStatusCode(HTTPResponseStatus.internalServerError), error as? Lambda.RuntimeError) - } - XCTAssertEqual(count, 1) - } - - func testLambdaResultIfShutsdownIsUnclean() { - let server = MockLambdaServer(behavior: BadBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - var count = 0 - let handler = CallbackLambdaHandler({ XCTFail("Should not be reached"); return $0.eventLoop.makeSucceededFuture($1) }) { context in - count += 1 - return context.eventLoop.makeFailedFuture(TestError("kaboom")) - } - - let eventLoop = eventLoopGroup.next() - let logger = Logger(label: "TestLogger") - let lifecycle = Lambda.Lifecycle(eventLoop: eventLoop, logger: logger, factory: { - $0.eventLoop.makeSucceededFuture(handler) - }) - - XCTAssertNoThrow(_ = try eventLoop.flatSubmit { lifecycle.start() }.wait()) - XCTAssertThrowsError(_ = try lifecycle.shutdownFuture.wait()) { error in - guard case Lambda.RuntimeError.shutdownError(let shutdownError, .failure(let runtimeError)) = error else { - XCTFail("Unexpected error"); return - } - - XCTAssertEqual(shutdownError as? TestError, TestError("kaboom")) - XCTAssertEqual(runtimeError as? Lambda.RuntimeError, .badStatusCode(.internalServerError)) - } - XCTAssertEqual(count, 1) - } -} - -struct BadBehavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report a response") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report an error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report an error") - return .failure(.internalServerError) - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift deleted file mode 100644 index dd87eb61..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift +++ /dev/null @@ -1,72 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import XCTest - -class LambdaRunnerTest: XCTestCase { - func testSuccess() { - struct Behavior: LambdaServerBehavior { - let requestId = UUID().uuidString - let event = "hello" - func getInvocation() -> GetInvocationResult { - .success((self.requestId, self.event)) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - XCTAssertEqual(self.event, response, "expecting response to match") - return .success(()) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertNoThrow(try runLambda(behavior: Behavior(), handler: EchoHandler())) - } - - func testFailure() { - struct Behavior: LambdaServerBehavior { - static let error = "boom" - let requestId = UUID().uuidString - func getInvocation() -> GetInvocationResult { - .success((requestId: self.requestId, event: "hello")) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should report error") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - XCTAssertEqual(Behavior.error, error.errorMessage, "expecting error to match") - return .success(()) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertNoThrow(try runLambda(behavior: Behavior(), handler: FailedHandler(Behavior.error))) - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift deleted file mode 100644 index 94c8ac62..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift +++ /dev/null @@ -1,344 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import Logging -import NIO -import NIOFoundationCompat -import NIOHTTP1 -import NIOTestUtils -import XCTest - -class LambdaRuntimeClientTest: XCTestCase { - func testSuccess() { - let behavior = Behavior() - XCTAssertNoThrow(try runLambda(behavior: behavior, handler: EchoHandler())) - XCTAssertEqual(behavior.state, 6) - } - - func testFailure() { - let behavior = Behavior() - XCTAssertNoThrow(try runLambda(behavior: behavior, handler: FailedHandler("boom"))) - XCTAssertEqual(behavior.state, 10) - } - - func testBootstrapFailure() { - let behavior = Behavior() - XCTAssertThrowsError(try runLambda(behavior: behavior, factory: { $0.eventLoop.makeFailedFuture(TestError("boom")) })) { error in - XCTAssertEqual(error as? TestError, TestError("boom")) - } - XCTAssertEqual(behavior.state, 1) - } - - func testGetInvocationServerInternalError() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report results") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: EchoHandler())) { error in - XCTAssertEqual(error as? Lambda.RuntimeError, .badStatusCode(.internalServerError)) - } - } - - func testGetInvocationServerNoBodyError() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .success(("1", "")) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report results") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: EchoHandler())) { error in - XCTAssertEqual(error as? Lambda.RuntimeError, .noBody) - } - } - - func testGetInvocationServerMissingHeaderRequestIDError() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - // no request id -> no context - .success(("", "hello")) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report results") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: EchoHandler())) { error in - XCTAssertEqual(error as? Lambda.RuntimeError, .invocationMissingHeader(AmazonHeaders.requestID)) - } - } - - func testProcessResponseInternalServerError() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .success((requestId: "1", event: "event")) - } - - func processResponse(requestId: String, response: String?) -> Result { - .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: EchoHandler())) { error in - XCTAssertEqual(error as? Lambda.RuntimeError, .badStatusCode(.internalServerError)) - } - } - - func testProcessErrorInternalServerError() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .success((requestId: "1", event: "event")) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report results") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: FailedHandler("boom"))) { error in - XCTAssertEqual(error as? Lambda.RuntimeError, .badStatusCode(.internalServerError)) - } - } - - func testProcessInitErrorOnBootstrapFailure() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - XCTFail("should not get invocation") - return .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report results") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - .failure(.internalServerError) - } - } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), factory: { $0.eventLoop.makeFailedFuture(TestError("boom")) })) { error in - XCTAssertEqual(error as? TestError, TestError("boom")) - } - } - - func testErrorResponseToJSON() { - // we want to check if quotes and back slashes are correctly escaped - let windowsError = ErrorResponse( - errorType: "error", - errorMessage: #"underlyingError: "An error with a windows path C:\Windows\""# - ) - let windowsBytes = windowsError.toJSONBytes() - XCTAssertEqual(#"{"errorType":"error","errorMessage":"underlyingError: \"An error with a windows path C:\\Windows\\\""}"#, String(decoding: windowsBytes, as: Unicode.UTF8.self)) - - // we want to check if unicode sequences work - let emojiError = ErrorResponse( - errorType: "error", - errorMessage: #"🥑👨‍👩‍👧‍👧👩‍👩‍👧‍👧👨‍👨‍👧"# - ) - let emojiBytes = emojiError.toJSONBytes() - XCTAssertEqual(#"{"errorType":"error","errorMessage":"🥑👨‍👩‍👧‍👧👩‍👩‍👧‍👧👨‍👨‍👧"}"#, String(decoding: emojiBytes, as: Unicode.UTF8.self)) - } - - func testInitializationErrorReport() { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let server = NIOHTTP1TestServer(group: eventLoopGroup) - defer { XCTAssertNoThrow(try server.stop()) } - - let logger = Logger(label: "TestLogger") - let client = Lambda.RuntimeClient(eventLoop: eventLoopGroup.next(), configuration: .init(address: "127.0.0.1:\(server.serverPort)")) - let result = client.reportInitializationError(logger: logger, error: TestError("boom")) - - var inboundHeader: HTTPServerRequestPart? - XCTAssertNoThrow(inboundHeader = try server.readInbound()) - guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { XCTFail("Expected to get a head first"); return } - XCTAssertEqual(head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) - XCTAssertEqual(head.headers["user-agent"], ["Swift-Lambda/Unknown"]) - - var inboundBody: HTTPServerRequestPart? - XCTAssertNoThrow(inboundBody = try server.readInbound()) - guard case .body(let body) = try? XCTUnwrap(inboundBody) else { XCTFail("Expected body after head"); return } - XCTAssertEqual(try JSONDecoder().decode(ErrorResponse.self, from: body).errorMessage, "boom") - - XCTAssertEqual(try server.readInbound(), .end(nil)) - - XCTAssertNoThrow(try server.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .accepted)))) - XCTAssertNoThrow(try server.writeOutbound(.end(nil))) - XCTAssertNoThrow(try result.wait()) - } - - func testInvocationErrorReport() { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let server = NIOHTTP1TestServer(group: eventLoopGroup) - defer { XCTAssertNoThrow(try server.stop()) } - - let logger = Logger(label: "TestLogger") - let client = Lambda.RuntimeClient(eventLoop: eventLoopGroup.next(), configuration: .init(address: "127.0.0.1:\(server.serverPort)")) - - let header = HTTPHeaders([ - (AmazonHeaders.requestID, "test"), - (AmazonHeaders.deadline, String(Date(timeIntervalSinceNow: 60).millisSinceEpoch)), - (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), - (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - ]) - var inv: Lambda.Invocation? - XCTAssertNoThrow(inv = try Lambda.Invocation(headers: header)) - guard let invocation = inv else { return } - - let result = client.reportResults(logger: logger, invocation: invocation, result: Result.failure(TestError("boom"))) - - var inboundHeader: HTTPServerRequestPart? - XCTAssertNoThrow(inboundHeader = try server.readInbound()) - guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { XCTFail("Expected to get a head first"); return } - XCTAssertEqual(head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) - XCTAssertEqual(head.headers["user-agent"], ["Swift-Lambda/Unknown"]) - - var inboundBody: HTTPServerRequestPart? - XCTAssertNoThrow(inboundBody = try server.readInbound()) - guard case .body(let body) = try? XCTUnwrap(inboundBody) else { XCTFail("Expected body after head"); return } - XCTAssertEqual(try JSONDecoder().decode(ErrorResponse.self, from: body).errorMessage, "boom") - - XCTAssertEqual(try server.readInbound(), .end(nil)) - - XCTAssertNoThrow(try server.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .accepted)))) - XCTAssertNoThrow(try server.writeOutbound(.end(nil))) - XCTAssertNoThrow(try result.wait()) - } - - func testInvocationSuccessResponse() { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let server = NIOHTTP1TestServer(group: eventLoopGroup) - defer { XCTAssertNoThrow(try server.stop()) } - - let logger = Logger(label: "TestLogger") - let client = Lambda.RuntimeClient(eventLoop: eventLoopGroup.next(), configuration: .init(address: "127.0.0.1:\(server.serverPort)")) - - let header = HTTPHeaders([ - (AmazonHeaders.requestID, "test"), - (AmazonHeaders.deadline, String(Date(timeIntervalSinceNow: 60).millisSinceEpoch)), - (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), - (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - ]) - var inv: Lambda.Invocation? - XCTAssertNoThrow(inv = try Lambda.Invocation(headers: header)) - guard let invocation = inv else { return } - - let result = client.reportResults(logger: logger, invocation: invocation, result: Result.success(nil)) - - var inboundHeader: HTTPServerRequestPart? - XCTAssertNoThrow(inboundHeader = try server.readInbound()) - guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { XCTFail("Expected to get a head first"); return } - XCTAssertFalse(head.headers.contains(name: "lambda-runtime-function-error-type")) - XCTAssertEqual(head.headers["user-agent"], ["Swift-Lambda/Unknown"]) - - XCTAssertEqual(try server.readInbound(), .end(nil)) - - XCTAssertNoThrow(try server.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .accepted)))) - XCTAssertNoThrow(try server.writeOutbound(.end(nil))) - XCTAssertNoThrow(try result.wait()) - } - - class Behavior: LambdaServerBehavior { - var state = 0 - - func processInitError(error: ErrorResponse) -> Result { - self.state += 1 - return .success(()) - } - - func getInvocation() -> GetInvocationResult { - self.state += 2 - return .success(("1", "hello")) - } - - func processResponse(requestId: String, response: String?) -> Result { - self.state += 4 - return .success(()) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - self.state += 8 - return .success(()) - } - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift deleted file mode 100644 index 30fed618..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift +++ /dev/null @@ -1,361 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import Logging -import NIO -import XCTest - -class LambdaTest: XCTestCase { - func testSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 10 ... 20) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 10 ... 20) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: FailedHandler("boom")) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testBootstrapOnce() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = String - typealias Out = String - - var initialized = false - - init(context: Lambda.InitializationContext) { - XCTAssertFalse(self.initialized) - self.initialized = true - } - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.success(event)) - } - } - - let maxTimes = Int.random(in: 10 ... 20) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, factory: Handler.init) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testBootstrapFailure() { - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let result = Lambda.run(factory: { $0.eventLoop.makeFailedFuture(TestError("kaboom")) }) - assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) - } - - func testBootstrapFailure2() { - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = String - typealias Out = Void - - init(context: Lambda.InitializationContext) throws { - throw TestError("kaboom") - } - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.failure(TestError("should not be called"))) - } - } - - let result = Lambda.run(factory: Handler.init) - assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) - } - - func testBootstrapFailureAndReportErrorFailure() { - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - XCTFail("should not get invocation") - return .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report a response") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report an error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - .failure(.internalServerError) - } - } - - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let result = Lambda.run(factory: { $0.eventLoop.makeFailedFuture(TestError("kaboom")) }) - assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) - } - - func testStartStopInDebugMode() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let signal = Signal.ALRM - let maxTimes = 1000 - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes, stopSignal: signal)) - - DispatchQueue(label: "test").async { - // we need to schedule the signal before we start the long running `Lambda.run`, since - // `Lambda.run` will block the main thread. - usleep(100_000) - kill(getpid(), signal.rawValue) - } - let result = Lambda.run(configuration: configuration, factory: { $0.eventLoop.makeSucceededFuture(EchoHandler()) }) - - switch result { - case .success(let invocationCount): - XCTAssertGreaterThan(invocationCount, 0, "should have stopped before any request made") - XCTAssertLessThan(invocationCount, maxTimes, "should have stopped before \(maxTimes)") - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - } - - func testTimeout() { - let timeout: Int64 = 100 - let server = MockLambdaServer(behavior: Behavior(requestId: "timeout", event: "\(timeout * 2)")) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1), - runtimeEngine: .init(requestTimeout: .milliseconds(timeout))) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) - assertLambdaLifecycleResult(result, shouldFailWithError: Lambda.RuntimeError.upstreamError("timeout")) - } - - func testDisconnect() { - let server = MockLambdaServer(behavior: Behavior(requestId: "disconnect")) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1)) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) - assertLambdaLifecycleResult(result, shouldFailWithError: Lambda.RuntimeError.upstreamError("connectionResetByPeer")) - } - - func testBigEvent() { - let event = String(repeating: "*", count: 104_448) - let server = MockLambdaServer(behavior: Behavior(event: event, result: .success(event))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1)) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) - assertLambdaLifecycleResult(result, shoudHaveRun: 1) - } - - func testKeepAliveServer() { - let server = MockLambdaServer(behavior: Behavior(), keepAlive: true) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = 10 - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testNoKeepAliveServer() { - let server = MockLambdaServer(behavior: Behavior(), keepAlive: false) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = 10 - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: EchoHandler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testServerFailure() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Behavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } - } - - let result = Lambda.run(handler: EchoHandler()) - assertLambdaLifecycleResult(result, shouldFailWithError: Lambda.RuntimeError.badStatusCode(.internalServerError)) - } - - func testDeadline() { - let delta = Int.random(in: 1 ... 600) - - let milli1 = Date(timeIntervalSinceNow: Double(delta)).millisSinceEpoch - let milli2 = (DispatchWallTime.now() + .seconds(delta)).millisSinceEpoch - XCTAssertEqual(Double(milli1), Double(milli2), accuracy: 2.0) - - let now1 = DispatchWallTime.now() - let now2 = DispatchWallTime(millisSinceEpoch: Date().millisSinceEpoch) - XCTAssertEqual(Double(now2.rawValue), Double(now1.rawValue), accuracy: 2_000_000.0) - - let future1 = DispatchWallTime.now() + .seconds(delta) - let future2 = DispatchWallTime(millisSinceEpoch: Date(timeIntervalSinceNow: Double(delta)).millisSinceEpoch) - XCTAssertEqual(Double(future1.rawValue), Double(future2.rawValue), accuracy: 2_000_000.0) - - let past1 = DispatchWallTime.now() - .seconds(delta) - let past2 = DispatchWallTime(millisSinceEpoch: Date(timeIntervalSinceNow: Double(-delta)).millisSinceEpoch) - XCTAssertEqual(Double(past1.rawValue), Double(past2.rawValue), accuracy: 2_000_000.0) - - let context = Lambda.Context(requestID: UUID().uuidString, - traceID: UUID().uuidString, - invokedFunctionARN: UUID().uuidString, - deadline: .now() + .seconds(1), - cognitoIdentity: nil, - clientContext: nil, - logger: Logger(label: "test"), - eventLoop: MultiThreadedEventLoopGroup(numberOfThreads: 1).next(), - allocator: ByteBufferAllocator()) - XCTAssertGreaterThan(context.deadline, .now()) - - let expiredContext = Lambda.Context(requestID: context.requestID, - traceID: context.traceID, - invokedFunctionARN: context.invokedFunctionARN, - deadline: .now() - .seconds(1), - cognitoIdentity: context.cognitoIdentity, - clientContext: context.clientContext, - logger: context.logger, - eventLoop: context.eventLoop, - allocator: context.allocator) - XCTAssertLessThan(expiredContext.deadline, .now()) - } - - func testGetRemainingTime() { - let context = Lambda.Context(requestID: UUID().uuidString, - traceID: UUID().uuidString, - invokedFunctionARN: UUID().uuidString, - deadline: .now() + .seconds(1), - cognitoIdentity: nil, - clientContext: nil, - logger: Logger(label: "test"), - eventLoop: MultiThreadedEventLoopGroup(numberOfThreads: 1).next(), - allocator: ByteBufferAllocator()) - XCTAssertLessThanOrEqual(context.getRemainingTime(), .seconds(1)) - XCTAssertGreaterThan(context.getRemainingTime(), .milliseconds(800)) - } -} - -private struct Behavior: LambdaServerBehavior { - let requestId: String - let event: String - let result: Result - - init(requestId: String = UUID().uuidString, event: String = "hello", result: Result = .success("hello")) { - self.requestId = requestId - self.event = event - self.result = result - } - - func getInvocation() -> GetInvocationResult { - .success((requestId: self.requestId, event: self.event)) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - switch self.result { - case .success(let expected): - XCTAssertEqual(expected, response, "expecting response to match") - return .success(()) - case .failure: - XCTFail("unexpected to fail, but succeeded with: \(response ?? "undefined")") - return .failure(.internalServerError) - } - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - switch self.result { - case .success: - XCTFail("unexpected to succeed, but failed with: \(error)") - return .failure(.internalServerError) - case .failure(let expected): - XCTAssertEqual(expected.description, error.errorMessage, "expecting error to match") - return .success(()) - } - } - - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) - } -} - -struct FailedBootstrapBehavior: LambdaServerBehavior { - func getInvocation() -> GetInvocationResult { - XCTFail("should not get invocation") - return .failure(.internalServerError) - } - - func processResponse(requestId: String, response: String?) -> Result { - XCTFail("should not report a response") - return .failure(.internalServerError) - } - - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTFail("should not report an error") - return .failure(.internalServerError) - } - - func processInitError(error: ErrorResponse) -> Result { - .success(()) - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift deleted file mode 100644 index e7160307..00000000 --- a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift +++ /dev/null @@ -1,95 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntimeCore -import Logging -import NIO -import XCTest - -func runLambda(behavior: LambdaServerBehavior, handler: Lambda.Handler) throws { - try runLambda(behavior: behavior, factory: { $0.eventLoop.makeSucceededFuture(handler) }) -} - -func runLambda(behavior: LambdaServerBehavior, factory: @escaping Lambda.HandlerFactory) throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - let logger = Logger(label: "TestLogger") - let configuration = Lambda.Configuration(runtimeEngine: .init(requestTimeout: .milliseconds(100))) - let runner = Lambda.Runner(eventLoop: eventLoopGroup.next(), configuration: configuration) - let server = try MockLambdaServer(behavior: behavior).start().wait() - defer { XCTAssertNoThrow(try server.stop().wait()) } - try runner.initialize(logger: logger, factory: factory).flatMap { handler in - runner.run(logger: logger, handler: handler) - }.wait() -} - -struct EchoHandler: LambdaHandler { - typealias In = String - typealias Out = String - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.success(event)) - } -} - -struct FailedHandler: LambdaHandler { - typealias In = String - typealias Out = Void - - private let reason: String - - public init(_ reason: String) { - self.reason = reason - } - - func handle(context: Lambda.Context, event: String, callback: (Result) -> Void) { - callback(.failure(TestError(self.reason))) - } -} - -func assertLambdaLifecycleResult(_ result: Result, shoudHaveRun: Int = 0, shouldFailWithError: Error? = nil, file: StaticString = #file, line: UInt = #line) { - switch result { - case .success where shouldFailWithError != nil: - XCTFail("should fail with \(shouldFailWithError!)", file: file, line: line) - case .success(let count) where shouldFailWithError == nil: - XCTAssertEqual(shoudHaveRun, count, "should have run \(shoudHaveRun) times", file: file, line: line) - case .failure(let error) where shouldFailWithError == nil: - XCTFail("should succeed, but failed with \(error)", file: file, line: line) - case .failure(let error) where shouldFailWithError != nil: - XCTAssertEqual(String(describing: shouldFailWithError!), String(describing: error), "expected error to mactch", file: file, line: line) - default: - XCTFail("invalid state") - } -} - -struct TestError: Error, Equatable, CustomStringConvertible { - let description: String - - init(_ description: String) { - self.description = description - } -} - -internal extension Date { - var millisSinceEpoch: Int64 { - Int64(self.timeIntervalSince1970 * 1000) - } -} - -extension Lambda.RuntimeError: Equatable { - public static func == (lhs: Lambda.RuntimeError, rhs: Lambda.RuntimeError) -> Bool { - // technically incorrect, but good enough for our tests - String(describing: lhs) == String(describing: rhs) - } -} diff --git a/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift b/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift new file mode 100644 index 00000000..537847d8 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import Synchronization +import Testing + +struct CollectEverythingLogHandler: LogHandler { + var metadata: Logger.Metadata = [:] + var logLevel: Logger.Level = .info + let logStore: LogStore + + final class LogStore: Sendable { + struct Entry: Sendable { + var level: Logger.Level + var message: String + var metadata: [String: String] + } + + let logs: Mutex<[Entry]> = .init([]) + + func append(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?) { + self.logs.withLock { entries in + entries.append( + Entry( + level: level, + message: message.description, + metadata: metadata?.mapValues { $0.description } ?? [:] + ) + ) + } + } + + func clear() { + self.logs.withLock { + $0.removeAll() + } + } + + enum LogFieldExpectedValue: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { + case exactMatch(String) + case beginsWith(String) + case wildcard + case predicate((String) -> Bool) + + init(stringLiteral value: String) { + self = .exactMatch(value) + } + } + + @discardableResult + func assertContainsLog( + _ message: String, + _ metadata: (String, LogFieldExpectedValue)..., + sourceLocation: SourceLocation = #_sourceLocation + ) -> [Entry] { + var candidates = self.getAllLogsWithMessage(message) + if candidates.isEmpty { + Issue.record("Logs do not contain entry with message: \(message)", sourceLocation: sourceLocation) + return [] + } + for (key, value) in metadata { + var errorMsg: String + switch value { + case .wildcard: + candidates = candidates.filter { $0.metadata.contains { $0.key == key } } + errorMsg = "Logs do not contain entry with message: \(message) and metadata: \(key) *" + case .predicate(let predicate): + candidates = candidates.filter { $0.metadata[key].map(predicate) ?? false } + errorMsg = + "Logs do not contain entry with message: \(message) and metadata: \(key) matching predicate" + case .beginsWith(let prefix): + candidates = candidates.filter { $0.metadata[key]?.hasPrefix(prefix) ?? false } + errorMsg = "Logs do not contain entry with message: \(message) and metadata: \(key), \(value)" + case .exactMatch(let value): + candidates = candidates.filter { $0.metadata[key] == value } + errorMsg = "Logs do not contain entry with message: \(message) and metadata: \(key), \(value)" + } + if candidates.isEmpty { + Issue.record("Error: \(errorMsg)", sourceLocation: sourceLocation) + return [] + } + } + return candidates + } + + func assertDoesNotContainMessage(_ message: String, sourceLocation: SourceLocation = #_sourceLocation) { + let candidates = self.getAllLogsWithMessage(message) + if candidates.count > 0 { + Issue.record("Logs contain entry with message: \(message)", sourceLocation: sourceLocation) + } + } + + func getAllLogs() -> [Entry] { + self.logs.withLock { $0 } + } + + func getAllLogsWithMessage(_ message: String) -> [Entry] { + self.getAllLogs().filter { $0.message == message } + } + } + + init(logStore: LogStore) { + self.logStore = logStore + } + + func log( + level: Logger.Level, + message: Logger.Message, + metadata: Logger.Metadata?, + source: String, + file: String, + function: String, + line: UInt + ) { + self.logStore.append(level: level, message: message, metadata: self.metadata.merging(metadata ?? [:]) { $1 }) + } + + subscript(metadataKey key: String) -> Logger.Metadata.Value? { + get { + self.metadata[key] + } + set { + self.metadata[key] = newValue + } + } +} diff --git a/Tests/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift b/Tests/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift new file mode 100644 index 00000000..840a17c6 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift @@ -0,0 +1,184 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOEmbedded +import NIOHTTP1 +import XCTest + +@testable import AWSLambdaRuntime + +final class ControlPlaneRequestEncoderTests: XCTestCase { + let host = "192.168.0.1" + + var client: EmbeddedChannel! + var server: EmbeddedChannel! + + override func setUp() { + self.client = EmbeddedChannel(handler: ControlPlaneRequestEncoderHandler(host: self.host)) + self.server = EmbeddedChannel(handlers: [ + ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .dropBytes)), + NIOHTTPServerRequestAggregator(maxContentLength: 1024 * 1024), + ]) + } + + override func tearDown() { + XCTAssertNoThrow(try self.client.finish(acceptAlreadyClosed: false)) + XCTAssertNoThrow(try self.server.finish(acceptAlreadyClosed: false)) + self.client = nil + self.server = nil + } + + func testNextRequest() { + var request: NIOHTTPServerRequestFull? + XCTAssertNoThrow(request = try self.sendRequest(.next)) + + XCTAssertEqual(request?.head.isKeepAlive, true) + XCTAssertEqual(request?.head.method, .GET) + XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/next") + XCTAssertEqual(request?.head.version, .http1_1) + XCTAssertEqual(request?.head.headers["host"], [self.host]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) + + XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) + } + + func testPostInvocationSuccessWithoutBody() { + let requestID = UUID().uuidString + var request: NIOHTTPServerRequestFull? + XCTAssertNoThrow(request = try self.sendRequest(.invocationResponse(requestID, nil))) + + XCTAssertEqual(request?.head.isKeepAlive, true) + XCTAssertEqual(request?.head.method, .POST) + XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/response") + XCTAssertEqual(request?.head.version, .http1_1) + XCTAssertEqual(request?.head.headers["host"], [self.host]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) + XCTAssertEqual(request?.head.headers["content-length"], ["0"]) + + XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) + } + + func testPostInvocationSuccessWithBody() { + let requestID = UUID().uuidString + let payload = ByteBuffer(string: "hello swift lambda!") + + var request: NIOHTTPServerRequestFull? + XCTAssertNoThrow(request = try self.sendRequest(.invocationResponse(requestID, payload))) + + XCTAssertEqual(request?.head.isKeepAlive, true) + XCTAssertEqual(request?.head.method, .POST) + XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/response") + XCTAssertEqual(request?.head.version, .http1_1) + XCTAssertEqual(request?.head.headers["host"], [self.host]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) + XCTAssertEqual(request?.head.headers["content-length"], ["\(payload.readableBytes)"]) + XCTAssertEqual(request?.body, payload) + + XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) + } + + func testPostInvocationErrorWithBody() { + let requestID = UUID().uuidString + let error = ErrorResponse(errorType: "SomeError", errorMessage: "An error happened") + var request: NIOHTTPServerRequestFull? + XCTAssertNoThrow(request = try self.sendRequest(.invocationError(requestID, error))) + + XCTAssertEqual(request?.head.isKeepAlive, true) + XCTAssertEqual(request?.head.method, .POST) + XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/error") + XCTAssertEqual(request?.head.version, .http1_1) + XCTAssertEqual(request?.head.headers["host"], [self.host]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) + XCTAssertEqual(request?.head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) + let expectedBody = #"{"errorType":"SomeError","errorMessage":"An error happened"}"# + + XCTAssertEqual(request?.head.headers["content-length"], ["\(expectedBody.utf8.count)"]) + XCTAssertEqual( + try request?.body?.getString(at: 0, length: XCTUnwrap(request?.body?.readableBytes)), + expectedBody + ) + + XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) + } + + func testPostStartupError() { + let error = ErrorResponse(errorType: "StartupError", errorMessage: "Urgh! Startup failed. 😨") + var request: NIOHTTPServerRequestFull? + XCTAssertNoThrow(request = try self.sendRequest(.initializationError(error))) + + XCTAssertEqual(request?.head.isKeepAlive, true) + XCTAssertEqual(request?.head.method, .POST) + XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/init/error") + XCTAssertEqual(request?.head.version, .http1_1) + XCTAssertEqual(request?.head.headers["host"], [self.host]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) + XCTAssertEqual(request?.head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) + let expectedBody = #"{"errorType":"StartupError","errorMessage":"Urgh! Startup failed. 😨"}"# + XCTAssertEqual(request?.head.headers["content-length"], ["\(expectedBody.utf8.count)"]) + XCTAssertEqual( + try request?.body?.getString(at: 0, length: XCTUnwrap(request?.body?.readableBytes)), + expectedBody + ) + + XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) + } + + func testMultipleNextAndResponseSuccessRequests() { + for _ in 0..<1000 { + var nextRequest: NIOHTTPServerRequestFull? + XCTAssertNoThrow(nextRequest = try self.sendRequest(.next)) + XCTAssertEqual(nextRequest?.head.method, .GET) + XCTAssertEqual(nextRequest?.head.uri, "/2018-06-01/runtime/invocation/next") + + let requestID = UUID().uuidString + let payload = ByteBuffer(string: "hello swift lambda!") + var successRequest: NIOHTTPServerRequestFull? + XCTAssertNoThrow(successRequest = try self.sendRequest(.invocationResponse(requestID, payload))) + XCTAssertEqual(successRequest?.head.method, .POST) + XCTAssertEqual(successRequest?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/response") + } + } + + func sendRequest(_ request: ControlPlaneRequest) throws -> NIOHTTPServerRequestFull? { + try self.client.writeOutbound(request) + while let part = try self.client.readOutbound(as: ByteBuffer.self) { + XCTAssertNoThrow(try self.server.writeInbound(part)) + } + return try self.server.readInbound(as: NIOHTTPServerRequestFull.self) + } +} + +private final class ControlPlaneRequestEncoderHandler: ChannelOutboundHandler { + typealias OutboundIn = ControlPlaneRequest + typealias OutboundOut = ByteBuffer + + private var encoder: ControlPlaneRequestEncoder + + init(host: String) { + self.encoder = ControlPlaneRequestEncoder(host: host) + } + + func handlerAdded(context: ChannelHandlerContext) { + self.encoder.writerAdded(context: context) + } + + func handlerRemoved(context: ChannelHandlerContext) { + self.encoder.writerRemoved(context: context) + } + + func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + self.encoder.writeRequest(self.unwrapOutboundIn(data), context: context, promise: promise) + } +} diff --git a/Tests/AWSLambdaRuntimeTests/InvocationTests.swift b/Tests/AWSLambdaRuntimeTests/InvocationTests.swift new file mode 100644 index 00000000..ea4eef1f --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/InvocationTests.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOHTTP1 +import Testing + +@testable import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite +struct InvocationTest { + @Test + func testInvocationTraceID() throws { + let headers = HTTPHeaders([ + (AmazonHeaders.requestID, "test"), + (AmazonHeaders.deadline, String(Date(timeIntervalSinceNow: 60).millisSinceEpoch)), + (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), + ]) + + var maybeInvocation: InvocationMetadata? + + #expect(throws: Never.self) { maybeInvocation = try InvocationMetadata(headers: headers) } + let invocation = try #require(maybeInvocation) + #expect(!invocation.traceID.isEmpty) + } +} diff --git a/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift b/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift deleted file mode 100644 index 9aa3f72a..00000000 --- a/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift +++ /dev/null @@ -1,92 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@testable import AWSLambdaRuntime -@testable import AWSLambdaRuntimeCore -import Logging -import NIO -import NIOFoundationCompat -import XCTest - -class CodableLambdaTest: XCTestCase { - var eventLoopGroup: EventLoopGroup! - let allocator = ByteBufferAllocator() - - override func setUp() { - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - } - - override func tearDown() { - try! self.eventLoopGroup.syncShutdownGracefully() - } - - func testCodableVoidClosureWrapper() { - let request = Request(requestId: UUID().uuidString) - var inputBuffer: ByteBuffer? - var outputBuffer: ByteBuffer? - - let closureWrapper = CodableVoidClosureWrapper { (_, _: Request, completion) in - XCTAssertEqual(request, request) - completion(.success(())) - } - - XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) - XCTAssertNoThrow(outputBuffer = try closureWrapper.handle(context: self.newContext(), event: XCTUnwrap(inputBuffer)).wait()) - XCTAssertNil(outputBuffer) - } - - func testCodableClosureWrapper() { - let request = Request(requestId: UUID().uuidString) - var inputBuffer: ByteBuffer? - var outputBuffer: ByteBuffer? - var response: Response? - - let closureWrapper = CodableClosureWrapper { (_, req: Request, completion: (Result) -> Void) in - XCTAssertEqual(request, request) - completion(.success(Response(requestId: req.requestId))) - } - - XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) - XCTAssertNoThrow(outputBuffer = try closureWrapper.handle(context: self.newContext(), event: XCTUnwrap(inputBuffer)).wait()) - XCTAssertNoThrow(response = try JSONDecoder().decode(Response.self, from: XCTUnwrap(outputBuffer))) - XCTAssertEqual(response?.requestId, request.requestId) - } - - // convencience method - func newContext() -> Lambda.Context { - Lambda.Context(requestID: UUID().uuidString, - traceID: "abc123", - invokedFunctionARN: "aws:arn:", - deadline: .now() + .seconds(3), - cognitoIdentity: nil, - clientContext: nil, - logger: Logger(label: "test"), - eventLoop: self.eventLoopGroup.next(), - allocator: ByteBufferAllocator()) - } -} - -private struct Request: Codable, Equatable { - let requestId: String - init(requestId: String) { - self.requestId = requestId - } -} - -private struct Response: Codable, Equatable { - let requestId: String - init(requestId: String) { - self.requestId = requestId - } -} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift b/Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift new file mode 100644 index 00000000..7714c84a --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift @@ -0,0 +1,300 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import Logging +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct LambdaMockWriter: LambdaRuntimeClientResponseStreamWriter { + var underlying: LambdaMockClient + + init(underlying: LambdaMockClient) { + self.underlying = underlying + } + + func write(_ buffer: ByteBuffer) async throws { + try await self.underlying.write(buffer) + } + + func finish() async throws { + try await self.underlying.finish() + } + + func writeAndFinish(_ buffer: ByteBuffer) async throws { + try await self.underlying.write(buffer) + try await self.underlying.finish() + } + + func reportError(_ error: any Error) async throws { + await self.underlying.reportError(error) + } +} + +enum LambdaError: Error, Equatable { + case cannotCallNextEndpointWhenAlreadyWaitingForEvent + case cannotCallNextEndpointWhenAlreadyProcessingAnEvent + case cannotReportResultWhenNoEventHasBeenProcessed + case cancelError + case handlerError +} + +final actor LambdaMockClient: LambdaRuntimeClientProtocol { + typealias Writer = LambdaMockWriter + + private struct StateMachine { + private enum State { + // The Lambda has just started, or an event has finished processing and the runtime is ready to receive more events. + // Expecting a next() call by the runtime. + case initialState + + // The next endpoint has been called but no event has arrived yet. + case waitingForNextEvent(eventArrivedHandler: CheckedContinuation) + + // The handler is processing the event. Buffers written to the writer are accumulated. + case handlerIsProcessing( + accumulatedResponse: [ByteBuffer], + eventProcessedHandler: CheckedContinuation + ) + } + + private var state: State = .initialState + + // Queue incoming events if the runtime is busy handling an event. + private var eventQueue = [Event]() + + enum InvokeAction { + // The next endpoint is waiting for an event. Deliver this newly arrived event to it. + case readyToProcess(_ eventArrivedHandler: CheckedContinuation) + + // The next endpoint has not been called yet. This event has been added to the queue. + case wait + } + + enum NextAction { + // There is an event available to be processed. + case readyToProcess(Invocation) + + // No events available yet. Wait for an event to arrive. + case wait + + case fail(LambdaError) + } + + enum CancelNextAction { + case none + + case cancelContinuation(CheckedContinuation) + } + + enum ResultAction { + case readyForMore + + case fail(LambdaError) + } + + enum FailProcessingAction { + case none + + case throwContinuation(CheckedContinuation) + } + + mutating func next(_ eventArrivedHandler: CheckedContinuation) -> NextAction { + switch self.state { + case .initialState: + if self.eventQueue.isEmpty { + // No event available yet -- store the continuation for the next invoke() call. + self.state = .waitingForNextEvent(eventArrivedHandler: eventArrivedHandler) + return .wait + } else { + // An event is already waiting to be processed + let event = self.eventQueue.removeFirst() // TODO: use Deque + + self.state = .handlerIsProcessing( + accumulatedResponse: [], + eventProcessedHandler: event.eventProcessedHandler + ) + return .readyToProcess(event.invocation) + } + case .waitingForNextEvent: + return .fail(.cannotCallNextEndpointWhenAlreadyWaitingForEvent) + case .handlerIsProcessing: + return .fail(.cannotCallNextEndpointWhenAlreadyProcessingAnEvent) + } + } + + mutating func invoke(_ event: Event) -> InvokeAction { + switch self.state { + case .initialState, .handlerIsProcessing: + // next() hasn't been called yet. Add to the event queue. + self.eventQueue.append(event) + return .wait + case .waitingForNextEvent(let eventArrivedHandler): + // The runtime is already waiting for an event + self.state = .handlerIsProcessing( + accumulatedResponse: [], + eventProcessedHandler: event.eventProcessedHandler + ) + return .readyToProcess(eventArrivedHandler) + } + } + + mutating func writeResult(buffer: ByteBuffer) -> ResultAction { + switch self.state { + case .handlerIsProcessing(var accumulatedResponse, let eventProcessedHandler): + accumulatedResponse.append(buffer) + self.state = .handlerIsProcessing( + accumulatedResponse: accumulatedResponse, + eventProcessedHandler: eventProcessedHandler + ) + return .readyForMore + case .initialState, .waitingForNextEvent: + return .fail(.cannotReportResultWhenNoEventHasBeenProcessed) + } + } + + mutating func finish() throws { + switch self.state { + case .handlerIsProcessing(let accumulatedResponse, let eventProcessedHandler): + let finalResult: ByteBuffer = accumulatedResponse.reduce(ByteBuffer()) { (accumulated, current) in + var accumulated = accumulated + accumulated.writeBytes(current.readableBytesView) + return accumulated + } + + eventProcessedHandler.resume(returning: finalResult) + // reset back to the initial state + self.state = .initialState + case .initialState, .waitingForNextEvent: + throw LambdaError.cannotReportResultWhenNoEventHasBeenProcessed + } + } + + mutating func cancelNext() -> CancelNextAction { + switch self.state { + case .initialState, .handlerIsProcessing: + return .none + case .waitingForNextEvent(let eventArrivedHandler): + self.state = .initialState + return .cancelContinuation(eventArrivedHandler) + } + } + + mutating func failProcessing() -> FailProcessingAction { + switch self.state { + case .initialState, .waitingForNextEvent: + // Cannot report an error for an event if the event is not currently being processed. + fatalError() + case .handlerIsProcessing(_, let eventProcessedHandler): + return .throwContinuation(eventProcessedHandler) + } + } + } + + private var stateMachine = StateMachine() + + struct Event { + let invocation: Invocation + let eventProcessedHandler: CheckedContinuation + } + + func invoke(event: ByteBuffer, requestID: String = UUID().uuidString) async throws -> ByteBuffer { + try await withCheckedThrowingContinuation { eventProcessedHandler in + do { + let metadata = try InvocationMetadata( + headers: .init([ + ("Lambda-Runtime-Aws-Request-Id", "\(requestID)"), // arbitrary values + ("Lambda-Runtime-Deadline-Ms", "100"), + ("Lambda-Runtime-Invoked-Function-Arn", "100"), + ]) + ) + let invocation = Invocation(metadata: metadata, event: event) + + let invokeAction = self.stateMachine.invoke( + Event( + invocation: invocation, + eventProcessedHandler: eventProcessedHandler + ) + ) + + switch invokeAction { + case .readyToProcess(let eventArrivedHandler): + // nextInvocation had been called earlier and is currently waiting for an event; deliver + eventArrivedHandler.resume(returning: invocation) + case .wait: + // The event has been added to the event queue; wait for it to be picked up + break + } + } catch { + eventProcessedHandler.resume(throwing: error) + } + } + } + + func nextInvocation() async throws -> (Invocation, Writer) { + try await withTaskCancellationHandler { + let invocation = try await withCheckedThrowingContinuation { eventArrivedHandler in + switch self.stateMachine.next(eventArrivedHandler) { + case .readyToProcess(let event): + eventArrivedHandler.resume(returning: event) + case .fail(let error): + eventArrivedHandler.resume(throwing: error) + case .wait: + break + } + } + return (invocation, Writer(underlying: self)) + } onCancel: { + Task { + await self.cancelNextInvocation() + } + } + } + + private func cancelNextInvocation() { + switch self.stateMachine.cancelNext() { + case .none: + break + case .cancelContinuation(let continuation): + continuation.resume(throwing: LambdaError.cancelError) + } + } + + func write(_ buffer: ByteBuffer) async throws { + switch self.stateMachine.writeResult(buffer: buffer) { + case .readyForMore: + break + case .fail(let error): + throw error + } + } + + func finish() async throws { + try self.stateMachine.finish() + } + + func reportError(_ error: any Error) { + switch self.stateMachine.failProcessing() { + case .none: + break + case .throwContinuation(let continuation): + continuation.resume(throwing: error) + } + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift new file mode 100644 index 00000000..a144e20c --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift @@ -0,0 +1,252 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import Testing + +@testable import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite("LambdaRequestID tests") +struct LambdaRequestIDTest { + @Test + func testInitFromStringSuccess() { + let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + var buffer = ByteBuffer(string: string) + + let requestID = buffer.readRequestID() + #expect(buffer.readerIndex == 36) + #expect(buffer.readableBytes == 0) + #expect(requestID?.uuidString == UUID(uuidString: string)?.uuidString) + #expect(requestID?.uppercased == string) + } + + @Test + func testInitFromLowercaseStringSuccess() { + let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F".lowercased() + var originalBuffer = ByteBuffer(string: string) + + let requestID = originalBuffer.readRequestID() + #expect(originalBuffer.readerIndex == 36) + #expect(originalBuffer.readableBytes == 0) + #expect(requestID?.uuidString == UUID(uuidString: string)?.uuidString) + #expect(requestID?.lowercased == string) + + var newBuffer = ByteBuffer() + originalBuffer.moveReaderIndex(to: 0) + #expect(throws: Never.self) { try newBuffer.writeRequestID(#require(requestID)) } + #expect(newBuffer == originalBuffer) + } + + @Test + func testInitFromStringMissingCharacterAtEnd() { + let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5" + var buffer = ByteBuffer(string: string) + + let readableBeforeRead = buffer.readableBytes + let requestID = buffer.readRequestID() + #expect(requestID == nil) + #expect(buffer.readerIndex == 0) + #expect(buffer.readableBytes == readableBeforeRead) + } + + @Test + func testInitFromStringInvalidCharacterAtEnd() { + let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5H" + var buffer = ByteBuffer(string: string) + + let readableBeforeRead = buffer.readableBytes + let requestID = buffer.readRequestID() + #expect(requestID == nil) + #expect(buffer.readerIndex == 0) + #expect(buffer.readableBytes == readableBeforeRead) + } + + @Test( + "Init from String with invalid separator character", + arguments: [ + // with _ instead of - + "E621E1F8-C36C-495A-93FC_0C247A3E6E5F", + "E621E1F8-C36C-495A_93FC-0C247A3E6E5F", + "E621E1F8-C36C_495A-93FC-0C247A3E6E5F", + "E621E1F8_C36C-495A-93FC-0C247A3E6E5F", + + // with 0 instead of - + "E621E1F8-C36C-495A-93FC00C247A3E6E5F", + "E621E1F8-C36C-495A093FC-0C247A3E6E5F", + "E621E1F8-C36C0495A-93FC-0C247A3E6E5F", + "E621E1F80C36C-495A-93FC-0C247A3E6E5F", + ] + ) + func testInitFromStringInvalidSeparatorCharacter(_ input: String) { + + var buffer = ByteBuffer(string: input) + + let readableBeforeRead = buffer.readableBytes + let requestID = buffer.readRequestID() + #expect(requestID == nil) + #expect(buffer.readerIndex == 0) + #expect(buffer.readableBytes == readableBeforeRead) + } + + #if os(macOS) + @Test + func testInitFromNSStringSuccess() { + let nsString = NSMutableString(capacity: 16) + nsString.append("E621E1F8") + nsString.append("-") + nsString.append("C36C") + nsString.append("-") + nsString.append("495A") + nsString.append("-") + nsString.append("93FC") + nsString.append("-") + nsString.append("0C247A3E6E5F") + + // TODO: I would love to enforce that the nsstring is not contiguous + // here to enforce a special code path. I have no idea how to + // achieve this though at the moment + // XCTAssertFalse((nsString as String).isContiguousUTF8) + let requestID = LambdaRequestID(uuidString: nsString as String) + #expect(requestID?.uuidString == LambdaRequestID(uuidString: nsString as String)?.uuidString) + #expect(requestID?.uppercased == nsString as String) + } + #endif + + @Test + func testUnparse() { + let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + let requestID = LambdaRequestID(uuidString: string) + #expect(string.lowercased() == requestID?.lowercased) + } + + @Test + func testDescription() { + let requestID = LambdaRequestID() + let fduuid = UUID(uuid: requestID.uuid) + + #expect(fduuid.description == requestID.description) + #expect(fduuid.debugDescription == requestID.debugDescription) + } + + @Test + func testFoundationInteropFromFoundation() { + let fduuid = UUID() + let requestID = LambdaRequestID(uuid: fduuid.uuid) + + #expect(fduuid.uuid.0 == requestID.uuid.0) + #expect(fduuid.uuid.1 == requestID.uuid.1) + #expect(fduuid.uuid.2 == requestID.uuid.2) + #expect(fduuid.uuid.3 == requestID.uuid.3) + #expect(fduuid.uuid.4 == requestID.uuid.4) + #expect(fduuid.uuid.5 == requestID.uuid.5) + #expect(fduuid.uuid.6 == requestID.uuid.6) + #expect(fduuid.uuid.7 == requestID.uuid.7) + #expect(fduuid.uuid.8 == requestID.uuid.8) + #expect(fduuid.uuid.9 == requestID.uuid.9) + #expect(fduuid.uuid.10 == requestID.uuid.10) + #expect(fduuid.uuid.11 == requestID.uuid.11) + #expect(fduuid.uuid.12 == requestID.uuid.12) + #expect(fduuid.uuid.13 == requestID.uuid.13) + #expect(fduuid.uuid.14 == requestID.uuid.14) + #expect(fduuid.uuid.15 == requestID.uuid.15) + } + + @Test + func testFoundationInteropToFoundation() { + let requestID = LambdaRequestID() + let fduuid = UUID(uuid: requestID.uuid) + + #expect(fduuid.uuid.0 == requestID.uuid.0) + #expect(fduuid.uuid.1 == requestID.uuid.1) + #expect(fduuid.uuid.2 == requestID.uuid.2) + #expect(fduuid.uuid.3 == requestID.uuid.3) + #expect(fduuid.uuid.4 == requestID.uuid.4) + #expect(fduuid.uuid.5 == requestID.uuid.5) + #expect(fduuid.uuid.6 == requestID.uuid.6) + #expect(fduuid.uuid.7 == requestID.uuid.7) + #expect(fduuid.uuid.8 == requestID.uuid.8) + #expect(fduuid.uuid.9 == requestID.uuid.9) + #expect(fduuid.uuid.10 == requestID.uuid.10) + #expect(fduuid.uuid.11 == requestID.uuid.11) + #expect(fduuid.uuid.12 == requestID.uuid.12) + #expect(fduuid.uuid.13 == requestID.uuid.13) + #expect(fduuid.uuid.14 == requestID.uuid.14) + #expect(fduuid.uuid.15 == requestID.uuid.15) + } + + @Test + func testHashing() { + let requestID = LambdaRequestID() + let fduuid = UUID(uuid: requestID.uuid) + #expect(fduuid.hashValue == requestID.hashValue) + + var _uuid = requestID.uuid + _uuid.0 = _uuid.0 > 0 ? _uuid.0 - 1 : 1 + #expect(UUID(uuid: _uuid).hashValue != requestID.hashValue) + } + + @Test + func testEncoding() throws { + struct Test: Codable { + let requestID: LambdaRequestID + } + let requestID = LambdaRequestID() + let test = Test(requestID: requestID) + + var data: Data? + #expect(throws: Never.self) { data = try JSONEncoder().encode(test) } + #expect( + try String(decoding: #require(data), as: Unicode.UTF8.self) == #"{"requestID":"\#(requestID.uuidString)"}"# + ) + } + + @Test + func testDecodingSuccess() { + struct Test: Codable { + let requestID: LambdaRequestID + } + let requestID = LambdaRequestID() + let data = #"{"requestID":"\#(requestID.uuidString)"}"#.data(using: .utf8) + + var result: Test? + #expect(throws: Never.self) { result = try JSONDecoder().decode(Test.self, from: #require(data)) } + #expect(result?.requestID == requestID) + } + + @Test + func testDecodingFailure() { + struct Test: Codable { + let requestID: LambdaRequestID + } + let requestID = LambdaRequestID() + var requestIDString = requestID.uuidString + _ = requestIDString.removeLast() + let data = #"{"requestID":"\#(requestIDString)"}"#.data(using: .utf8) + + #expect(throws: DecodingError.self) { + try JSONDecoder().decode(Test.self, from: #require(data)) + } + } + + @Test + func testStructSize() { + #expect(MemoryLayout.size == 16) + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift new file mode 100644 index 00000000..3253238e --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift @@ -0,0 +1,108 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOCore +import Testing + +@testable import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite +struct LambdaRunLoopTests { + struct MockEchoHandler: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + context.logger.info("Test") + try await responseWriter.writeAndFinish(event) + } + } + + struct FailingHandler: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + context.logger.info("Test") + throw LambdaError.handlerError + } + } + + let mockClient = LambdaMockClient() + let mockEchoHandler = MockEchoHandler() + let failingHandler = FailingHandler() + + @Test func testRunLoop() async throws { + let inputEvent = ByteBuffer(string: "Test Invocation Event") + + try await withThrowingTaskGroup(of: Void.self) { group in + let logStore = CollectEverythingLogHandler.LogStore() + group.addTask { + try await Lambda.runLoop( + runtimeClient: self.mockClient, + handler: self.mockEchoHandler, + logger: Logger( + label: "RunLoopTest", + factory: { _ in CollectEverythingLogHandler(logStore: logStore) } + ) + ) + } + + let requestID = UUID().uuidString + let response = try await self.mockClient.invoke(event: inputEvent, requestID: requestID) + #expect(response == inputEvent) + logStore.assertContainsLog("Test", ("aws-request-id", .exactMatch(requestID))) + + group.cancelAll() + } + } + + @Test func testRunLoopError() async throws { + let inputEvent = ByteBuffer(string: "Test Invocation Event") + + await withThrowingTaskGroup(of: Void.self) { group in + let logStore = CollectEverythingLogHandler.LogStore() + group.addTask { + try await Lambda.runLoop( + runtimeClient: self.mockClient, + handler: self.failingHandler, + logger: Logger( + label: "RunLoopTest", + factory: { _ in CollectEverythingLogHandler(logStore: logStore) } + ) + ) + } + + let requestID = UUID().uuidString + await #expect( + throws: LambdaError.handlerError, + performing: { + try await self.mockClient.invoke(event: inputEvent, requestID: requestID) + } + ) + logStore.assertContainsLog("Test", ("aws-request-id", .exactMatch(requestID))) + + group.cancelAll() + } + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift new file mode 100644 index 00000000..53afbaf6 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOCore +import NIOPosix +import Testing + +import struct Foundation.UUID + +@testable import AWSLambdaRuntime + +@Suite +struct LambdaRuntimeClientTests { + + let logger = { + var logger = Logger(label: "NewLambdaClientRuntimeTest") + logger.logLevel = .trace + return logger + }() + + @Test + func testSimpleInvocations() async throws { + struct HappyBehavior: LambdaServerBehavior { + let requestId = UUID().uuidString + let event = "hello" + + func getInvocation() -> GetInvocationResult { + .success((self.requestId, self.event)) + } + + func processResponse(requestId: String, response: String?) -> Result { + #expect(self.requestId == requestId) + #expect(self.event == response) + return .success(()) + } + + func processError(requestId: String, error: ErrorResponse) -> Result { + Issue.record("should not report error") + return .failure(.internalServerError) + } + + func processInitError(error: ErrorResponse) -> Result { + Issue.record("should not report init error") + return .failure(.internalServerError) + } + } + + try await withMockServer(behaviour: HappyBehavior()) { port in + let configuration = LambdaRuntimeClient.Configuration(ip: "127.0.0.1", port: port) + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: configuration, + eventLoop: NIOSingletons.posixEventLoopGroup.next(), + logger: self.logger + ) { runtimeClient in + do { + let (invocation, writer) = try await runtimeClient.nextInvocation() + let expected = ByteBuffer(string: "hello") + #expect(invocation.event == expected) + try await writer.writeAndFinish(expected) + } + + do { + let (invocation, writer) = try await runtimeClient.nextInvocation() + let expected = ByteBuffer(string: "hello") + #expect(invocation.event == expected) + try await writer.write(ByteBuffer(string: "h")) + try await writer.write(ByteBuffer(string: "e")) + try await writer.write(ByteBuffer(string: "l")) + try await writer.write(ByteBuffer(string: "l")) + try await writer.write(ByteBuffer(string: "o")) + try await writer.finish() + } + } + } + } + + @Test + func testCancellation() async throws { + struct HappyBehavior: LambdaServerBehavior { + let requestId = UUID().uuidString + let event = "hello" + + func getInvocation() -> GetInvocationResult { + .success((self.requestId, self.event)) + } + + func processResponse(requestId: String, response: String?) -> Result { + #expect(self.requestId == requestId) + #expect(self.event == response) + return .success(()) + } + + func processError(requestId: String, error: ErrorResponse) -> Result { + Issue.record("should not report error") + return .failure(.internalServerError) + } + + func processInitError(error: ErrorResponse) -> Result { + Issue.record("should not report init error") + return .failure(.internalServerError) + } + } + + try await withMockServer(behaviour: HappyBehavior()) { port in + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: "127.0.0.1", port: port), + eventLoop: NIOSingletons.posixEventLoopGroup.next(), + logger: self.logger + ) { runtimeClient in + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while true { + let (_, writer) = try await runtimeClient.nextInvocation() + // Wrap this is a task so cancellation isn't propagated to the write calls + try await Task { + try await writer.write(ByteBuffer(string: "hello")) + try await writer.finish() + }.value + } + } + // wait a small amount to ensure we are waiting for continuation + try await Task.sleep(for: .milliseconds(100)) + group.cancelAll() + } + } + } + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift b/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift similarity index 68% rename from Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift rename to Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift index 9708ccc1..11f43ba4 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift +++ b/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2017-2021 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,15 +12,43 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore -import Foundation // for JSON import Logging -import NIO +import NIOCore import NIOHTTP1 +import NIOPosix -internal final class MockLambdaServer { +@testable import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +func withMockServer( + behaviour: some LambdaServerBehavior, + port: Int = 0, + keepAlive: Bool = true, + _ body: (_ port: Int) async throws -> Result +) async throws -> Result { + let eventLoopGroup = NIOSingletons.posixEventLoopGroup + let server = MockLambdaServer(behavior: behaviour, port: port, keepAlive: keepAlive, eventLoopGroup: eventLoopGroup) + let port = try await server.start() + + let result: Swift.Result + do { + result = .success(try await body(port)) + } catch { + result = .failure(error) + } + + try? await server.stop() + return try result.get() +} + +final class MockLambdaServer { private let logger = Logger(label: "MockLambdaServer") - private let behavior: LambdaServerBehavior + private let behavior: Behavior private let host: String private let port: Int private let keepAlive: Bool @@ -29,8 +57,14 @@ internal final class MockLambdaServer { private var channel: Channel? private var shutdown = false - public init(behavior: LambdaServerBehavior, host: String = "127.0.0.1", port: Int = 7000, keepAlive: Bool = true) { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + init( + behavior: Behavior, + host: String = "127.0.0.1", + port: Int = 7000, + keepAlive: Bool = true, + eventLoopGroup: MultiThreadedEventLoopGroup + ) { + self.group = NIOSingletons.posixEventLoopGroup self.behavior = behavior self.host = host self.port = port @@ -41,39 +75,47 @@ internal final class MockLambdaServer { assert(shutdown) } - func start() -> EventLoopFuture { - let bootstrap = ServerBootstrap(group: group) + fileprivate func start() async throws -> Int { + let logger = self.logger + let keepAlive = self.keepAlive + let behavior = self.behavior + + let channel = try await ServerBootstrap(group: group) .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .childChannelInitializer { channel in - channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in - channel.pipeline.addHandler(HTTPHandler(logger: self.logger, keepAlive: self.keepAlive, behavior: self.behavior)) + do { + try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: true) + try channel.pipeline.syncOperations.addHandler( + HTTPHandler(logger: logger, keepAlive: keepAlive, behavior: behavior) + ) + return channel.eventLoop.makeSucceededVoidFuture() + } catch { + return channel.eventLoop.makeFailedFuture(error) } } - return bootstrap.bind(host: self.host, port: self.port).flatMap { channel in - self.channel = channel - guard let localAddress = channel.localAddress else { - return channel.eventLoop.makeFailedFuture(ServerError.cantBind) - } - self.logger.info("\(self) started and listening on \(localAddress)") - return channel.eventLoop.makeSucceededFuture(self) + .bind(host: self.host, port: self.port) + .get() + + self.channel = channel + guard let localAddress = channel.localAddress else { + throw ServerError.cantBind } + self.logger.info("\(self) started and listening on \(localAddress)") + return localAddress.port! } - func stop() -> EventLoopFuture { + fileprivate func stop() async throws { self.logger.info("stopping \(self)") - guard let channel = self.channel else { - return self.group.next().makeFailedFuture(ServerError.notReady) - } - return channel.close().always { _ in - self.shutdown = true - self.logger.info("\(self) stopped") - } + let channel = self.channel! + try? await channel.close().get() + self.shutdown = true + self.logger.info("\(self) stopped") } } -internal final class HTTPHandler: ChannelInboundHandler { - public typealias InboundIn = HTTPServerRequestPart - public typealias OutboundOut = HTTPServerResponsePart +final class HTTPHandler: ChannelInboundHandler { + typealias InboundIn = HTTPServerRequestPart + typealias OutboundOut = HTTPServerResponsePart private let logger: Logger private let keepAlive: Bool @@ -81,7 +123,7 @@ internal final class HTTPHandler: ChannelInboundHandler { private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - public init(logger: Logger, keepAlive: Bool, behavior: LambdaServerBehavior) { + init(logger: Logger, keepAlive: Bool, behavior: LambdaServerBehavior) { self.logger = logger self.keepAlive = keepAlive self.behavior = behavior @@ -179,7 +221,12 @@ internal final class HTTPHandler: ChannelInboundHandler { self.writeResponse(context: context, status: responseStatus, headers: responseHeaders, body: responseBody) } - func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: String? = nil) { + func writeResponse( + context: ChannelHandlerContext, + status: HTTPResponseStatus, + headers: [(String, String)]? = nil, + body: String? = nil + ) { var headers = HTTPHeaders(headers ?? []) headers.add(name: "Content-Length", value: "\(body?.utf8.count ?? 0)") if !self.keepAlive { @@ -187,66 +234,71 @@ internal final class HTTPHandler: ChannelInboundHandler { } let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: status, headers: headers) + let logger = self.logger context.write(wrapOutboundOut(.head(head))).whenFailure { error in - self.logger.error("\(self) write error \(error)") + logger.error("write error \(error)") } if let b = body { var buffer = context.channel.allocator.buffer(capacity: b.utf8.count) buffer.writeString(b) context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in - self.logger.error("\(self) write error \(error)") + logger.error("write error \(error)") } } + let loopBoundContext = NIOLoopBound(context, eventLoop: context.eventLoop) + + let keepAlive = self.keepAlive context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in if case .failure(let error) = result { - self.logger.error("\(self) write error \(error)") + logger.error("write error \(error)") } - if !self.keepAlive { + if !keepAlive { + let context = loopBoundContext.value context.close().whenFailure { error in - self.logger.error("\(self) close error \(error)") + logger.error("close error \(error)") } } } } } -internal protocol LambdaServerBehavior { +protocol LambdaServerBehavior: Sendable { func getInvocation() -> GetInvocationResult func processResponse(requestId: String, response: String?) -> Result func processError(requestId: String, error: ErrorResponse) -> Result func processInitError(error: ErrorResponse) -> Result } -internal typealias GetInvocationResult = Result<(String, String), GetWorkError> +typealias GetInvocationResult = Result<(String, String), GetWorkError> -internal enum GetWorkError: Int, Error { +enum GetWorkError: Int, Error { case badRequest = 400 case tooManyRequests = 429 case internalServerError = 500 } -internal enum ProcessResponseError: Int, Error { +enum ProcessResponseError: Int, Error { case badRequest = 400 case payloadTooLarge = 413 case tooManyRequests = 429 case internalServerError = 500 } -internal enum ProcessErrorError: Int, Error { +enum ProcessErrorError: Int, Error { case invalidErrorShape = 299 case badRequest = 400 case internalServerError = 500 } -internal enum ServerError: Error { +enum ServerError: Error { case notReady case cantBind } -private extension ErrorResponse { - static func fromJson(_ s: String) -> ErrorResponse? { +extension ErrorResponse { + fileprivate static func fromJson(_ s: String) -> ErrorResponse? { let decoder = JSONDecoder() do { if let data = s.data(using: .utf8) { diff --git a/Tests/AWSLambdaRuntimeTests/NewLambda+CodableTests.swift b/Tests/AWSLambdaRuntimeTests/NewLambda+CodableTests.swift new file mode 100644 index 00000000..35d56225 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/NewLambda+CodableTests.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import Logging +import NIOCore +import Testing + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Suite +struct JSONTests { + + let logger = Logger(label: "JSONTests") + + struct Foo: Codable { + var bar: String + } + + @Test + func testEncodingConformance() { + let encoder = LambdaJSONOutputEncoder(JSONEncoder()) + let foo = Foo(bar: "baz") + var byteBuffer = ByteBuffer() + + #expect(throws: Never.self) { + try encoder.encode(foo, into: &byteBuffer) + } + + #expect(byteBuffer == ByteBuffer(string: #"{"bar":"baz"}"#)) + } + + @Test + func testJSONHandlerWithOutput() async { + let jsonEncoder = JSONEncoder() + let jsonDecoder = JSONDecoder() + + let closureHandler = ClosureHandler { (foo: Foo, context) in + foo + } + + var handler = LambdaCodableAdapter( + encoder: jsonEncoder, + decoder: jsonDecoder, + handler: LambdaHandlerAdapter(handler: closureHandler) + ) + + let event = ByteBuffer(string: #"{"bar":"baz"}"#) + let writer = MockLambdaWriter() + let context = LambdaContext.__forTestsOnly( + requestID: UUID().uuidString, + traceID: UUID().uuidString, + invokedFunctionARN: "arn:", + timeout: .milliseconds(6000), + logger: self.logger + ) + + await #expect(throws: Never.self) { + try await handler.handle(event, responseWriter: writer, context: context) + } + + let result = await writer.output + #expect(result == ByteBuffer(string: #"{"bar":"baz"}"#)) + } +} + +final actor MockLambdaWriter: LambdaResponseStreamWriter { + private var _buffer: ByteBuffer? + + var output: ByteBuffer? { + self._buffer + } + + func writeAndFinish(_ buffer: ByteBuffer) async throws { + self._buffer = buffer + } + + func write(_ buffer: ByteBuffer) async throws { + fatalError("Unexpected call") + } + + func finish() async throws { + fatalError("Unexpected call") + } +} diff --git a/Tests/LinuxMain.swift b/Tests/AWSLambdaRuntimeTests/Utils.swift similarity index 63% rename from Tests/LinuxMain.swift rename to Tests/AWSLambdaRuntimeTests/Utils.swift index 58f2ccfd..49a3cfbb 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/AWSLambdaRuntimeTests/Utils.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2017-2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,4 +12,14 @@ // //===----------------------------------------------------------------------===// -preconditionFailure("use `swift test --enable-test-discovery`") +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension Date { + var millisSinceEpoch: Int64 { + Int64(self.timeIntervalSince1970 * 1000) + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift b/Tests/AWSLambdaRuntimeTests/UtilsTest.swift similarity index 95% rename from Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift rename to Tests/AWSLambdaRuntimeTests/UtilsTest.swift index c5fc4ab5..0b3b5917 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift +++ b/Tests/AWSLambdaRuntimeTests/UtilsTest.swift @@ -12,16 +12,17 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import XCTest +@testable import AWSLambdaRuntime + class UtilsTest: XCTestCase { func testGenerateXRayTraceID() { // the time and identifier should be in hexadecimal digits let invalidCharacters = CharacterSet(charactersIn: "abcdef0123456789").inverted let numTests = 1000 var values = Set() - for _ in 0 ..< numTests { + for _ in 0..) -> Void) in - callback(.success(Response(message: "echo" + request.name))) - } - - let request = Request(name: UUID().uuidString) - var response: Response? - XCTAssertNoThrow(response = try Lambda.test(myLambda, with: request)) - XCTAssertEqual(response?.message, "echo" + request.name) - } - - func testCodableVoidClosure() { - struct Request: Codable { - let name: String - } - - let myLambda = { (_: Lambda.Context, _: Request, callback: (Result) -> Void) in - callback(.success(())) - } - - let request = Request(name: UUID().uuidString) - XCTAssertNoThrow(try Lambda.test(myLambda, with: request)) - } - - func testLambdaHandler() { - struct Request: Codable { - let name: String - } - - struct Response: Codable { - let message: String - } - - struct MyLambda: LambdaHandler { - typealias In = Request - typealias Out = Response - - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - XCTAssertFalse(context.eventLoop.inEventLoop) - callback(.success(Response(message: "echo" + event.name))) - } - } - - let request = Request(name: UUID().uuidString) - var response: Response? - XCTAssertNoThrow(response = try Lambda.test(MyLambda(), with: request)) - XCTAssertEqual(response?.message, "echo" + request.name) - } - - func testEventLoopLambdaHandler() { - struct MyLambda: EventLoopLambdaHandler { - typealias In = String - typealias Out = String - - func handle(context: Lambda.Context, event: String) -> EventLoopFuture { - XCTAssertTrue(context.eventLoop.inEventLoop) - return context.eventLoop.makeSucceededFuture("echo" + event) - } - } - - let input = UUID().uuidString - var result: String? - XCTAssertNoThrow(result = try Lambda.test(MyLambda(), with: input)) - XCTAssertEqual(result, "echo" + input) - } - - func testFailure() { - struct MyError: Error {} - - struct MyLambda: LambdaHandler { - typealias In = String - typealias Out = Void - - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) { - callback(.failure(MyError())) - } - } - - XCTAssertThrowsError(try Lambda.test(MyLambda(), with: UUID().uuidString)) { error in - XCTAssert(error is MyError) - } - } - - func testAsyncLongRunning() { - var executed: Bool = false - let myLambda = { (_: Lambda.Context, _: String, callback: @escaping (Result) -> Void) in - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.5) { - executed = true - callback(.success(())) - } - } - - XCTAssertNoThrow(try Lambda.test(myLambda, with: UUID().uuidString)) - XCTAssertTrue(executed) - } - - func testConfigValues() { - let timeout: TimeInterval = 4 - let config = Lambda.TestConfig( - requestID: UUID().uuidString, - traceID: UUID().uuidString, - invokedFunctionARN: "arn:\(UUID().uuidString)", - timeout: .seconds(4) - ) - - let myLambda = { (ctx: Lambda.Context, _: String, callback: @escaping (Result) -> Void) in - XCTAssertEqual(ctx.requestID, config.requestID) - XCTAssertEqual(ctx.traceID, config.traceID) - XCTAssertEqual(ctx.invokedFunctionARN, config.invokedFunctionARN) - - let secondsSinceEpoch = Double(Int64(bitPattern: ctx.deadline.rawValue)) / -1_000_000_000 - XCTAssertEqual(Date(timeIntervalSince1970: secondsSinceEpoch).timeIntervalSinceNow, timeout, accuracy: 0.1) - - callback(.success(())) - } - - XCTAssertNoThrow(try Lambda.test(myLambda, with: UUID().uuidString, using: config)) - } -} diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index c2a1e8de..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -ARG swift_version=5.2 -# FIXME when 5.2 images are available -ARG base_image=swift:$swift_version-amazonlinux2 -FROM $base_image -# needed to do again after FROM due to docker limitation -ARG swift_version - -# dependencies -RUN yum install -y wget perl-Digest-SHA -RUN yum install -y lsof dnsutils netcat-openbsd net-tools curl jq # used by integration tests - -# tools -RUN mkdir -p $HOME/.tools -RUN echo 'export PATH="$HOME/.tools:$PATH"' >> $HOME/.profile - -# script to allow mapping framepointers on linux (until part of the toolchain) -RUN wget -q https://raw.githubusercontent.com/apple/swift/master/utils/symbolicate-linux-fatal -O $HOME/.tools/symbolicate-linux-fatal -RUN chmod 755 $HOME/.tools/symbolicate-linux-fatal - -# swiftformat (until part of the toolchain) - -ARG swiftformat_version=0.44.6 -RUN git clone --branch $swiftformat_version --depth 1 https://github.com/nicklockwood/SwiftFormat $HOME/.tools/swift-format -RUN cd $HOME/.tools/swift-format && swift build -c release -RUN ln -s $HOME/.tools/swift-format/.build/release/swiftformat $HOME/.tools/swiftformat diff --git a/docker/Dockerfile.ubuntu b/docker/Dockerfile.ubuntu deleted file mode 100644 index cfd977cd..00000000 --- a/docker/Dockerfile.ubuntu +++ /dev/null @@ -1,36 +0,0 @@ -ARG swift_version=5.0 -ARG ubuntu_version=bionic -ARG base_image=swift:$swift_version-$ubuntu_version -FROM $base_image -# needed to do again after FROM due to docker limitation -ARG swift_version -ARG ubuntu_version - -# set as UTF-8 -RUN apt-get update && apt-get install -y locales locales-all -ENV LC_ALL en_US.UTF-8 -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US.UTF-8 - -# dependencies -RUN apt-get update && apt-get install -y wget -RUN apt-get update && apt-get install -y lsof dnsutils netcat-openbsd net-tools curl jq # used by integration tests - -# ruby and jazzy for docs generation -RUN apt-get update && apt-get install -y ruby ruby-dev libsqlite3-dev -RUN gem install jazzy --no-ri --no-rdoc - -# tools -RUN mkdir -p $HOME/.tools -RUN echo 'export PATH="$HOME/.tools:$PATH"' >> $HOME/.profile - -# script to allow mapping framepointers on linux (until part of the toolchain) -RUN wget -q https://raw.githubusercontent.com/apple/swift/master/utils/symbolicate-linux-fatal -O $HOME/.tools/symbolicate-linux-fatal -RUN chmod 755 $HOME/.tools/symbolicate-linux-fatal - -# swiftformat (until part of the toolchain) - -ARG swiftformat_version=0.44.6 -RUN git clone --branch $swiftformat_version --depth 1 https://github.com/nicklockwood/SwiftFormat $HOME/.tools/swift-format -RUN cd $HOME/.tools/swift-format && swift build -c release -RUN ln -s $HOME/.tools/swift-format/.build/release/swiftformat $HOME/.tools/swiftformat diff --git a/docker/docker-compose.1804.52.yaml b/docker/docker-compose.1804.52.yaml deleted file mode 100644 index 29c77277..00000000 --- a/docker/docker-compose.1804.52.yaml +++ /dev/null @@ -1,20 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: swift-aws-lambda:18.04-5.2 - build: - dockerfile: Dockerfile.ubuntu - args: - ubuntu_version: "bionic" - swift_version: "5.2" - - test: - image: swift-aws-lambda:18.04-5.2 - - test-samples: - image: swift-aws-lambda:18.04-5.2 - - shell: - image: swift-aws-lambda:18.04-5.2 diff --git a/docker/docker-compose.al2.52.yaml b/docker/docker-compose.al2.52.yaml deleted file mode 100644 index 30f12089..00000000 --- a/docker/docker-compose.al2.52.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: swift-aws-lambda:al2-5.2 - build: - args: - swift_version: "5.2" - - test: - image: swift-aws-lambda:al2-5.2 - - test-samples: - image: swift-aws-lambda:al2-5.2 - - shell: - image: swift-aws-lambda:al2-5.2 diff --git a/docker/docker-compose.al2.53.yaml b/docker/docker-compose.al2.53.yaml deleted file mode 100644 index 628a1892..00000000 --- a/docker/docker-compose.al2.53.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: "3" - -services: - - runtime-setup: - image: swift-aws-lambda:al2-5.3 - build: - args: - swift_version: "5.3" - - test: - image: swift-aws-lambda:al2-5.3 - - test-samples: - image: swift-aws-lambda:al2-5.3 - - shell: - image: swift-aws-lambda:al2-5.3 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml deleted file mode 100644 index f3996271..00000000 --- a/docker/docker-compose.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# this file is not designed to be run directly -# instead, use the docker-compose.. files -# eg docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.1804.52.yaml run test -version: "3" - -services: - - runtime-setup: - image: swift-aws-lambda:default - build: - context: . - dockerfile: Dockerfile - - common: &common - image: swift-aws-lambda:default - depends_on: [runtime-setup] - volumes: - - ~/.ssh:/root/.ssh - - ..:/code:z - working_dir: /code - cap_drop: - - CAP_NET_RAW - - CAP_NET_BIND_SERVICE - - sanity: - <<: *common - command: /bin/bash -cl "./scripts/sanity.sh" - - test: - <<: *common - command: /bin/bash -cl "swift test --enable-test-discovery -Xswiftc -warnings-as-errors $${SANITIZER_ARG-}" - - test-samples: - <<: *common - command: >- - /bin/bash -clx " - swift build --package-path Examples/LambdaFunctions && - swift build --package-path Examples/LocalDebugging/MyLambda" - - # util - - shell: - <<: *common - entrypoint: /bin/bash diff --git a/projects.md b/projects.md new file mode 100644 index 00000000..d6566d64 --- /dev/null +++ b/projects.md @@ -0,0 +1,13 @@ +# Projects using Swift AWS Lambda Runtime library + +Here you will find a list of code repositories that has in common the usage of the **Swift AWS Lambda Runtime** library. + +Provide a link to your code repository and a short description about your example. + +- [GlobantPlus](https://github.com/fitomad/TechTalk-AWS-Lamba-Swift/). An imaginary streaming services with a tvOS application that interacts with the AWS API Gateway, AWS SQS services and a set of Lambdas that cover different aspects. Repository's documentation available in Spanish🇪🇸 and English🇺🇸. +- [DocUploader](https://github.com/SwiftPackageIndex/DocUploader). DocUploader is a component of the Swift Package Index site. It receives zipped DocC documentation archives which can be quite large via an S3 inbox and proceeds to unzip and upload the documentation files to S3. +- [Vapor's PennyBot](https://github.com/vapor/penny-bot). A collections of lambdas that handle events from GitHub for automatically creating releases for Vapor, handling new sponsors, automatically answering questions on Discord and providing 'pennies' - internet karma - to community members who help others or contribute to Vapor. +- [Breeze](https://github.com/swift-serverless/Breeze) A Serverless API Template Generator for Server-Side Swift. It supports template generation, creating swift package and deployment scripts for: + - Serverless REST API based on AWS APIGateway, Lambda, DynamoDB + - GitHub Webhook + - Webhook diff --git a/readme.md b/readme.md index 377eeec3..37596ed2 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,17 @@ -# Swift AWS Lambda Runtime +> [!IMPORTANT] +> The documentation included here refers to the Swift AWS Lambda Runtime v2 (code from the main branch). If you're developing for the runtime v1.x, check this [readme](https://github.com/swift-server/swift-aws-lambda-runtime/blob/v1/readme.md) instead. + +This guide contains the following sections: + +- [The Swift AWS Lambda Runtime](#the-swift-aws-lambda-runtime) +- [Pre-requisites](#pre-requisites) +- [Getting started](#getting-started) +- [Developing your Swift Lambda functions](#developing-your-swift-lambda-functions) +- [Testing Locally](#testing-locally) +- [Deploying your Swift Lambda functions](#deploying-your-swift-lambda-functions) +- [Swift AWS Lambda Runtime - Design Principles](#swift-aws-lambda-runtime---design-principles)% + +## The Swift AWS Lambda Runtime Many modern systems have client components like iOS, macOS or watchOS applications as well as server components that those clients interact with. Serverless functions are often the easiest and most efficient way for client application developers to extend their applications into the cloud. @@ -10,348 +23,424 @@ Combine this with Swift's developer friendliness, expressiveness, and emphasis o 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. -## Project status +## Pre-requisites + +- Ensure you have the Swift 6.x toolchain installed. You can [install Swift toolchains](https://www.swift.org/install/macos/) from Swift.org -This is the beginning of a community-driven open-source project actively seeking contributions. -While the core API is considered stable, the API may still evolve as we get closer to a `1.0` version. -There are several areas which need additional attention, including but not limited to: +- When developing on macOS, be sure you use macOS 15 (Sequoia) or a more recent macOS version. -* Further performance tuning -* Additional trigger events -* Additional documentation and best practices -* Additional examples +- To build and archive your Lambda function, you need to [install docker](https://docs.docker.com/desktop/install/mac-install/). + +- To deploy the Lambda function and invoke it, you must have [an AWS account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) and [install and configure the `aws` command line](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). + +- Some examples are using [AWS SAM](https://aws.amazon.com/serverless/sam/). Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) before deploying these examples. ## Getting started -If you have never used AWS Lambda or Docker before, check out this [getting started guide](https://fabianfett.de/getting-started-with-swift-aws-lambda-runtime) which helps you with every step from zero to a running Lambda. +To get started, read [the Swift AWS Lambda runtime tutorial](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/tutorials/table-of-content). It provides developers with detailed step-by-step instructions to develop, build, and deploy a Lambda function. -First, create a SwiftPM project and pull Swift AWS Lambda Runtime as dependency into your project +We also wrote a comprehensive [deployment guide](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntime/deployment). - ```swift - // swift-tools-version:5.2 +Or, if you're impatient to start with runtime v2, try these six steps: - import PackageDescription +The `Examples/_MyFirstFunction` contains a script that goes through the steps described in this section. - let package = Package( - name: "my-lambda", - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0"), - ], - targets: [ - .target(name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ]), - ] - ) - ``` +If you are really impatient, just type: -Next, create a `main.swift` and implement your Lambda. +```bash +cd Examples/_MyFirstFunction +./create_and_deploy_function.sh +``` - ### Using Closures +Otherwise, continue reading. - The simplest way to use `AWSLambdaRuntime` is to pass in a closure, for example: +1. Create a new Swift executable project - ```swift - // Import the module - import AWSLambdaRuntime +```bash +mkdir MyLambda && cd MyLambda +swift package init --type executable +``` - // in this example we are receiving and responding with strings - Lambda.run { (context, name: String, callback: @escaping (Result) -> Void) in - callback(.success("Hello, \(name)")) - } - ``` +2. Prepare your `Package.swift` file - More commonly, the event would be a JSON, which is modeled using `Codable`, for example: + 2.1 Add the Swift AWS Lambda Runtime as a dependency - ```swift - // Import the module - import AWSLambdaRuntime + ```bash + swift package add-dependency https://github.com/swift-server/swift-aws-lambda-runtime.git --branch main + swift package add-target-dependency AWSLambdaRuntime MyLambda --package swift-aws-lambda-runtime + ``` - // Request, uses Codable for transparent JSON encoding - private struct Request: Codable { - let name: String - } + 2.2 (Optional - only on macOS) Add `platforms` after `name` - // Response, uses Codable for transparent JSON encoding - private struct Response: Codable { - let message: String - } + ``` + platforms: [.macOS(.v15)], + ``` - // In this example we are receiving and responding with `Codable`. - Lambda.run { (context, request: Request, callback: @escaping (Result) -> Void) in - callback(.success(Response(message: "Hello, \(request.name)"))) - } - ``` + 2.3 Your `Package.swift` file must look like this - Since most Lambda functions are triggered by events originating in the AWS platform like `SNS`, `SQS` or `APIGateway`, the package also includes a `AWSLambdaEvents` module that provides implementations for most common AWS event types further simplifying writing Lambda functions. For example, handling an `SQS` message: + ```swift + // swift-tools-version: 6.0 - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents + import PackageDescription - // In this example we are receiving an SQS Message, with no response (Void). - Lambda.run { (context, message: SQS.Message, callback: @escaping (Result) -> Void) in - ... - callback(.success(Void())) - } - ``` + let package = Package( + name: "MyLambda", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ] + ), + ] + ) + ``` - Modeling Lambda functions as Closures is both simple and safe. Swift AWS Lambda Runtime will ensure that the user-provided code is offloaded from the network processing thread such that even if the code becomes slow to respond or gets hang, the underlying process can continue to function. This safety comes at a small performance penalty from context switching between threads. In many cases, the simplicity and safety of using the Closure based API is often preferred over the complexity of the performance-oriented API. +3. Scaffold a minimal Lambda function -### Using EventLoopLambdaHandler +The runtime comes with a plugin to generate the code of a simple AWS Lambda function: - Performance sensitive Lambda functions may choose to use a more complex API which allows user code to run on the same thread as the networking handlers. Swift AWS Lambda Runtime uses [SwiftNIO](https://github.com/apple/swift-nio) as its underlying networking engine which means the APIs are based on [SwiftNIO](https://github.com/apple/swift-nio) concurrency primitives like the `EventLoop` and `EventLoopFuture`. For example: +```bash +swift package lambda-init --allow-writing-to-package-directory +``` - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents - import NIO +Your `Sources/main.swift` file must look like this. + +```swift +import AWSLambdaRuntime + +// in this example we are receiving and responding with strings - // Our Lambda handler, conforms to EventLoopLambdaHandler - struct Handler: EventLoopLambdaHandler { - typealias In = SNS.Message // Request type - typealias Out = Void // Response type +let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + return String(event.reversed()) +} - // In this example we are receiving an SNS Message, with no response (Void). - func handle(context: Lambda.Context, event: In) -> EventLoopFuture { - ... - context.eventLoop.makeSucceededFuture(Void()) - } - } +try await runtime.run() +``` - Lambda.run(Handler()) - ``` +4. Build & archive the package - Beyond the small cognitive complexity of using the `EventLoopFuture` based APIs, note these APIs should be used with extra care. An `EventLoopLambdaHandler` will execute the user code on the same `EventLoop` (thread) as the library, making processing faster but requiring the user code to never call blocking APIs as it might prevent the underlying process from functioning. +The runtime comes with a plugin to compile on Amazon Linux and create a ZIP archive: -## Deploying to AWS Lambda +```bash +swift package archive --allow-network-connections docker +``` -To deploy Lambda functions to AWS Lambda, you need to compile the code for Amazon Linux which is the OS used on AWS Lambda microVMs, package it as a Zip file, and upload to AWS. +If there is no error, the ZIP archive is ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip` -AWS offers several tools to interact and deploy Lambda functions to AWS Lambda including [SAM](https://aws.amazon.com/serverless/sam/) and the [AWS CLI](https://aws.amazon.com/cli/). The [Examples Directory](/Examples) includes complete sample build and deployment scripts that utilize these tools. +5. Deploy to AWS -Note the examples mentioned above use dynamic linking, therefore bundle the required Swift libraries in the Zip package along side the executable. You may choose to link the Lambda function statically (using `-static-stdlib`) which could improve performance but requires additional linker flags. +There are multiple ways to deploy to AWS ([SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html), [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started), [AWS Cloud Development Kit (CDK)](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html), [AWS Console](https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html)) that are covered later in this doc. -To build the Lambda function for Amazon Linux, use the Docker image published by Swift.org on [Swift toolchains and Docker images for Amazon Linux 2](https://swift.org/download/), as demonstrated in the examples. +Here is how to deploy using the `aws` command line. -## Architecture +```bash +aws lambda create-function \ +--function-name MyLambda \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam:::role/lambda_basic_execution +``` -The library defines three protocols for the implementation of a Lambda Handler. From low-level to more convenient: +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. -### ByteBufferLambdaHandler +Be sure to replace with your actual AWS account ID (for example: 012345678901). -An `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns a `ByteBuffer?` asynchronously. +> [!IMPORTANT] +> Before creating a function, you need to have a `lambda_basic_execution` IAM role in your AWS account. +> +> You can create this role in two ways: +> 1. Using AWS Console +> 2. Running the commands in the `create_lambda_execution_role()` function in [`Examples/_MyFirstFunction/create_iam_role.sh`](https://github.com/swift-server/swift-aws-lambda-runtime/blob/8dff649920ab0c66bb039d15ae48d9d5764db71a/Examples/_MyFirstFunction/create_and_deploy_function.sh#L40C1-L40C31) -`ByteBufferLambdaHandler` is the lowest level protocol designed to power the higher level `EventLoopLambdaHandler` and `LambdaHandler` based APIs. Users are not expected to use this protocol, though some performance sensitive applications that operate at the `ByteBuffer` level or have special serialization needs may choose to do so. +6. Invoke your Lambda function -```swift -public protocol ByteBufferLambdaHandler { - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: The event or request payload encoded as `ByteBuffer`. - /// - /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. - /// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error` - func handle(context: Lambda.Context, event: ByteBuffer) -> EventLoopFuture +```bash +aws lambda invoke \ +--function-name MyLambda \ +--payload $(echo \"Hello World\" | base64) \ +out.txt && cat out.txt && rm out.txt +``` + +This should print + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" } +"dlroW olleH" ``` -### EventLoopLambdaHandler +## Developing your Swift Lambda functions -`EventLoopLambdaHandler` is a strongly typed, `EventLoopFuture` based asynchronous processing protocol for a Lambda that takes a user defined In and returns a user defined Out. +### Receive and respond with JSON objects -`EventLoopLambdaHandler` extends `ByteBufferLambdaHandler`, providing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer?` encoding for `Codable` and String. +Typically, your Lambda functions will receive an input parameter expressed as JSON and will respond with some other JSON. The Swift AWS Lambda runtime automatically takes care of encoding and decoding JSON objects when your Lambda function handler accepts `Decodable` and returns `Encodable` conforming types. -`EventLoopLambdaHandler` executes the user provided Lambda on the same `EventLoop` as the core runtime engine, making the processing fast but requires more care from the implementation to never block the `EventLoop`. It it designed for performance sensitive applications that use `Codable` or String based Lambda functions. +Here is an example of a minimal function that accepts a JSON object as input and responds with another JSON object. ```swift -public protocol EventLoopLambdaHandler: ByteBufferLambdaHandler { - associatedtype In - associatedtype Out - - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. - /// - /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. - /// The `EventLoopFuture` should be completed with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In) -> EventLoopFuture - - /// Encode a response of type `Out` to `ByteBuffer` - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - parameters: - /// - allocator: A `ByteBufferAllocator` to help allocate the `ByteBuffer`. - /// - value: Response of type `Out`. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(allocator: ByteBufferAllocator, value: Out) throws -> ByteBuffer? - - /// Decode a`ByteBuffer` to a request or event of type `In` - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - /// - parameters: - /// - buffer: The `ByteBuffer` to decode. - /// - /// - Returns: A request or event of type `In`. - func decode(buffer: ByteBuffer) throws -> In +import AWSLambdaRuntime + +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int } + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// the Lambda runtime +let runtime = LambdaRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} + +// start the loop +try await runtime.run() ``` -### LambdaHandler +You can learn how to deploy and invoke this function in [the Hello JSON example README file](Examples/HelloJSON/README.md). + +### Lambda Streaming Response -`LambdaHandler` is a strongly typed, completion handler based asynchronous processing protocol for a Lambda that takes a user defined In and returns a user defined Out. +You can configure your Lambda function to stream response payloads back to clients. Response streaming can benefit latency sensitive applications by improving time to first byte (TTFB) performance. This is because you can send partial responses back to the client as they become available. Additionally, you can use response streaming to build functions that return larger payloads. Response stream payloads have a soft limit of 20 MB as compared to the 6 MB limit for buffered responses. Streaming a response also means that your function doesn’t need to fit the entire response in memory. For very large responses, this can reduce the amount of memory you need to configure for your function. -`LambdaHandler` extends `ByteBufferLambdaHandler`, performing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding for `Codable` and String. +Streaming responses incurs a cost. For more information, see [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/). -`LambdaHandler` offloads the user provided Lambda execution to a `DispatchQueue` making processing safer but slower. +You can stream responses through [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), the AWS SDK, or using the Lambda [InvokeWithResponseStream](https://docs.aws.amazon.com/lambda/latest/dg/API_InvokeWithResponseStream.html) API. In this example, we create an authenticated Lambda function URL. + +Here is an example of a minimal function that streams 10 numbers with an interval of one second for each number. ```swift -public protocol LambdaHandler: EventLoopLambdaHandler { - /// Defines to which `DispatchQueue` the Lambda execution is offloaded to. - var offloadQueue: DispatchQueue { get } - - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. - /// - callback: Completion handler to report the result of the Lambda back to the runtime engine. - /// The completion handler expects a `Result` with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) +import AWSLambdaRuntime +import NIOCore + +struct SendNumbersWithPause: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + for i in 1...10 { + // Send partial data + try await responseWriter.write(ByteBuffer(string: "\(i)\n")) + // Perform some long asynchronous work + try await Task.sleep(for: .milliseconds(1000)) + } + // All data has been sent. Close off the response stream. + try await responseWriter.finish() + } } + +let runtime = LambdaRuntime.init(handler: SendNumbersWithPause()) +try await runtime.run() ``` -### Closures +You can learn how to deploy and invoke this function in [the streaming example README file](Examples/Streaming/README.md). -In addition to protocol-based Lambda, the library provides support for Closure-based ones, as demonstrated in the overview section above. Closure-based Lambdas are based on the `LambdaHandler` protocol which mean they are safer. For most use cases, Closure-based Lambda is a great fit and users are encouraged to use them. +### Integration with AWS Services -The library includes implementations for `Codable` and String based Lambda. Since AWS Lambda is primarily JSON based, this covers the most common use cases. + Most Lambda functions are triggered by events originating in other AWS services such as `Amazon SNS`, `Amazon SQS` or `AWS APIGateway`. -```swift -public typealias CodableClosure = (Lambda.Context, In, @escaping (Result) -> Void) -> Void -``` + The [Swift AWS Lambda Events](http://github.com/swift-server/swift-aws-lambda-events) package includes an `AWSLambdaEvents` module that provides implementations for most common AWS event types further simplifying writing Lambda functions. -```swift -public typealias StringClosure = (Lambda.Context, String, @escaping (Result) -> Void) -> Void + Here is an example Lambda function invoked when the AWS APIGateway receives an HTTP request. + + ```swift +import AWSLambdaEvents +import AWSLambdaRuntime + +let runtime = LambdaRuntime { + (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in + + APIGatewayV2Response(statusCode: .ok, body: "Hello World!") +} + +try await runtime.run() ``` -This design allows for additional event types as well, and such Lambda implementation can extend one of the above protocols and provided their own `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding. + You can learn how to deploy and invoke this function in [the API Gateway example README file](Examples/APIGateway/README.md). -### Context +### Integration with Swift Service LifeCycle -When calling the user provided Lambda function, the library provides a `Context` class that provides metadata about the execution context, as well as utilities for logging and allocating buffers. +Support for [Swift Service Lifecycle](https://github.com/swift-server/swift-service-lifecycle) is currently being implemented. You can follow https://github.com/swift-server/swift-aws-lambda-runtime/issues/374 for more details and teh current status. Your contributions are welcome. +### Use Lambda Background Tasks + +Background tasks allow code to execute asynchronously after the main response has been returned, enabling additional processing without affecting response latency. This approach is ideal for scenarios like logging, data updates, or notifications that can be deferred. The code leverages Lambda's "Response Streaming" feature, which is effective for balancing real-time user responsiveness with the ability to perform extended tasks post-response. For more information about Lambda background tasks, see [this AWS blog post](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/). + + +Here is an example of a minimal function that waits 10 seconds after it returned a response but before the handler returns. ```swift -public final class Context { - /// The request ID, which identifies the request that triggered the function invocation. - public let requestID: String +import AWSLambdaRuntime +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + context.logger.debug("BackgroundProcessingHandler - message received") + try await outputWriter.write(Greeting(echoedMessage: event.message)) + + // Perform some background work, e.g: + context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.") + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning") + return + } +} + +let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler()) +let runtime = LambdaRuntime.init(handler: adapter) +try await runtime.run() +``` + +You can learn how to deploy and invoke this function in [the background tasks example README file](Examples/BackgroundTasks/README.md). - /// The AWS X-Ray tracing header. - public let traceID: String +## Testing Locally - /// The ARN of the Lambda function, version, or alias that's specified in the invocation. - public let invokedFunctionARN: String +Before deploying your code to AWS Lambda, you can test it locally by running the executable target on your local machine. It will look like this on CLI: - /// The timestamp that the function times out - public let deadline: DispatchWallTime +```sh +swift run +``` - /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. - public let cognitoIdentity: String? +When not running inside a Lambda execution environment, it starts a local HTTP server listening on port 7000. You can invoke your local Lambda function by sending an HTTP POST request to `http://127.0.0.1:7000/invoke`. + +The request must include the JSON payload expected as an `event` by your function. You can create a text file with the JSON payload documented by AWS or captured from a trace. In this example, we used [the APIGatewayv2 JSON payload from the documentation](https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html#apigateway-example-event), saved as `events/create-session.json` text file. + +Then we use curl to invoke the local endpoint with the test JSON payload. + +```sh +curl -v --header "Content-Type:\ application/json" --data @events/create-session.json http://127.0.0.1:7000/invoke +* Trying 127.0.0.1:7000... +* Connected to 127.0.0.1 (127.0.0.1) port 7000 +> POST /invoke HTTP/1.1 +> Host: 127.0.0.1:7000 +> User-Agent: curl/8.4.0 +> Accept: */* +> Content-Type:\ application/json +> Content-Length: 1160 +> +< HTTP/1.1 200 OK +< content-length: 247 +< +* Connection #0 to host 127.0.0.1 left intact +{"statusCode":200,"isBase64Encoded":false,"body":"...","headers":{"Access-Control-Allow-Origin":"*","Content-Type":"application\/json; charset=utf-8","Access-Control-Allow-Headers":"*"}} +``` +### Modifying the local endpoint - /// For invocations from the AWS Mobile SDK, data about the client application and device. - public let clientContext: String? +By default, when using the local Lambda server, it listens on the `/invoke` endpoint. - /// `Logger` to log with - /// - /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. - public let logger: Logger +Some testing tools, such as the [AWS Lambda runtime interface emulator](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html), require a different endpoint. In that case, you can use the `LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT` environment variable to force the runtime to listen on a different endpoint. - /// The `EventLoop` the Lambda is executed on. Use this to schedule work with. - /// This is useful when implementing the `EventLoopLambdaHandler` protocol. - /// - /// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care. - /// Most importantly the `EventLoop` must never be blocked. - public let eventLoop: EventLoop +Example: - /// `ByteBufferAllocator` to allocate `ByteBuffer` - /// This is useful when implementing `EventLoopLambdaHandler` - public let allocator: ByteBufferAllocator -} +```sh +LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run ``` -### Configuration +## Deploying your Swift Lambda functions -The library’s behavior can be fine tuned using environment variables based configuration. The library supported the following environment variables: +There is a full deployment guide available in [the documentation](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntime/deployment). -* `LOG_LEVEL`: Define the logging level as defined by [SwiftLog](https://github.com/apple/swift-log). Set to INFO by default. -* `MAX_REQUESTS`: Max cycles the library should handle before exiting. Set to none by default. -* `STOP_SIGNAL`: Signal to capture for termination. Set to TERM by default. -* `REQUEST_TIMEOUT`: Max time to wait for responses to come back from the AWS Runtime engine. Set to none by default. +There are multiple ways to deploy your Swift code to AWS Lambda. The very first time, you'll probably use the AWS Console to create a new Lambda function and upload your code as a zip file. However, as you iterate on your code, you'll want to automate the deployment process. +To take full advantage of the cloud, we recommend using Infrastructure as Code (IaC) tools like the [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) or [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/). These tools allow you to define your infrastructure and deployment process as code, which can be version-controlled and automated. -### AWS Lambda Runtime Engine Integration +Alternatively, you might also consider using popular third-party tools like [Serverless Framework](https://www.serverless.com/), [Terraform](https://www.terraform.io/), or [Pulumi](https://www.pulumi.com/) to deploy Lambda functions and create and manage AWS infrastructure. -The library is designed to integrate with AWS Lambda Runtime Engine via the [AWS Lambda Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) which was introduced as part of [AWS Lambda Custom Runtimes](https://aws.amazon.com/about-aws/whats-new/2018/11/aws-lambda-now-supports-custom-runtimes-and-layers/) in 2018. The latter is an HTTP server that exposes three main RESTful endpoint: +Here is a short example that shows how to deploy using SAM. -* `/runtime/invocation/next` -* `/runtime/invocation/response` -* `/runtime/invocation/error` +**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) -A single Lambda execution workflow is made of the following steps: +When using SAM, you describe your deployment in a YAML text file. +The [API Gateway example directory](Examples/APIGateway/template.yaml) contains a file named `template.yaml` that you can use as a starting point. -1. The library calls AWS Lambda Runtime Engine `/next` endpoint to retrieve the next invocation request. -2. The library parses the response HTTP headers and populate the Context object. -3. The library reads the `/next` response body and attempt to decode it. Typically it decodes to user provided `In` type which extends `Decodable`, but users may choose to write Lambda functions that receive the input as String or `ByteBuffer` which require less, or no decoding. -4. The library hands off the `Context` and `In` event to the user provided handler. In the case of `LambdaHandler` based handler this is done on a dedicated `DispatchQueue`, providing isolation between user's and the library's code. -5. User provided handler processes the request asynchronously, invoking a callback or returning a future upon completion, which returns a Result type with the Out or Error populated. -6. In case of error, the library posts to AWS Lambda Runtime Engine `/error` endpoint to provide the error details, which will show up on AWS Lambda logs. -7. In case of success, the library will attempt to encode the response. Typically it encodes from user provided `Out` type which extends `Encodable`, but users may choose to write Lambda functions that return a String or `ByteBuffer`, which require less, or no encoding. The library then posts the response to AWS Lambda Runtime Engine `/response` endpoint to provide the response to the callee. +To deploy your Lambda function and create the infrastructure, type the following `sam` command. -The library encapsulates the workflow via the internal `LambdaRuntimeClient` and `LambdaRunner` structs respectively. +```bash +sam deploy \ +--resolve-s3 \ +--template-file template.yaml \ +--stack-name APIGatewayLambda \ +--capabilities CAPABILITY_IAM +``` -### Lifecycle Management +At the end of the deployment, the script lists the API Gateway endpoint. +The output is similar to this one. + +``` +----------------------------------------------------------------------------------------------------------------------------- +Outputs +----------------------------------------------------------------------------------------------------------------------------- +Key APIGatewayEndpoint +Description API Gateway endpoint URL" +Value https://a5q74es3k2.execute-api.us-east-1.amazonaws.com +----------------------------------------------------------------------------------------------------------------------------- +``` -AWS Lambda Runtime Engine controls the Application lifecycle and in the happy case never terminates the application, only suspends it's execution when no work is avaialble. +Please refer to the full deployment guide available in [the documentation](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntime) for more details. -As such, the library main entry point is designed to run forever in a blocking fashion, performing the workflow described above in an endless loop. +## Swift AWS Lambda Runtime - Design Principles -That loop is broken if/when an internal error occurs, such as a failure to communicate with AWS Lambda Runtime Engine API, or under other unexpected conditions. +The [design document](Sources/AWSLambdaRuntime/Documentation.docc/Proposals/0001-v2-api.md) details the v2 API proposal for the swift-aws-lambda-runtime library, which aims to enhance the developer experience for building serverless functions in Swift. -By default, the library also registers a Signal handler that traps `INT` and `TERM` , which are typical Signals used in modern deployment platforms to communicate shutdown request. +The proposal has been reviewed and [incorporated feedback from the community](https://forums.swift.org/t/aws-lambda-v2-api-proposal/73819). The full v2 API design document is available [in this repository](Sources/AWSLambdaRuntime/Documentation.docc/Proposals/0001-v2-api.md). -### Integration with AWS Platform Events +### Key Design Principles -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, the 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: +The v2 API prioritizes the following principles: -* [APIGateway Proxy](https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html) -* [S3 Events](https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html) -* [SES Events](https://docs.aws.amazon.com/lambda/latest/dg/services-ses.html) -* [SNS Events](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html) -* [SQS Events](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html) -* [CloudWatch Events](https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html) +- Readability and Maintainability: Extensive use of `async`/`await` improves code clarity and simplifies maintenance. -**Note**: Each one of the integration points mentioned above includes a set of `Codable` structs that mirror AWS' data model for these APIs. +- Developer Control: Developers own the `main()` function and have the flexibility to inject dependencies into the `LambdaRuntime`. This allows you to manage service lifecycles efficiently using [Swift Service Lifecycle](https://github.com/swift-server/swift-service-lifecycle) for structured concurrency. -## Performance +- Simplified Codable Support: The `LambdaCodableAdapter` struct eliminates the need for verbose boilerplate code when encoding and decoding events and responses. -Lambda functions performance is usually measured across two axes: +### New Capabilities -- **Cold start times**: The time it takes for a Lambda function to startup, ask for an invocation and process the first invocation. +The v2 API introduces two new features: -- **Warm invocation times**: The time it takes for a Lambda function to process an invocation after the Lambda has been invoked at least once. +[Response Streaming](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/]): This functionality is ideal for handling large responses that need to be sent incrementally.   -Larger packages size (Zip file uploaded to AWS Lambda) negatively impact the cold start time, since AWS needs to download and unpack the package before starting the process. +[Background Work](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/): Schedule tasks to run after returning a response to the AWS Lambda control plane. -Swift provides great Unicode support via [ICU](http://site.icu-project.org/home). Therefore, Swift-based Lambda functions include the ICU libraries which tend to be large. This impacts the download time mentioned above and an area for further optimization. Some of the alternatives worth exploring are using the system ICU that comes with Amazon Linux (albeit older than the one Swift ships with) or working to remove the ICU dependency altogether. We welcome ideas and contributions to this end. +These new capabilities provide greater flexibility and control when building serverless functions in Swift with the swift-aws-lambda-runtime library. diff --git a/scripts/generate_contributors_list.sh b/scripts/generate_contributors_list.sh deleted file mode 100755 index d745e21e..00000000 --- a/scripts/generate_contributors_list.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) - -cat > "$here/../CONTRIBUTORS.txt" <<- EOF - For the purpose of tracking copyright, this is the list of individuals and - organizations who have contributed source code to SwiftAWSLambdaRuntime. - - For employees of an organization/company where the copyright of work done - by employees of that company is held by the company itself, only the company - needs to be listed here. - - ## COPYRIGHT HOLDERS - - - Apple Inc. (all contributors with '@apple.com') - - ### Contributors - - $contributors - - **Updating this list** - - 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\` -EOF diff --git a/scripts/linux_performance_setup.sh b/scripts/linux_performance_setup.sh index 7c11cbbd..f02ac66b 100755 --- a/scripts/linux_performance_setup.sh +++ b/scripts/linux_performance_setup.sh @@ -20,14 +20,14 @@ apt-get install -y vim htop strace linux-tools-common linux-tools-generic libc6- echo 0 > /proc/sys/kernel/kptr_restrict -cd /usr/bin +pushd /usr/bin || exit 1 rm -rf perf ln -s /usr/lib/linux-tools/*/perf perf -cd - +popd || exit 1 -cd /opt +pushd /opt || exit 1 git clone https://github.com/brendangregg/FlameGraph.git -cd - +popd || exit 1 # build the code in relase mode with debug symbols # swift build -c release -Xswiftc -g diff --git a/scripts/performance_test.sh b/scripts/performance_test.sh index 77904eca..e4157cb0 100755 --- a/scripts/performance_test.sh +++ b/scripts/performance_test.sh @@ -3,7 +3,7 @@ ## ## This source file is part of the SwiftAWSLambdaRuntime open source project ## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -27,9 +27,11 @@ if [[ $(uname -s) == "Linux" ]]; then fi swift build -c release -Xswiftc -g +LAMBDA_USE_LOCAL_DEPS=../.. swift build --package-path Examples/HelloWorld -c release -Xswiftc -g +LAMBDA_USE_LOCAL_DEPS=../.. swift build --package-path Examples/HelloJSON -c release -Xswiftc -g cleanup() { - kill -9 $server_pid + kill -9 $server_pid # ignore-unacceptable-language } trap "cleanup" ERR @@ -45,35 +47,35 @@ results=() export MODE=string # start (fork) mock server -pkill -9 MockServer && echo "killed previous servers" && sleep 1 +pkill -9 MockServer && echo "killed previous servers" && sleep 1 # ignore-unacceptable-language echo "starting server in $MODE mode" (./.build/release/MockServer) & server_pid=$! sleep 1 -kill -0 $server_pid # check server is alive +kill -0 $server_pid # check server is alive # ignore-unacceptable-language # cold start echo "running $MODE mode cold test" cold=() export MAX_REQUESTS=1 -for (( i=0; i<$cold_iterations; i++ )); do +for (( i=0; i Checking format... " -FIRST_OUT="$(git status --porcelain)" -swiftformat . > /dev/null 2>&1 -SECOND_OUT="$(git status --porcelain)" -if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then - printf "\033[0;31mformatting issues!\033[0m\n" - git --no-pager diff - exit 1 -else - printf "\033[0;32mokay.\033[0m\n" -fi - -printf "=> Checking license headers\n" -tmp=$(mktemp /tmp/.swift-aws-lambda-sanity_XXXXXX) - -for language in swift-or-c bash dtrace; do - printf " * $language... " - declare -a matching_files - declare -a exceptions - expections=( ) - matching_files=( -name '*' ) - case "$language" in - swift-or-c) - exceptions=( -name Package.swift ) - matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' ) - cat > "$tmp" <<"EOF" -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -EOF - ;; - bash) - matching_files=( -name '*.sh' ) - cat > "$tmp" <<"EOF" -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## -EOF - ;; - dtrace) - matching_files=( -name '*.d' ) - cat > "$tmp" <<"EOF" -#!/usr/sbin/dtrace -q -s -/*===----------------------------------------------------------------------===* - * - * This source file is part of the SwiftAWSLambdaRuntime open source project - * - * Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors - * Licensed under Apache License v2.0 - * - * See LICENSE.txt for license information - * See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors - * - * SPDX-License-Identifier: Apache-2.0 - * - *===----------------------------------------------------------------------===*/ -EOF - ;; - *) - echo >&2 "ERROR: unknown language '$language'" - ;; - esac - - expected_lines=$(cat "$tmp" | wc -l) - expected_sha=$(cat "$tmp" | shasum) - - ( - cd "$here/.." - find . \ - \( \! -path '*/.build/*' -a \ - \( "${matching_files[@]}" \) -a \ - \( \! \( "${exceptions[@]}" \) \) \) | while read line; do - if [[ "$(cat "$line" | replace_acceptable_years | head -n $expected_lines | shasum)" != "$expected_sha" ]]; then - printf "\033[0;31mmissing headers in file '$line'!\033[0m\n" - diff -u <(cat "$line" | replace_acceptable_years | head -n $expected_lines) "$tmp" - exit 1 - fi - done - printf "\033[0;32mokay.\033[0m\n" - ) -done - -rm "$tmp" diff --git a/scripts/ubuntu-install-swift.sh b/scripts/ubuntu-install-swift.sh new file mode 100644 index 00000000..5ff58f46 --- /dev/null +++ b/scripts/ubuntu-install-swift.sh @@ -0,0 +1,70 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +sudo apt update && sudo apt -y upgrade + +# Install Swift 6.0.3 +sudo apt-get -y install \ + binutils \ + git \ + gnupg2 \ + libc6-dev \ + libcurl4-openssl-dev \ + libedit2 \ + libgcc-13-dev \ + libncurses-dev \ + libpython3-dev \ + libsqlite3-0 \ + libstdc++-13-dev \ + libxml2-dev \ + libz3-dev \ + pkg-config \ + tzdata \ + unzip \ + zip \ + zlib1g-dev + +wget https://download.swift.org/swift-6.0.3-release/ubuntu2404-aarch64/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-ubuntu24.04-aarch64.tar.gz + +tar xfvz swift-6.0.3-RELEASE-ubuntu24.04-aarch64.tar.gz + +export PATH=/home/ubuntu/swift-6.0.3-RELEASE-ubuntu24.04-aarch64/usr/bin:"${PATH}" + +swift --version + +# Install Docker +sudo apt-get update +sudo apt-get install -y ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +# Add the repository to Apt sources: +# shellcheck source=/etc/os-release +# shellcheck disable=SC1091 +. /etc/os-release +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $VERSION_CODENAME stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +sudo apt-get update + +sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +# Add the current user to the docker group +sudo usermod -aG docker "$USER" + +# LOGOUT and LOGIN to apply the changes +exit 0 diff --git a/scripts/ubuntu-test-plugin.sh b/scripts/ubuntu-test-plugin.sh new file mode 100644 index 00000000..19d74609 --- /dev/null +++ b/scripts/ubuntu-test-plugin.sh @@ -0,0 +1,28 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +# Connect with ssh + +export PATH=/home/ubuntu/swift-6.0.3-RELEASE-ubuntu24.04-aarch64/usr/bin:"${PATH}" + +# clone a project +git clone https://github.com/swift-server/swift-aws-lambda-runtime.git + +# be sure Swift is install. +# Youc an install swift with the following command: ./scripts/ubuntu-install-swift.sh + +# build the project +cd swift-aws-lambda-runtime/Examples/ResourcesPackaging/ || exit 1 +LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker