├── .github ├── FUNDING.yml └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE.txt ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── LambdaEvents │ ├── ALB.swift │ ├── APIGateway.swift │ ├── AWSNumber.swift │ ├── Cloudwatch.swift │ ├── DynamoDB+AttributeValue.swift │ ├── DynamoDB.swift │ ├── S3.swift │ ├── SNS.swift │ ├── SQS.swift │ └── Utils │ │ ├── DecodableBody.swift │ │ └── HTTPHeaders+Codable.swift ├── LambdaRuntime │ ├── Context.swift │ ├── Environment.swift │ ├── Runtime+ALB.swift │ ├── Runtime+APIGateway.swift │ ├── Runtime+Codable.swift │ ├── Runtime.swift │ ├── RuntimeAPIClient.swift │ └── RuntimeError.swift └── LambdaRuntimeTestUtils │ ├── Environment+TestUtils.swift │ └── Invocation+TestUtils.swift ├── Tests ├── LambdaRuntimeTests │ ├── AWSNumberTests.swift │ ├── ContextTests.swift │ ├── Events │ │ ├── ALBTests.swift │ │ ├── APIGatewayTests.swift │ │ ├── CloudwatchTests.swift │ │ ├── DecodableBodyTests.swift │ │ ├── DynamoDB+AttributeValueTests.swift │ │ ├── DynamoDBTests.swift │ │ ├── S3Tests.swift │ │ ├── SNSTests.swift │ │ └── SQSTests.swift │ ├── Runtime+CodableTests.swift │ ├── RuntimeAPIClientTests.swift │ ├── RuntimeTests.swift │ └── Utils │ │ └── MockLambdaRuntimeAPI.swift └── LinuxMain.swift ├── docker └── Dockerfile ├── docs ├── Add-Layer-to-Function.png ├── Develop.md ├── Function-Create.png ├── Invocation-Success.png ├── Layer-Copy-Arn.png └── Upload-Lambda-zip.png └── examples ├── EventSources ├── Package.resolved ├── Package.swift ├── README.md ├── Sources │ └── EventSources │ │ └── main.swift ├── makefile └── template.yaml ├── SquareNumber ├── Package.resolved ├── Package.swift ├── Sources │ └── SquareNumber │ │ └── main.swift ├── makefile └── template.yaml ├── TodoAPIGateway ├── Package.resolved ├── Package.swift ├── README.md ├── Sources │ ├── TodoAPIGateway │ │ ├── TodoController.swift │ │ └── main.swift │ └── TodoService │ │ ├── DynamoTodoStore.swift │ │ ├── TodoError.swift │ │ ├── TodoItem.swift │ │ └── TodoStore.swift ├── Tests │ ├── LinuxMain.swift │ ├── TodoAPIGatewayTests │ │ └── TodoAPIGatewayTests.swift │ └── TodoServiceTests │ │ └── DynamoTodoStoreTests.swift ├── makefile └── template.yaml └── URLRequestWithSession ├── Package.resolved ├── Package.swift ├── Sources └── URLRequestWithSession │ └── main.swift ├── makefile └── template.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: fabianfett 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | "Build-Examples": 12 | runs-on: ubuntu-18.04 13 | strategy: 14 | matrix: 15 | example: 16 | - EventSources 17 | - SquareNumber 18 | - TodoAPIGateway 19 | - URLRequestWithSession 20 | env: 21 | SWIFT_VERSION: 5.2.1 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v1 25 | with: 26 | fetch-depth: 1 27 | - name: Install ruby 28 | uses: actions/setup-ruby@v1 29 | - name: Build Docker Swift Dev Image 30 | run: docker build --build-arg SWIFT_VERSION=${SWIFT_VERSION} -t fabianfett/amazonlinux-swift:${SWIFT_VERSION}-amazonlinux2-dev ./docker 31 | - name: Build example 32 | run: | 33 | cd examples/${{ matrix.example }} 34 | make package_lambda 35 | - name: Install sam cli 36 | if: matrix.example == 'SquareNumber' 37 | run: sudo pip install aws-sam-cli 38 | - name: Download layer 39 | if: matrix.example == 'SquareNumber' 40 | run: | 41 | cd examples/${{ matrix.example }} 42 | make download_layer 43 | - name: Run example 44 | if: matrix.example == 'SquareNumber' 45 | run: | 46 | cd examples/${{ matrix.example }} 47 | echo '{"number": 9 }' | sam local invoke -v . "SquareNumberFunction" 48 | echo '{"number": 3 }' | sam local invoke -v . "PrintNumberFunction" 49 | 50 | "tuxOS-Tests": 51 | runs-on: ubuntu-latest 52 | strategy: 53 | matrix: 54 | images: 55 | - swift:5.1.5 56 | - swift:5.2.1 57 | container: 58 | image: ${{ matrix.images }} 59 | volumes: 60 | - /workspace:/src 61 | options: --workdir /src 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v1 65 | with: 66 | fetch-depth: 1 67 | - name: Install dependencies 68 | run: apt-get update && apt-get install -y zlib1g-dev zip openssl libssl-dev 69 | - name: Test 70 | run: swift test --enable-code-coverage --enable-test-discovery 71 | - name: Convert coverage files 72 | run: llvm-cov export -format="lcov" .build/debug/swift-lambda-runtimePackageTests.xctest -instr-profile .build/debug/codecov/default.profdata > info.lcov 73 | - name: Upload to codecov.io 74 | uses: codecov/codecov-action@v1.0.3 75 | with: 76 | token: ${{secrets.CODECOV_TOKEN}} 77 | 78 | "macOS-Tests": 79 | runs-on: macOS-latest 80 | steps: 81 | - name: Checkout 82 | uses: actions/checkout@v1 83 | with: 84 | fetch-depth: 1 85 | - name: Show all Xcode versions 86 | run: ls -an /Applications/ | grep Xcode* 87 | - name: Change Xcode command line tools 88 | run: sudo xcode-select -s /Applications/Xcode_11.4.app/Contents/Developer 89 | - name: SPM Build 90 | run: swift build 91 | - name: SPM Tests 92 | run: swift test --parallel -Xswiftc -DDEBUG 93 | - name: Xcode Tests 94 | run: | 95 | swift package generate-xcodeproj 96 | xcodebuild -quiet -parallel-testing-enabled YES -scheme swift-lambda-runtime-Package -enableCodeCoverage YES build test 97 | - name: Codecov 98 | run: bash <(curl -s https://codecov.io/bash) -J 'LambdaRuntime' -t ${{secrets.CODECOV_TOKEN}} 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build 3 | /*.xcodeproj 4 | xcuserdata 5 | packaged.yaml 6 | examples/**/bootstrap 7 | examples/**/lambda.zip 8 | examples/**/swift-lambda-layer -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "037b70291941fe43de668066eb6fb802c5e181d2", 10 | "version": "1.1.1" 11 | } 12 | }, 13 | { 14 | "package": "swift-base64-kit", 15 | "repositoryURL": "https://github.com/fabianfett/swift-base64-kit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "3ffa48a7047fc9ac6581cd53ab1df29466d8f13b", 19 | "version": "0.2.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-log", 24 | "repositoryURL": "https://github.com/apple/swift-log.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", 28 | "version": "1.2.0" 29 | } 30 | }, 31 | { 32 | "package": "swift-nio", 33 | "repositoryURL": "https://github.com/apple/swift-nio.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "e876fb37410e0036b98b5361bb18e6854739572b", 37 | "version": "2.16.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-nio-extras", 42 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "b4dbfacff47fb8d0f9e0a422d8d37935a9f10570", 46 | "version": "1.4.0" 47 | } 48 | }, 49 | { 50 | "package": "swift-nio-ssl", 51 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "ae213938e151964aa691f0e902462fbe06baeeb6", 55 | "version": "2.7.1" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-lambda-runtime", 8 | products: [ 9 | .library( 10 | name: "LambdaRuntime", 11 | targets: ["LambdaRuntime"] 12 | ), 13 | .library( 14 | name: "LambdaEvents", 15 | targets: ["LambdaEvents"] 16 | ), 17 | .library( 18 | name: "LambdaRuntimeTestUtils", 19 | targets: ["LambdaRuntimeTestUtils"] 20 | ), 21 | ], 22 | dependencies: [ 23 | .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.13.0")), 24 | .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.1.1")), 25 | .package(url: "https://github.com/swift-server/async-http-client.git", .upToNextMajor(from: "1.0.0")), 26 | .package(url: "https://github.com/fabianfett/swift-base64-kit.git", .upToNextMajor(from: "0.2.0")), 27 | ], 28 | targets: [ 29 | .target(name: "LambdaEvents", dependencies: [ 30 | .product(name: "NIO", package: "swift-nio"), 31 | .product(name: "NIOHTTP1", package: "swift-nio"), 32 | .product(name: "NIOFoundationCompat", package: "swift-nio"), 33 | .product(name: "Base64Kit", package: "swift-base64-kit") 34 | ]), 35 | .target(name: "LambdaRuntime", dependencies: [ 36 | .byName(name: "LambdaEvents"), 37 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 38 | .product(name: "NIO", package: "swift-nio"), 39 | .product(name: "NIOHTTP1", package: "swift-nio"), 40 | .product(name: "NIOFoundationCompat", package: "swift-nio"), 41 | .product(name: "Logging", package: "swift-log"), 42 | ]), 43 | .target(name: "LambdaRuntimeTestUtils", dependencies: [ 44 | .byName(name: "LambdaRuntime"), 45 | .product(name: "NIOHTTP1", package: "swift-nio"), 46 | ]), 47 | .testTarget(name: "LambdaRuntimeTests", dependencies: [ 48 | .byName(name: "LambdaEvents"), 49 | .byName(name: "LambdaRuntime"), 50 | .byName(name: "LambdaRuntimeTestUtils"), 51 | .product(name: "NIO", package: "swift-nio"), 52 | .product(name: "NIOTestUtils", package: "swift-nio"), 53 | .product(name: "Logging", package: "swift-log"), 54 | ]) 55 | ] 56 | ) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-lambda-runtime 2 | 3 | ⚠️ This project is unmaintained legacy code, that never reached a version 1.0. It has been obsoleted by the [`swift-server/swift-aws-lambda-runtime`](https://github.com/swift-server/swift-aws-lambda-runtime) which contains a faster, more optimized AWS Lambda Runtime with a better API that should satisfy more needs. 4 | 5 | [![Swift 5.2](https://img.shields.io/badge/Swift-5.2-blue.svg)](https://swift.org/download/) 6 | [![github-actions](https://github.com/fabianfett/swift-lambda-runtime/workflows/CI/badge.svg)](https://github.com/fabianfett/swift-lambda-runtime/actions) 7 | [![codecov](https://codecov.io/gh/fabianfett/swift-lambda-runtime/branch/master/graph/badge.svg)](https://codecov.io/gh/fabianfett/swift-lambda-runtime) 8 | 9 | An AWS Lambda Swift runtime on top of SwiftNIO with some ready-to-use AWS Events. It is intended to be used with the [Swift on Amazon Linux](https://fabianfett.de/amazonlinux-swift) project which ensures that Swift executables can be run on Amazon Linux. 10 | 11 | An APIGateway Lambda looks like this: 12 | 13 | ```swift 14 | import LambdaRuntime 15 | import NIO 16 | 17 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 18 | defer { try! group.syncShutdownGracefully() } 19 | 20 | struct Input: Codable { 21 | let name: String 22 | } 23 | 24 | struct Greeting: Codable { 25 | let greeting: String 26 | } 27 | 28 | let handler = APIGateway.handler() { (request, ctx) in 29 | do { 30 | let payload = try request.decodeBody(Input.self) 31 | 32 | let response = try APIGateway.Response( 33 | statusCode: .ok, 34 | payload: Greeting(greeting: "Hello \(payload.name)")) 35 | 36 | return ctx.eventLoop.makeSucceededFuture(response) 37 | } 38 | catch { 39 | return ctx.eventLoop.makeFailedFuture(error) 40 | } 41 | } 42 | 43 | let runtime = try Runtime.createRuntime(eventLoopGroup: group, handler: handler) 44 | defer { try! runtime.syncShutdown() } 45 | try runtime.start().wait() 46 | ``` 47 | 48 | If you want to run your [Vapor](https://github.com/vapor/vapor) app on Lambda behind an APIGateway please checkout [`vapor-lambda-runtime`](https://github.com/fabianfett/vapor-lambda-runtime), which builds on top of this package. 49 | 50 | ## Status 51 | 52 | - [x] Runs natively on Amazon Linux and links against system libraries. Uses the Lambda Layer created by the [`amazonlinux-swift`](https://github.com/fabianfett/amazonlinux-swift) project. 53 | - [x] Built on top of `Swift-NIO` 54 | - [x] Integration with Swift [`Logging`](https://github.com/apple/swift-log) 55 | - [x] Ready-to-use [AWS Events](https://github.com/fabianfett/swift-lambda-runtime/tree/master/Sources/LambdaRuntime/Events) structs to get started as fast as possible. Currently implemented: Application Load Balancer, APIGateway, Cloudwatch Scheduled Events, DynamoDB Streams, S3, SNS and SQS Messages. More coming soon. 56 | - [x] [Tested integration](https://github.com/fabianfett/swift-lambda-runtime/blob/master/examples/TodoAPIGateway/Sources/TodoAPIGateway/main.swift) with [`aws-swift-sdk`](https://github.com/swift-aws/aws-sdk-swift) 57 | - [x] [Two examples](https://github.com/fabianfett/swift-lambda-runtime/tree/master/examples) to get you up and running as fast as possible (including an [API-Gateway Todo-List](http://todobackend.com/client/index.html?https://mwpixnkbzj.execute-api.eu-central-1.amazonaws.com/test/todos)) 58 | - [x] Unit and end-to-end tests 59 | - [x] CI workflow with GitHub Actions 60 | 61 | Alternatives: There is another project to run Swift within AWS-Lambda: [Swift-Sprinter](https://github.com/swift-sprinter/aws-lambda-swift-sprinter). 62 | 63 | 64 | ## Create and run your first Swift Lambda 65 | 66 | This should help you to get started with Swift on AWS Lambda. The focus is primarily on the AWS console, since it is the easiest way to begin with. Of course you can use the [aws-cli](https://aws.amazon.com/cli/), [sam-cli](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-layers.html), the [serverless-framework](https://serverless.com/framework/docs/providers/aws/guide/layers/), [cloudformation](https://aws.amazon.com/cloudformation/) or whatever tooling you prefer at every step of your way. I even encourage you to do so in a production environment. Noone likes clicky architectures. 🤯 If you are looking for an example, check out the [sam-template](https://github.com/fabianfett/swift-lambda-runtime/blob/master/examples/TodoAPIGateway/template.yaml) in the [TodoBackend](https://github.com/fabianfett/swift-lambda-runtime/tree/master/examples/TodoAPIGateway) example. 67 | 68 | *Note: The following instructions were recorded on 19.12.2019 and the GUI may have changed since then. Feel free to start an issue if you see a different one.* 69 | 70 | The Swift version used here is `5.2.1`. You can look up available versions of Swift on Amazonlinux [here](https://fabianfett.de/amazonlinux-swift). You may want to use a later version if that works for you! 71 | 72 | ### Step 1: Develop your lambda 73 | 74 | Create a new Swift Package Manager project. For simplicity reasons we will focus solely on squaring numbers with our Lambda function. 75 | 76 | ```bash 77 | $ mkdir SquareNumbers 78 | $ cd SquareNumbers 79 | $ swift package init --type executable 80 | ``` 81 | 82 | The easiest way to go forward from here is to drag the newly created `Package.swift` onto Xcode to open the Swift package in Xcode. 83 | 84 | Next, we will need to include the `LambdaRuntime` and `SwiftNIO` as dependencies. For that open the `Package.swift` and modify it so that it looks like this: 85 | 86 | ```swift 87 | // swift-tools-version:5.2 88 | // The swift-tools-version declares the minimum version of Swift required to build this package. 89 | 90 | import PackageDescription 91 | 92 | let package = Package( 93 | name: "SquareNumber", 94 | dependencies: [ 95 | .package(url: "https://github.com/fabianfett/swift-lambda-runtime.git", .upToNextMajor(from: "0.6.0")), 96 | .package(url: "https://github.com/apple/swift-nio", .upToNextMajor(from: "2.13.0")), 97 | ], 98 | targets: [ 99 | .target( 100 | name: "SquareNumber", 101 | dependencies: [ 102 | .product(name: "LambdaRuntime", package: "swift-lambda-runtime"), 103 | .product(name: "NIO", package: "swift-nio"), 104 | ] 105 | ), 106 | ] 107 | ) 108 | ``` 109 | 110 | Then open your `main.swift` and create your function. As mentioned earlier, in this example we just want to square numbers although your function can do whatever you want. 111 | 112 | ```swift 113 | import LambdaRuntime 114 | import NIO 115 | 116 | struct Input: Codable { 117 | let number: Double 118 | } 119 | 120 | struct Output: Codable { 121 | let result: Double 122 | } 123 | 124 | func squareNumber(input: Input, context: Context) -> EventLoopFuture { 125 | let squaredNumber = input.number * input.number 126 | return context.eventLoop.makeSucceededFuture(Output(result: squaredNumber)) 127 | } 128 | 129 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 130 | defer { try! group.syncShutdownGracefully() } 131 | 132 | do { 133 | let runtime = try Runtime.createRuntime( 134 | eventLoopGroup: group, 135 | handler: Runtime.codable(squareNumber)) 136 | 137 | defer { try! runtime.syncShutdown() } 138 | 139 | try runtime.start().wait() 140 | } 141 | catch { 142 | print("\(error)") 143 | } 144 | ``` 145 | 146 | ### Step 3: Built your lambda 147 | 148 | Your lambda needs to be built for the Amazon Linux environment. For that we use Docker to compile the Lambda. Please be aware that you need to use the same Swift version for compiling your lambda as you will use for running it. [ABI Stability is not a thing on Linux](https://swift.org/blog/abi-stability-and-more/). 149 | 150 | For this we will first need to build a development Docker image in order to compile your code on Linux. Create a Docker file and include the following code: 151 | 152 | ```Dockerfile 153 | ARG SWIFT_VERSION=5.2.1 154 | FROM fabianfett/amazonlinux-swift:$SWIFT_VERSION-amazonlinux2 155 | 156 | # needed to do again after FROM due to docker limitation 157 | ARG SWIFT_VERSION 158 | 159 | RUN yum -y update && \ 160 | yum -y install zlib-devel kernel-devel gcc-c++ openssl-devel 161 | ``` 162 | 163 | To create your Docker image run: 164 | 165 | ```bash 166 | docker build --build-arg SWIFT_VERSION=5.2.1 -t lambda-swift-dev:5.2.1 . 167 | ``` 168 | 169 | Now we can compile our lambda with the new image. 170 | 171 | ```bash 172 | # build your lambda in the linux environment 173 | $ docker run --rm --volume "$(pwd)/:/src" --workdir "/src/" lambda-swift-dev:5.2.1 swift build -c release 174 | ``` 175 | 176 | This will create a `SquareNumber` executable in your `./build/release` folder. Let's grab the executable and rename it to `bootstrap`. 177 | 178 | ```bash 179 | # copy your executable to your local folder and rename it to bootstrap 180 | $ cp .build/release/$(EXAMPLE_EXECUTABLE) ./bootstrap 181 | ``` 182 | 183 | Last: We need to zip the bootstrap before uploading to AWS. 184 | 185 | ```bash 186 | # zip your bootstrap 187 | $ zip -j lambda.zip ./bootstrap 188 | ``` 189 | 190 | ### Step 4: Create your lambda on AWS 191 | 192 | Open your AWS Console and navigate to [Lambda](https://console.aws.amazon.com/lambda/home). Select "Functions" in the side navigation and click on "Create function" in the upper right corner. Give your function a name. I'll choose "SquareNumbers" and select the runtime "Provide your own bootstrap". 193 | 194 | You'll see a screen that looks like this. 195 | 196 | ![Create your function](docs/Function-Create.png) 197 | 198 | First we need to select our Swift runtime. We do so by clicking "Layers" below the function name in the center of the screen. The lower part of the screen changes and we can see an "Add Layer" button in the center. Let's click that button. On the next screen we need to select "Provide a layer version ARN" and there we enter the ARN that fits the Swift version that we've used to compile. For Swift `5.2.1` this is `arn:aws:lambda::426836788079:layer:Swift:12`. Do not forget to replace `` with the AWS region identifier you operate in. Next we click "Add". 199 | 200 | ![Add the Swift layer to your Function](docs/Add-Layer-to-Function.png) 201 | 202 | Now you should see a layer below our function. Next we click on the function name. You should see the section "Function Code" in the lower part of the screen. Select "Upload a zip file" in the "Code entry type". Click on "Upload" and select your `lambda.zip`. In the "Handler" field you can fill in whatever you want (at least one character), since this field is not used by our runtime‌. Next click "Save". 203 | 204 | ![Upload your lambda code](docs/Upload-Lambda-zip.png) 205 | 206 | ### Step 5: Invoke your lambda 207 | 208 | The only thing left is to invoke your lambda. Select "Test" (in the upper right corner) and change your test payload to whatever json you want to supply to your function. Since I want numbers squared mine is as follows: 209 | 210 | ```json 211 | { 212 | "number": 3 213 | } 214 | ``` 215 | 216 | Since AWS wants to reuse your event for tests over and over again, you need to give your test event a name. Mine is "Number3". Click "Save" and you can click "Test" again, and this time your lambda will be execute. If everything went well, you should see a screen like this: 217 | 218 | ![The lambda invocation is a success!](docs/Invocation-Success.png) 219 | 220 | ## What's next? 221 | 222 | Great! You've made it so far. In my point of view, you should now familiarize yourself with some tooling around AWS Lambda. 223 | 224 | ### Lambda deployment/testing tooling 225 | 226 | It may be serverless or aws-sam, as noone wants or should build Lambda services by just clicking around in the AWS Console. The TodoList example is [setup with aws-sam](https://github.com/fabianfett/swift-lambda-runtime/blob/master/examples/TodoAPIGateway/template.yaml). If you need more help about how to get started with aws-sam, please reach out by opening a GitHub issue. 227 | 228 | ### aws-sdk 229 | 230 | There are two projects providing you an API to interact with AWS resources. 231 | 232 | - [`aws-sdk-swift`](https://github.com/swift-aws/aws-sdk-swift) A community driven effort. The [TodoList example](https://github.com/fabianfett/swift-lambda-runtime/tree/master/examples/TodoAPIGateway) uses this sdk to [query DynamoDB](https://github.com/fabianfett/swift-lambda-runtime/blob/master/examples/TodoAPIGateway/Sources/TodoService/DynamoTodoStore.swift). 233 | - [`smoke-aws`](https://github.com/amzn/smoke-aws) An Amazon (not AWS 😉) driven effort. Please be aware that this sdk does not return `EventLoopFuture`s. Therefore integrating may be a little tricky. Not tested. 234 | 235 | ### Logging 236 | 237 | If you want to log something inside your lambda you can use the [`logger` property](https://github.com/fabianfett/swift-lambda-runtime/blob/master/Sources/LambdaRuntime/Context.swift#L15) on the `Context` class. The `logger` is based on [`swift-log`](https://github.com/apple/swift-log) and should for this reason be compatible with lot's of other server-side Swift projects. By default the [RequestId is exposed](https://github.com/fabianfett/swift-lambda-runtime/blob/master/Sources/LambdaRuntime/Context.swift#L21) as metadata. An example can be found [here](https://github.com/fabianfett/swift-lambda-runtime/blob/master/examples/URLRequestWithSession/Sources/URLRequestWithSession/main.swift). 238 | 239 | ### EventLoop 240 | 241 | The EventLoop, on which your function is executed, can be accessed via the [`eventLoop` property](https://github.com/fabianfett/swift-lambda-runtime/blob/master/Sources/LambdaRuntime/Context.swift#L16) on the `Context` class. An example can be found [here](https://github.com/fabianfett/swift-lambda-runtime/blob/master/examples/URLRequestWithSession/Sources/URLRequestWithSession/main.swift). 242 | 243 | ## Contributing 244 | 245 | Please feel welcome and encouraged to contribute to swift-lambda-runtime. The current version of swift-lambda-runtime has a long way to go before being ready for production use and help is always welcome. 246 | 247 | If you've found a bug, have a suggestion or need help getting started, please open an Issue or a PR. If you use this package, I'd be grateful for sharing your experience. 248 | 249 | Focus areas for the time being: 250 | - Implementing [all aws lambda resource events](https://github.com/aws/aws-lambda-go/tree/master/events). Those should be quite easy for a first PR. Just grab one and go! 251 | - Fixing all the bugs and performance bottlenecks of the first release. 252 | 253 | ## Credits 254 | 255 | - [Toni Suter](https://github.com/tonisuter/aws-lambda-swift) started the project to bring Swift to AWS Lambda. 256 | - [grpc-swift](https://github.com/grpc/grpc-swift) influenced how the `Logger` and `EventLoop` are exposed to the user using the `Context`. 257 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/ALB.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NIOHTTP1 4 | 5 | // https://github.com/aws/aws-lambda-go/blob/master/events/alb.go 6 | public struct ALB { 7 | 8 | /// ALBTargetGroupRequest contains data originating from the ALB Lambda target group integration 9 | public struct TargetGroupRequest: DecodableBody { 10 | 11 | /// ALBTargetGroupRequestContext contains the information to identify the load balancer invoking the lambda 12 | public struct Context: Codable { 13 | public let elb: ELBContext 14 | } 15 | 16 | public let httpMethod: HTTPMethod 17 | public let path: String 18 | public let queryStringParameters: [String: [String]] 19 | public let headers: HTTPHeaders 20 | public let requestContext: Context 21 | public let isBase64Encoded: Bool 22 | public let body: String? 23 | } 24 | 25 | /// ELBContext contains the information to identify the ARN invoking the lambda 26 | public struct ELBContext: Codable { 27 | public let targetGroupArn: String 28 | } 29 | 30 | public struct TargetGroupResponse { 31 | 32 | public let statusCode : HTTPResponseStatus 33 | public let statusDescription: String? 34 | public let headers : HTTPHeaders? 35 | public let body : String 36 | public let isBase64Encoded : Bool 37 | 38 | public init( 39 | statusCode: HTTPResponseStatus, 40 | statusDescription: String? = nil, 41 | headers: HTTPHeaders? = nil, 42 | body: String = "", 43 | isBase64Encoded: Bool = false) 44 | { 45 | self.statusCode = statusCode 46 | self.statusDescription = statusDescription 47 | self.headers = headers 48 | self.body = body 49 | self.isBase64Encoded = isBase64Encoded 50 | } 51 | } 52 | } 53 | 54 | // MARK: - Request - 55 | 56 | extension ALB.TargetGroupRequest: Decodable { 57 | 58 | enum CodingKeys: String, CodingKey { 59 | case httpMethod = "httpMethod" 60 | case path = "path" 61 | case queryStringParameters = "queryStringParameters" 62 | case multiValueQueryStringParameters = "multiValueQueryStringParameters" 63 | case headers = "headers" 64 | case multiValueHeaders = "multiValueHeaders" 65 | case requestContext = "requestContext" 66 | case isBase64Encoded = "isBase64Encoded" 67 | case body = "body" 68 | } 69 | 70 | public init(from decoder: Decoder) throws { 71 | let container = try decoder.container(keyedBy: CodingKeys.self) 72 | 73 | let method = try container.decode(String.self, forKey: .httpMethod) 74 | self.httpMethod = HTTPMethod(rawValue: method) 75 | 76 | self.path = try container.decode(String.self, forKey: .path) 77 | 78 | // crazy multiple headers 79 | // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers 80 | 81 | if let multiValueQueryStringParameters = 82 | try container.decodeIfPresent([String: [String]].self, forKey: .multiValueQueryStringParameters) 83 | { 84 | self.queryStringParameters = multiValueQueryStringParameters 85 | } 86 | else { 87 | let singleValueQueryStringParameters = try container.decode( 88 | [String: String].self, 89 | forKey: .queryStringParameters) 90 | self.queryStringParameters = singleValueQueryStringParameters.mapValues { [$0] } 91 | } 92 | 93 | if let multiValueHeaders = 94 | try container.decodeIfPresent([String: [String]].self, forKey: .multiValueHeaders) 95 | { 96 | self.headers = HTTPHeaders(awsHeaders: multiValueHeaders) 97 | } 98 | else { 99 | let singleValueHeaders = try container.decode( 100 | [String: String].self, 101 | forKey: .headers) 102 | let multiValueHeaders = singleValueHeaders.mapValues { [$0] } 103 | self.headers = HTTPHeaders(awsHeaders: multiValueHeaders) 104 | } 105 | 106 | self.requestContext = try container.decode(Context.self, forKey: .requestContext) 107 | self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded) 108 | 109 | let body = try container.decode(String.self, forKey: .body) 110 | self.body = body != "" ? body : nil 111 | } 112 | 113 | } 114 | 115 | // MARK: - Response - 116 | 117 | extension ALB.TargetGroupResponse: Encodable { 118 | 119 | public static let MultiValueHeadersEnabledKey = 120 | CodingUserInfoKey(rawValue: "ALB.TargetGroupResponse.MultiValueHeadersEnabledKey")! 121 | 122 | enum CodingKeys: String, CodingKey { 123 | case statusCode 124 | case statusDescription 125 | case headers 126 | case multiValueHeaders 127 | case body 128 | case isBase64Encoded 129 | } 130 | 131 | public func encode(to encoder: Encoder) throws { 132 | var container = encoder.container(keyedBy: CodingKeys.self) 133 | try container.encode(statusCode.code, forKey: .statusCode) 134 | 135 | let multiValueHeaderSupport = 136 | encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] as? Bool ?? false 137 | 138 | switch (multiValueHeaderSupport, headers) { 139 | case (true, .none): 140 | try container.encode([String:String](), forKey: .multiValueHeaders) 141 | case (false, .none): 142 | try container.encode([String:[String]](), forKey: .headers) 143 | case (true, .some(let headers)): 144 | var multiValueHeaders: [String: [String]] = [:] 145 | headers.forEach { (name, value) in 146 | var values = multiValueHeaders[name] ?? [] 147 | values.append(value) 148 | multiValueHeaders[name] = values 149 | } 150 | try container.encode(multiValueHeaders, forKey: .multiValueHeaders) 151 | case (false, .some(let headers)): 152 | var singleValueHeaders: [String: String] = [:] 153 | headers.forEach { (name, value) in 154 | singleValueHeaders[name] = value 155 | } 156 | try container.encode(singleValueHeaders, forKey: .headers) 157 | } 158 | 159 | try container.encodeIfPresent(statusDescription, forKey: .statusDescription) 160 | try container.encodeIfPresent(body, forKey: .body) 161 | try container.encodeIfPresent(isBase64Encoded, forKey: .isBase64Encoded) 162 | } 163 | 164 | } 165 | 166 | extension ALB.TargetGroupResponse { 167 | 168 | public init( 169 | statusCode : HTTPResponseStatus, 170 | statusDescription: String? = nil, 171 | headers : HTTPHeaders? = nil, 172 | payload : Payload, 173 | encoder : JSONEncoder = JSONEncoder()) throws 174 | { 175 | var headers = headers ?? HTTPHeaders() 176 | headers.add(name: "Content-Type", value: "application/json") 177 | 178 | self.statusCode = statusCode 179 | self.statusDescription = statusDescription 180 | self.headers = headers 181 | 182 | let buffer = try encoder.encodeAsByteBuffer(payload, allocator: ByteBufferAllocator()) 183 | self.body = buffer.getString(at: 0, length: buffer.readableBytes) ?? "" 184 | self.isBase64Encoded = false 185 | } 186 | 187 | /// Use this method to send any arbitrary byte buffer back to the API Gateway. 188 | /// Sadly Apple currently doesn't seem to be confident enough to advertise 189 | /// their base64 implementation publically. SAD. SO SAD. Therefore no 190 | /// ByteBuffer for you my friend. 191 | public init( 192 | statusCode : HTTPResponseStatus, 193 | statusDescription: String? = nil, 194 | headers : HTTPHeaders? = nil, 195 | buffer : NIO.ByteBuffer) 196 | { 197 | let headers = headers ?? HTTPHeaders() 198 | 199 | self.statusCode = statusCode 200 | self.statusDescription = statusDescription 201 | self.headers = headers 202 | self.body = buffer.withUnsafeReadableBytes { (ptr) -> String in 203 | return String(base64Encoding: ptr) 204 | } 205 | self.isBase64Encoded = true 206 | } 207 | 208 | } 209 | 210 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/APIGateway.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NIOHTTP1 4 | import NIOFoundationCompat 5 | import Base64Kit 6 | 7 | // https://github.com/aws/aws-lambda-go/blob/master/events/apigw.go 8 | 9 | public struct APIGateway { 10 | 11 | /// APIGatewayRequest contains data coming from the API Gateway 12 | public struct Request: DecodableBody { 13 | 14 | public struct Context: Codable { 15 | 16 | public struct Identity: Codable { 17 | public let cognitoIdentityPoolId: String? 18 | 19 | public let apiKey: String? 20 | public let userArn: String? 21 | public let cognitoAuthenticationType: String? 22 | public let caller: String? 23 | public let userAgent: String? 24 | public let user: String? 25 | 26 | public let cognitoAuthenticationProvider: String? 27 | public let sourceIp: String? 28 | public let accountId: String? 29 | } 30 | 31 | public let resourceId: String 32 | public let apiId: String 33 | public let resourcePath: String 34 | public let httpMethod: String 35 | public let requestId: String 36 | public let accountId: String 37 | public let stage: String 38 | 39 | public let identity: Identity 40 | public let extendedRequestId: String? 41 | public let path: String 42 | } 43 | 44 | public let resource: String 45 | public let path: String 46 | public let httpMethod: HTTPMethod 47 | 48 | public let queryStringParameters: [String: String]? 49 | public let multiValueQueryStringParameters: [String:[String]]? 50 | public let headers: HTTPHeaders 51 | public let pathParameters: [String:String]? 52 | public let stageVariables: [String:String]? 53 | 54 | public let requestContext: Request.Context 55 | public let body: String? 56 | public let isBase64Encoded: Bool 57 | } 58 | 59 | public struct Response { 60 | 61 | public let statusCode : HTTPResponseStatus 62 | public let headers : HTTPHeaders? 63 | public let body : String? 64 | public let isBase64Encoded: Bool? 65 | 66 | public init( 67 | statusCode: HTTPResponseStatus, 68 | headers: HTTPHeaders? = nil, 69 | body: String? = nil, 70 | isBase64Encoded: Bool? = nil) 71 | { 72 | self.statusCode = statusCode 73 | self.headers = headers 74 | self.body = body 75 | self.isBase64Encoded = isBase64Encoded 76 | } 77 | } 78 | } 79 | 80 | // MARK: - Request - 81 | 82 | extension APIGateway.Request: Decodable { 83 | 84 | enum CodingKeys: String, CodingKey { 85 | 86 | case resource = "resource" 87 | case path = "path" 88 | case httpMethod = "httpMethod" 89 | 90 | case queryStringParameters = "queryStringParameters" 91 | case multiValueQueryStringParameters = "multiValueQueryStringParameters" 92 | case headers = "headers" 93 | case multiValueHeaders = "multiValueHeaders" 94 | case pathParameters = "pathParameters" 95 | case stageVariables = "stageVariables" 96 | 97 | case requestContext = "requestContext" 98 | case body = "body" 99 | case isBase64Encoded = "isBase64Encoded" 100 | } 101 | 102 | public init(from decoder: Decoder) throws { 103 | let container = try decoder.container(keyedBy: CodingKeys.self) 104 | 105 | let method = try container.decode(String.self, forKey: .httpMethod) 106 | self.httpMethod = HTTPMethod(rawValue: method) 107 | self.path = try container.decode(String.self, forKey: .path) 108 | self.resource = try container.decode(String.self, forKey: .resource) 109 | 110 | self.queryStringParameters = try container.decodeIfPresent( 111 | [String: String].self, 112 | forKey: .queryStringParameters) 113 | self.multiValueQueryStringParameters = try container.decodeIfPresent( 114 | [String: [String]].self, 115 | forKey: .multiValueQueryStringParameters) 116 | 117 | let awsHeaders = try container.decode([String: [String]].self, forKey: .multiValueHeaders) 118 | self.headers = HTTPHeaders(awsHeaders: awsHeaders) 119 | 120 | self.pathParameters = try container.decodeIfPresent([String:String].self, forKey: .pathParameters) 121 | self.stageVariables = try container.decodeIfPresent([String:String].self, forKey: .stageVariables) 122 | 123 | self.requestContext = try container.decode(Context.self, forKey: .requestContext) 124 | self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded) 125 | self.body = try container.decodeIfPresent(String.self, forKey: .body) 126 | } 127 | } 128 | 129 | extension APIGateway.Request { 130 | 131 | @available(*, deprecated, renamed: "decodeBody(_:decoder:)") 132 | public func payload(_ type: Payload.Type, decoder: JSONDecoder = JSONDecoder()) throws -> Payload { 133 | return try self.decodeBody(Payload.self, decoder: decoder) 134 | } 135 | } 136 | 137 | // MARK: - Response - 138 | 139 | extension APIGateway.Response: Encodable { 140 | 141 | enum CodingKeys: String, CodingKey { 142 | case statusCode 143 | case headers 144 | case body 145 | case isBase64Encoded 146 | } 147 | 148 | private struct HeaderKeys: CodingKey { 149 | var stringValue: String 150 | 151 | init?(stringValue: String) { 152 | self.stringValue = stringValue 153 | } 154 | var intValue: Int? { 155 | fatalError("unexpected use") 156 | } 157 | init?(intValue: Int) { 158 | fatalError("unexpected use") 159 | } 160 | } 161 | 162 | public func encode(to encoder: Encoder) throws { 163 | var container = encoder.container(keyedBy: CodingKeys.self) 164 | try container.encode(statusCode.code, forKey: .statusCode) 165 | 166 | if let headers = headers { 167 | var headerContainer = container.nestedContainer(keyedBy: HeaderKeys.self, forKey: .headers) 168 | try headers.forEach { (name, value) in 169 | try headerContainer.encode(value, forKey: HeaderKeys(stringValue: name)!) 170 | } 171 | } 172 | 173 | try container.encodeIfPresent(body, forKey: .body) 174 | try container.encodeIfPresent(isBase64Encoded, forKey: .isBase64Encoded) 175 | } 176 | 177 | } 178 | 179 | extension APIGateway.Response { 180 | 181 | public init( 182 | statusCode: HTTPResponseStatus, 183 | headers : HTTPHeaders? = nil, 184 | payload : Payload, 185 | encoder : JSONEncoder = JSONEncoder()) throws 186 | { 187 | var headers = headers ?? HTTPHeaders() 188 | headers.add(name: "Content-Type", value: "application/json") 189 | 190 | self.statusCode = statusCode 191 | self.headers = headers 192 | 193 | let buffer = try encoder.encodeAsByteBuffer(payload, allocator: ByteBufferAllocator()) 194 | self.body = buffer.getString(at: 0, length: buffer.readableBytes) 195 | self.isBase64Encoded = false 196 | } 197 | 198 | /// Use this method to send any arbitrary byte buffer back to the API Gateway. 199 | /// Sadly Apple currently doesn't seem to be confident enough to advertise 200 | /// their base64 implementation publically. SAD. SO SAD. Therefore no 201 | /// ByteBuffer for you my friend. 202 | public init( 203 | statusCode: HTTPResponseStatus, 204 | headers : HTTPHeaders? = nil, 205 | buffer : NIO.ByteBuffer) 206 | { 207 | let headers = headers ?? HTTPHeaders() 208 | 209 | self.statusCode = statusCode 210 | self.headers = headers 211 | self.body = buffer.withUnsafeReadableBytes { (ptr) -> String in 212 | return String(base64Encoding: ptr) 213 | } 214 | self.isBase64Encoded = true 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/AWSNumber.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct AWSNumber: Codable, Equatable { 4 | 5 | public let stringValue: String 6 | 7 | public var int: Int? { 8 | return Int(stringValue) 9 | } 10 | 11 | public var double: Double? { 12 | return Double(stringValue) 13 | } 14 | 15 | public init(int: Int) { 16 | stringValue = String(int) 17 | } 18 | 19 | public init(double: Double) { 20 | stringValue = String(double) 21 | } 22 | 23 | public init(from decoder: Decoder) throws { 24 | let container = try decoder.singleValueContainer() 25 | stringValue = try container.decode(String.self) 26 | } 27 | 28 | public func encode(to encoder: Encoder) throws { 29 | var container = encoder.singleValueContainer() 30 | try container.encode(stringValue) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/Cloudwatch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Cloudwatch { 4 | 5 | public struct Event { 6 | public let id : String 7 | public let detailType : String 8 | public let source : String 9 | public let accountId : String 10 | public let time : Date 11 | public let region : String 12 | public let resources : [String] 13 | public let detail : Detail 14 | } 15 | 16 | public struct ScheduledEvent: Codable {} 17 | 18 | fileprivate static let dateFormatter: DateFormatter = Cloudwatch.createDateFormatter() 19 | fileprivate static func createDateFormatter() -> DateFormatter { 20 | let formatter = DateFormatter() 21 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 22 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 23 | formatter.locale = Locale(identifier: "en_US_POSIX") 24 | return formatter 25 | } 26 | } 27 | 28 | extension Cloudwatch.Event: Decodable { 29 | 30 | enum CodingKeys: String, CodingKey { 31 | case id = "id" 32 | case detailType = "detail-type" 33 | case source = "source" 34 | case accountId = "account" 35 | case time = "time" 36 | case region = "region" 37 | case resources = "resources" 38 | case detail = "detail" 39 | } 40 | 41 | public init(from decoder: Decoder) throws { 42 | let container = try decoder.container(keyedBy: CodingKeys.self) 43 | 44 | self.id = try container.decode(String.self, forKey: .id) 45 | self.detailType = try container.decode(String.self, forKey: .detailType) 46 | self.source = try container.decode(String.self, forKey: .source) 47 | self.accountId = try container.decode(String.self, forKey: .accountId) 48 | 49 | let dateString = try container.decode(String.self, forKey: .time) 50 | guard let time = Cloudwatch.dateFormatter.date(from: dateString) else { 51 | let dateFormat = String(describing: Cloudwatch.dateFormatter.dateFormat) 52 | throw DecodingError.dataCorruptedError(forKey: .time, in: container, debugDescription: 53 | "Expected date to be in format `\(dateFormat)`, but `\(dateFormat) does not forfill format`") 54 | } 55 | self.time = time 56 | 57 | self.region = try container.decode(String.self, forKey: .region) 58 | self.resources = try container.decode([String].self, forKey: .resources) 59 | self.detail = try container.decode(Detail.self, forKey: .detail) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/DynamoDB+AttributeValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import Base64Kit 4 | 5 | extension DynamoDB { 6 | 7 | public enum AttributeValue { 8 | case boolean(Bool) 9 | case binary(NIO.ByteBuffer) 10 | case binarySet([NIO.ByteBuffer]) 11 | case string(String) 12 | case stringSet([String]) 13 | case null 14 | case number(AWSNumber) 15 | case numberSet([AWSNumber]) 16 | 17 | case list([AttributeValue]) 18 | case map([String: AttributeValue]) 19 | } 20 | 21 | } 22 | 23 | extension DynamoDB.AttributeValue: Decodable { 24 | 25 | enum CodingKeys: String, CodingKey { 26 | case binary = "B" 27 | case bool = "BOOL" 28 | case binarySet = "BS" 29 | case list = "L" 30 | case map = "M" 31 | case number = "N" 32 | case numberSet = "NS" 33 | case null = "NULL" 34 | case string = "S" 35 | case stringSet = "SS" 36 | } 37 | 38 | public init(from decoder: Decoder) throws { 39 | 40 | let container = try decoder.container(keyedBy: CodingKeys.self) 41 | let allocator = ByteBufferAllocator() 42 | 43 | guard container.allKeys.count == 1, let key = container.allKeys.first else { 44 | let context = DecodingError.Context( 45 | codingPath: container.codingPath, 46 | debugDescription: "Expected exactly one key, but got \(container.allKeys.count)") 47 | throw DecodingError.dataCorrupted(context) 48 | } 49 | 50 | switch key { 51 | case .binary: 52 | let encoded = try container.decode(String.self, forKey: .binary) 53 | let bytes = try encoded.base64decoded() 54 | var buffer = allocator.buffer(capacity: bytes.count) 55 | buffer.setBytes(bytes, at: 0) 56 | self = .binary(buffer) 57 | 58 | case .bool: 59 | let value = try container.decode(Bool.self, forKey: .bool) 60 | self = .boolean(value) 61 | 62 | case .binarySet: 63 | let values = try container.decode([String].self, forKey: .binarySet) 64 | let buffers = try values.map { (encoded) -> ByteBuffer in 65 | let bytes = try encoded.base64decoded() 66 | var buffer = allocator.buffer(capacity: bytes.count) 67 | buffer.setBytes(bytes, at: 0) 68 | return buffer 69 | } 70 | self = .binarySet(buffers) 71 | 72 | case .list: 73 | let values = try container.decode([DynamoDB.AttributeValue].self, forKey: .list) 74 | self = .list(values) 75 | 76 | case .map: 77 | let value = try container.decode([String: DynamoDB.AttributeValue].self, forKey: .map) 78 | self = .map(value) 79 | 80 | case .number: 81 | let value = try container.decode(AWSNumber.self, forKey: .number) 82 | self = .number(value) 83 | 84 | case .numberSet: 85 | let values = try container.decode([AWSNumber].self, forKey: .numberSet) 86 | self = .numberSet(values) 87 | 88 | case .null: 89 | self = .null 90 | 91 | case .string: 92 | let value = try container.decode(String.self, forKey: .string) 93 | self = .string(value) 94 | 95 | case .stringSet: 96 | let values = try container.decode([String].self, forKey: .stringSet) 97 | self = .stringSet(values) 98 | } 99 | } 100 | } 101 | 102 | extension DynamoDB.AttributeValue: Equatable { 103 | 104 | static public func == (lhs: Self, rhs: Self) -> Bool { 105 | switch (lhs, rhs) { 106 | case (.boolean(let lhs), .boolean(let rhs)): 107 | return lhs == rhs 108 | case (.binary(let lhs), .binary(let rhs)): 109 | return lhs == rhs 110 | case (.binarySet(let lhs), .binarySet(let rhs)): 111 | return lhs == rhs 112 | case (.string(let lhs), .string(let rhs)): 113 | return lhs == rhs 114 | case (.stringSet(let lhs), .stringSet(let rhs)): 115 | return lhs == rhs 116 | case (.null, .null): 117 | return true 118 | case (.number(let lhs), .number(let rhs)): 119 | return lhs == rhs 120 | case (.numberSet(let lhs), .numberSet(let rhs)): 121 | return lhs == rhs 122 | case (.list(let lhs), .list(let rhs)): 123 | return lhs == rhs 124 | case (.map(let lhs), .map(let rhs)): 125 | return lhs == rhs 126 | default: 127 | return false 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/DynamoDB.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | 4 | /// https://github.com/aws/aws-lambda-go/blob/master/events/dynamodb.go 5 | public struct DynamoDB { 6 | 7 | public struct Event: Decodable { 8 | public let records: [EventRecord] 9 | 10 | public enum CodingKeys: String, CodingKey { 11 | case records = "Records" 12 | } 13 | } 14 | 15 | public enum KeyType: String, Codable { 16 | case hash = "HASH" 17 | case range = "RANGE" 18 | } 19 | 20 | public enum OperationType: String, Codable { 21 | case insert = "INSERT" 22 | case modify = "MODIFY" 23 | case remove = "REMOVE" 24 | } 25 | 26 | public enum SharedIteratorType: String, Codable { 27 | case trimHorizon = "TRIM_HORIZON" 28 | case latest = "LATEST" 29 | case atSequenceNumber = "AT_SEQUENCE_NUMBER" 30 | case afterSequenceNumber = "AFTER_SEQUENCE_NUMBER" 31 | } 32 | 33 | public enum StreamStatus: String, Codable { 34 | case enabling = "ENABLING" 35 | case enabled = "ENABLED" 36 | case disabling = "DISABLING" 37 | case disabled = "DISABLED" 38 | } 39 | 40 | public enum StreamViewType: String, Codable { 41 | /// the entire item, as it appeared after it was modified. 42 | case newImage = "NEW_IMAGE" 43 | /// the entire item, as it appeared before it was modified. 44 | case oldImage = "OLD_IMAGE" 45 | /// both the new and the old item images of the item. 46 | case newAndOldImages = "NEW_AND_OLD_IMAGES" 47 | /// only the key attributes of the modified item. 48 | case keysOnly = "KEYS_ONLY" 49 | } 50 | 51 | public struct EventRecord: Decodable { 52 | /// The region in which the GetRecords request was received. 53 | public let awsRegion: String 54 | 55 | /// The main body of the stream record, containing all of the DynamoDB-specific 56 | /// fields. 57 | public let change: StreamRecord 58 | 59 | /// A globally unique identifier for the event that was recorded in this stream 60 | /// record. 61 | public let eventId: String 62 | 63 | /// The type of data modification that was performed on the DynamoDB table: 64 | /// * INSERT - a new item was added to the table. 65 | /// * MODIFY - one or more of an existing item's attributes were modified. 66 | /// * REMOVE - the item was deleted from the table 67 | public let eventName: OperationType 68 | 69 | /// The AWS service from which the stream record originated. For DynamoDB Streams, 70 | /// this is aws:dynamodb. 71 | public let eventSource: String 72 | 73 | /// The version number of the stream record format. This number is updated whenever 74 | /// the structure of Record is modified. 75 | /// 76 | /// Client applications must not assume that eventVersion will remain at a particular 77 | /// value, as this number is subject to change at any time. In general, eventVersion 78 | /// will only increase as the low-level DynamoDB Streams API evolves. 79 | public let eventVersion: String 80 | 81 | /// The event source ARN of DynamoDB 82 | public let eventSourceArn: String 83 | 84 | /// Items that are deleted by the Time to Live process after expiration have 85 | /// the following fields: 86 | /// * Records[].userIdentity.type 87 | /// 88 | /// "Service" 89 | /// * Records[].userIdentity.principalId 90 | /// 91 | /// "dynamodb.amazonaws.com" 92 | public let userIdentity: UserIdentity? 93 | 94 | public enum CodingKeys: String, CodingKey { 95 | case awsRegion = "awsRegion" 96 | case change = "dynamodb" 97 | case eventId = "eventID" 98 | case eventName = "eventName" 99 | case eventSource = "eventSource" 100 | case eventVersion = "eventVersion" 101 | case eventSourceArn = "eventSourceARN" 102 | case userIdentity = "userIdentity" 103 | } 104 | } 105 | 106 | public struct StreamRecord { 107 | /// The approximate date and time when the stream record was created, in UNIX 108 | /// epoch time (http://www.epochconverter.com/) format. 109 | public let approximateCreationDateTime: Date? 110 | 111 | /// The primary key attribute(s) for the DynamoDB item that was modified. 112 | public let keys: [String: AttributeValue] 113 | 114 | /// The item in the DynamoDB table as it appeared after it was modified. 115 | public let newImage: [String: AttributeValue]? 116 | 117 | /// The item in the DynamoDB table as it appeared before it was modified. 118 | public let oldImage: [String: AttributeValue]? 119 | 120 | /// The sequence number of the stream record. 121 | public let sequenceNumber: String 122 | 123 | /// The size of the stream record, in bytes. 124 | public let sizeBytes: Int64 125 | 126 | /// The type of data from the modified DynamoDB item that was captured in this 127 | /// stream record. 128 | public let streamViewType: StreamViewType 129 | } 130 | 131 | public struct UserIdentity: Codable { 132 | public let type : String 133 | public let principalId: String 134 | } 135 | 136 | } 137 | 138 | extension DynamoDB.StreamRecord: Decodable { 139 | 140 | enum CodingKeys: String, CodingKey { 141 | case approximateCreationDateTime = "ApproximateCreationDateTime" 142 | case keys = "Keys" 143 | case newImage = "NewImage" 144 | case oldImage = "OldImage" 145 | case sequenceNumber = "SequenceNumber" 146 | case sizeBytes = "SizeBytes" 147 | case streamViewType = "StreamViewType" 148 | } 149 | 150 | public init(from decoder: Decoder) throws { 151 | let container = try decoder.container(keyedBy: CodingKeys.self) 152 | 153 | self.keys = try container.decode( 154 | [String: DynamoDB.AttributeValue].self, 155 | forKey: .keys) 156 | 157 | self.newImage = try container.decodeIfPresent( 158 | [String: DynamoDB.AttributeValue].self, 159 | forKey: .newImage) 160 | self.oldImage = try container.decodeIfPresent( 161 | [String: DynamoDB.AttributeValue].self, 162 | forKey: .oldImage) 163 | 164 | self.sequenceNumber = try container.decode(String.self, forKey: .sequenceNumber) 165 | self.sizeBytes = try container.decode(Int64.self, forKey: .sizeBytes) 166 | self.streamViewType = try container.decode(DynamoDB.StreamViewType.self, forKey: .streamViewType) 167 | 168 | if let timestamp = try container.decodeIfPresent(TimeInterval.self, forKey: .approximateCreationDateTime) { 169 | self.approximateCreationDateTime = Date(timeIntervalSince1970: timestamp) 170 | } 171 | else { 172 | self.approximateCreationDateTime = nil 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/S3.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | 4 | // https://github.com/aws/aws-lambda-go/blob/master/events/s3.go 5 | public struct S3 { 6 | 7 | public struct Event: Decodable { 8 | public struct Record { 9 | public let eventVersion: String 10 | public let eventSource: String 11 | public let awsRegion: String 12 | public let eventTime: Date 13 | public let eventName: String 14 | public let userIdentity: UserIdentity 15 | public let requestParameters: RequestParameters 16 | public let responseElements: [String: String] 17 | public let s3: Entity 18 | } 19 | 20 | public let records: [Record] 21 | 22 | public enum CodingKeys: String, CodingKey { 23 | case records = "Records" 24 | } 25 | } 26 | 27 | public struct RequestParameters: Codable, Equatable { 28 | public let sourceIPAddress: String 29 | } 30 | 31 | public struct UserIdentity: Codable, Equatable { 32 | public let principalId: String 33 | } 34 | 35 | public struct Entity: Codable { 36 | public let configurationId : String 37 | public let schemaVersion : String 38 | public let bucket : Bucket 39 | public let object : Object 40 | 41 | enum CodingKeys: String, CodingKey { 42 | case configurationId = "configurationId" 43 | case schemaVersion = "s3SchemaVersion" 44 | case bucket = "bucket" 45 | case object = "object" 46 | } 47 | } 48 | 49 | public struct Bucket: Codable { 50 | public let name : String 51 | public let ownerIdentity: UserIdentity 52 | public let arn : String 53 | } 54 | 55 | public struct Object: Codable { 56 | public let key : String 57 | public let size : UInt64 58 | public let urlDecodedKey: String? 59 | public let versionId : String? 60 | public let eTag : String 61 | public let sequencer : String 62 | } 63 | } 64 | 65 | extension S3.Event.Record: Decodable { 66 | 67 | enum CodingKeys: String, CodingKey { 68 | case eventVersion 69 | case eventSource 70 | case awsRegion 71 | case eventTime 72 | case eventName 73 | case userIdentity 74 | case requestParameters 75 | case responseElements 76 | case s3 77 | } 78 | 79 | public init(from decoder: Decoder) throws { 80 | let container = try decoder.container(keyedBy: CodingKeys.self) 81 | 82 | self.eventVersion = try container.decode(String.self, forKey: .eventVersion) 83 | self.eventSource = try container.decode(String.self, forKey: .eventSource) 84 | self.awsRegion = try container.decode(String.self, forKey: .awsRegion) 85 | 86 | let dateString = try container.decode(String.self, forKey: .eventTime) 87 | guard let timestamp = S3.Event.Record.dateFormatter.date(from: dateString) else { 88 | let dateFormat = String(describing: S3.Event.Record.dateFormatter.dateFormat) 89 | throw DecodingError.dataCorruptedError(forKey: .eventTime, in: container, debugDescription: 90 | "Expected date to be in format `\(dateFormat)`, but `\(dateFormat) does not forfill format`") 91 | } 92 | self.eventTime = timestamp 93 | 94 | self.eventName = try container.decode(String.self, forKey: .eventName) 95 | self.userIdentity = try container.decode(S3.UserIdentity.self, forKey: .userIdentity) 96 | self.requestParameters = try container.decode(S3.RequestParameters.self, forKey: .requestParameters) 97 | self.responseElements = try container.decodeIfPresent([String:String].self, forKey: .responseElements) ?? [:] 98 | self.s3 = try container.decode(S3.Entity.self, forKey: .s3) 99 | } 100 | 101 | private static let dateFormatter: DateFormatter = S3.Event.Record.createDateFormatter() 102 | private static func createDateFormatter() -> DateFormatter { 103 | let formatter = DateFormatter() 104 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 105 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 106 | formatter.locale = Locale(identifier: "en_US_POSIX") 107 | return formatter 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/SNS.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import Base64Kit 4 | 5 | /// https://github.com/aws/aws-lambda-go/blob/master/events/sns.go 6 | public struct SNS { 7 | 8 | public struct Event: Decodable { 9 | 10 | public struct Record: Decodable { 11 | public let eventVersion: String 12 | public let eventSubscriptionArn: String 13 | public let eventSource: String 14 | public let sns: Message 15 | 16 | public enum CodingKeys: String, CodingKey { 17 | case eventVersion = "EventVersion" 18 | case eventSubscriptionArn = "EventSubscriptionArn" 19 | case eventSource = "EventSource" 20 | case sns = "Sns" 21 | } 22 | } 23 | 24 | public let records: [Record] 25 | 26 | public enum CodingKeys: String, CodingKey { 27 | case records = "Records" 28 | } 29 | } 30 | 31 | public struct Message { 32 | 33 | public enum Attribute { 34 | case string(String) 35 | case binary(ByteBuffer) 36 | } 37 | 38 | public let signature : String 39 | public let messageId : String 40 | public let type : String 41 | public let topicArn : String 42 | public let messageAttributes: [String: Attribute] 43 | public let signatureVersion : String 44 | public let timestamp : Date 45 | public let signingCertURL : String 46 | public let message : String 47 | public let unsubscribeUrl : String 48 | public let subject : String? 49 | 50 | } 51 | } 52 | 53 | extension SNS.Message: Decodable { 54 | 55 | enum CodingKeys: String, CodingKey { 56 | case signature = "Signature" 57 | case messageId = "MessageId" 58 | case type = "Type" 59 | case topicArn = "TopicArn" 60 | case messageAttributes = "MessageAttributes" 61 | case signatureVersion = "SignatureVersion" 62 | case timestamp = "Timestamp" 63 | case signingCertURL = "SigningCertUrl" 64 | case message = "Message" 65 | case unsubscribeUrl = "UnsubscribeUrl" 66 | case subject = "Subject" 67 | } 68 | 69 | public init(from decoder: Decoder) throws { 70 | let container = try decoder.container(keyedBy: CodingKeys.self) 71 | 72 | self.signature = try container.decode(String.self, forKey: .signature) 73 | self.messageId = try container.decode(String.self, forKey: .messageId) 74 | self.type = try container.decode(String.self, forKey: .type) 75 | self.topicArn = try container.decode(String.self, forKey: .topicArn) 76 | self.messageAttributes = try container.decode([String: Attribute].self, forKey: .messageAttributes) 77 | self.signatureVersion = try container.decode(String.self, forKey: .signatureVersion) 78 | 79 | let dateString = try container.decode(String.self, forKey: .timestamp) 80 | guard let timestamp = SNS.Message.dateFormatter.date(from: dateString) else { 81 | let dateFormat = String(describing: SNS.Message.dateFormatter.dateFormat) 82 | throw DecodingError.dataCorruptedError(forKey: .timestamp, in: container, debugDescription: 83 | "Expected date to be in format `\(dateFormat)`, but `\(dateFormat) does not forfill format`") 84 | } 85 | self.timestamp = timestamp 86 | 87 | self.signingCertURL = try container.decode(String.self, forKey: .signingCertURL) 88 | self.message = try container.decode(String.self, forKey: .message) 89 | self.unsubscribeUrl = try container.decode(String.self, forKey: .unsubscribeUrl) 90 | self.subject = try container.decodeIfPresent(String.self, forKey: .subject) 91 | } 92 | 93 | private static let dateFormatter: DateFormatter = SNS.Message.createDateFormatter() 94 | private static func createDateFormatter() -> DateFormatter { 95 | let formatter = DateFormatter() 96 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 97 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 98 | formatter.locale = Locale(identifier: "en_US_POSIX") 99 | return formatter 100 | } 101 | 102 | } 103 | 104 | extension SNS.Message: DecodableBody { 105 | 106 | public var body: String? { 107 | return self.message != "" ? self.message : nil 108 | } 109 | 110 | @available(*, deprecated, renamed: "decodeBody(_:decoder:)") 111 | public func payload(decoder: JSONDecoder = JSONDecoder()) throws -> Payload { 112 | return try self.decodeBody(Payload.self, decoder: decoder) 113 | } 114 | } 115 | 116 | extension SNS.Message.Attribute: Equatable {} 117 | 118 | extension SNS.Message.Attribute: Codable { 119 | 120 | enum CodingKeys: String, CodingKey { 121 | case dataType = "Type" 122 | case dataValue = "Value" 123 | } 124 | 125 | public init(from decoder: Decoder) throws { 126 | let container = try decoder.container(keyedBy: CodingKeys.self) 127 | 128 | let dataType = try container.decode(String.self, forKey: .dataType) 129 | // https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html#SNSMessageAttributes.DataTypes 130 | switch dataType { 131 | case "String": 132 | let value = try container.decode(String.self, forKey: .dataValue) 133 | self = .string(value) 134 | case "Binary": 135 | let base64encoded = try container.decode(String.self, forKey: .dataValue) 136 | let bytes = try base64encoded.base64decoded() 137 | 138 | var buffer = ByteBufferAllocator().buffer(capacity: bytes.count) 139 | buffer.writeBytes(bytes) 140 | buffer.moveReaderIndex(to: bytes.count) 141 | 142 | self = .binary(buffer) 143 | default: 144 | throw DecodingError.dataCorruptedError(forKey: .dataType, in: container, debugDescription: """ 145 | Unexpected value \"\(dataType)\" for key \(CodingKeys.dataType). 146 | Expected `String` or `Binary`. 147 | """) 148 | } 149 | } 150 | 151 | public func encode(to encoder: Encoder) throws { 152 | var container = encoder.container(keyedBy: CodingKeys.self) 153 | 154 | switch self { 155 | case .binary(let byteBuffer): 156 | let base64 = byteBuffer.withUnsafeReadableBytes { (pointer) -> String in 157 | return String(base64Encoding: pointer) 158 | } 159 | 160 | try container.encode("Binary", forKey: .dataType) 161 | try container.encode(base64, forKey: .dataValue) 162 | case .string(let string): 163 | try container.encode("String", forKey: .dataType) 164 | try container.encode(string, forKey: .dataValue) 165 | } 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/SQS.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | 4 | /// https://github.com/aws/aws-lambda-go/blob/master/events/sqs.go 5 | public struct SQS { 6 | 7 | public struct Event: Decodable { 8 | public let records: [Message] 9 | 10 | enum CodingKeys: String, CodingKey { 11 | case records = "Records" 12 | } 13 | } 14 | 15 | public struct Message: DecodableBody { 16 | 17 | /// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html 18 | public enum Attribute { 19 | case string(String) 20 | case binary(ByteBuffer) 21 | case number(AWSNumber) 22 | } 23 | 24 | public let messageId : String 25 | public let receiptHandle : String 26 | public let body : String? 27 | public let md5OfBody : String 28 | public let md5OfMessageAttributes : String? 29 | public let attributes : [String: String] 30 | public let messageAttributes : [String: Attribute] 31 | public let eventSourceArn : String 32 | public let eventSource : String 33 | public let awsRegion : String 34 | } 35 | } 36 | 37 | extension SQS.Message: Decodable { 38 | 39 | enum CodingKeys: String, CodingKey { 40 | case messageId 41 | case receiptHandle 42 | case body 43 | case md5OfBody 44 | case md5OfMessageAttributes 45 | case attributes 46 | case messageAttributes 47 | case eventSourceArn = "eventSourceARN" 48 | case eventSource 49 | case awsRegion 50 | } 51 | 52 | public init(from decoder: Decoder) throws { 53 | 54 | let container = try decoder.container(keyedBy: CodingKeys.self) 55 | self.messageId = try container.decode(String.self, forKey: .messageId) 56 | self.receiptHandle = try container.decode(String.self, forKey: .receiptHandle) 57 | self.md5OfBody = try container.decode(String.self, forKey: .md5OfBody) 58 | self.md5OfMessageAttributes = try container.decodeIfPresent(String.self, forKey: .md5OfMessageAttributes) 59 | self.attributes = try container.decode([String: String].self, forKey: .attributes) 60 | self.messageAttributes = try container.decode([String: Attribute].self, forKey: .messageAttributes) 61 | self.eventSourceArn = try container.decode(String.self, forKey: .eventSourceArn) 62 | self.eventSource = try container.decode(String.self, forKey: .eventSource) 63 | self.awsRegion = try container.decode(String.self, forKey: .awsRegion) 64 | 65 | let body = try container.decode(String?.self, forKey: .body) 66 | self.body = body != "" ? body : nil 67 | } 68 | 69 | } 70 | 71 | extension SQS.Message.Attribute: Equatable { } 72 | 73 | extension SQS.Message.Attribute: Codable { 74 | 75 | enum CodingKeys: String, CodingKey { 76 | case dataType 77 | case stringValue 78 | case binaryValue 79 | 80 | // BinaryListValue and StringListValue are unimplemented since 81 | // they are not implemented as discussed here: 82 | // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html 83 | } 84 | 85 | public init(from decoder: Decoder) throws { 86 | let container = try decoder.container(keyedBy: CodingKeys.self) 87 | 88 | let dataType = try container.decode(String.self, forKey: .dataType) 89 | switch dataType { 90 | case "String": 91 | let value = try container.decode(String.self, forKey: .stringValue) 92 | self = .string(value) 93 | case "Number": 94 | let value = try container.decode(AWSNumber.self, forKey: .stringValue) 95 | self = .number(value) 96 | case "Binary": 97 | let base64encoded = try container.decode(String.self, forKey: .binaryValue) 98 | let bytes = try base64encoded.base64decoded() 99 | 100 | var buffer = ByteBufferAllocator().buffer(capacity: bytes.count) 101 | buffer.writeBytes(bytes) 102 | buffer.moveReaderIndex(to: bytes.count) 103 | 104 | self = .binary(buffer) 105 | default: 106 | throw DecodingError.dataCorruptedError(forKey: .dataType, in: container, debugDescription: """ 107 | Unexpected value \"\(dataType)\" for key \(CodingKeys.dataType). 108 | Expected `String`, `Binary` or `Number`. 109 | """) 110 | } 111 | } 112 | 113 | public func encode(to encoder: Encoder) throws { 114 | var container = encoder.container(keyedBy: CodingKeys.self) 115 | 116 | switch self { 117 | case .binary(let byteBuffer): 118 | let base64 = byteBuffer.withUnsafeReadableBytes { (pointer) -> String in 119 | return String(base64Encoding: pointer) 120 | } 121 | 122 | try container.encode("Binary", forKey: .dataType) 123 | try container.encode(base64, forKey: .stringValue) 124 | case .string(let string): 125 | try container.encode("String", forKey: .dataType) 126 | try container.encode(string, forKey: .binaryValue) 127 | case .number(let number): 128 | try container.encode("Number", forKey: .dataType) 129 | try container.encode(number, forKey: .binaryValue) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/Utils/DecodableBody.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol DecodableBody { 4 | 5 | var body: String? { get } 6 | var isBase64Encoded: Bool { get } 7 | 8 | } 9 | 10 | public extension DecodableBody { 11 | 12 | var isBase64Encoded: Bool { 13 | return false 14 | } 15 | 16 | } 17 | 18 | public extension DecodableBody { 19 | 20 | func decodeBody(_ type: T.Type, decoder: JSONDecoder = JSONDecoder()) throws -> T { 21 | 22 | // I would really like to not use Foundation.Data at all, but well 23 | // the NIOFoundationCompat just creates an internal Data as well. 24 | // So let's save one malloc and copy and just use Data. 25 | let payload = self.body ?? "" 26 | 27 | let data: Data 28 | if self.isBase64Encoded { 29 | let bytes = try payload.base64decoded() 30 | data = Data(bytes) 31 | } 32 | else { 33 | // TBD: Can this ever fail? I wouldn't think so... 34 | data = payload.data(using: .utf8)! 35 | } 36 | 37 | return try decoder.decode(T.self, from: data) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/LambdaEvents/Utils/HTTPHeaders+Codable.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | extension HTTPHeaders { 4 | 5 | init(awsHeaders: [String: [String]]) { 6 | var nioHeaders: [(String, String)] = [] 7 | awsHeaders.forEach { (key, values) in 8 | values.forEach { (value) in 9 | nioHeaders.append((key, value)) 10 | } 11 | } 12 | 13 | self = HTTPHeaders(nioHeaders) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/LambdaRuntime/Context.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NIOHTTP1 4 | import Logging 5 | 6 | /// TBD: What shall the context be? A struct? A class? 7 | public class Context { 8 | 9 | public let environment : Environment 10 | public let invocation : Invocation 11 | 12 | public let traceId : String 13 | public let requestId : String 14 | 15 | public let logger : Logger 16 | public let eventLoop : EventLoop 17 | public let deadlineDate: Date 18 | 19 | public init(environment: Environment, invocation: Invocation, eventLoop: EventLoop) { 20 | 21 | var logger = Logger(label: "AWSLambda.request-logger") 22 | logger[metadataKey: "RequestId"] = .string(invocation.requestId) 23 | logger[metadataKey: "TraceId" ] = .string(invocation.traceId) 24 | 25 | self.environment = environment 26 | self.invocation = invocation 27 | self.eventLoop = eventLoop 28 | self.logger = logger 29 | self.requestId = invocation.requestId 30 | self.traceId = invocation.traceId 31 | self.deadlineDate = invocation.deadlineDate 32 | } 33 | 34 | public func getRemainingTime() -> TimeInterval { 35 | return deadlineDate.timeIntervalSinceNow 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/LambdaRuntime/Environment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Environment { 4 | 5 | public let lambdaRuntimeAPI: String 6 | public let handlerName : String 7 | 8 | public let functionName : String 9 | public let functionVersion : String 10 | public let logGroupName : String 11 | public let logStreamName : String 12 | public let memoryLimitInMB : String 13 | public let accessKeyId : String 14 | public let secretAccessKey : String 15 | public let sessionToken : String? 16 | public let region : String 17 | 18 | public init() throws { 19 | try self.init(ProcessInfo.processInfo.environment) 20 | } 21 | 22 | init(_ env: [String: String]) throws { 23 | 24 | guard let awsLambdaRuntimeAPI = env["AWS_LAMBDA_RUNTIME_API"] else { 25 | throw RuntimeError.missingEnvironmentVariable("AWS_LAMBDA_RUNTIME_API") 26 | } 27 | 28 | guard let handlerName = env["_HANDLER"] else { 29 | throw RuntimeError.missingEnvironmentVariable("_HANDLER") 30 | } 31 | 32 | self.lambdaRuntimeAPI = awsLambdaRuntimeAPI 33 | self.handlerName = handlerName 34 | 35 | self.functionName = env["AWS_LAMBDA_FUNCTION_NAME"] ?? "" 36 | self.functionVersion = env["AWS_LAMBDA_FUNCTION_VERSION"] ?? "" 37 | self.logGroupName = env["AWS_LAMBDA_LOG_GROUP_NAME"] ?? "" 38 | self.logStreamName = env["AWS_LAMBDA_LOG_STREAM_NAME"] ?? "" 39 | self.memoryLimitInMB = env["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"] ?? "" 40 | 41 | self.accessKeyId = env["AWS_ACCESS_KEY_ID"] ?? "" 42 | self.secretAccessKey = env["AWS_SECRET_ACCESS_KEY"] ?? "" 43 | self.sessionToken = env["AWS_SESSION_TOKEN"] 44 | 45 | self.region = env["AWS_REGION"] ?? "us-east-1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/LambdaRuntime/Runtime+ALB.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | @_exported import LambdaEvents 4 | 5 | extension ALB { 6 | 7 | public static func handler( 8 | multiValueHeadersEnabled: Bool = false, 9 | _ handler: @escaping (ALB.TargetGroupRequest, Context) -> EventLoopFuture) 10 | -> ((NIO.ByteBuffer, Context) -> EventLoopFuture) 11 | { 12 | // reuse as much as possible 13 | let encoder = JSONEncoder() 14 | let decoder = JSONDecoder() 15 | encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = multiValueHeadersEnabled 16 | 17 | return { (inputBytes: NIO.ByteBuffer, ctx: Context) -> EventLoopFuture in 18 | 19 | let req: ALB.TargetGroupRequest 20 | do { 21 | req = try decoder.decode(ALB.TargetGroupRequest.self, from: inputBytes) 22 | } 23 | catch { 24 | return ctx.eventLoop.makeFailedFuture(error) 25 | } 26 | 27 | return handler(req, ctx) 28 | .flatMapErrorThrowing() { (error) -> ALB.TargetGroupResponse in 29 | ctx.logger.error("Unhandled error. Responding with HTTP 500: \(error).") 30 | return ALB.TargetGroupResponse(statusCode: .internalServerError) 31 | } 32 | .flatMapThrowing { (result: ALB.TargetGroupResponse) -> NIO.ByteBuffer in 33 | return try encoder.encodeAsByteBuffer(result, allocator: ByteBufferAllocator()) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/LambdaRuntime/Runtime+APIGateway.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | @_exported import LambdaEvents 4 | 5 | extension APIGateway { 6 | 7 | public static func handler( 8 | _ handler: @escaping (APIGateway.Request, Context) -> EventLoopFuture) 9 | -> ((NIO.ByteBuffer, Context) -> EventLoopFuture) 10 | { 11 | // reuse as much as possible 12 | let encoder = JSONEncoder() 13 | let decoder = JSONDecoder() 14 | 15 | return { (inputBytes: NIO.ByteBuffer, ctx: Context) -> EventLoopFuture in 16 | 17 | let req: APIGateway.Request 18 | do { 19 | req = try decoder.decode(APIGateway.Request.self, from: inputBytes) 20 | } 21 | catch { 22 | return ctx.eventLoop.makeFailedFuture(error) 23 | } 24 | 25 | return handler(req, ctx) 26 | .flatMapErrorThrowing() { (error) -> APIGateway.Response in 27 | ctx.logger.error("Unhandled error. Responding with HTTP 500: \(error).") 28 | return APIGateway.Response(statusCode: .internalServerError) 29 | } 30 | .flatMapThrowing { (result: Response) -> NIO.ByteBuffer in 31 | return try encoder.encodeAsByteBuffer(result, allocator: ByteBufferAllocator()) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/LambdaRuntime/Runtime+Codable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NIOFoundationCompat 4 | 5 | extension Runtime { 6 | 7 | /// wrapper to use for the register function that wraps the encoding and decoding 8 | public static func codable( 9 | decoder: JSONDecoder = JSONDecoder(), 10 | _ handler: @escaping (Event, Context) -> EventLoopFuture) 11 | -> ((NIO.ByteBuffer, Context) -> EventLoopFuture) 12 | { 13 | return { (inputBytes: NIO.ByteBuffer, ctx: Context) -> EventLoopFuture in 14 | let input: Event 15 | do { 16 | input = try decoder.decode(Event.self, from: inputBytes) 17 | } 18 | catch { 19 | let payload = inputBytes.getString(at: 0, length: inputBytes.readableBytes) 20 | ctx.logger.error("Could not decode to type `\(String(describing: Event.self))`: \(error), payload: \(String(describing: payload))") 21 | return ctx.eventLoop.makeFailedFuture(error) 22 | } 23 | 24 | return handler(input, ctx) 25 | .flatMapThrowing { (encodable) -> NIO.ByteBuffer in 26 | return try JSONEncoder().encodeAsByteBuffer(encodable, allocator: ByteBufferAllocator()) 27 | } 28 | } 29 | } 30 | 31 | public static func codable( 32 | decoder: JSONDecoder = JSONDecoder(), 33 | _ handler: @escaping (Event, Context) -> EventLoopFuture) 34 | -> ((NIO.ByteBuffer, Context) -> EventLoopFuture) 35 | { 36 | return { (inputBytes: NIO.ByteBuffer, ctx: Context) -> EventLoopFuture in 37 | let input: Event 38 | do { 39 | input = try decoder.decode(Event.self, from: inputBytes) 40 | } 41 | catch { 42 | let payload = inputBytes.getString(at: 0, length: inputBytes.readableBytes) 43 | ctx.logger.error("Could not decode to type `\(String(describing: Event.self))`: \(error), payload: \(String(describing: payload))") 44 | return ctx.eventLoop.makeFailedFuture(error) 45 | } 46 | 47 | return handler(input, ctx).map { return nil } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/LambdaRuntime/Runtime.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncHTTPClient 3 | import NIO 4 | import NIOHTTP1 5 | import NIOFoundationCompat 6 | 7 | public struct InvocationError: Codable { 8 | let errorMessage: String 9 | } 10 | 11 | final public class Runtime { 12 | 13 | public typealias Handler = (NIO.ByteBuffer, Context) -> EventLoopFuture 14 | 15 | public let eventLoopGroup: EventLoopGroup 16 | public let runtimeLoop : EventLoop 17 | 18 | public let environment : Environment 19 | public let handler : Handler 20 | 21 | // MARK: - Private Properties - 22 | 23 | private let client: LambdaRuntimeAPI 24 | 25 | private var shutdownPromise: EventLoopPromise? 26 | private var isShutdown: Bool = false 27 | 28 | // MARK: - Public Methods - 29 | 30 | /// the runtime shall be initialised with an EventLoopGroup, that is used throughout the lambda 31 | public static func createRuntime(eventLoopGroup: EventLoopGroup, environment: Environment? = nil, handler: @escaping Handler) 32 | throws -> Runtime 33 | { 34 | let env = try environment ?? Environment() 35 | 36 | let client = RuntimeAPIClient( 37 | eventLoopGroup: eventLoopGroup, 38 | lambdaRuntimeAPI: env.lambdaRuntimeAPI) 39 | let runtime = Runtime( 40 | eventLoopGroup: eventLoopGroup, 41 | client: client, 42 | environment: env, 43 | handler: handler) 44 | 45 | return runtime 46 | } 47 | 48 | init(eventLoopGroup: EventLoopGroup, 49 | client: LambdaRuntimeAPI, 50 | environment: Environment, 51 | handler: @escaping Handler) 52 | { 53 | self.eventLoopGroup = eventLoopGroup 54 | self.runtimeLoop = eventLoopGroup.next() 55 | 56 | self.client = client 57 | self.environment = environment 58 | self.handler = handler 59 | 60 | // TODO: post init error 61 | // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-initerror 62 | } 63 | 64 | // MARK: Runtime loop 65 | 66 | public func start() -> EventLoopFuture { 67 | precondition(self.shutdownPromise == nil) 68 | 69 | self.shutdownPromise = self.runtimeLoop.makePromise(of: Void.self) 70 | self.runtimeLoop.execute { 71 | self.runner() 72 | } 73 | 74 | return self.shutdownPromise!.futureResult 75 | } 76 | 77 | public func syncShutdown() throws { 78 | 79 | self.runtimeLoop.execute { 80 | self.isShutdown = true 81 | } 82 | 83 | try self.shutdownPromise?.futureResult.wait() 84 | try self.client.syncShutdown() 85 | } 86 | 87 | // MARK: - Private Methods - 88 | 89 | private func runner() { 90 | precondition(self.runtimeLoop.inEventLoop) 91 | 92 | _ = self.client.getNextInvocation() 93 | .hop(to: self.runtimeLoop) 94 | .flatMap { (invocation, byteBuffer) -> EventLoopFuture in 95 | 96 | // TBD: Does it make sense to also set this env variable? 97 | setenv("_X_AMZN_TRACE_ID", invocation.traceId, 0) 98 | 99 | let context = Context( 100 | environment: self.environment, 101 | invocation: invocation, 102 | eventLoop: self.runtimeLoop) 103 | 104 | return self.handler(byteBuffer, context) 105 | .flatMap { (byteBuffer) -> EventLoopFuture in 106 | return self.client.postInvocationResponse(for: context.requestId, httpBody: byteBuffer) 107 | } 108 | .flatMapError { (error) -> EventLoopFuture in 109 | return self.client.postInvocationError(for: context.requestId, error: error) 110 | } 111 | .flatMapErrorThrowing { (error) in 112 | context.logger.error("Could not post lambda result to runtime. error: \(error)") 113 | } 114 | } 115 | .hop(to: self.runtimeLoop) 116 | .whenComplete() { (_) in 117 | precondition(self.runtimeLoop.inEventLoop) 118 | 119 | if !self.isShutdown { 120 | self.runner() 121 | } 122 | else { 123 | self.shutdownPromise?.succeed(Void()) 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/LambdaRuntime/RuntimeAPIClient.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | import NIOHTTP1 3 | import NIOFoundationCompat 4 | import AsyncHTTPClient 5 | import Foundation 6 | 7 | public struct Invocation { 8 | public let requestId : String 9 | public let deadlineDate : Date 10 | public let invokedFunctionArn: String 11 | public let traceId : String 12 | public let clientContext : String? 13 | public let cognitoIdentity : String? 14 | 15 | init(headers: HTTPHeaders) throws { 16 | 17 | guard let requestId = headers["Lambda-Runtime-Aws-Request-Id"].first else { 18 | throw RuntimeError.invocationMissingHeader("Lambda-Runtime-Aws-Request-Id") 19 | } 20 | 21 | guard let unixTimeMilliseconds = headers["Lambda-Runtime-Deadline-Ms"].first, 22 | let timeInterval = TimeInterval(unixTimeMilliseconds) 23 | else 24 | { 25 | throw RuntimeError.invocationMissingHeader("Lambda-Runtime-Deadline-Ms") 26 | } 27 | 28 | guard let invokedFunctionArn = headers["Lambda-Runtime-Invoked-Function-Arn"].first else { 29 | throw RuntimeError.invocationMissingHeader("Lambda-Runtime-Invoked-Function-Arn") 30 | } 31 | 32 | guard let traceId = headers["Lambda-Runtime-Trace-Id"].first else { 33 | throw RuntimeError.invocationMissingHeader("Lambda-Runtime-Trace-Id") 34 | } 35 | 36 | self.requestId = requestId 37 | self.deadlineDate = Date(timeIntervalSince1970: timeInterval / 1000) 38 | self.invokedFunctionArn = invokedFunctionArn 39 | self.traceId = traceId 40 | self.clientContext = headers["Lambda-Runtime-Client-Context"].first 41 | self.cognitoIdentity = headers["Lambda-Runtime-Cognito-Identity"].first 42 | } 43 | 44 | } 45 | 46 | /// This protocol defines the Lambda Runtime API as defined here. 47 | /// The sole purpose of this protocol is to define stubs to make 48 | /// testing easier. 49 | /// Therefore use is internal only. 50 | /// https://docs.aws.amazon.com/en_pv/lambda/latest/dg/runtimes-api.html 51 | protocol LambdaRuntimeAPI { 52 | 53 | func getNextInvocation() -> EventLoopFuture<(Invocation, NIO.ByteBuffer)> 54 | func postInvocationResponse(for requestId: String, httpBody: NIO.ByteBuffer?) -> EventLoopFuture 55 | func postInvocationError(for requestId: String, error: Error) -> EventLoopFuture 56 | 57 | func syncShutdown() throws 58 | 59 | } 60 | 61 | final class RuntimeAPIClient { 62 | 63 | let httpClient : HTTPClient 64 | 65 | /// the local domain to call, to get the next task/invocation 66 | /// as defined here: https://docs.aws.amazon.com/en_pv/lambda/latest/dg/runtimes-api.html#runtimes-api-next 67 | let lambdaRuntimeAPI: String 68 | 69 | init(eventLoopGroup: EventLoopGroup, lambdaRuntimeAPI: String) { 70 | 71 | self.httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup)) 72 | self.lambdaRuntimeAPI = lambdaRuntimeAPI 73 | 74 | } 75 | } 76 | 77 | extension RuntimeAPIClient: LambdaRuntimeAPI { 78 | 79 | func getNextInvocation() -> EventLoopFuture<(Invocation, NIO.ByteBuffer)> { 80 | return self.httpClient 81 | .get(url: "http://\(lambdaRuntimeAPI)/2018-06-01/runtime/invocation/next") 82 | .flatMapErrorThrowing { (error) -> HTTPClient.Response in 83 | throw RuntimeError.endpointError(error.localizedDescription) 84 | } 85 | .flatMapThrowing { (response) -> (Invocation, NIO.ByteBuffer) in 86 | guard let data = response.body else { 87 | throw RuntimeError.invocationMissingData 88 | } 89 | 90 | return (try Invocation(headers: response.headers), data) 91 | } 92 | } 93 | 94 | func postInvocationResponse(for requestId: String, httpBody: NIO.ByteBuffer?) -> EventLoopFuture { 95 | let url = "http://\(lambdaRuntimeAPI)/2018-06-01/runtime/invocation/\(requestId)/response" 96 | let body = httpBody != nil ? HTTPClient.Body.byteBuffer(httpBody!) : nil 97 | return self.httpClient.post(url: url, body: body) 98 | .map { (_) -> Void in } 99 | } 100 | 101 | func postInvocationError(for requestId: String, error: Error) -> EventLoopFuture { 102 | let errorMessage = String(describing: error) 103 | let invocationError = InvocationError(errorMessage: errorMessage) 104 | let jsonEncoder = JSONEncoder() 105 | let httpBody = try! jsonEncoder.encodeAsByteBuffer(invocationError, allocator: ByteBufferAllocator()) 106 | 107 | let url = "http://\(lambdaRuntimeAPI)/2018-06-01/runtime/invocation/\(requestId)/error" 108 | 109 | return self.httpClient.post(url: url, body: .byteBuffer(httpBody)) 110 | .map { (_) -> Void in } 111 | } 112 | 113 | func syncShutdown() throws { 114 | try self.httpClient.syncShutdown() 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /Sources/LambdaRuntime/RuntimeError.swift: -------------------------------------------------------------------------------- 1 | enum RuntimeError: Error, Equatable { 2 | case unknown 3 | 4 | case missingEnvironmentVariable(String) 5 | case invalidHandlerName 6 | 7 | case invocationMissingHeader(String) 8 | case invocationMissingData 9 | 10 | case unknownLambdaHandler(String) 11 | 12 | case endpointError(String) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/LambdaRuntimeTestUtils/Environment+TestUtils.swift: -------------------------------------------------------------------------------- 1 | @testable import LambdaRuntime 2 | 3 | extension Environment { 4 | 5 | public static func forTesting( 6 | lambdaRuntimeAPI: String? = nil, 7 | handler : String? = nil, 8 | functionName : String? = nil, 9 | functionVersion : String? = nil, 10 | logGroupName : String? = nil, 11 | logStreamName : String? = nil, 12 | memoryLimitInMB : String? = nil, 13 | accessKeyId : String? = nil, 14 | secretAccessKey : String? = nil, 15 | sessionToken : String? = nil) 16 | throws -> Environment 17 | { 18 | var env = [String: String]() 19 | 20 | env["AWS_LAMBDA_RUNTIME_API"] = lambdaRuntimeAPI ?? "localhost" 21 | env["_HANDLER"] = handler ?? "lambda.handler" 22 | 23 | env["AWS_LAMBDA_FUNCTION_NAME"] = functionName ?? "TestFunction" 24 | env["AWS_LAMBDA_FUNCTION_VERSION"] = functionVersion ?? "1" 25 | env["AWS_LAMBDA_LOG_GROUP_NAME"] = logGroupName ?? "TestFunctionLogGroupName" 26 | env["AWS_LAMBDA_LOG_STREAM_NAME"] = logStreamName ?? "TestFunctionLogStreamName" 27 | env["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"] = memoryLimitInMB ?? "512" 28 | env["AWS_ACCESS_KEY_ID"] = accessKeyId ?? "" 29 | env["AWS_SECRET_ACCESS_KEY"] = secretAccessKey ?? "" 30 | env["AWS_SESSION_TOKEN"] = sessionToken ?? "" 31 | 32 | return try Environment(env) 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/LambdaRuntimeTestUtils/Invocation+TestUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOHTTP1 3 | @testable import LambdaRuntime 4 | 5 | extension Invocation { 6 | 7 | public static func forTesting( 8 | requestId : String = UUID().uuidString.lowercased(), 9 | timeout : TimeInterval = 1, 10 | functionArn: String = "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime", 11 | traceId : String = "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1") 12 | throws -> Invocation 13 | { 14 | let deadline = String(Int(Date(timeIntervalSinceNow: timeout).timeIntervalSince1970 * 1000)) 15 | 16 | let headers = HTTPHeaders([ 17 | ("Lambda-Runtime-Aws-Request-Id" , requestId), 18 | ("Lambda-Runtime-Deadline-Ms" , deadline), 19 | ("Lambda-Runtime-Invoked-Function-Arn", functionArn), 20 | ("Lambda-Runtime-Trace-Id" , traceId), 21 | ]) 22 | 23 | return try Invocation(headers: headers) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/AWSNumberTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import LambdaRuntime 4 | 5 | class AWSNumberTests: XCTestCase { 6 | 7 | // MARK: - Int - 8 | 9 | func testInteger() { 10 | let number = AWSNumber(int: 5) 11 | XCTAssertEqual(number.stringValue, "5") 12 | XCTAssertEqual(number.int, 5) 13 | XCTAssertEqual(number.double, 5) 14 | } 15 | 16 | func testIntCoding() { 17 | do { 18 | let number = AWSNumber(int: 3) 19 | struct TestStruct: Codable { 20 | let number: AWSNumber 21 | } 22 | 23 | // Test: Encoding 24 | 25 | let test = TestStruct(number: number) 26 | let data = try JSONEncoder().encode(test) 27 | let json = String(data: data, encoding: .utf8) 28 | XCTAssertEqual(json, "{\"number\":\"3\"}") 29 | 30 | // Test: Decoding 31 | 32 | let decoded = try JSONDecoder().decode(TestStruct.self, from: data) 33 | XCTAssertEqual(decoded.number.int, 3) 34 | } 35 | catch { 36 | XCTFail("unexpected error: \(error)") 37 | } 38 | } 39 | 40 | // MARK: - Double - 41 | 42 | func testDouble() { 43 | let number = AWSNumber(double: 3.14) 44 | XCTAssertEqual(number.stringValue, "3.14") 45 | XCTAssertEqual(number.int, nil) 46 | XCTAssertEqual(number.double, 3.14) 47 | } 48 | 49 | func testDoubleCoding() { 50 | do { 51 | let number = AWSNumber(double: 6.25) 52 | struct TestStruct: Codable { 53 | let number: AWSNumber 54 | } 55 | 56 | // Test: Encoding 57 | 58 | let test = TestStruct(number: number) 59 | let data = try JSONEncoder().encode(test) 60 | let json = String(data: data, encoding: .utf8) 61 | XCTAssertEqual(json, "{\"number\":\"6.25\"}") 62 | 63 | // Test: Decoding 64 | 65 | let decoded = try JSONDecoder().decode(TestStruct.self, from: data) 66 | XCTAssertEqual(decoded.number.int, nil) 67 | XCTAssertEqual(decoded.number.double, 6.25) 68 | } 69 | catch { 70 | XCTFail("unexpected error: \(error)") 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/ContextTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import NIO 4 | @testable import LambdaRuntime 5 | import LambdaRuntimeTestUtils 6 | 7 | class ContextTests: XCTestCase { 8 | 9 | public func testDeadline() { 10 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 11 | defer { 12 | XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) 13 | } 14 | 15 | do { 16 | let timeout: TimeInterval = 3 17 | let context = try Context( 18 | environment: .forTesting(), 19 | invocation: .forTesting(timeout: timeout), 20 | eventLoop: eventLoopGroup.next()) 21 | 22 | let remaining = context.getRemainingTime() 23 | 24 | XCTAssert(timeout > remaining && remaining > timeout * 0.99, "Expected the remaining time to be within 99%") 25 | } 26 | catch { 27 | XCTFail("unexpected error: \(error)") 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Events/ALBTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import LambdaRuntime 4 | 5 | class ALBTests: XCTestCase { 6 | 7 | static let exampleSingleValueHeadersPayload = """ 8 | { 9 | "requestContext":{ 10 | "elb":{ 11 | "targetGroupArn": "arn:aws:elasticloadbalancing:eu-central-1:079477498937:targetgroup/EinSternDerDeinenNamenTraegt/621febf5a44b2ce5" 12 | } 13 | }, 14 | "httpMethod": "GET", 15 | "path": "/", 16 | "queryStringParameters": {}, 17 | "headers":{ 18 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 19 | "accept-encoding": "gzip, deflate", 20 | "accept-language": "en-us", 21 | "connection": "keep-alive", 22 | "host": "event-testl-1wa3wrvmroilb-358275751.eu-central-1.elb.amazonaws.com", 23 | "upgrade-insecure-requests": "1", 24 | "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", 25 | "x-amzn-trace-id": "Root=1-5e189143-ad18a2b0a7728cd0dac45e10", 26 | "x-forwarded-for": "90.187.8.137", 27 | "x-forwarded-port": "80", 28 | "x-forwarded-proto": "http" 29 | }, 30 | "body":"", 31 | "isBase64Encoded":false 32 | } 33 | """ 34 | 35 | func testRequestWithSingleValueHeadersPayload() { 36 | let data = ALBTests.exampleSingleValueHeadersPayload.data(using: .utf8)! 37 | do { 38 | let decoder = JSONDecoder() 39 | 40 | let event = try decoder.decode(ALB.TargetGroupRequest.self, from: data) 41 | 42 | XCTAssertEqual(event.httpMethod, .GET) 43 | XCTAssertEqual(event.body, nil) 44 | XCTAssertEqual(event.isBase64Encoded, false) 45 | XCTAssertEqual(event.headers.count, 11) 46 | XCTAssertEqual(event.path, "/") 47 | XCTAssertEqual(event.queryStringParameters, [:]) 48 | } 49 | catch { 50 | XCTFail("Unexpected error: \(error)") 51 | } 52 | } 53 | 54 | // MARK: - Response - 55 | 56 | private struct TestStruct: Codable { 57 | let hello: String 58 | } 59 | 60 | private struct SingleValueHeadersResponse: Codable, Equatable { 61 | let statusCode: Int 62 | let body: String 63 | let isBase64Encoded: Bool 64 | let headers: [String: String] 65 | } 66 | 67 | private struct MultiValueHeadersResponse: Codable, Equatable { 68 | let statusCode: Int 69 | let body: String 70 | let isBase64Encoded: Bool 71 | let multiValueHeaders: [String: [String]] 72 | } 73 | 74 | func testJSONResponseWithSingleValueHeaders() throws { 75 | let response = try ALB.TargetGroupResponse(statusCode: .ok, payload: TestStruct(hello: "world")) 76 | let encoder = JSONEncoder() 77 | encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = false 78 | let data = try encoder.encode(response) 79 | 80 | let expected = SingleValueHeadersResponse( 81 | statusCode: 200, body: "{\"hello\":\"world\"}", 82 | isBase64Encoded: false, 83 | headers: ["Content-Type": "application/json"]) 84 | 85 | let result = try JSONDecoder().decode(SingleValueHeadersResponse.self, from: data) 86 | XCTAssertEqual(result, expected) 87 | } 88 | 89 | func testJSONResponseWithMultiValueHeaders() throws { 90 | let response = try ALB.TargetGroupResponse(statusCode: .ok, payload: TestStruct(hello: "world")) 91 | let encoder = JSONEncoder() 92 | encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = true 93 | let data = try encoder.encode(response) 94 | 95 | let expected = MultiValueHeadersResponse( 96 | statusCode: 200, body: "{\"hello\":\"world\"}", 97 | isBase64Encoded: false, 98 | multiValueHeaders: ["Content-Type": ["application/json"]]) 99 | 100 | let result = try JSONDecoder().decode(MultiValueHeadersResponse.self, from: data) 101 | XCTAssertEqual(result, expected) 102 | } 103 | 104 | func testEmptyResponseWithMultiValueHeaders() throws { 105 | let response = ALB.TargetGroupResponse(statusCode: .ok) 106 | let encoder = JSONEncoder() 107 | encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = true 108 | let data = try encoder.encode(response) 109 | 110 | let expected = MultiValueHeadersResponse( 111 | statusCode: 200, body: "", 112 | isBase64Encoded: false, 113 | multiValueHeaders: [:]) 114 | 115 | let result = try JSONDecoder().decode(MultiValueHeadersResponse.self, from: data) 116 | XCTAssertEqual(result, expected) 117 | } 118 | 119 | func testEmptyResponseWithSingleValueHeaders() throws { 120 | let response = ALB.TargetGroupResponse(statusCode: .ok) 121 | let encoder = JSONEncoder() 122 | encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = false 123 | let data = try encoder.encode(response) 124 | 125 | let expected = SingleValueHeadersResponse( 126 | statusCode: 200, body: "", 127 | isBase64Encoded: false, 128 | headers: [:]) 129 | 130 | let result = try JSONDecoder().decode(SingleValueHeadersResponse.self, from: data) 131 | XCTAssertEqual(result, expected) 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Events/APIGatewayTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import NIO 4 | import NIOHTTP1 5 | import NIOFoundationCompat 6 | @testable import LambdaRuntime 7 | import LambdaRuntimeTestUtils 8 | 9 | class APIGatewayTests: XCTestCase { 10 | 11 | static let exampleGetPayload = """ 12 | {"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} 13 | """ 14 | 15 | static let todoPostPayload = """ 16 | {"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} 17 | """ 18 | 19 | // MARK: - Handler - 20 | 21 | func testHandlerSuccess() { 22 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 23 | defer { 24 | XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) 25 | } 26 | 27 | do { 28 | let timeout: TimeInterval = 3 29 | let context = try Context( 30 | environment: .forTesting(), 31 | invocation: .forTesting(timeout: timeout), 32 | eventLoop: eventLoopGroup.next()) 33 | 34 | let payload = APIGatewayTests.exampleGetPayload 35 | let length = payload.utf8.count 36 | var testPayload = ByteBufferAllocator().buffer(capacity: length) 37 | testPayload.setString(payload, at: 0) 38 | testPayload.moveWriterIndex(forwardBy: length) 39 | 40 | let handler = APIGateway.handler { (request, context) -> EventLoopFuture in 41 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(statusCode: .ok)) 42 | } 43 | 44 | guard let result = try handler(testPayload, context).wait() else { 45 | XCTFail("expected a payload") 46 | return 47 | } 48 | 49 | let response = try JSONDecoder().decode(JSONResponse.self, from: result) 50 | XCTAssertEqual(response.statusCode, 200) 51 | } 52 | catch { 53 | XCTFail("Unexpected error: \(error)") 54 | } 55 | 56 | } 57 | 58 | // MARK: - Request - 59 | 60 | // MARK: Decoding 61 | 62 | func testRequestDecodingExampleGetRequest() { 63 | do { 64 | let data = APIGatewayTests.exampleGetPayload.data(using: .utf8)! 65 | let request = try JSONDecoder().decode(APIGateway.Request.self, from: data) 66 | 67 | XCTAssertEqual(request.path, "/test") 68 | XCTAssertEqual(request.httpMethod, .GET) 69 | } 70 | catch { 71 | XCTFail("Unexpected error: \(error)") 72 | } 73 | } 74 | 75 | func testRequestDecodingTodoPostRequest() { 76 | 77 | struct Todo: Decodable { 78 | let title: String 79 | } 80 | 81 | do { 82 | let data = APIGatewayTests.todoPostPayload.data(using: .utf8)! 83 | let request = try JSONDecoder().decode(APIGateway.Request.self, from: data) 84 | 85 | XCTAssertEqual(request.path, "/todos") 86 | XCTAssertEqual(request.httpMethod, .POST) 87 | 88 | let todo = try request.decodeBody(Todo.self) 89 | XCTAssertEqual(todo.title, "a todo") 90 | } 91 | catch { 92 | XCTFail("Unexpected error: \(error)") 93 | } 94 | } 95 | 96 | 97 | 98 | // MARK: - Response - 99 | 100 | // MARK: Encoding 101 | 102 | struct JSONResponse: Codable { 103 | let statusCode: UInt 104 | let headers: [String: String]? 105 | let body: String? 106 | let isBase64Encoded: Bool? 107 | } 108 | 109 | func testResponseEncoding() { 110 | 111 | let resp = APIGateway.Response( 112 | statusCode: .ok, 113 | headers: HTTPHeaders([("Server", "Test")]), 114 | body: "abc123") 115 | 116 | do { 117 | let data = try JSONEncoder().encodeAsByteBuffer(resp, allocator: ByteBufferAllocator()) 118 | let json = try JSONDecoder().decode(JSONResponse.self, from: data) 119 | 120 | XCTAssertEqual(json.statusCode, resp.statusCode.code) 121 | XCTAssertEqual(json.body, resp.body) 122 | XCTAssertEqual(json.isBase64Encoded, resp.isBase64Encoded) 123 | } 124 | catch { 125 | XCTFail("unexpected error: \(error)") 126 | } 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Events/CloudwatchTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import LambdaRuntime 4 | 5 | class CloudwatchTests: XCTestCase { 6 | 7 | static let scheduledEventPayload = """ 8 | { 9 | "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 10 | "detail-type": "Scheduled Event", 11 | "source": "aws.events", 12 | "account": "123456789012", 13 | "time": "1970-01-01T00:00:00Z", 14 | "region": "us-east-1", 15 | "resources": [ 16 | "arn:aws:events:us-east-1:123456789012:rule/ExampleRule" 17 | ], 18 | "detail": {} 19 | } 20 | """ 21 | 22 | func testScheduledEventFromJSON() { 23 | let data = CloudwatchTests.scheduledEventPayload.data(using: .utf8)! 24 | do { 25 | let decoder = JSONDecoder() 26 | let event = try decoder.decode(Cloudwatch.Event.self, from: data) 27 | 28 | XCTAssertEqual(event.id , "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") 29 | XCTAssertEqual(event.detailType, "Scheduled Event") 30 | XCTAssertEqual(event.source , "aws.events") 31 | XCTAssertEqual(event.accountId , "123456789012") 32 | XCTAssertEqual(event.time , Date(timeIntervalSince1970: 0)) 33 | XCTAssertEqual(event.region , "us-east-1") 34 | XCTAssertEqual(event.resources , ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) 35 | } 36 | catch { 37 | XCTFail("Unexpected error: \(error)") 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Events/DecodableBodyTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import Base64Kit 4 | @testable import LambdaRuntime 5 | 6 | class DecodableBodyTests: XCTestCase { 7 | 8 | struct TestEvent: DecodableBody { 9 | let body: String? 10 | let isBase64Encoded: Bool 11 | } 12 | 13 | struct TestPayload: Codable { 14 | let hello: String 15 | } 16 | 17 | func testSimplePayloadFromEvent() { 18 | do { 19 | let event = TestEvent(body: "{\"hello\":\"world\"}", isBase64Encoded: false) 20 | let payload = try event.decodeBody(TestPayload.self) 21 | 22 | XCTAssertEqual(payload.hello, "world") 23 | } 24 | catch { 25 | XCTFail("Unexpected error: \(error)") 26 | } 27 | } 28 | 29 | func testBase64PayloadFromEvent() { 30 | do { 31 | let event = TestEvent(body: "eyJoZWxsbyI6IndvcmxkIn0=", isBase64Encoded: true) 32 | let payload = try event.decodeBody(TestPayload.self) 33 | 34 | XCTAssertEqual(payload.hello, "world") 35 | } 36 | catch { 37 | XCTFail("Unexpected error: \(error)") 38 | } 39 | } 40 | 41 | func testNoDataFromEvent() { 42 | do { 43 | let event = TestEvent(body: "", isBase64Encoded: false) 44 | _ = try event.decodeBody(TestPayload.self) 45 | 46 | XCTFail("Did not expect to reach this point") 47 | } 48 | catch DecodingError.dataCorrupted(_) { 49 | return // expected error 50 | } 51 | catch { 52 | XCTFail("Unexpected error: \(error)") 53 | } 54 | 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Events/DynamoDB+AttributeValueTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import NIO 4 | @testable import LambdaRuntime 5 | 6 | class DynamoDBAttributeValueTests: XCTestCase { 7 | 8 | func testBoolDecoding() throws { 9 | 10 | let json = "{\"BOOL\": true}" 11 | let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 12 | 13 | XCTAssertEqual(value, .boolean(true)) 14 | } 15 | 16 | func testBinaryDecoding() throws { 17 | 18 | let json = "{\"B\": \"YmFzZTY0\"}" 19 | let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 20 | 21 | var buffer = ByteBufferAllocator().buffer(capacity: 6) 22 | buffer.setString("base64", at: 0) 23 | XCTAssertEqual(value, .binary(buffer)) 24 | } 25 | 26 | func testBinarySetDecoding() throws { 27 | 28 | let json = "{\"BS\": [\"YmFzZTY0\", \"YWJjMTIz\"]}" 29 | let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 30 | 31 | var buffer1 = ByteBufferAllocator().buffer(capacity: 6) 32 | buffer1.setString("base64", at: 0) 33 | 34 | var buffer2 = ByteBufferAllocator().buffer(capacity: 6) 35 | buffer2.setString("abc123", at: 0) 36 | 37 | XCTAssertEqual(value, .binarySet([buffer1, buffer2])) 38 | } 39 | 40 | func testStringDecoding() throws { 41 | 42 | let json = "{\"S\": \"huhu\"}" 43 | let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 44 | 45 | XCTAssertEqual(value, .string("huhu")) 46 | } 47 | 48 | func testStringSetDecoding() throws { 49 | 50 | let json = "{\"SS\": [\"huhu\", \"haha\"]}" 51 | let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 52 | 53 | XCTAssertEqual(value, .stringSet(["huhu", "haha"])) 54 | } 55 | 56 | func testNullDecoding() throws { 57 | let json = "{\"NULL\": true}" 58 | let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 59 | 60 | XCTAssertEqual(value, .null) 61 | } 62 | 63 | func testNumberDecoding() throws { 64 | let json = "{\"N\": \"1.2345\"}" 65 | let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 66 | 67 | XCTAssertEqual(value, .number(AWSNumber(double: 1.2345))) 68 | } 69 | 70 | func testNumberSetDecoding() throws { 71 | let json = "{\"NS\": [\"1.2345\", \"-19\"]}" 72 | let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 73 | 74 | XCTAssertEqual(value, .numberSet([AWSNumber(double: 1.2345), AWSNumber(int: -19)])) 75 | } 76 | 77 | func testListDecoding() throws { 78 | let json = "{\"L\": [{\"NS\": [\"1.2345\", \"-19\"]}, {\"S\": \"huhu\"}]}" 79 | let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 80 | 81 | XCTAssertEqual(value, .list([.numberSet([AWSNumber(double: 1.2345), AWSNumber(int: -19)]), .string("huhu")])) 82 | } 83 | 84 | func testMapDecoding() throws { 85 | let json = "{\"M\": {\"numbers\": {\"NS\": [\"1.2345\", \"-19\"]}, \"string\": {\"S\": \"huhu\"}}}" 86 | let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 87 | 88 | XCTAssertEqual(value, .map([ 89 | "numbers": .numberSet([AWSNumber(double: 1.2345), AWSNumber(int: -19)]), 90 | "string": .string("huhu") 91 | ])) 92 | } 93 | 94 | func testEmptyDecoding() throws { 95 | let json = "{\"haha\": 1}" 96 | do { 97 | _ = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) 98 | XCTFail("Did not expect to reach this point") 99 | } 100 | catch { 101 | switch error { 102 | case DecodingError.dataCorrupted(let context): 103 | // expected error 104 | XCTAssertEqual(context.codingPath.count, 0) 105 | default: 106 | XCTFail("Unexpected error: \(String(describing: error))") 107 | } 108 | } 109 | 110 | } 111 | 112 | func testEquatable() { 113 | XCTAssertEqual(DynamoDB.AttributeValue.boolean(true), .boolean(true)) 114 | XCTAssertNotEqual(DynamoDB.AttributeValue.boolean(true), .boolean(false)) 115 | XCTAssertNotEqual(DynamoDB.AttributeValue.boolean(true), .string("haha")) 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Events/DynamoDBTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import LambdaRuntime 4 | 5 | class DynamoDBTests: XCTestCase { 6 | 7 | static let streamEventPayload = """ 8 | { 9 | "Records": [ 10 | { 11 | "eventID": "1", 12 | "eventVersion": "1.0", 13 | "dynamodb": { 14 | "ApproximateCreationDateTime": 1.578648338E9, 15 | "Keys": { 16 | "Id": { 17 | "N": "101" 18 | } 19 | }, 20 | "NewImage": { 21 | "Message": { 22 | "S": "New item!" 23 | }, 24 | "Id": { 25 | "N": "101" 26 | } 27 | }, 28 | "StreamViewType": "NEW_AND_OLD_IMAGES", 29 | "SequenceNumber": "111", 30 | "SizeBytes": 26 31 | }, 32 | "awsRegion": "eu-central-1", 33 | "eventName": "INSERT", 34 | "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", 35 | "eventSource": "aws:dynamodb" 36 | }, 37 | { 38 | "eventID": "2", 39 | "eventVersion": "1.0", 40 | "dynamodb": { 41 | "ApproximateCreationDateTime": 1.578648338E9, 42 | "OldImage": { 43 | "Message": { 44 | "S": "New item!" 45 | }, 46 | "Id": { 47 | "N": "101" 48 | } 49 | }, 50 | "SequenceNumber": "222", 51 | "Keys": { 52 | "Id": { 53 | "N": "101" 54 | } 55 | }, 56 | "SizeBytes": 59, 57 | "NewImage": { 58 | "Message": { 59 | "S": "This item has changed" 60 | }, 61 | "Id": { 62 | "N": "101" 63 | } 64 | }, 65 | "StreamViewType": "NEW_AND_OLD_IMAGES" 66 | }, 67 | "awsRegion": "eu-central-1", 68 | "eventName": "MODIFY", 69 | "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", 70 | "eventSource": "aws:dynamodb" 71 | }, 72 | { 73 | "eventID": "3", 74 | "eventVersion": "1.0", 75 | "dynamodb": { 76 | "ApproximateCreationDateTime":1.578648338E9, 77 | "Keys": { 78 | "Id": { 79 | "N": "101" 80 | } 81 | }, 82 | "SizeBytes": 38, 83 | "SequenceNumber": "333", 84 | "OldImage": { 85 | "Message": { 86 | "S": "This item has changed" 87 | }, 88 | "Id": { 89 | "N": "101" 90 | } 91 | }, 92 | "StreamViewType": "NEW_AND_OLD_IMAGES" 93 | }, 94 | "awsRegion": "eu-central-1", 95 | "eventName": "REMOVE", 96 | "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", 97 | "eventSource": "aws:dynamodb" 98 | } 99 | ] 100 | } 101 | """ 102 | 103 | func testScheduledEventFromJSON() { 104 | let data = DynamoDBTests.streamEventPayload.data(using: .utf8)! 105 | do { 106 | let event = try JSONDecoder().decode(DynamoDB.Event.self, from: data) 107 | 108 | XCTAssertEqual(event.records.count, 3) 109 | 110 | } 111 | catch { 112 | XCTFail("Unexpected error: \(error)") 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Events/S3Tests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import NIO 4 | @testable import LambdaRuntime 5 | @testable import LambdaEvents 6 | 7 | class S3Tests: XCTestCase { 8 | 9 | static let eventPayload = """ 10 | { 11 | "Records": [ 12 | { 13 | "eventVersion":"2.1", 14 | "eventSource":"aws:s3", 15 | "awsRegion":"eu-central-1", 16 | "eventTime":"2020-01-13T09:25:40.621Z", 17 | "eventName":"ObjectCreated:Put", 18 | "userIdentity":{ 19 | "principalId":"AWS:AAAAAAAJ2MQ4YFQZ7AULJ" 20 | }, 21 | "requestParameters":{ 22 | "sourceIPAddress":"123.123.123.123" 23 | }, 24 | "responseElements":{ 25 | "x-amz-request-id":"01AFA1430E18C358", 26 | "x-amz-id-2":"JsbNw6sHGFwgzguQjbYcew//bfAeZITyTYLfjuu1U4QYqCq5CPlSyYLtvWQS+gw0RxcroItGwm8=" 27 | }, 28 | "s3":{ 29 | "s3SchemaVersion":"1.0", 30 | "configurationId":"98b55bc4-3c0c-4007-b727-c6b77a259dde", 31 | "bucket":{ 32 | "name":"eventsources", 33 | "ownerIdentity":{ 34 | "principalId":"AAAAAAAAAAAAAA" 35 | }, 36 | "arn":"arn:aws:s3:::eventsources" 37 | }, 38 | "object":{ 39 | "key":"Hi.md", 40 | "size":2880, 41 | "eTag":"91a7f2c3ae81bcc6afef83979b463f0e", 42 | "sequencer":"005E1C37948E783A6E" 43 | } 44 | } 45 | } 46 | ] 47 | } 48 | """ 49 | 50 | struct TestStruct: Decodable { 51 | let hello: String 52 | } 53 | 54 | func testSimpleEventFromJSON() { 55 | let data = S3Tests.eventPayload.data(using: .utf8)! 56 | do { 57 | let event = try JSONDecoder().decode(S3.Event.self, from: data) 58 | 59 | guard let record = event.records.first else { 60 | XCTFail("Expected to have one record"); return 61 | } 62 | 63 | XCTAssertEqual(record.eventVersion, "2.1") 64 | XCTAssertEqual(record.eventSource, "aws:s3") 65 | XCTAssertEqual(record.awsRegion, "eu-central-1") 66 | XCTAssertEqual(record.eventName, "ObjectCreated:Put") 67 | XCTAssertEqual(record.eventTime, Date(timeIntervalSince1970: 1578907540.621)) 68 | XCTAssertEqual(record.userIdentity, S3.UserIdentity(principalId: "AWS:AAAAAAAJ2MQ4YFQZ7AULJ")) 69 | XCTAssertEqual(record.requestParameters, S3.RequestParameters(sourceIPAddress: "123.123.123.123")) 70 | XCTAssertEqual(record.responseElements.count, 2) 71 | XCTAssertEqual(record.s3.schemaVersion, "1.0") 72 | XCTAssertEqual(record.s3.configurationId, "98b55bc4-3c0c-4007-b727-c6b77a259dde") 73 | XCTAssertEqual(record.s3.bucket.name, "eventsources") 74 | XCTAssertEqual(record.s3.bucket.ownerIdentity, S3.UserIdentity(principalId: "AAAAAAAAAAAAAA")) 75 | XCTAssertEqual(record.s3.bucket.arn, "arn:aws:s3:::eventsources") 76 | XCTAssertEqual(record.s3.object.key, "Hi.md") 77 | XCTAssertEqual(record.s3.object.size, 2880) 78 | XCTAssertEqual(record.s3.object.eTag, "91a7f2c3ae81bcc6afef83979b463f0e") 79 | XCTAssertEqual(record.s3.object.sequencer, "005E1C37948E783A6E") 80 | } 81 | catch { 82 | XCTFail("Unexpected error: \(error)") 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Events/SNSTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import NIO 4 | @testable import LambdaRuntime 5 | 6 | class SNSTests: XCTestCase { 7 | 8 | static let eventPayload = """ 9 | { 10 | "Records": [ 11 | { 12 | "EventSource": "aws:sns", 13 | "EventVersion": "1.0", 14 | "EventSubscriptionArn": "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c", 15 | "Sns": { 16 | "Type": "Notification", 17 | "MessageId": "bdb6900e-1ae9-5b4b-b7fc-c681fde222e3", 18 | "TopicArn": "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5", 19 | "Subject": null, 20 | "Message": "{\\\"hello\\\": \\\"world\\\"}", 21 | "Timestamp": "2020-01-08T14:18:51.203Z", 22 | "SignatureVersion": "1", 23 | "Signature": "LJMF/xmMH7A1gNy2unLA3hmzyf6Be+zS/Yeiiz9tZbu6OG8fwvWZeNOcEZardhSiIStc0TF7h9I+4Qz3omCntaEfayzTGmWN8itGkn2mfn/hMFmPbGM8gEUz3+jp1n6p+iqP3XTx92R0LBIFrU3ylOxSo8+SCOjA015M93wfZzwj0WPtynji9iAvvtf15d8JxPUu1T05BRitpFd5s6ZXDHtVQ4x/mUoLUN8lOVp+rs281/ZdYNUG/V5CwlyUDTOERdryTkBJ/GO1NNPa+6m04ywJFa5d+BC8mDcUcHhhXXjpTEbt8AHBmswK3nudHrVMRO/G4zmssxU2P7ii5+gCfA==", 24 | "SigningCertUrl": "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem", 25 | "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", 26 | "MessageAttributes": { 27 | "binary":{ 28 | "Type": "Binary", 29 | "Value": "YmFzZTY0" 30 | }, 31 | "string":{ 32 | "Type": "String", 33 | "Value": "abc123" 34 | } 35 | } 36 | } 37 | } 38 | ] 39 | } 40 | """ 41 | 42 | struct TestStruct: Decodable { 43 | let hello: String 44 | } 45 | 46 | func testSimpleEventFromJSON() { 47 | let data = SNSTests.eventPayload.data(using: .utf8)! 48 | do { 49 | let event = try JSONDecoder().decode(SNS.Event.self, from: data) 50 | 51 | guard let record = event.records.first else { 52 | XCTFail("Expected to have one record"); return 53 | } 54 | 55 | XCTAssertEqual(record.eventSource, "aws:sns") 56 | XCTAssertEqual(record.eventVersion, "1.0") 57 | XCTAssertEqual(record.eventSubscriptionArn, "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c") 58 | 59 | XCTAssertEqual(record.sns.type, "Notification") 60 | XCTAssertEqual(record.sns.messageId, "bdb6900e-1ae9-5b4b-b7fc-c681fde222e3") 61 | XCTAssertEqual(record.sns.topicArn, "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5") 62 | XCTAssertEqual(record.sns.message, "{\"hello\": \"world\"}") 63 | XCTAssertEqual(record.sns.timestamp, Date(timeIntervalSince1970: 1578493131.203)) 64 | XCTAssertEqual(record.sns.signatureVersion, "1") 65 | XCTAssertEqual(record.sns.signature, "LJMF/xmMH7A1gNy2unLA3hmzyf6Be+zS/Yeiiz9tZbu6OG8fwvWZeNOcEZardhSiIStc0TF7h9I+4Qz3omCntaEfayzTGmWN8itGkn2mfn/hMFmPbGM8gEUz3+jp1n6p+iqP3XTx92R0LBIFrU3ylOxSo8+SCOjA015M93wfZzwj0WPtynji9iAvvtf15d8JxPUu1T05BRitpFd5s6ZXDHtVQ4x/mUoLUN8lOVp+rs281/ZdYNUG/V5CwlyUDTOERdryTkBJ/GO1NNPa+6m04ywJFa5d+BC8mDcUcHhhXXjpTEbt8AHBmswK3nudHrVMRO/G4zmssxU2P7ii5+gCfA==") 66 | XCTAssertEqual(record.sns.signingCertURL, "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem") 67 | 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") 68 | 69 | XCTAssertEqual(record.sns.messageAttributes.count, 2) 70 | 71 | var binaryBuffer = ByteBufferAllocator().buffer(capacity: 6) 72 | binaryBuffer.setString("base64", at: 0) 73 | XCTAssertEqual(record.sns.messageAttributes["binary"], .binary(binaryBuffer)) 74 | XCTAssertEqual(record.sns.messageAttributes["string"], .string("abc123")) 75 | 76 | let payload = try record.sns.decodeBody(TestStruct.self) 77 | XCTAssertEqual(payload.hello, "world") 78 | } 79 | catch { 80 | XCTFail("Unexpected error: \(error)") 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Events/SQSTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import NIO 4 | @testable import LambdaRuntime 5 | 6 | class SQSTests: XCTestCase { 7 | 8 | static let testPayload = """ 9 | { 10 | "Records": [ 11 | { 12 | "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", 13 | "receiptHandle": "MessageReceiptHandle", 14 | "body": "Hello from SQS!", 15 | "attributes": { 16 | "ApproximateReceiveCount": "1", 17 | "SentTimestamp": "1523232000000", 18 | "SenderId": "123456789012", 19 | "ApproximateFirstReceiveTimestamp": "1523232000001" 20 | }, 21 | "messageAttributes": { 22 | "number":{ 23 | "stringValue":"123", 24 | "stringListValues":[], 25 | "binaryListValues":[], 26 | "dataType":"Number" 27 | }, 28 | "string":{ 29 | "stringValue":"abc123", 30 | "stringListValues":[], 31 | "binaryListValues":[], 32 | "dataType":"String" 33 | }, 34 | "binary":{ 35 | "dataType": "Binary", 36 | "stringListValues":[], 37 | "binaryListValues":[], 38 | "binaryValue":"YmFzZTY0" 39 | }, 40 | 41 | }, 42 | "md5OfBody": "7b270e59b47ff90a553787216d55d91d", 43 | "eventSource": "aws:sqs", 44 | "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", 45 | "awsRegion": "us-east-1" 46 | } 47 | ] 48 | } 49 | """ 50 | 51 | 52 | func testSimpleEventFromJSON() { 53 | let data = SQSTests.testPayload.data(using: .utf8)! 54 | do { 55 | let decoder = JSONDecoder() 56 | let event = try decoder.decode(SQS.Event.self, from: data) 57 | 58 | XCTAssertEqual(event.records.count, 1) 59 | 60 | guard let message = event.records.first else { 61 | XCTFail("Expected to have one message in the event") 62 | return 63 | } 64 | 65 | XCTAssertEqual(message.messageId , "19dd0b57-b21e-4ac1-bd88-01bbb068cb78") 66 | XCTAssertEqual(message.receiptHandle , "MessageReceiptHandle") 67 | XCTAssertEqual(message.body , "Hello from SQS!") 68 | XCTAssertEqual(message.attributes.count, 4) 69 | 70 | var binaryBuffer = ByteBufferAllocator().buffer(capacity: 6) 71 | binaryBuffer.setString("base64", at: 0) 72 | XCTAssertEqual(message.messageAttributes, [ 73 | "number": .number(AWSNumber(int: 123)), 74 | "string": .string("abc123"), 75 | "binary": .binary(binaryBuffer) 76 | ]) 77 | XCTAssertEqual(message.md5OfBody , "7b270e59b47ff90a553787216d55d91d") 78 | XCTAssertEqual(message.eventSource , "aws:sqs") 79 | XCTAssertEqual(message.eventSourceArn , "arn:aws:sqs:us-east-1:123456789012:MyQueue") 80 | XCTAssertEqual(message.awsRegion , "us-east-1") 81 | } 82 | catch { 83 | XCTFail("Unexpected error: \(error)") 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Runtime+CodableTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import NIO 4 | import NIOHTTP1 5 | @testable import LambdaRuntime 6 | import LambdaRuntimeTestUtils 7 | 8 | class RuntimeCodableTests: XCTestCase { 9 | 10 | override func setUp() { 11 | 12 | } 13 | 14 | override func tearDown() { 15 | 16 | } 17 | 18 | struct TestRequest: Codable { 19 | let name: String 20 | } 21 | 22 | struct TestResponse: Codable, Equatable { 23 | let greeting: String 24 | } 25 | 26 | func testCodableHandlerWithResultSuccess() { 27 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 28 | defer { 29 | XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) 30 | } 31 | let handler = Runtime.codable { (req: TestRequest, ctx) -> EventLoopFuture in 32 | return ctx.eventLoop.makeSucceededFuture(TestResponse(greeting: "Hello \(req.name)!")) 33 | } 34 | 35 | do { 36 | let inputBytes = try JSONEncoder().encodeAsByteBuffer(TestRequest(name: "world"), allocator: ByteBufferAllocator()) 37 | let ctx = try Context(environment: .forTesting(), invocation: .forTesting(), eventLoop: eventLoopGroup.next()) 38 | 39 | let response = try handler(inputBytes, ctx).flatMapThrowing { (bytes) -> TestResponse in 40 | return try JSONDecoder().decode(TestResponse.self, from: bytes!) 41 | }.wait() 42 | 43 | XCTAssertEqual(response, TestResponse(greeting: "Hello world!")) 44 | } 45 | catch { 46 | XCTFail("Unexpected error: \(error)") 47 | } 48 | } 49 | 50 | func testCodableHandlerWithResultInvalidInput() { 51 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 52 | defer { 53 | XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) 54 | } 55 | let handler = Runtime.codable { (req: TestRequest, ctx) -> EventLoopFuture in 56 | return ctx.eventLoop.makeSucceededFuture(TestResponse(greeting: "Hello \(req.name)!")) 57 | } 58 | 59 | do { 60 | var inputBytes = try JSONEncoder().encodeAsByteBuffer(TestRequest(name: "world"), allocator: ByteBufferAllocator()) 61 | inputBytes.setString("asd", at: 0) // destroy the json 62 | let ctx = try Context(environment: .forTesting(), invocation: .forTesting(), eventLoop: eventLoopGroup.next()) 63 | 64 | _ = try handler(inputBytes, ctx).flatMapThrowing { (outputBytes) -> TestResponse in 65 | XCTFail("The function should not be invoked.") 66 | return try JSONDecoder().decode(TestResponse.self, from: outputBytes!) 67 | }.wait() 68 | 69 | XCTFail("Did not expect to succeed.") 70 | } 71 | catch DecodingError.dataCorrupted(_) { 72 | // this is our expected case 73 | } 74 | catch { 75 | XCTFail("Expected to have an data corrupted error") 76 | } 77 | } 78 | 79 | func testCodableHandlerWithoutResultSuccess() { 80 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 81 | defer { 82 | XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) 83 | } 84 | let handler = Runtime.codable { (req: TestRequest, ctx) -> EventLoopFuture in 85 | return ctx.eventLoop.makeSucceededFuture(Void()) 86 | } 87 | 88 | do { 89 | let inputBytes = try JSONEncoder().encodeAsByteBuffer(TestRequest(name: "world"), allocator: ByteBufferAllocator()) 90 | let ctx = try Context(environment: .forTesting(), invocation: .forTesting(), eventLoop: eventLoopGroup.next()) 91 | 92 | _ = try handler(inputBytes, ctx).wait() 93 | } 94 | catch { 95 | XCTFail("Unexpected error: \(error)") 96 | } 97 | } 98 | 99 | func testCodableHandlerWithoutResultInvalidInput() { 100 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 101 | defer { 102 | XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) 103 | } 104 | let handler = Runtime.codable { (req: TestRequest, ctx) -> EventLoopFuture in 105 | return ctx.eventLoop.makeSucceededFuture(Void()) 106 | } 107 | 108 | do { 109 | var inputBytes = try JSONEncoder().encodeAsByteBuffer(TestRequest(name: "world"), allocator: ByteBufferAllocator()) 110 | inputBytes.setString("asd", at: 0) // destroy the json 111 | let ctx = try Context(environment: .forTesting(), invocation: .forTesting(), eventLoop: eventLoopGroup.next()) 112 | 113 | _ = try handler(inputBytes, ctx).wait() 114 | 115 | XCTFail("Did not expect to succeed.") 116 | } 117 | catch DecodingError.dataCorrupted(_) { 118 | // this is our expected case 119 | } 120 | catch { 121 | XCTFail("Unexpected error: \(error)") 122 | } 123 | } 124 | 125 | func testCodableHandlerWithoutResultFailure() { 126 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 127 | defer { 128 | XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) 129 | } 130 | let handler = Runtime.codable { (req: TestRequest, ctx) -> EventLoopFuture in 131 | return ctx.eventLoop.makeFailedFuture(RuntimeError.unknown) 132 | } 133 | 134 | do { 135 | let inputBytes = try JSONEncoder().encodeAsByteBuffer(TestRequest(name: "world"), allocator: ByteBufferAllocator()) 136 | let ctx = try Context(environment: .forTesting(), invocation: .forTesting(), eventLoop: eventLoopGroup.next()) 137 | 138 | _ = try handler(inputBytes, ctx).wait() 139 | 140 | XCTFail("Did not expect to reach this point") 141 | } 142 | catch RuntimeError.unknown { 143 | // expected case 144 | } 145 | catch { 146 | XCTFail("Unexpected error: \(error)") 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/RuntimeAPIClientTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import NIO 4 | import NIOHTTP1 5 | import NIOTestUtils 6 | @testable import LambdaRuntime 7 | 8 | class RuntimeAPIClientTests: XCTestCase { 9 | 10 | struct InvocationBody: Codable { 11 | let test: String 12 | } 13 | 14 | func testGetNextInvocationHappyPathTest() { 15 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 16 | let web = NIOHTTP1TestServer(group: group) 17 | let client = RuntimeAPIClient(eventLoopGroup: group, lambdaRuntimeAPI: "localhost:\(web.serverPort)") 18 | 19 | defer { 20 | XCTAssertNoThrow(try client.syncShutdown()) 21 | XCTAssertNoThrow(try web.stop()) 22 | XCTAssertNoThrow(try group.syncShutdownGracefully()) 23 | } 24 | 25 | let result = client.getNextInvocation() 26 | 27 | XCTAssertNoThrow(try XCTAssertEqual( 28 | web.readInbound(), 29 | HTTPServerRequestPart.head(.init(version: .init(major: 1, minor: 1), method: .GET, uri: "/2018-06-01/runtime/invocation/next", headers: 30 | HTTPHeaders([("Host", "localhost"), ("Connection", "close"), ("Content-Length", "0")]))))) 31 | XCTAssertNoThrow(try XCTAssertEqual( 32 | web.readInbound(), 33 | HTTPServerRequestPart.end(nil))) 34 | 35 | let now = UInt(Date().timeIntervalSinceNow * 1000 + 1000) 36 | 37 | XCTAssertNoThrow(try web.writeOutbound( 38 | .head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: HTTPHeaders([ 39 | ("Lambda-Runtime-Aws-Request-Id", UUID().uuidString), 40 | ("Lambda-Runtime-Deadline-Ms", "\(now)"), 41 | ("Lambda-Runtime-Invoked-Function-Arn", "fancy:arn"), 42 | ("Lambda-Runtime-Trace-Id", "aTraceId"), 43 | ("Lambda-Runtime-Client-Context", "someContext"), 44 | ("Lambda-Runtime-Cognito-Identity", "someIdentity"), 45 | ]))))) 46 | 47 | XCTAssertNoThrow(try web.writeOutbound( 48 | .body(.byteBuffer(try JSONEncoder().encodeAsByteBuffer(InvocationBody(test: "abc"), allocator: ByteBufferAllocator()))))) 49 | XCTAssertNoThrow(try web.writeOutbound(.end(nil))) 50 | 51 | XCTAssertNoThrow(try result.wait()) 52 | 53 | } 54 | 55 | struct InvocationResponse: Codable { 56 | let msg: String 57 | } 58 | 59 | func testPostInvocationResponseHappyPath() { 60 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 61 | let web = NIOHTTP1TestServer(group: group) 62 | let client = RuntimeAPIClient(eventLoopGroup: group, lambdaRuntimeAPI: "localhost:\(web.serverPort)") 63 | defer { 64 | XCTAssertNoThrow(try web.stop()) 65 | XCTAssertNoThrow(try client.syncShutdown()) 66 | XCTAssertNoThrow(try group.syncShutdownGracefully()) 67 | } 68 | 69 | do { 70 | let invocationId = "abc" 71 | let resp = InvocationResponse(msg: "hello world!") 72 | let body = try JSONEncoder().encodeAsByteBuffer(resp, allocator: ByteBufferAllocator()) 73 | let result = client.postInvocationResponse(for: invocationId, httpBody: body) 74 | 75 | XCTAssertNoThrow(try XCTAssertEqual( 76 | web.readInbound(), 77 | HTTPServerRequestPart.head(.init(version: .init(major: 1, minor: 1), method: .POST, 78 | uri: "/2018-06-01/runtime/invocation/\(invocationId)/response", 79 | headers: HTTPHeaders([("Host", "localhost"), ("Connection", "close"), ("Content-Length", "\(body.readableBytes)")]))))) 80 | XCTAssertNoThrow(try XCTAssertEqual( 81 | web.readInbound(), 82 | HTTPServerRequestPart.body(body))) 83 | XCTAssertNoThrow(try XCTAssertEqual( 84 | web.readInbound(), 85 | HTTPServerRequestPart.end(nil))) 86 | 87 | XCTAssertNoThrow(try web.writeOutbound( 88 | .head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: HTTPHeaders([]))))) 89 | XCTAssertNoThrow(try web.writeOutbound(.end(nil))) 90 | 91 | XCTAssertNoThrow(try result.wait()) 92 | 93 | } 94 | catch { 95 | XCTFail("unexpected error: \(error)") 96 | } 97 | } 98 | 99 | enum TestError: Error { 100 | case unknown 101 | } 102 | 103 | func testPostInvocationErrorHappyPath() { 104 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 105 | let web = NIOHTTP1TestServer(group: group) 106 | let client = RuntimeAPIClient(eventLoopGroup: group, lambdaRuntimeAPI: "localhost:\(web.serverPort)") 107 | defer { 108 | XCTAssertNoThrow(try web.stop()) 109 | XCTAssertNoThrow(try client.syncShutdown()) 110 | XCTAssertNoThrow(try group.syncShutdownGracefully()) 111 | } 112 | 113 | do { 114 | let invocationId = "abc" 115 | let error = TestError.unknown 116 | let result = client.postInvocationError(for: invocationId, error: error) 117 | 118 | let respError = InvocationError(errorMessage: String(describing: error)) 119 | let body = try JSONEncoder().encodeAsByteBuffer(respError, allocator: ByteBufferAllocator()) 120 | 121 | XCTAssertNoThrow(try XCTAssertEqual( 122 | web.readInbound(), 123 | HTTPServerRequestPart.head(.init(version: .init(major: 1, minor: 1), method: .POST, 124 | uri: "/2018-06-01/runtime/invocation/\(invocationId)/error", 125 | headers: HTTPHeaders([("Host", "localhost"), ("Connection", "close"), ("Content-Length", "\(body.readableBytes)")]))))) 126 | XCTAssertNoThrow(try XCTAssertEqual( 127 | web.readInbound(), 128 | HTTPServerRequestPart.body(body))) 129 | XCTAssertNoThrow(try XCTAssertEqual( 130 | web.readInbound(), 131 | HTTPServerRequestPart.end(nil))) 132 | 133 | XCTAssertNoThrow(try web.writeOutbound( 134 | .head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: HTTPHeaders([]))))) 135 | XCTAssertNoThrow(try web.writeOutbound(.end(nil))) 136 | 137 | XCTAssertNoThrow(try result.wait()) 138 | 139 | } 140 | catch { 141 | XCTFail("unexpected error: \(error)") 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/RuntimeTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import NIO 4 | @testable import LambdaRuntime 5 | 6 | class RuntimeTests: XCTestCase { 7 | 8 | // MARK: - Test Setup - 9 | 10 | func testCreateRuntimeHappyPath() { 11 | 12 | setenv("AWS_LAMBDA_RUNTIME_API", "localhost", 1) 13 | setenv("_HANDLER", "haha", 1) 14 | 15 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 16 | defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) } 17 | 18 | do { 19 | let runtime = try Runtime.createRuntime(eventLoopGroup: group) { (_, ctx) in 20 | return ctx.eventLoop.makeSucceededFuture(nil) 21 | } 22 | defer { XCTAssertNoThrow(try runtime.syncShutdown()) } 23 | XCTAssert(runtime.eventLoopGroup === group) 24 | } 25 | catch { 26 | XCTFail("Unexpected error: \(error)") 27 | } 28 | } 29 | 30 | func testCreateRuntimeMissingLambdaRuntimeAPI() { 31 | unsetenv("AWS_LAMBDA_RUNTIME_API") 32 | 33 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 34 | defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) } 35 | 36 | do { 37 | let runtime = try Runtime.createRuntime(eventLoopGroup: group) { (_, ctx) in 38 | return ctx.eventLoop.makeSucceededFuture(nil) 39 | } 40 | defer { XCTAssertNoThrow(try runtime.syncShutdown()) } 41 | XCTFail("Did not expect to succeed") 42 | } 43 | catch let error as RuntimeError { 44 | XCTAssertEqual(error, RuntimeError.missingEnvironmentVariable("AWS_LAMBDA_RUNTIME_API")) 45 | } 46 | catch { 47 | XCTFail("Unexpected error: \(error)") 48 | } 49 | } 50 | 51 | // MARK: - Test Running - 52 | 53 | // func testRegisterAFunction() { 54 | // let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 55 | // let client = MockLambdaRuntimeAPI(eventLoopGroup: group) 56 | // defer { 57 | // XCTAssertNoThrow(try client.syncShutdown()) 58 | // XCTAssertNoThrow(try group.syncShutdownGracefully()) 59 | // } 60 | // 61 | // do { 62 | // let env = try Environment.forTesting(handler: "lambda.testFunction") 63 | // 64 | // let runtime = Runtime(eventLoopGroup: group, client: client, environment: env) 65 | // let expectation = self.expectation(description: "test function is hit") 66 | // var hits = 0 67 | // runtime.register(for: "testFunction") { (req, ctx) -> EventLoopFuture in 68 | // expectation.fulfill() 69 | // hits += 1 70 | // return ctx.eventLoop.makeSucceededFuture(ByteBufferAllocator().buffer(capacity: 0)) 71 | // } 72 | // 73 | // _ = runtime.start() 74 | // 75 | // self.wait(for: [expectation], timeout: 3) 76 | // 77 | // XCTAssertNoThrow(try runtime.syncShutdown()) 78 | // XCTAssertEqual(hits, 1) 79 | // } 80 | // catch { 81 | // XCTFail("Unexpected error: \(error)") 82 | // } 83 | // } 84 | 85 | } 86 | 87 | -------------------------------------------------------------------------------- /Tests/LambdaRuntimeTests/Utils/MockLambdaRuntimeAPI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | @testable import LambdaRuntime 4 | 5 | class MockLambdaRuntimeAPI { 6 | 7 | let eventLoopGroup : EventLoopGroup 8 | let runLoop : EventLoop 9 | let maxInvocations : Int 10 | var invocationCount: Int = 0 11 | 12 | private var isShutdown = false 13 | 14 | init(eventLoopGroup: EventLoopGroup, maxInvocations: Int) { 15 | self.eventLoopGroup = eventLoopGroup 16 | self.runLoop = eventLoopGroup.next() 17 | self.maxInvocations = maxInvocations 18 | } 19 | } 20 | 21 | struct TestRequest: Codable { 22 | let name: String 23 | } 24 | 25 | struct TestResponse: Codable { 26 | let greeting: String 27 | } 28 | 29 | extension MockLambdaRuntimeAPI: LambdaRuntimeAPI { 30 | 31 | func getNextInvocation() -> EventLoopFuture<(Invocation, ByteBuffer)> { 32 | do { 33 | let invocation = try Invocation.forTesting() 34 | let payload = try JSONEncoder().encodeAsByteBuffer( 35 | TestRequest(name: "world"), 36 | allocator: ByteBufferAllocator()) 37 | return self.runLoop.makeSucceededFuture((invocation, payload)) 38 | } 39 | catch { 40 | return self.runLoop.makeFailedFuture(error) 41 | } 42 | } 43 | 44 | func postInvocationResponse(for requestId: String, httpBody: ByteBuffer?) -> EventLoopFuture { 45 | return self.runLoop.makeSucceededFuture(Void()) 46 | } 47 | 48 | func postInvocationError(for requestId: String, error: Error) -> EventLoopFuture { 49 | return self.runLoop.makeSucceededFuture(Void()) 50 | } 51 | 52 | func syncShutdown() throws { 53 | self.runLoop.execute { 54 | self.isShutdown = true 55 | } 56 | } 57 | 58 | } 59 | 60 | extension Invocation { 61 | 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabianfett/swift-lambda-runtime/bd06eed3708bc02db6c4efe62dc9bad4fd64ac7d/Tests/LinuxMain.swift -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile is used to compile our examples, by just adding some dev 2 | # dependencies. 3 | ARG SWIFT_VERSION=5.0 4 | FROM fabianfett/amazonlinux-swift:$SWIFT_VERSION-amazonlinux2 5 | 6 | # needed to do again after FROM due to docker limitation 7 | ARG SWIFT_VERSION 8 | 9 | RUN yum -y update && \ 10 | yum -y install zlib-devel kernel-devel gcc-c++ openssl-devel -------------------------------------------------------------------------------- /docs/Add-Layer-to-Function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabianfett/swift-lambda-runtime/bd06eed3708bc02db6c4efe62dc9bad4fd64ac7d/docs/Add-Layer-to-Function.png -------------------------------------------------------------------------------- /docs/Develop.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | build 4 | ```bash 5 | $ docker build -t swift-dev:5.1.2 . 6 | ``` 7 | 8 | run docker in interactive mode 9 | ``` 10 | $ docker run -it --rm -v $(pwd):"/src" --workdir "/src" swift-dev:5.1.2 11 | ``` -------------------------------------------------------------------------------- /docs/Function-Create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabianfett/swift-lambda-runtime/bd06eed3708bc02db6c4efe62dc9bad4fd64ac7d/docs/Function-Create.png -------------------------------------------------------------------------------- /docs/Invocation-Success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabianfett/swift-lambda-runtime/bd06eed3708bc02db6c4efe62dc9bad4fd64ac7d/docs/Invocation-Success.png -------------------------------------------------------------------------------- /docs/Layer-Copy-Arn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabianfett/swift-lambda-runtime/bd06eed3708bc02db6c4efe62dc9bad4fd64ac7d/docs/Layer-Copy-Arn.png -------------------------------------------------------------------------------- /docs/Upload-Lambda-zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabianfett/swift-lambda-runtime/bd06eed3708bc02db6c4efe62dc9bad4fd64ac7d/docs/Upload-Lambda-zip.png -------------------------------------------------------------------------------- /examples/EventSources/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "51dc885a30ca704b02fa803099b0a9b5b38067b6", 10 | "version": "1.0.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-base64-kit", 15 | "repositoryURL": "https://github.com/fabianfett/swift-base64-kit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "3ffa48a7047fc9ac6581cd53ab1df29466d8f13b", 19 | "version": "0.2.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-log", 24 | "repositoryURL": "https://github.com/apple/swift-log.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", 28 | "version": "1.2.0" 29 | } 30 | }, 31 | { 32 | "package": "swift-nio", 33 | "repositoryURL": "https://github.com/apple/swift-nio.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "ff01888051cd7efceb1bf8319c1dd3986c4bf6fc", 37 | "version": "2.10.1" 38 | } 39 | }, 40 | { 41 | "package": "swift-nio-extras", 42 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "53808818c2015c45247cad74dc05c7a032c96a2f", 46 | "version": "1.3.2" 47 | } 48 | }, 49 | { 50 | "package": "swift-nio-ssl", 51 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "ccf96bbe65ecc7c1558ab0dba7ffabdea5c1d31f", 55 | "version": "2.4.4" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /examples/EventSources/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "EventSources", 8 | dependencies: [ 9 | .package(path: "../.."), 10 | .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.9.0")), 11 | .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.1.1")), 12 | ], 13 | targets: [ 14 | .target( 15 | name: "EventSources", 16 | dependencies: ["LambdaRuntime", "Logging", "NIO", "NIOFoundationCompat", "NIOHTTP1"]), 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /examples/EventSources/README.md: -------------------------------------------------------------------------------- 1 | # EventSources 2 | 3 | This package/executable can be used to test the different Event types. This especially usefull during development of new Event types. We use the sam `template.yaml` to deploy the external resources needed for testing. Please be aware, if you deploy the given `template.yaml` costs may occur (especially when using the LoadBalancer). 4 | -------------------------------------------------------------------------------- /examples/EventSources/Sources/EventSources/main.swift: -------------------------------------------------------------------------------- 1 | import LambdaRuntime 2 | import NIO 3 | import Logging 4 | import Foundation 5 | 6 | LoggingSystem.bootstrap(StreamLogHandler.standardError) 7 | 8 | 9 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 10 | defer { try! group.syncShutdownGracefully() } 11 | let logger = Logger(label: "AWSLambda.EventSources") 12 | 13 | struct SNSBody: Codable { 14 | let name: String 15 | let whatevar: String 16 | } 17 | 18 | func handleSNS(event: SNS.Event, ctx: Context) -> EventLoopFuture { 19 | do { 20 | let message = event.records.first!.sns 21 | let _ = try message.decodeBody(SNSBody.self) 22 | 23 | // handle your message 24 | 25 | return ctx.eventLoop.makeSucceededFuture(Void()) 26 | } 27 | catch { 28 | return ctx.eventLoop.makeFailedFuture(error) 29 | } 30 | } 31 | 32 | func handleSQS(event: SQS.Event, ctx: Context) -> EventLoopFuture { 33 | ctx.logger.info("Payload: \(String(describing: event))") 34 | 35 | return ctx.eventLoop.makeSucceededFuture(Void()) 36 | } 37 | 38 | func handleDynamoStream(event: DynamoDB.Event, ctx: Context) -> EventLoopFuture { 39 | ctx.logger.info("Payload: \(String(describing: event))") 40 | 41 | return ctx.eventLoop.makeSucceededFuture(Void()) 42 | } 43 | 44 | func handleCloudwatchSchedule(event: Cloudwatch.Event, ctx: Context) 45 | -> EventLoopFuture 46 | { 47 | ctx.logger.info("Payload: \(String(describing: event))") 48 | 49 | return ctx.eventLoop.makeSucceededFuture(Void()) 50 | } 51 | 52 | func handleAPIRequest(req: APIGateway.Request, ctx: Context) -> EventLoopFuture { 53 | ctx.logger.info("Payload: \(String(describing: req))") 54 | 55 | struct Payload: Encodable { 56 | let path: String 57 | let method: String 58 | } 59 | 60 | let payload = Payload(path: req.path, method: req.httpMethod.rawValue) 61 | let response = try! APIGateway.Response(statusCode: .ok, payload: payload) 62 | 63 | return ctx.eventLoop.makeSucceededFuture(response) 64 | } 65 | 66 | func handleS3(event: S3.Event, ctx: Context) -> EventLoopFuture { 67 | ctx.logger.info("Payload: \(String(describing: event))") 68 | 69 | return ctx.eventLoop.makeSucceededFuture(Void()) 70 | } 71 | 72 | func handleLoadBalancerRequest(req: ALB.TargetGroupRequest, ctx: Context) -> 73 | EventLoopFuture 74 | { 75 | ctx.logger.info("Payload: \(String(describing: req))") 76 | 77 | struct Payload: Encodable { 78 | let path: String 79 | let method: String 80 | } 81 | 82 | let payload = Payload(path: req.path, method: req.httpMethod.rawValue) 83 | let response = try! ALB.TargetGroupResponse(statusCode: .ok, payload: payload) 84 | 85 | return ctx.eventLoop.makeSucceededFuture(response) 86 | } 87 | 88 | func printPayload(buffer: NIO.ByteBuffer, ctx: Context) -> EventLoopFuture { 89 | let payload = buffer.getString(at: 0, length: buffer.readableBytes) 90 | ctx.logger.error("Payload: \(String(describing: payload))") 91 | 92 | return ctx.eventLoop.makeSucceededFuture(nil) 93 | } 94 | 95 | func printOriginalPayload(_ handler: @escaping (NIO.ByteBuffer, Context) -> EventLoopFuture) 96 | -> ((NIO.ByteBuffer, Context) -> EventLoopFuture) 97 | { 98 | return { (buffer, ctx) in 99 | let payload = buffer.getString(at: 0, length: buffer.readableBytes) 100 | ctx.logger.info("Payload: \(String(describing: payload))") 101 | 102 | return handler(buffer, ctx) 103 | } 104 | } 105 | 106 | do { 107 | logger.info("start runtime") 108 | let environment = try Environment() 109 | let handler: Runtime.Handler 110 | 111 | switch environment.handlerName { 112 | case "sns": 113 | handler = printOriginalPayload(Runtime.codable(handleSNS)) 114 | case "sqs": 115 | handler = printOriginalPayload(Runtime.codable(handleSQS)) 116 | case "dynamo": 117 | handler = printOriginalPayload(Runtime.codable(handleDynamoStream)) 118 | case "schedule": 119 | handler = printOriginalPayload(Runtime.codable(handleCloudwatchSchedule)) 120 | case "api": 121 | handler = printOriginalPayload(APIGateway.handler(handleAPIRequest)) 122 | case "s3": 123 | handler = printOriginalPayload(Runtime.codable(handleS3)) 124 | case "loadbalancer": 125 | handler = printOriginalPayload(ALB.handler(multiValueHeadersEnabled: true, handleLoadBalancerRequest)) 126 | default: 127 | handler = printPayload 128 | } 129 | 130 | let runtime = try Runtime.createRuntime(eventLoopGroup: group, handler: handler) 131 | defer { try! runtime.syncShutdown() } 132 | logger.info("starting runloop") 133 | 134 | try runtime.start().wait() 135 | } 136 | catch { 137 | logger.error("error: \(String(describing: error))") 138 | } 139 | 140 | 141 | -------------------------------------------------------------------------------- /examples/EventSources/makefile: -------------------------------------------------------------------------------- 1 | # Example settings 2 | LAMBDA_NAME=EventSources 3 | EXECUTABLE=$(LAMBDA_NAME) 4 | LAMBDA_ZIP=lambda.zip 5 | 6 | SWIFT_VERSION=5.2.1 7 | SWIFT_DOCKER_IMAGE=fabianfett/amazonlinux-swift:${SWIFT_VERSION}-amazonlinux2-dev 8 | 9 | clean_lambda: 10 | rm bootstrap || true 11 | rm lambda.zip || true 12 | 13 | build_lambda: 14 | docker run \ 15 | --rm \ 16 | --volume "$(shell pwd)/../..:/src" \ 17 | --workdir "/src/examples/$(LAMBDA_NAME)/" \ 18 | $(SWIFT_DOCKER_IMAGE) \ 19 | swift build -c release 20 | 21 | package_lambda: build_lambda 22 | cp .build/release/$(EXECUTABLE) ./bootstrap 23 | zip -r -j $(LAMBDA_ZIP) ./bootstrap 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/EventSources/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Parameters: 4 | SwiftLayer: 5 | Type: String 6 | Description: The arn of the swift layer. 7 | Default: arn:aws:lambda:eu-central-1:426836788079:layer:Swift:11 8 | 9 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 10 | Globals: 11 | Function: 12 | Timeout: 3 13 | 14 | Resources: 15 | 16 | # --- VPC 17 | 18 | VPC: 19 | Type: "AWS::EC2::VPC" 20 | Properties: 21 | CidrBlock: 10.0.0.0/16 22 | EnableDnsSupport: true 23 | EnableDnsHostnames: true 24 | InstanceTenancy: default 25 | Tags: 26 | - Key: Name 27 | Value: "Test-VPC" 28 | 29 | PublicSubnetCentralA: 30 | Type: "AWS::EC2::Subnet" 31 | Properties: 32 | AvailabilityZone: eu-central-1a 33 | CidrBlock: 10.0.0.0/24 34 | VpcId: !Ref VPC 35 | 36 | PublicSubnetCentralB: 37 | Type: "AWS::EC2::Subnet" 38 | Properties: 39 | AvailabilityZone: eu-central-1b 40 | CidrBlock: 10.0.10.0/24 41 | VpcId: !Ref VPC 42 | 43 | InternetGateway: 44 | Type: "AWS::EC2::InternetGateway" 45 | 46 | InternetGatewayAttachToVPC: 47 | Type: "AWS::EC2::VPCGatewayAttachment" 48 | Properties: 49 | InternetGatewayId: !Ref InternetGateway 50 | VpcId: !Ref VPC 51 | 52 | PublicRouteTable: 53 | Type: "AWS::EC2::RouteTable" 54 | Properties: 55 | VpcId: !Ref VPC 56 | 57 | InternetRouteForPublicSubnets: 58 | Type: "AWS::EC2::Route" 59 | Properties: 60 | DestinationCidrBlock: 0.0.0.0/0 61 | GatewayId: !Ref InternetGateway 62 | RouteTableId: !Ref PublicRouteTable 63 | 64 | PublicSubnetCentralARouteTableAssociation: 65 | Type: "AWS::EC2::SubnetRouteTableAssociation" 66 | Properties: 67 | RouteTableId: !Ref PublicRouteTable 68 | SubnetId: !Ref PublicSubnetCentralA 69 | 70 | PublicSubnetCentralBRouteTableAssociation: 71 | Type: "AWS::EC2::SubnetRouteTableAssociation" 72 | Properties: 73 | RouteTableId: !Ref PublicRouteTable 74 | SubnetId: !Ref PublicSubnetCentralB 75 | 76 | # --- cloudwatch schedule 77 | 78 | ConsumeCloudwatchScheduleLambda: 79 | Type: AWS::Serverless::Function 80 | Properties: 81 | CodeUri: lambda.zip 82 | Handler: "schedule" 83 | Runtime: provided 84 | Layers: 85 | - !Ref SwiftLayer 86 | Events: 87 | schedule: 88 | Type: Schedule 89 | Properties: 90 | Schedule: rate(5 minutes) 91 | Enabled: True 92 | 93 | # --- sns 94 | 95 | SNSTopic: 96 | Type: AWS::SNS::Topic 97 | 98 | ConsumeSNSTopicLambda: 99 | Type: AWS::Serverless::Function 100 | Properties: 101 | CodeUri: lambda.zip 102 | Handler: "sns" 103 | Runtime: provided 104 | Layers: 105 | - !Ref SwiftLayer 106 | Policies: 107 | - SNSCrudPolicy: 108 | TopicName: !GetAtt SNSTopic.TopicName 109 | Events: 110 | sns: 111 | Type: SNS 112 | Properties: 113 | Topic: !Ref SNSTopic 114 | 115 | # --- sqs 116 | 117 | SQSQueue: 118 | Type: AWS::SQS::Queue 119 | 120 | ConsumeSQSQueueLambda: 121 | Type: AWS::Serverless::Function 122 | Properties: 123 | CodeUri: lambda.zip 124 | Handler: "sqs" 125 | Runtime: provided 126 | Layers: 127 | - !Ref SwiftLayer 128 | Policies: 129 | - SQSPollerPolicy: 130 | QueueName: !GetAtt SQSQueue.QueueName 131 | Events: 132 | sqs: 133 | Type: SQS 134 | Properties: 135 | Queue: !GetAtt SQSQueue.Arn 136 | BatchSize: 10 137 | Enabled: true 138 | 139 | # --- dynamo 140 | 141 | EventSourcesTestTable: 142 | Type: "AWS::DynamoDB::Table" 143 | Properties: 144 | BillingMode: PAY_PER_REQUEST 145 | AttributeDefinitions: 146 | - AttributeName: ListId 147 | AttributeType: S 148 | - AttributeName: TodoId 149 | AttributeType: S 150 | KeySchema: 151 | - AttributeName: ListId 152 | KeyType: HASH 153 | - AttributeName: TodoId 154 | KeyType: RANGE 155 | StreamSpecification: 156 | StreamViewType: NEW_AND_OLD_IMAGES 157 | TableName: "EventSourcesTestTable" 158 | 159 | ConsumeDynamoDBStreamLambda: 160 | Type: AWS::Serverless::Function 161 | Properties: 162 | CodeUri: lambda.zip 163 | Handler: "dynamo" 164 | Runtime: provided 165 | Layers: 166 | - !Ref SwiftLayer 167 | Policies: 168 | - SQSPollerPolicy: 169 | QueueName: !GetAtt SQSQueue.QueueName 170 | Events: 171 | dynamo: 172 | Type: DynamoDB 173 | Properties: 174 | Stream: !GetAtt EventSourcesTestTable.StreamArn 175 | StartingPosition: TRIM_HORIZON 176 | BatchSize: 10 177 | MaximumBatchingWindowInSeconds: 10 178 | Enabled: true 179 | ParallelizationFactor: 8 180 | MaximumRetryAttempts: 100 181 | BisectBatchOnFunctionError: true 182 | MaximumRecordAgeInSeconds: 86400 183 | 184 | # --- api 185 | 186 | HandleAPIRequestLambda: 187 | Type: AWS::Serverless::Function 188 | Properties: 189 | CodeUri: lambda.zip 190 | Handler: "api" 191 | Runtime: provided 192 | Layers: 193 | - !Ref SwiftLayer 194 | Events: 195 | api: 196 | Type: Api 197 | Properties: 198 | Path: /{proxy+} 199 | Method: ANY 200 | 201 | # --- s3 202 | 203 | S3TestEventBucket: 204 | Type: "AWS::S3::Bucket" 205 | Properties: 206 | AccessControl: Private 207 | 208 | HandleS3Event: 209 | Type: AWS::Serverless::Function 210 | Properties: 211 | CodeUri: lambda.zip 212 | Handler: "s3" 213 | Runtime: provided 214 | Layers: 215 | - !Ref SwiftLayer 216 | Events: 217 | s3: 218 | Type: S3 219 | Properties: 220 | Bucket: !Ref S3TestEventBucket 221 | Events: s3:ObjectCreated:* 222 | 223 | # --- load balancer 224 | 225 | HandleLoadBalancerLambda: 226 | Type: AWS::Serverless::Function 227 | Properties: 228 | CodeUri: lambda.zip 229 | Handler: "loadbalancer" 230 | Runtime: provided 231 | Layers: 232 | - !Ref SwiftLayer 233 | 234 | HandleLoadBalancerLambdaInvokePermission: 235 | Type: AWS::Lambda::Permission 236 | Properties: 237 | FunctionName: !GetAtt HandleLoadBalancerLambda.Arn 238 | Action: lambda:InvokeFunction 239 | Principal: elasticloadbalancing.amazonaws.com 240 | 241 | TestLoadBalancerSecurityGroup: 242 | Type: AWS::EC2::SecurityGroup 243 | Properties: 244 | GroupDescription: External ELB Security Group 245 | SecurityGroupIngress: 246 | - CidrIp: 0.0.0.0/0 247 | FromPort: 80 248 | ToPort: 80 249 | IpProtocol: tcp 250 | - CidrIp: 0.0.0.0/0 251 | FromPort: 443 252 | ToPort: 443 253 | IpProtocol: tcp 254 | SecurityGroupEgress: 255 | - CidrIp: 0.0.0.0/0 256 | IpProtocol: -1 257 | VpcId: !Ref VPC 258 | 259 | TestLoadBalancer: 260 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 261 | Properties: 262 | Scheme: internet-facing 263 | Type: application 264 | Subnets: 265 | - !Ref PublicSubnetCentralA 266 | - !Ref PublicSubnetCentralB 267 | SecurityGroups: 268 | - !Ref TestLoadBalancerSecurityGroup 269 | 270 | TestLoadBalancerListener: 271 | Type: "AWS::ElasticLoadBalancingV2::Listener" 272 | Properties: 273 | DefaultActions: 274 | - Type: forward 275 | TargetGroupArn: !Ref TestLoadBalancerTargetGroup 276 | LoadBalancerArn: !Ref TestLoadBalancer 277 | Port: 80 278 | Protocol: HTTP 279 | 280 | TestLoadBalancerTargetGroup: 281 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 282 | DependsOn: 283 | - HandleLoadBalancerLambdaInvokePermission 284 | Properties: 285 | Name: EinSternDerDeinenNamenTraegt 286 | Targets: 287 | - Id: !GetAtt HandleLoadBalancerLambda.Arn 288 | TargetGroupAttributes: 289 | - Key: lambda.multi_value_headers.enabled 290 | Value: true 291 | TargetType: lambda 292 | 293 | 294 | 295 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /examples/SquareNumber/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "51dc885a30ca704b02fa803099b0a9b5b38067b6", 10 | "version": "1.0.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-base64-kit", 15 | "repositoryURL": "https://github.com/fabianfett/swift-base64-kit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "3ffa48a7047fc9ac6581cd53ab1df29466d8f13b", 19 | "version": "0.2.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-log", 24 | "repositoryURL": "https://github.com/apple/swift-log.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "e8aabbe95db22e064ad42f1a4a9f8982664c70ed", 28 | "version": "1.1.1" 29 | } 30 | }, 31 | { 32 | "package": "swift-nio", 33 | "repositoryURL": "https://github.com/apple/swift-nio.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "8066b0f581604e3711979307a4377457e2b0f007", 37 | "version": "2.9.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-nio-extras", 42 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "ed97628fa310c314c4a5cd8038445054b2991f07", 46 | "version": "1.3.1" 47 | } 48 | }, 49 | { 50 | "package": "swift-nio-ssl", 51 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "e5c1af45ac934ac0a6117b2927a51d845cf4f705", 55 | "version": "2.4.3" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /examples/SquareNumber/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SquareNumber", 8 | dependencies: [ 9 | .package(path: "../../"), 10 | ], 11 | targets: [ 12 | .target( 13 | name: "SquareNumber", 14 | dependencies: ["LambdaRuntime"] 15 | ), 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /examples/SquareNumber/Sources/SquareNumber/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import LambdaRuntime 3 | import NIO 4 | 5 | struct Input: Codable { 6 | let number: Double 7 | } 8 | 9 | struct Output: Codable { 10 | let result: Double 11 | } 12 | 13 | func squareNumber(input: Input, context: Context) -> EventLoopFuture { 14 | let squaredNumber = input.number * input.number 15 | return context.eventLoop.makeSucceededFuture(Output(result: squaredNumber)) 16 | } 17 | 18 | func printNumber(input: Input, context: Context) -> EventLoopFuture { 19 | context.logger.info("Number is: \(input.number)") 20 | return context.eventLoop.makeSucceededFuture(Void()) 21 | } 22 | 23 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 24 | defer { 25 | try! group.syncShutdownGracefully() 26 | } 27 | 28 | do { 29 | 30 | let handler: Runtime.Handler 31 | switch ProcessInfo.processInfo.environment["_HANDLER"] { 32 | case "printNumber": 33 | handler = Runtime.codable(printNumber) 34 | default: 35 | handler = Runtime.codable(squareNumber) 36 | } 37 | 38 | let runtime = try Runtime.createRuntime(eventLoopGroup: group, handler: handler) 39 | defer { try! runtime.syncShutdown() } 40 | 41 | try runtime.start().wait() 42 | } 43 | catch { 44 | print(String(describing: error)) 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/SquareNumber/makefile: -------------------------------------------------------------------------------- 1 | # Example settings 2 | LAMBDA_NAME=SquareNumber 3 | EXECUTABLE=$(LAMBDA_NAME) 4 | LAMBDA_ZIP=lambda.zip 5 | 6 | SWIFT_VERSION=5.2.1 7 | SWIFT_DOCKER_IMAGE=fabianfett/amazonlinux-swift:${SWIFT_VERSION}-amazonlinux2-dev 8 | 9 | clean_lambda: 10 | rm bootstrap || true 11 | rm lambda.zip || true 12 | 13 | build_lambda: 14 | docker run \ 15 | --rm \ 16 | --volume "$(shell pwd)/../..:/src" \ 17 | --workdir "/src/examples/$(LAMBDA_NAME)/" \ 18 | $(SWIFT_DOCKER_IMAGE) \ 19 | swift build -c release 20 | 21 | package_lambda: build_lambda 22 | cp .build/release/$(EXECUTABLE) ./bootstrap 23 | zip -r -j $(LAMBDA_ZIP) ./bootstrap 24 | 25 | download_layer: 26 | curl -o swift-${SWIFT_VERSION}-RELEASE.zip https://amazonlinux-swift.s3.eu-central-1.amazonaws.com/layers/swift-${SWIFT_VERSION}-RELEASE.zip 27 | unzip swift-${SWIFT_VERSION}-RELEASE.zip -d swift-lambda-layer 28 | rm swift-${SWIFT_VERSION}-RELEASE.zip 29 | -------------------------------------------------------------------------------- /examples/SquareNumber/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | sam-app 5 | 6 | Sample SAM Template for sam-app 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Function: 11 | Timeout: 3 12 | 13 | Resources: 14 | 15 | SwiftLayer: 16 | Type: AWS::Serverless::LayerVersion 17 | Properties: 18 | ContentUri: swift-lambda-layer/ 19 | 20 | SquareNumberFunction: 21 | Type: AWS::Serverless::Function 22 | Properties: 23 | CodeUri: lambda.zip 24 | Handler: "squareNumber" 25 | Runtime: provided 26 | Layers: 27 | - !Ref SwiftLayer 28 | 29 | PrintNumberFunction: 30 | Type: AWS::Serverless::Function 31 | Properties: 32 | CodeUri: lambda.zip 33 | Handler: "printNumber" 34 | Runtime: provided 35 | Layers: 36 | - !Ref SwiftLayer 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "037b70291941fe43de668066eb6fb802c5e181d2", 10 | "version": "1.1.1" 11 | } 12 | }, 13 | { 14 | "package": "AWSSDKSwift", 15 | "repositoryURL": "https://github.com/swift-aws/aws-sdk-swift.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "dc2daf1744e1f0b580b4a59c0dd34ced06ecbb6a", 19 | "version": "4.5.0" 20 | } 21 | }, 22 | { 23 | "package": "AWSSDKSwiftCore", 24 | "repositoryURL": "https://github.com/swift-aws/aws-sdk-swift-core.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "fc92ffde6111ebaecc4a23b0596c5efea950c192", 28 | "version": "4.4.0" 29 | } 30 | }, 31 | { 32 | "package": "HypertextApplicationLanguage", 33 | "repositoryURL": "https://github.com/swift-aws/HypertextApplicationLanguage.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "aa2c9141d491682f17b2310aed17b9adfc006256", 37 | "version": "1.1.1" 38 | } 39 | }, 40 | { 41 | "package": "INIParser", 42 | "repositoryURL": "https://github.com/swift-aws/Perfect-INIParser.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "42de0efc7a01105e19b80d533d3d282a98277f6c", 46 | "version": "3.0.3" 47 | } 48 | }, 49 | { 50 | "package": "swift-base64-kit", 51 | "repositoryURL": "https://github.com/fabianfett/swift-base64-kit.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "3ffa48a7047fc9ac6581cd53ab1df29466d8f13b", 55 | "version": "0.2.0" 56 | } 57 | }, 58 | { 59 | "package": "swift-log", 60 | "repositoryURL": "https://github.com/apple/swift-log.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", 64 | "version": "1.2.0" 65 | } 66 | }, 67 | { 68 | "package": "swift-nio", 69 | "repositoryURL": "https://github.com/apple/swift-nio.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "e876fb37410e0036b98b5361bb18e6854739572b", 73 | "version": "2.16.0" 74 | } 75 | }, 76 | { 77 | "package": "swift-nio-extras", 78 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "b4dbfacff47fb8d0f9e0a422d8d37935a9f10570", 82 | "version": "1.4.0" 83 | } 84 | }, 85 | { 86 | "package": "swift-nio-ssl", 87 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "ae213938e151964aa691f0e902462fbe06baeeb6", 91 | "version": "2.7.1" 92 | } 93 | }, 94 | { 95 | "package": "swift-nio-transport-services", 96 | "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", 97 | "state": { 98 | "branch": null, 99 | "revision": "85a67aea7caf5396ed599543dd23cffeb6dbbf96", 100 | "version": "1.5.1" 101 | } 102 | } 103 | ] 104 | }, 105 | "version": 1 106 | } 107 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TodoAPIGateway", 8 | products: [ 9 | .executable(name: "TodoAPIGateway", targets: ["TodoAPIGateway"]), 10 | .library(name: "TodoService", targets: ["TodoService"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.16.0")), 14 | .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.1.1")), 15 | .package(url: "https://github.com/swift-aws/aws-sdk-swift.git", .upToNextMajor(from: "4.4.0")), 16 | .package(path: "../../"), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "TodoAPIGateway", 21 | dependencies: [ 22 | "LambdaRuntime", "Logging", "TodoService", "NIO", "NIOHTTP1", "DynamoDB" 23 | ] 24 | ), 25 | .testTarget( 26 | name: "TodoAPIGatewayTests", 27 | dependencies: ["TodoAPIGateway"]), 28 | .target( 29 | name: "TodoService", 30 | dependencies: ["DynamoDB"]), 31 | .testTarget( 32 | name: "TodoServiceTests", 33 | dependencies: ["TodoService"]) 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/README.md: -------------------------------------------------------------------------------- 1 | # TodoAPIGateway 2 | 3 | This package demonstrates how swift-aws-lambda can be used with aws-sdk-swift to 4 | implement a very simple Todo-Backend. As the persistent store we use a DynamoDB. 5 | 6 | https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html 7 | 8 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Sources/TodoAPIGateway/TodoController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NIOHTTP1 4 | import TodoService 5 | import LambdaRuntime 6 | 7 | class TodoController { 8 | 9 | let store : TodoStore 10 | 11 | static let sharedHeader = HTTPHeaders([ 12 | ("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE"), 13 | ("Access-Control-Allow-Origin" , "*"), 14 | ("Access-Control-Allow-Headers", "Content-Type"), 15 | ("Server", "Swift on AWS Lambda"), 16 | ]) 17 | 18 | init(store: TodoStore) { 19 | self.store = store 20 | } 21 | 22 | func listTodos(request: APIGateway.Request, context: Context) -> EventLoopFuture { 23 | return self.store.getTodos() 24 | .flatMapThrowing { (items) -> APIGateway.Response in 25 | return try APIGateway.Response( 26 | statusCode: .ok, 27 | headers: TodoController.sharedHeader, 28 | payload: items, 29 | encoder: self.createResponseEncoder(request)) 30 | } 31 | } 32 | 33 | struct NewTodo: Decodable { 34 | let title: String 35 | let order: Int? 36 | let completed: Bool? 37 | } 38 | 39 | func createTodo(request: APIGateway.Request, context: Context) -> EventLoopFuture { 40 | let newTodo: TodoItem 41 | do { 42 | let payload = try request.decodeBody(NewTodo.self) 43 | newTodo = TodoItem( 44 | id: UUID().uuidString.lowercased(), 45 | order: payload.order, 46 | title: payload.title, 47 | completed: payload.completed ?? false) 48 | } 49 | catch { 50 | return context.eventLoop.makeFailedFuture(error) 51 | } 52 | 53 | return self.store.createTodo(newTodo) 54 | .flatMapThrowing { (todo) -> APIGateway.Response in 55 | return try APIGateway.Response( 56 | statusCode: .created, 57 | headers: TodoController.sharedHeader, 58 | payload: todo, 59 | encoder: self.createResponseEncoder(request)) 60 | } 61 | } 62 | 63 | func deleteAll(request: APIGateway.Request, context: Context) -> EventLoopFuture { 64 | return self.store.deleteAllTodos() 65 | .flatMapThrowing { _ -> APIGateway.Response in 66 | return try APIGateway.Response( 67 | statusCode: .ok, 68 | headers: TodoController.sharedHeader, 69 | payload: [TodoItem](), 70 | encoder: self.createResponseEncoder(request)) 71 | } 72 | } 73 | 74 | func getTodo(request: APIGateway.Request, context: Context) -> EventLoopFuture { 75 | guard let id = request.pathParameters?["id"] else { 76 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(statusCode: .badRequest)) 77 | } 78 | 79 | return self.store.getTodo(id: id) 80 | .flatMapThrowing { (todo) -> APIGateway.Response in 81 | return try APIGateway.Response( 82 | statusCode: .ok, 83 | headers: TodoController.sharedHeader, 84 | payload: todo, 85 | encoder: self.createResponseEncoder(request)) 86 | } 87 | .flatMapErrorThrowing { (error) -> APIGateway.Response in 88 | switch error { 89 | case TodoError.notFound: 90 | return APIGateway.Response(statusCode: .notFound) 91 | default: 92 | throw error 93 | } 94 | } 95 | } 96 | 97 | func deleteTodo(request: APIGateway.Request, context: Context) -> EventLoopFuture { 98 | guard let id = request.pathParameters?["id"] else { 99 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(statusCode: .badRequest)) 100 | } 101 | 102 | return self.store.deleteTodos(ids: [id]) 103 | .flatMapThrowing { _ -> APIGateway.Response in 104 | return try APIGateway.Response( 105 | statusCode: .ok, 106 | headers: TodoController.sharedHeader, 107 | payload: [TodoItem](), 108 | encoder: self.createResponseEncoder(request)) 109 | } 110 | } 111 | 112 | func patchTodo(request: APIGateway.Request, context: Context) -> EventLoopFuture { 113 | guard let id = request.pathParameters?["id"] else { 114 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(statusCode: .badRequest)) 115 | } 116 | 117 | let patchTodo: PatchTodo 118 | do { 119 | patchTodo = try request.decodeBody(PatchTodo.self) 120 | } 121 | catch { 122 | return context.eventLoop.makeFailedFuture(error) 123 | } 124 | 125 | return self.store.patchTodo(id: id, patch: patchTodo) 126 | .flatMapThrowing { (todo) -> APIGateway.Response in 127 | return try APIGateway.Response( 128 | statusCode: .ok, 129 | headers: TodoController.sharedHeader, 130 | payload: todo, 131 | encoder: self.createResponseEncoder(request)) 132 | } 133 | } 134 | 135 | private func createResponseEncoder(_ request: APIGateway.Request) -> JSONEncoder { 136 | let encoder = JSONEncoder() 137 | 138 | guard let proto = request.headers["X-Forwarded-Proto"].first, 139 | let host = request.headers["Host"].first 140 | else 141 | { 142 | return encoder 143 | } 144 | 145 | if request.requestContext.apiId != "1234567890" { 146 | encoder.userInfo[.baseUrl] = URL(string: "\(proto)://\(host)/\(request.requestContext.stage)")! 147 | } 148 | else { //local 149 | encoder.userInfo[.baseUrl] = URL(string: "\(proto)://\(host)")! 150 | } 151 | 152 | return encoder 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Sources/TodoAPIGateway/main.swift: -------------------------------------------------------------------------------- 1 | import LambdaRuntime 2 | import NIO 3 | import Logging 4 | import Foundation 5 | import TodoService 6 | import AWSSDKSwiftCore 7 | 8 | LoggingSystem.bootstrap(StreamLogHandler.standardError) 9 | 10 | 11 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 12 | defer { try! group.syncShutdownGracefully() } 13 | let logger = Logger(label: "Lambda.TodoAPIGateway") 14 | 15 | do { 16 | logger.info("start runtime") 17 | 18 | let env = try Environment() 19 | let store = DynamoTodoStore( 20 | eventLoopGroup: group, 21 | tableName: "SwiftLambdaTodos", 22 | accessKeyId: env.accessKeyId, 23 | secretAccessKey: env.secretAccessKey, 24 | sessionToken: env.sessionToken, 25 | region: Region(rawValue: env.region)!) 26 | let controller = TodoController(store: store) 27 | 28 | logger.info("register function") 29 | 30 | let handler: Runtime.Handler 31 | switch env.handlerName { 32 | case "list": 33 | handler = APIGateway.handler(controller.listTodos) 34 | case "create": 35 | handler = APIGateway.handler(controller.createTodo) 36 | case "deleteAll": 37 | handler = APIGateway.handler(controller.deleteAll) 38 | case "getTodo": 39 | handler = APIGateway.handler(controller.getTodo) 40 | case "deleteTodo": 41 | handler = APIGateway.handler(controller.deleteTodo) 42 | case "patchTodo": 43 | handler = APIGateway.handler(controller.patchTodo) 44 | default: 45 | fatalError("Unexpected handler") 46 | } 47 | 48 | logger.info("starting runloop") 49 | 50 | let runtime = try Runtime.createRuntime(eventLoopGroup: group, environment: env, handler: handler) 51 | defer { try! runtime.syncShutdown() } 52 | try runtime.start().wait() 53 | } 54 | catch { 55 | logger.error("error: \(String(describing: error))") 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Sources/TodoService/DynamoTodoStore.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | import DynamoDB 3 | import AWSSDKSwiftCore 4 | 5 | public class DynamoTodoStore { 6 | 7 | let dynamo : DynamoDB 8 | let tableName: String 9 | let listName : String = "list" 10 | 11 | public init( 12 | eventLoopGroup: EventLoopGroup, 13 | tableName: String, 14 | accessKeyId: String, 15 | secretAccessKey: String, 16 | sessionToken: String?, 17 | region: Region) 18 | { 19 | self.dynamo = DynamoDB( 20 | accessKeyId: accessKeyId, 21 | secretAccessKey: secretAccessKey, 22 | sessionToken: sessionToken, 23 | region: region, 24 | eventLoopGroupProvider: .shared(eventLoopGroup)) 25 | self.tableName = tableName 26 | } 27 | 28 | } 29 | 30 | extension DynamoTodoStore: TodoStore { 31 | 32 | public func getTodos() -> EventLoopFuture<[TodoItem]> { 33 | return self.dynamo.query(.init( 34 | expressionAttributeValues: [":id" : .init(s: listName)], 35 | keyConditionExpression: "ListId = :id", 36 | tableName: tableName)) 37 | .map { (output) -> ([TodoItem]) in 38 | return output.items! 39 | .compactMap { (attributes) -> TodoItem? in 40 | return TodoItem(attributes: attributes) 41 | } 42 | .sorted { (t1, t2) -> Bool in 43 | switch (t1.order, t2.order) { 44 | case (.none, .none): 45 | return false 46 | case (.some(_), .none): 47 | return true 48 | case (.none, .some(_)): 49 | return false 50 | case (.some(let o1), .some(let o2)): 51 | return o1 < o2 52 | } 53 | } 54 | } 55 | } 56 | 57 | public func getTodo(id: String) -> EventLoopFuture { 58 | return self.dynamo.getItem(.init(key: ["ListId": .init(s: listName), "TodoId": .init(s: id)], tableName: tableName)) 59 | .flatMapThrowing { (output) throws -> TodoItem in 60 | guard let attributes = output.item else { 61 | throw TodoError.notFound 62 | } 63 | guard let todo = TodoItem(attributes: attributes) else { 64 | throw TodoError.missingAttributes 65 | } 66 | return todo 67 | } 68 | } 69 | 70 | public func createTodo(_ todo: TodoItem) -> EventLoopFuture { 71 | var attributes = todo.toDynamoItem() 72 | attributes["ListId"] = .init(s: self.listName) 73 | 74 | return self.dynamo.putItem(.init(item: attributes, tableName: tableName)) 75 | .map { _ in 76 | return todo 77 | } 78 | } 79 | 80 | public func patchTodo(id: String, patch: PatchTodo) -> EventLoopFuture { 81 | 82 | var updates: [String : DynamoDB.AttributeValueUpdate] = [:] 83 | if let title = patch.title { 84 | updates["Title"] = .init(action: .put, value: .init(s: title)) 85 | } 86 | 87 | if let order = patch.order { 88 | updates["Order"] = .init(action: .put, value: .init(n: String(order))) 89 | } 90 | 91 | if let completed = patch.completed { 92 | updates["Completed"] = .init(action: .put, value: .init(bool: completed)) 93 | } 94 | 95 | guard updates.count > 0 else { 96 | return self.getTodo(id: id) 97 | } 98 | 99 | let update = DynamoDB.UpdateItemInput( 100 | attributeUpdates: updates, 101 | key: ["ListId": .init(s: listName), "TodoId": .init(s: id)], 102 | returnValues: .allNew, 103 | tableName: tableName) 104 | 105 | return self.dynamo.updateItem(update) 106 | .flatMapThrowing { (output) -> TodoItem in 107 | guard let attributes = output.attributes else { 108 | throw TodoError.notFound 109 | } 110 | guard let todo = TodoItem(attributes: attributes) else { 111 | throw TodoError.missingAttributes 112 | } 113 | return todo 114 | } 115 | } 116 | 117 | public func deleteTodos(ids: [String]) -> EventLoopFuture { 118 | 119 | guard ids.count > 0 else { 120 | return self.dynamo.client.eventLoopGroup.next().makeSucceededFuture(Void()) 121 | } 122 | 123 | let writeRequests = ids.map { (id) in 124 | DynamoDB.WriteRequest(deleteRequest: .init(key: ["ListId": .init(s: listName), "TodoId": .init(s: id)])) 125 | } 126 | 127 | return self.dynamo.batchWriteItem(.init(requestItems: [tableName : writeRequests])) 128 | .map { _ in } 129 | } 130 | 131 | public func deleteAllTodos() -> EventLoopFuture { 132 | return self.getTodos() 133 | .flatMap { (todos) -> EventLoopFuture in 134 | let ids = todos.map() { $0.id } 135 | return self.deleteTodos(ids: ids) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Sources/TodoService/TodoError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Fabian Fett on 19.11.19. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum TodoError: Error { 11 | 12 | case notFound 13 | case missingAttributes 14 | case invalidRequest 15 | 16 | } 17 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Sources/TodoService/TodoItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import DynamoDB 3 | 4 | public struct TodoItem { 5 | 6 | public let id: String 7 | public let order: Int? 8 | 9 | /// Text to display 10 | public let title: String 11 | 12 | /// Whether completed or not 13 | public let completed: Bool 14 | 15 | public init(id: String, order: Int?, title: String, completed: Bool) { 16 | self.id = id 17 | self.order = order 18 | self.title = title 19 | self.completed = completed 20 | } 21 | } 22 | 23 | extension CodingUserInfoKey { 24 | public static let baseUrl = CodingUserInfoKey(rawValue: "de.fabianfett.TodoBackend.BaseURL")! 25 | } 26 | 27 | extension TodoItem : Codable { 28 | 29 | enum CodingKeys: String, CodingKey { 30 | case id 31 | case order 32 | case title 33 | case completed 34 | case url 35 | } 36 | 37 | public init(from decoder: Decoder) throws { 38 | let container = try decoder.container(keyedBy: CodingKeys.self) 39 | self.id = try container.decode(String.self, forKey: .id) 40 | self.title = try container.decode(String.self, forKey: .title) 41 | self.completed = try container.decode(Bool.self, forKey: .completed) 42 | self.order = try container.decodeIfPresent(Int.self, forKey: .order) 43 | } 44 | 45 | public func encode(to encoder: Encoder) throws { 46 | 47 | var container = encoder.container(keyedBy: CodingKeys.self) 48 | 49 | try container.encode(id, forKey: .id) 50 | try container.encode(order, forKey: .order) 51 | try container.encode(title, forKey: .title) 52 | try container.encode(completed, forKey: .completed) 53 | 54 | if let url = encoder.userInfo[.baseUrl] as? URL { 55 | let todoUrl = url.appendingPathComponent("/todos/\(id)") 56 | try container.encode(todoUrl, forKey: .url) 57 | } 58 | } 59 | 60 | } 61 | extension TodoItem : Equatable { } 62 | 63 | public func == (lhs: TodoItem, rhs: TodoItem) -> Bool { 64 | return lhs.id == rhs.id 65 | && lhs.order == rhs.order 66 | && lhs.title == rhs.title 67 | && lhs.completed == rhs.completed 68 | } 69 | 70 | extension TodoItem { 71 | 72 | func toDynamoItem() -> [String: DynamoDB.AttributeValue] { 73 | var result: [String: DynamoDB.AttributeValue] = [ 74 | "TodoId" : .init(s: self.id), 75 | "Title" : .init(s: self.title), 76 | "Completed": .init(bool: self.completed), 77 | ] 78 | 79 | if let order = order { 80 | result["Order"] = DynamoDB.AttributeValue(n: String(order)) 81 | } 82 | 83 | return result 84 | } 85 | 86 | init?(attributes: [String: DynamoDB.AttributeValue]) { 87 | guard let id = attributes["TodoId"]?.s, 88 | let title = attributes["Title"]?.s, 89 | let completed = attributes["Completed"]?.bool 90 | else 91 | { 92 | return nil 93 | } 94 | 95 | var order: Int? = nil 96 | if let orderString = attributes["Order"]?.n, let number = Int(orderString) { 97 | order = number 98 | } 99 | 100 | self.init(id: id, order: order, title: title, completed: completed) 101 | } 102 | 103 | } 104 | 105 | public struct PatchTodo: Codable { 106 | public let order : Int? 107 | public let title : String? 108 | public let completed: Bool? 109 | } 110 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Sources/TodoService/TodoStore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | 4 | public protocol TodoStore { 5 | 6 | func getTodos() -> EventLoopFuture<[TodoItem]> 7 | func getTodo(id: String) -> EventLoopFuture 8 | 9 | func createTodo(_ todo: TodoItem) -> EventLoopFuture 10 | 11 | func patchTodo(id: String, patch: PatchTodo) -> EventLoopFuture 12 | 13 | func deleteTodos(ids: [String]) -> EventLoopFuture 14 | func deleteAllTodos() -> EventLoopFuture 15 | 16 | } 17 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Tests/TodoAPIGatewayTests/TodoAPIGatewayTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class TodoAPIGatewayTests: XCTestCase { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/Tests/TodoServiceTests/DynamoTodoStoreTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import XCTest 4 | @testable import TodoService 5 | 6 | final class DynamoTodoStoreTests: XCTestCase { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/makefile: -------------------------------------------------------------------------------- 1 | # Example settings 2 | LAMBDA_NAME=TodoAPIGateway 3 | EXECUTABLE=$(LAMBDA_NAME) 4 | LAMBDA_ZIP=lambda.zip 5 | 6 | SWIFT_VERSION=5.2.1 7 | SWIFT_DOCKER_IMAGE=fabianfett/amazonlinux-swift:${SWIFT_VERSION}-amazonlinux2-dev 8 | 9 | clean_lambda: 10 | rm bootstrap || true 11 | rm lambda.zip || true 12 | 13 | build_lambda: 14 | docker run \ 15 | --rm \ 16 | --volume "$(shell pwd)/../..:/src" \ 17 | --workdir "/src/examples/$(LAMBDA_NAME)/" \ 18 | $(SWIFT_DOCKER_IMAGE) \ 19 | swift build -c release 20 | 21 | package_lambda: build_lambda 22 | cp .build/release/$(EXECUTABLE) ./bootstrap 23 | zip -r -j $(LAMBDA_ZIP) ./bootstrap 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/TodoAPIGateway/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | sam-app 5 | 6 | Sample SAM Template for sam-app 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Function: 11 | Timeout: 3 12 | 13 | Resources: 14 | 15 | DynamoDbFailedLoginsTable: 16 | Type: "AWS::DynamoDB::Table" 17 | Properties: 18 | BillingMode: PAY_PER_REQUEST 19 | AttributeDefinitions: 20 | - AttributeName: ListId 21 | AttributeType: S 22 | - AttributeName: TodoId 23 | AttributeType: S 24 | KeySchema: 25 | - AttributeName: ListId 26 | KeyType: HASH 27 | - AttributeName: TodoId 28 | KeyType: RANGE 29 | TableName: "SwiftLambdaTodos" 30 | 31 | APIGateway: 32 | Type: AWS::Serverless::Api 33 | Properties: 34 | StageName: test 35 | Cors: 36 | AllowMethods: "'OPTIONS,GET,POST,DELETE,PATCH'" 37 | AllowHeaders: "'Content-Type'" 38 | AllowOrigin : "'*'" 39 | AllowCredentials: "'*'" 40 | 41 | TodoAPIGatewayListFunction: 42 | Type: AWS::Serverless::Function 43 | Properties: 44 | CodeUri: lambda.zip 45 | Handler: "list" 46 | Runtime: provided 47 | Layers: 48 | - arn:aws:lambda:eu-central-1:426836788079:layer:Swift:11 49 | Policies: 50 | - DynamoDBReadPolicy: 51 | TableName: "SwiftLambdaTodos" 52 | Events: 53 | Api: 54 | Type: Api 55 | Properties: 56 | RestApiId: !Ref APIGateway 57 | Path: /todos 58 | Method: GET 59 | 60 | TodoAPIGatewayCreateFunction: 61 | Type: AWS::Serverless::Function 62 | Properties: 63 | CodeUri: lambda.zip 64 | Handler: "create" 65 | Runtime: provided 66 | Layers: 67 | - arn:aws:lambda:eu-central-1:426836788079:layer:Swift:11 68 | Policies: 69 | - DynamoDBCrudPolicy: 70 | TableName: "SwiftLambdaTodos" 71 | Events: 72 | Api: 73 | Type: Api 74 | Properties: 75 | RestApiId: !Ref APIGateway 76 | Path: /todos 77 | Method: POST 78 | 79 | TodoAPIGatewayDeleteAllFunction: 80 | Type: AWS::Serverless::Function 81 | Properties: 82 | CodeUri: lambda.zip 83 | Handler: "deleteAll" 84 | Runtime: provided 85 | Layers: 86 | - arn:aws:lambda:eu-central-1:426836788079:layer:Swift:11 87 | Policies: 88 | - DynamoDBCrudPolicy: 89 | TableName: "SwiftLambdaTodos" 90 | Events: 91 | Api: 92 | Type: Api 93 | Properties: 94 | RestApiId: !Ref APIGateway 95 | Path: /todos 96 | Method: DELETE 97 | 98 | TodoAPIGatewayGetTodo: 99 | Type: AWS::Serverless::Function 100 | Properties: 101 | CodeUri: lambda.zip 102 | Handler: "getTodo" 103 | Runtime: provided 104 | Layers: 105 | - arn:aws:lambda:eu-central-1:426836788079:layer:Swift:11 106 | Policies: 107 | - DynamoDBReadPolicy: 108 | TableName: "SwiftLambdaTodos" 109 | Events: 110 | Api: 111 | Type: Api 112 | Properties: 113 | RestApiId: !Ref APIGateway 114 | Path: /todos/{id} 115 | Method: GET 116 | 117 | TodoAPIGatewayDeleteTodo: 118 | Type: AWS::Serverless::Function 119 | Properties: 120 | CodeUri: lambda.zip 121 | Handler: "deleteTodo" 122 | Runtime: provided 123 | Layers: 124 | - arn:aws:lambda:eu-central-1:426836788079:layer:Swift:11 125 | Policies: 126 | - DynamoDBCrudPolicy: 127 | TableName: "SwiftLambdaTodos" 128 | Events: 129 | Api: 130 | Type: Api 131 | Properties: 132 | RestApiId: !Ref APIGateway 133 | Path: /todos/{id} 134 | Method: DELETE 135 | 136 | TodoAPIGatewayPatchTodo: 137 | Type: AWS::Serverless::Function 138 | Properties: 139 | CodeUri: lambda.zip 140 | Handler: "patchTodo" 141 | Runtime: provided 142 | Layers: 143 | - arn:aws:lambda:eu-central-1:426836788079:layer:Swift:11 144 | Policies: 145 | - DynamoDBCrudPolicy: 146 | TableName: "SwiftLambdaTodos" 147 | Events: 148 | Api: 149 | Type: Api 150 | Properties: 151 | RestApiId: !Ref APIGateway 152 | Path: /todos/{id} 153 | Method: PATCH 154 | -------------------------------------------------------------------------------- /examples/URLRequestWithSession/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "48e284d1ea6d0e8baac1af1c4ad8bd298670caf6", 10 | "version": "1.0.1" 11 | } 12 | }, 13 | { 14 | "package": "swift-base64-kit", 15 | "repositoryURL": "https://github.com/fabianfett/swift-base64-kit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "3ffa48a7047fc9ac6581cd53ab1df29466d8f13b", 19 | "version": "0.2.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-log", 24 | "repositoryURL": "https://github.com/apple/swift-log.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", 28 | "version": "1.2.0" 29 | } 30 | }, 31 | { 32 | "package": "swift-nio", 33 | "repositoryURL": "https://github.com/apple/swift-nio.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "3f04a5c056fa52e01b9ef43240988de792461e81", 37 | "version": "2.11.1" 38 | } 39 | }, 40 | { 41 | "package": "swift-nio-extras", 42 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "53808818c2015c45247cad74dc05c7a032c96a2f", 46 | "version": "1.3.2" 47 | } 48 | }, 49 | { 50 | "package": "swift-nio-ssl", 51 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "b015dcbe871d9c4d36bb343b0d37293521e3df07", 55 | "version": "2.4.5" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /examples/URLRequestWithSession/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "URLRequestWithSession", 8 | dependencies: [ 9 | .package(path: "../../"), 10 | ], 11 | targets: [ 12 | .target( 13 | name: "URLRequestWithSession", 14 | dependencies: ["LambdaRuntime"] 15 | ), 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /examples/URLRequestWithSession/Sources/URLRequestWithSession/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import LambdaRuntime 3 | import Dispatch 4 | import NIO 5 | import NIOHTTP1 6 | #if os(Linux) 7 | import FoundationNetworking 8 | #endif 9 | 10 | let session = URLSession(configuration: .default) 11 | func sendEcho(session: URLSession, completion: @escaping (Result) -> ()) { 12 | 13 | let urlRequest = URLRequest(url: URL(string: "https://postman-echo.com/get?foo1=bar1&foo2=bar2")!) 14 | 15 | let task = session.dataTask(with: urlRequest) { (data, response, error) in 16 | if let error = error { 17 | completion(.failure(error)) 18 | } 19 | 20 | guard let response = response as? HTTPURLResponse else { 21 | fatalError("unexpected response type") 22 | } 23 | 24 | let status = HTTPResponseStatus(statusCode: response.statusCode) 25 | 26 | completion(.success(status)) 27 | } 28 | task.resume() 29 | } 30 | 31 | func echoCall(input: ByteBuffer?, context: Context) -> EventLoopFuture { 32 | 33 | let promise = context.eventLoop.makePromise(of: ByteBuffer?.self) 34 | 35 | sendEcho(session: session) { (result) in 36 | switch result { 37 | case .success(let status): 38 | context.logger.info("HTTP call with NSURLSession success: \(status)") 39 | promise.succeed(nil) 40 | case .failure(let error): 41 | context.logger.error("HTTP call with NSURLSession failed: \(error)") 42 | promise.fail(error) 43 | } 44 | } 45 | 46 | return promise.futureResult 47 | } 48 | 49 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 50 | defer { try! group.syncShutdownGracefully() } 51 | 52 | do { 53 | let runtime = try Runtime.createRuntime(eventLoopGroup: group, handler: echoCall) 54 | defer { try! runtime.syncShutdown() } 55 | 56 | try runtime.start().wait() 57 | } 58 | catch { 59 | print("\(error)") 60 | } 61 | -------------------------------------------------------------------------------- /examples/URLRequestWithSession/makefile: -------------------------------------------------------------------------------- 1 | # Example settings 2 | LAMBDA_NAME=URLRequestWithSession 3 | EXECUTABLE=$(LAMBDA_NAME) 4 | LAMBDA_ZIP=lambda.zip 5 | 6 | SWIFT_VERSION=5.2.1 7 | SWIFT_DOCKER_IMAGE=fabianfett/amazonlinux-swift:${SWIFT_VERSION}-amazonlinux2-dev 8 | 9 | clean_lambda: 10 | rm bootstrap || true 11 | rm lambda.zip || true 12 | 13 | build_lambda: 14 | docker run \ 15 | --rm \ 16 | --volume "$(shell pwd)/../..:/src" \ 17 | --workdir "/src/examples/$(LAMBDA_NAME)/" \ 18 | $(SWIFT_DOCKER_IMAGE) \ 19 | swift build -c release 20 | 21 | package_lambda: build_lambda 22 | cp .build/release/$(EXECUTABLE) ./bootstrap 23 | zip -r -j $(LAMBDA_ZIP) ./bootstrap 24 | -------------------------------------------------------------------------------- /examples/URLRequestWithSession/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | sam-app 5 | 6 | Sample SAM Template for sam-app 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Function: 11 | Timeout: 3 12 | 13 | Resources: 14 | 15 | EchoCallFunction: 16 | Type: AWS::Serverless::Function 17 | Properties: 18 | CodeUri: lambda.zip 19 | Handler: "echoCall" 20 | Runtime: provided 21 | Layers: 22 | - arn:aws:lambda:eu-central-1:426836788079:layer:Swift:11 23 | 24 | 25 | 26 | --------------------------------------------------------------------------------