From e1ee4ea0ddff4e1252367cafa85fa0a7fd770ee5 Mon Sep 17 00:00:00 2001 From: tom doron Date: Wed, 25 Mar 2020 15:58:57 -0700 Subject: [PATCH 1/7] initial commit --- .dockerignore | 1 + .gitignore | 11 ++ .mailmap | 3 + .swiftformat | 14 ++ CODE_OF_CONDUCT.md | 55 +++++++ CONTRIBUTING.md | 68 +++++++++ CONTRIBUTORS.txt | 18 +++ Dockerfile | 19 +++ LICENSE.txt | 202 ++++++++++++++++++++++++++ Package.swift | 22 +++ README.md | 55 +++++++ Sources/MyLambda/main.swift | 182 +++++++++++++++++++++++ scripts/deploy.sh | 48 ++++++ scripts/generate_contributors_list.sh | 39 +++++ scripts/package.sh | 28 ++++ scripts/sanity.sh | 128 ++++++++++++++++ 16 files changed, 893 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .mailmap create mode 100644 .swiftformat create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTORS.txt create mode 100644 Dockerfile create mode 100644 LICENSE.txt create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/MyLambda/main.swift create mode 100755 scripts/deploy.sh create mode 100755 scripts/generate_contributors_list.sh create mode 100755 scripts/package.sh create mode 100755 scripts/sanity.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..24e5b0a1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.build diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c4f353cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +Package.pins +*.pem +/docs +Package.resolved +.podspecs +function.zip +.swiftpm diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000..c8ad743d --- /dev/null +++ b/.mailmap @@ -0,0 +1,3 @@ +Tomer Doron +Tomer Doron +Tomer Doron diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 00000000..19ee383f --- /dev/null +++ b/.swiftformat @@ -0,0 +1,14 @@ +# file options + +--swiftversion 5.1 +--exclude .build + +# format options + +--self insert +--patternlet inline +--stripunusedargs unnamed-only +--comments ignore +--ifdef no-indent + +# rules diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..ebdb302e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,55 @@ +# Code of Conduct +To be a truly great community, SwiftAWSLambdaRuntime needs to welcome developers from all walks of life, +with different backgrounds, and with a wide range of experience. A diverse and friendly +community will have more great ideas, more unique perspectives, and produce more great +code. We will work diligently to make the SwiftAWSLambdaRuntime community welcoming to everyone. + +To give clarity of what is expected of our members, SwiftAWSLambdaRuntime has adopted the code of conduct +defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source +communities, and we think it articulates our values well. The full text is copied below: + +### Contributor Code of Conduct v1.3 +As contributors and maintainers of this project, and in the interest of fostering an open and +welcoming community, we pledge to respect all people who contribute through reporting +issues, posting feature requests, updating documentation, submitting pull requests or patches, +and other activities. + +We are committed to making participation in this project a harassment-free experience for +everyone, regardless of level of experience, gender, gender identity and expression, sexual +orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or +nationality. + +Examples of unacceptable behavior by participants include: +- The use of sexualized language or imagery +- Personal attacks +- Trolling or insulting/derogatory comments +- Public or private harassment +- Publishing other’s private information, such as physical or electronic addresses, without explicit permission +- Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, +commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of +Conduct, or to ban temporarily or permanently any contributor for other behaviors that they +deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to fairly and +consistently applying these principles to every aspect of managing this project. Project +maintainers who do not follow or enforce the Code of Conduct may be permanently removed +from the project team. + +This code of conduct applies both within project spaces and in public spaces when an +individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by +contacting a project maintainer at [swift-server-conduct@group.apple.com](mailto:swift-server-conduct@group.apple.com). All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and appropriate to the +circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter +of an incident. + +*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](https://contributor-covenant.org/version/1/3/0/).* + +### Reporting +A working group of community members is committed to promptly addressing any [reported issues](mailto:swift-server-conduct@group.apple.com). +Working group members are volunteers appointed by the project lead, with a +preference for individuals with varied backgrounds and perspectives. Membership is expected +to change regularly, and may grow or shrink. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c9062d0e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,68 @@ +## Legal + +By submitting a pull request, you represent that you have the right to license +your contribution to Apple and the community, and agree by submitting the patch +that your contributions are licensed under the Apache 2.0 license (see +`LICENSE.txt`). + + +## How to submit a bug report + +Please ensure to specify the following: + +* SwiftAWSLambdaRuntime commit hash +* Contextual information (e.g. what you were trying to achieve with SwiftAWSLambdaRuntime) +* Simplest possible steps to reproduce + * More complex the steps are, lower the priority will be. + * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. +* Anything that might be relevant in your opinion, such as: + * Swift version or the output of `swift --version` + * OS version and the output of `uname -a` + * Network configuration + + +### Example + +``` +SwiftAWSLambdaRuntime commit hash: 22ec043dc9d24bb011b47ece4f9ee97ee5be2757 + +Context: +While load testing my Lambda written with SwiftAWSLambdaRuntime, I noticed +that one file descriptor is leaked per request. + +Steps to reproduce: +1. ... +2. ... +3. ... +4. ... + +$ swift --version +Swift version 4.0.2 (swift-4.0.2-RELEASE) +Target: x86_64-unknown-linux-gnu + +Operating system: Ubuntu Linux 16.04 64-bit + +$ uname -a +Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux + +My system has IPv6 disabled. +``` + +## Writing a Patch + +A good SwiftAWSLambdaRuntime patch is: + +1. Concise, and contains as few changes as needed to achieve the end result. +2. Tested, ensuring that any tests provided failed before the patch and pass after it. +3. Documented, adding API documentation as needed to cover new functions and properties. +4. Accompanied by a great commit message, using our commit message template. + +### Commit Message Template + +We require that your commit messages match our template. The easiest way to do that is to get git to help you by explicitly using the template. To do that, `cd` to the root of our repository and run: + + git config commit.template dev/git.commit.template + +## How to contribute your work + +Please open a pull request at https://github.com/apple/swift-nio. Make sure the CI passes, and then wait for code review. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt new file mode 100644 index 00000000..8668e4d4 --- /dev/null +++ b/CONTRIBUTORS.txt @@ -0,0 +1,18 @@ +For the purpose of tracking copyright, this is the list of individuals and +organizations who have contributed source code to SwiftAWSLambdaRuntime. + +For employees of an organization/company where the copyright of work done +by employees of that company is held by the company itself, only the company +needs to be listed here. + +## COPYRIGHT HOLDERS + +- Apple Inc. (all contributors with '@apple.com') + +### Contributors + +- Tomer Doron + +**Updating this list** + +Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..638fb33b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM fabianfett/amazonlinux-swift:5.2-branch-amazonlinux2 + +RUN yum -y install \ + git \ + libuuid-devel \ + libicu-devel \ + libedit-devel \ + libxml2-devel \ + sqlite-devel \ + python-devel \ + ncurses-devel \ + curl-devel \ + tzdata \ + libtool \ + libatomic\ + gcc-c++ \ + jq \ + tar \ + zip diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..a1159497 --- /dev/null +++ b/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version:5.2 + +import PackageDescription + +let package = Package( + name: "my-lambda", + platforms: [ + .macOS(.v10_13), + ], + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]), + ], + dependencies: [ + .package(url: "git@github.com:swift-server/swift-aws-lambda-runtime.git", .branch("master")), + ], + targets: [ + // lambda code is abstracted into a library since we cant have a test target depend on an executable + .target(name: "MyLambda", dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 00000000..0c33c7e3 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# SwiftAWSLambdaRuntimeSample + +This repository is a deployable example demonstrating how to package and deploy +a Swift based Lambda to AWS. + +Steps to deploy this sample to ASW: + +* Login to AWS Console and create an AWS Lambda with the following settings: + * Runtime: Custom runtime + * Handler: Can be any string, does not matter in this case + + +* Build, package and deploy the Lambda + + ``` + ./scripts/deploy.sh + ``` + + This script assumes you have AWS CLI installed and credentials setup in `~/.aws/credentials` + +* Test it with the following example payloads: + + `` + { + "requestId": "1", + "error": "none" + } + `` + + or + + `` + { + "requestId": "2", + "error": "managed" + } + `` + + or + + `` + { + "requestId": "3", + "error": "boom" + } + `` + + or + + `` + { + "requestId": "4", + "error": "fatal" + } + `` diff --git a/Sources/MyLambda/main.swift b/Sources/MyLambda/main.swift new file mode 100644 index 00000000..fec72fa1 --- /dev/null +++ b/Sources/MyLambda/main.swift @@ -0,0 +1,182 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import NIO + +// Lambda.run(StringLambda) +Lambda.run(CodableLambda) +// Lambda.run(APIGatewayProxyLambda()) +// Lambda.run(FakeAPIGatewayProxyLambda()) + +// MARK: - Lambda Samples + +func StringLambda(context: Lambda.Context, request: String, callback: (Result) -> Void) { + context.logger.debug("hello, string!") + callback(.success("hello, world!")) +} + +func CodableLambda(context: Lambda.Context, request: Request, callback: (Result) -> Void) { + context.logger.info("hello, json!") + switch request.error { + case .none: + callback(.success(Response(awsRequestId: context.requestId, requestId: request.requestId, status: .ok))) + case .managed: + callback(.success(Response(awsRequestId: context.requestId, requestId: request.requestId, status: .error))) + case .unmanaged(let error): + callback(.failure(UnmanagedError(description: error))) + case .fatal: + fatalError("crash!") + } +} + +struct APIGatewayProxyLambda: EventLoopLambdaHandler { + public typealias In = APIGatewayRequest + public typealias Out = APIGatewayResponse + + public init() {} + + public func handle(context: Lambda.Context, payload: APIGatewayRequest) -> EventLoopFuture { + context.logger.debug("hello, api gateway!") + return context.eventLoop.makeSucceededFuture(APIGatewayResponse(statusCode: 200, + headers: nil, + multiValueHeaders: nil, + body: "hello, world!", + isBase64Encoded: false)) + } +} + +struct FakeAPIGatewayProxyLambda: EventLoopLambdaHandler { + public typealias In = String + public typealias Out = String + + public init() {} + + public func handle(context: Lambda.Context, payload: String) -> EventLoopFuture { + context.logger.debug("hello, string!") + return context.eventLoop.makeSucceededFuture("{ \"statusCode\": 200, \"body\": \"hello, world!\" }") + } +} + +struct Request: Codable { + let requestId: String + let error: Error + + public init(requestId: String, error: Error? = nil) { + self.requestId = requestId + self.error = error ?? .none + } + + public enum Error: Codable, RawRepresentable { + case none + case managed + case unmanaged(String) + case fatal + + public init?(rawValue: String) { + switch rawValue { + case "none": + self = .none + case "managed": + self = .managed + case "fatal": + self = .fatal + default: + self = .unmanaged(rawValue) + } + } + + public var rawValue: String { + switch self { + case .none: + return "none" + case .managed: + return "managed" + case .fatal: + return "fatal" + case .unmanaged(let error): + return error + } + } + } +} + +struct Response: Codable { + let awsRequestId: String + let requestId: String + let status: Status + + public init(awsRequestId: String, requestId: String, status: Status) { + self.awsRequestId = awsRequestId + self.requestId = requestId + self.status = status + } + + public enum Status: Int, Codable { + case ok + case error + } +} + +struct UnmanagedError: Error { + let description: String +} + +struct APIGatewayRequest: Codable { + let resource: String + let path: String + let httpMethod: String? + let headers: [String: String]? + let multiValueHeaders: [String: [String]]? + let queryStringParameters: [String: String]? + let multiValueQueryStringParameters: [String: [String]]? + let pathParameters: [String: String]? + let stageVariables: [String: String]? + let requestContext: Context? + let body: String? + let isBase64Encoded: Bool? + + struct Context: Codable { + let accountId: String? + let resourceId: String? + let stage: String? + let requestId: String? + let identity: Identity? + let resourcePath: String? + let httpMethod: String? + let apiId: String + } + + struct Identity: Codable { + let cognitoIdentityPoolId: String? + let accountId: String? + let cognitoIdentityId: String? + let caller: String? + let apiKey: String? + let sourceIp: String? + let cognitoAuthenticationType: String? + let cognitoAuthenticationProvider: String? + let userArn: String? + let userAgent: String? + let user: String? + } +} + +struct APIGatewayResponse: Codable { + let statusCode: Int + let headers: [String: String]? + let multiValueHeaders: [String: [String]]? + let body: String? + let isBase64Encoded: Bool? +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 00000000..625ab760 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,48 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +lambda_name=SwiftSample +s3_bucket=swift-lambda-test +executable=$(swift package dump-package | sed -e 's|: null|: ""|g' | jq '.products[] | (select(.type.executable)) | .name' | sed -e 's|"||g') + +set -eu + +echo "-------------------------------------------------------------------------" +echo "preparing docker build image" +echo "-------------------------------------------------------------------------" +docker build . -t builder + +echo "-------------------------------------------------------------------------" +echo "updating code" +echo "-------------------------------------------------------------------------" +swift package update + +echo "-------------------------------------------------------------------------" +echo "building lambda executable" +echo "-------------------------------------------------------------------------" +docker run --rm -v `pwd`:/workspace -w /workspace builder bash -cl "swift build -c release -Xswiftc -g" +echo "done" + +echo "-------------------------------------------------------------------------" +echo "packaging lambda" +echo "-------------------------------------------------------------------------" +docker run --rm -v `pwd`:/workspace -w /workspace builder bash -cl "./scripts/package.sh $executable" + +echo "-------------------------------------------------------------------------" +echo "uploading to s3" +echo "-------------------------------------------------------------------------" + +aws s3 cp .build/lambda/lambda.zip s3://$s3_bucket/ +aws lambda update-function-code --function $lambda_name --s3-bucket $s3_bucket --s3-key lambda.zip diff --git a/scripts/generate_contributors_list.sh b/scripts/generate_contributors_list.sh new file mode 100755 index 00000000..d745e21e --- /dev/null +++ b/scripts/generate_contributors_list.sh @@ -0,0 +1,39 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +set -eu +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) + +cat > "$here/../CONTRIBUTORS.txt" <<- EOF + For the purpose of tracking copyright, this is the list of individuals and + organizations who have contributed source code to SwiftAWSLambdaRuntime. + + For employees of an organization/company where the copyright of work done + by employees of that company is held by the company itself, only the company + needs to be listed here. + + ## COPYRIGHT HOLDERS + + - Apple Inc. (all contributors with '@apple.com') + + ### Contributors + + $contributors + + **Updating this list** + + Please do not edit this file manually. It is generated using \`./scripts/generate_contributors_list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\` +EOF diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100755 index 00000000..ed9ac6b7 --- /dev/null +++ b/scripts/package.sh @@ -0,0 +1,28 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + + +set -eu + +executable=$1 + +target=.build/lambda +rm -rf "$target" +mkdir -p "$target" +cp ".build/release/$executable" "$target/" +cp -Pv /usr/lib/swift/linux/lib*so* /usr/lib64/libatomic.so.1* "$target" +cd "$target" +ln -s "$executable" "bootstrap" +zip --symlinks lambda.zip * diff --git a/scripts/sanity.sh b/scripts/sanity.sh new file mode 100755 index 00000000..d5f9d4f6 --- /dev/null +++ b/scripts/sanity.sh @@ -0,0 +1,128 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +set -eu + +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +function replace_acceptable_years() { + # this needs to replace all acceptable forms with 'YEARS' + sed -e 's/2017-2018/YEARS/' -e 's/2017-2020/YEARS/' -e 's/2019/YEARS/' -e 's/2020/YEARS/' +} + +printf "=> Checking format... " +FIRST_OUT="$(git status --porcelain)" +swiftformat . > /dev/null 2>&1 +SECOND_OUT="$(git status --porcelain)" +if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then + printf "\033[0;31mformatting issues!\033[0m\n" + git --no-pager diff + exit 1 +else + printf "\033[0;32mokay.\033[0m\n" +fi + +printf "=> Checking license headers\n" +tmp=$(mktemp /tmp/.swift-aws-lambda-sanity_XXXXXX) + +for language in swift-or-c bash dtrace; do + printf " * $language... " + declare -a matching_files + declare -a exceptions + expections=( ) + matching_files=( -name '*' ) + case "$language" in + swift-or-c) + exceptions=( -name Package.swift ) + matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' ) + cat > "$tmp" <<"EOF" +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +EOF + ;; + bash) + matching_files=( -name '*.sh' ) + cat > "$tmp" <<"EOF" +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## +EOF + ;; + dtrace) + matching_files=( -name '*.d' ) + cat > "$tmp" <<"EOF" +#!/usr/sbin/dtrace -q -s +/*===----------------------------------------------------------------------===* + * + * This source file is part of the SwiftAWSLambdaRuntime open source project + * + * Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors + * Licensed under Apache License v2.0 + * + * See LICENSE.txt for license information + * See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors + * + * SPDX-License-Identifier: Apache-2.0 + * + *===----------------------------------------------------------------------===*/ +EOF + ;; + *) + echo >&2 "ERROR: unknown language '$language'" + ;; + esac + + expected_lines=$(cat "$tmp" | wc -l) + expected_sha=$(cat "$tmp" | shasum) + + ( + cd "$here/.." + find . \ + \( \! -path './.build/*' -a \ + \( "${matching_files[@]}" \) -a \ + \( \! \( "${exceptions[@]}" \) \) \) | while read line; do + if [[ "$(cat "$line" | replace_acceptable_years | head -n $expected_lines | shasum)" != "$expected_sha" ]]; then + printf "\033[0;31mmissing headers in file '$line'!\033[0m\n" + diff -u <(cat "$line" | replace_acceptable_years | head -n $expected_lines) "$tmp" + exit 1 + fi + done + printf "\033[0;32mokay.\033[0m\n" + ) +done + +rm "$tmp" From 40d05992f7032da52a881c95a1a9c14b2951cb81 Mon Sep 17 00:00:00 2001 From: tomer doron Date: Thu, 2 Apr 2020 11:01:27 -0700 Subject: [PATCH 2/7] breakdown examples intro separate targets (#1) motivation: easier to reason about examples changes: * move each example to its own target * update deployment script to prompt for desired target when multiple targets exist * add code comments on what the example is about * add feature-reach example (CurrencyExchange) to help testing use of Foundation features that require curl and libxml --- Package.swift | 29 ++- Sources/APIGateway/main.swift | 85 +++++++++ Sources/Benchmark/main.swift | 31 ++++ Sources/CurrencyExchange/main.swift | 247 ++++++++++++++++++++++++++ Sources/ErrorHandling/main.swift | 101 +++++++++++ Sources/HelloWorld/main.swift | 20 +++ Sources/MyLambda/main.swift | 182 ------------------- scripts/deploy.sh | 43 +++-- scripts/generate_contributors_list.sh | 2 +- scripts/package.sh | 2 +- scripts/sanity.sh | 4 +- 11 files changed, 543 insertions(+), 203 deletions(-) create mode 100644 Sources/APIGateway/main.swift create mode 100644 Sources/Benchmark/main.swift create mode 100644 Sources/CurrencyExchange/main.swift create mode 100644 Sources/ErrorHandling/main.swift create mode 100644 Sources/HelloWorld/main.swift delete mode 100644 Sources/MyLambda/main.swift diff --git a/Package.swift b/Package.swift index a1159497..628eb531 100644 --- a/Package.swift +++ b/Package.swift @@ -3,19 +3,40 @@ import PackageDescription let package = Package( - name: "my-lambda", + name: "swift-aws-lambda-runtime-samples", platforms: [ .macOS(.v10_13), ], products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), + // introductory example + .executable(name: "HelloWorld", targets: ["HelloWorld"]), + // good for benchmarking + .executable(name: "Benchmark", targets: ["Benchmark"]), + // demonstrate different types of error handling + .executable(name: "ErrorHandling", targets: ["ErrorHandling"]), + // demostrate how to integrate with AWS API Gateway + .executable(name: "APIGateway", targets: ["APIGateway"]), + // fully featured example with domain specific business logic + .executable(name: "CurrencyExchange", targets: ["CurrencyExchange"]), + ], dependencies: [ .package(url: "git@github.com:swift-server/swift-aws-lambda-runtime.git", .branch("master")), ], targets: [ - // lambda code is abstracted into a library since we cant have a test target depend on an executable - .target(name: "MyLambda", dependencies: [ + .target(name: "HelloWorld", dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ]), + .target(name: "Benchmark", dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ]), + .target(name: "ErrorHandling", dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ]), + .target(name: "APIGateway", dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ]), + .target(name: "CurrencyExchange", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), ]), ] diff --git a/Sources/APIGateway/main.swift b/Sources/APIGateway/main.swift new file mode 100644 index 00000000..e3606640 --- /dev/null +++ b/Sources/APIGateway/main.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import NIO + +// MARK: - Run Lambda + +Lambda.run(APIGatewayProxyLambda()) + +// MARK: - Handler, Request and Response + +// FIXME: Use proper Event abstractions once added to AWSLambdaRuntime +struct APIGatewayProxyLambda: EventLoopLambdaHandler { + public typealias In = APIGatewayRequest + public typealias Out = APIGatewayResponse + + public func handle(context: Lambda.Context, payload: APIGatewayRequest) -> EventLoopFuture { + context.logger.debug("hello, api gateway!") + return context.eventLoop.makeSucceededFuture(APIGatewayResponse(statusCode: 200, + headers: nil, + multiValueHeaders: nil, + body: "hello, world!", + isBase64Encoded: false)) + } +} + +struct APIGatewayRequest: Codable { + let resource: String + let path: String + let httpMethod: String? + let headers: [String: String]? + let multiValueHeaders: [String: [String]]? + let queryStringParameters: [String: String]? + let multiValueQueryStringParameters: [String: [String]]? + let pathParameters: [String: String]? + let stageVariables: [String: String]? + let requestContext: Context? + let body: String? + let isBase64Encoded: Bool? + + struct Context: Codable { + let accountId: String? + let resourceId: String? + let stage: String? + let requestId: String? + let identity: Identity? + let resourcePath: String? + let httpMethod: String? + let apiId: String + } + + struct Identity: Codable { + let cognitoIdentityPoolId: String? + let accountId: String? + let cognitoIdentityId: String? + let caller: String? + let apiKey: String? + let sourceIp: String? + let cognitoAuthenticationType: String? + let cognitoAuthenticationProvider: String? + let userArn: String? + let userAgent: String? + let user: String? + } +} + +struct APIGatewayResponse: Codable { + let statusCode: Int + let headers: [String: String]? + let multiValueHeaders: [String: [String]]? + let body: String? + let isBase64Encoded: Bool? +} diff --git a/Sources/Benchmark/main.swift b/Sources/Benchmark/main.swift new file mode 100644 index 00000000..2a61d9d3 --- /dev/null +++ b/Sources/Benchmark/main.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import NIO + +// If you would like to benchmark Swift's Lambda Runtime, +// use this example which is more performant. +// `EventLoopLambdaHandler` does not offload the Lambda processing to a separate thread +// while the closure-based handlers do. +Lambda.run(BenchmarkHandler()) + +struct BenchmarkHandler: EventLoopLambdaHandler { + typealias In = String + typealias Out = String + + func handle(context: Lambda.Context, payload: String) -> EventLoopFuture { + context.eventLoop.makeSucceededFuture("hello, world!") + } +} diff --git a/Sources/CurrencyExchange/main.swift b/Sources/CurrencyExchange/main.swift new file mode 100644 index 00000000..22c30563 --- /dev/null +++ b/Sources/CurrencyExchange/main.swift @@ -0,0 +1,247 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import Dispatch +import Foundation +#if canImport(FoundationNetworking) && canImport(FoundationXML) +import FoundationNetworking +import FoundationXML +#endif +import Logging + +// MARK: - Run Lambda + +Lambda.run { (context: Lambda.Context, _: Request, callback: @escaping (Result<[Exchange], Error>) -> Void) in + let calculator = ExchangeRatesCalculator() + calculator.run(logger: context.logger, callback: callback) +} + +// MARK: - Business Logic + +// This is a contrived example performing currency exchange rate lookup and conversion using URLSession and XML parsing +struct ExchangeRatesCalculator { + static let currencies = ["EUR", "USD", "JPY"] + static let currenciesEmojies = [ + "EUR": "💶", + "JPY": "💴", + "USD": "💵", + ] + + let locale: Locale + let calendar: Calendar + + init() { + // This is data from HMRC, the UK tax authority. Therefore we want to use their locale when interpreting data from the server. + self.locale = Locale(identifier: "en_GB") + // Use the UK calendar, not the system one. + var calendar = self.locale.calendar + calendar.timeZone = TimeZone(identifier: "UTC")! + self.calendar = calendar + } + + func run(logger: Logger, callback: @escaping (Result<[Exchange], Swift.Error>) -> Void) { + let startDate = Date() + let months = (1 ... 12).map { + self.calendar.date(byAdding: DateComponents(month: -$0), to: startDate)! + } + + self.download(logger: logger, + months: months, + monthIndex: months.startIndex, + currencies: Self.currencies, + state: [:]) { result in + + switch result { + case .failure(let error): + return callback(.failure(error)) + case .success(let downloadedDataByMonth): + logger.debug("Downloads complete") + + var result = [Exchange]() + var previousData: [String: Decimal?] = [:] + for (_, exchangeRateData) in downloadedDataByMonth.filter({ $1.period != nil }).sorted(by: { $0.key < $1.key }) { + for (currencyCode, rate) in exchangeRateData.ratesByCurrencyCode.sorted(by: { $0.key < $1.key }) { + if let rate = rate, let currencyEmoji = Self.currenciesEmojies[currencyCode] { + let change: Exchange.Change + switch previousData[currencyCode] { + case .some(.some(let previousRate)) where rate > previousRate: + change = .up + case .some(.some(let previousRate)) where rate < previousRate: + change = .down + case .some(.some(let previousRate)) where rate == previousRate: + change = .none + default: + change = .unknown + } + result.append(Exchange(date: exchangeRateData.period!.start, + from: .init(symbol: "GBP", emoji: "💷"), + to: .init(symbol: currencyCode, emoji: currencyEmoji), + rate: rate, + change: change)) + } + } + previousData = exchangeRateData.ratesByCurrencyCode + } + callback(.success(result)) + } + } + } + + private func download(logger: Logger, + months: [Date], + monthIndex: Array.Index, + currencies: [String], + state: [Date: ExchangeRates], + callback: @escaping ((Result<[Date: ExchangeRates], Swift.Error>) -> Void)) { + if monthIndex == months.count { + return callback(.success(state)) + } + + var newState = state + + let month = months[monthIndex] + let url = self.exchangeRatesURL(forMonthContaining: month) + logger.debug("requesting exchange rate from \(url)") + let dataTask = URLSession.shared.dataTask(with: url) { data, _, error in + do { + guard let data = data else { + throw error! + } + let exchangeRates = try self.parse(data: data, currencyCodes: Set(currencies)) + newState[month] = exchangeRates + logger.debug("Finished downloading month: \(month)") + if let period = exchangeRates.period { + logger.debug("Got data covering period: \(period)") + } + } catch { + return callback(.failure(error)) + } + self.download(logger: logger, + months: months, + monthIndex: monthIndex.advanced(by: 1), + currencies: currencies, + state: newState, + callback: callback) + } + dataTask.resume() + } + + private func parse(data: Data, currencyCodes: Set) throws -> ExchangeRates { + let document = try XMLDocument(data: data) + let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(identifier: "Etc/UTC")! + dateFormatter.dateFormat = "dd/MMM/yy" + let interval: DateInterval? + if let period = try document.nodes(forXPath: "/exchangeRateMonthList/@Period").first?.stringValue, + period.count == 26 { + // "01/Sep/2018 to 30/Sep/2018" + let startString = period[period.startIndex ..< period.index(period.startIndex, offsetBy: 11)] + let to = period[startString.endIndex ..< period.index(startString.endIndex, offsetBy: 4)] + let endString = period[to.endIndex ..< period.index(to.endIndex, offsetBy: 11)] + if let startDate = dateFormatter.date(from: String(startString)), + let startDay = calendar.dateInterval(of: .day, for: startDate), + to == " to ", + let endDate = dateFormatter.date(from: String(endString)), + let endDay = calendar.dateInterval(of: .day, for: endDate) { + interval = DateInterval(start: startDay.start, end: endDay.end) + } else { + interval = nil + } + } else { + interval = nil + } + + let ratesByCurrencyCode: [String: Decimal?] = Dictionary(uniqueKeysWithValues: try currencyCodes.map { + let xpathCurrency = $0.replacingOccurrences(of: "'", with: "'") + if let rateString = try document.nodes(forXPath: "/exchangeRateMonthList/exchangeRate/currencyCode[text()='\(xpathCurrency)']/../rateNew/text()").first?.stringValue, + // We must parse the decimal data using the UK locale, not the system one. + let rate = Decimal(string: rateString, locale: self.locale) { + return ($0, rate) + } else { + return ($0, nil) + } + }) + + return (period: interval, ratesByCurrencyCode: ratesByCurrencyCode) + } + + private func makeUTCDateFormatter(dateFormat: String) -> DateFormatter { + let utcTimeZone = TimeZone(identifier: "UTC")! + let result = DateFormatter() + result.locale = Locale(identifier: "en_US_POSIX") + result.timeZone = utcTimeZone + result.dateFormat = dateFormat + return result + } + + private func exchangeRatesURL(forMonthContaining date: Date) -> URL { + let exchangeRatesBaseURL = URL(string: "https://www.hmrc.gov.uk/softwaredevelopers/rates")! + let dateFormatter = self.makeUTCDateFormatter(dateFormat: "MMyy") + return exchangeRatesBaseURL.appendingPathComponent("exrates-monthly-\(dateFormatter.string(from: date)).xml") + } + + private typealias ExchangeRates = (period: DateInterval?, ratesByCurrencyCode: [String: Decimal?]) + + private struct Error: Swift.Error, CustomStringConvertible { + let description: String + } +} + +// MARK: - Request and Response + +struct Request: Decodable {} + +struct Exchange: Encodable { + @DateCoding + var date: Date + let from: Currency + let to: Currency + let rate: Decimal + let change: Change + + struct Currency: Encodable { + let symbol: String + let emoji: String + } + + enum Change: String, Encodable { + case up + case down + case none + case unknown + } + + @propertyWrapper + public struct DateCoding: Encodable { + public let wrappedValue: Date + + public init(wrappedValue: Date) { + self.wrappedValue = wrappedValue + } + + func encode(to encoder: Encoder) throws { + let string = Self.dateFormatter.string(from: self.wrappedValue) + var container = encoder.singleValueContainer() + try container.encode(string) + } + + private static var dateFormatter: ISO8601DateFormatter { + let dateFormatter = ISO8601DateFormatter() + dateFormatter.timeZone = TimeZone(identifier: "UTC")! + dateFormatter.formatOptions = [.withYear, .withMonth, .withDashSeparatorInDate] + return dateFormatter + } + } +} diff --git a/Sources/ErrorHandling/main.swift b/Sources/ErrorHandling/main.swift new file mode 100644 index 00000000..ab8e04be --- /dev/null +++ b/Sources/ErrorHandling/main.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +// MARK: - Run Lambda + +// switch over the error type "requested" by thr request, and trigger sucg error accordingly +Lambda.run { (context: Lambda.Context, request: Request, callback: (Result) -> Void) in + switch request.error { + // no error here! + case .none: + callback(.success(Response(awsRequestId: context.requestId, requestId: request.requestId, status: .ok))) + // trigger a "managed" error - domain specific business logic failure + case .managed: + callback(.success(Response(awsRequestId: context.requestId, requestId: request.requestId, status: .error))) + // trigger an "unmanaged" error - an unexpected Swift Error triggered while processing the request + case .unmanaged(let error): + callback(.failure(UnmanagedError(description: error))) + // trigger a "fatal" error - a panic type error which will crash the process + case .fatal: + fatalError("crash!") + } +} + +// MARK: - Request and Response + +struct Request: Codable { + let requestId: String + let error: Error + + public init(requestId: String, error: Error? = nil) { + self.requestId = requestId + self.error = error ?? .none + } + + public enum Error: Codable, RawRepresentable { + case none + case managed + case unmanaged(String) + case fatal + + public init?(rawValue: String) { + switch rawValue { + case "none": + self = .none + case "managed": + self = .managed + case "fatal": + self = .fatal + default: + self = .unmanaged(rawValue) + } + } + + public var rawValue: String { + switch self { + case .none: + return "none" + case .managed: + return "managed" + case .fatal: + return "fatal" + case .unmanaged(let error): + return error + } + } + } +} + +struct Response: Codable { + let awsRequestId: String + let requestId: String + let status: Status + + public init(awsRequestId: String, requestId: String, status: Status) { + self.awsRequestId = awsRequestId + self.requestId = requestId + self.status = status + } + + public enum Status: Int, Codable { + case ok + case error + } +} + +struct UnmanagedError: Error { + let description: String +} diff --git a/Sources/HelloWorld/main.swift b/Sources/HelloWorld/main.swift new file mode 100644 index 00000000..7535da97 --- /dev/null +++ b/Sources/HelloWorld/main.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +// introductory example, the obligatory "hello, world!" +Lambda.run { (_: Lambda.Context, _: String, callback: (Result) -> Void) in + callback(.success("hello, world!")) +} diff --git a/Sources/MyLambda/main.swift b/Sources/MyLambda/main.swift deleted file mode 100644 index fec72fa1..00000000 --- a/Sources/MyLambda/main.swift +++ /dev/null @@ -1,182 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import AWSLambdaRuntime -import NIO - -// Lambda.run(StringLambda) -Lambda.run(CodableLambda) -// Lambda.run(APIGatewayProxyLambda()) -// Lambda.run(FakeAPIGatewayProxyLambda()) - -// MARK: - Lambda Samples - -func StringLambda(context: Lambda.Context, request: String, callback: (Result) -> Void) { - context.logger.debug("hello, string!") - callback(.success("hello, world!")) -} - -func CodableLambda(context: Lambda.Context, request: Request, callback: (Result) -> Void) { - context.logger.info("hello, json!") - switch request.error { - case .none: - callback(.success(Response(awsRequestId: context.requestId, requestId: request.requestId, status: .ok))) - case .managed: - callback(.success(Response(awsRequestId: context.requestId, requestId: request.requestId, status: .error))) - case .unmanaged(let error): - callback(.failure(UnmanagedError(description: error))) - case .fatal: - fatalError("crash!") - } -} - -struct APIGatewayProxyLambda: EventLoopLambdaHandler { - public typealias In = APIGatewayRequest - public typealias Out = APIGatewayResponse - - public init() {} - - public func handle(context: Lambda.Context, payload: APIGatewayRequest) -> EventLoopFuture { - context.logger.debug("hello, api gateway!") - return context.eventLoop.makeSucceededFuture(APIGatewayResponse(statusCode: 200, - headers: nil, - multiValueHeaders: nil, - body: "hello, world!", - isBase64Encoded: false)) - } -} - -struct FakeAPIGatewayProxyLambda: EventLoopLambdaHandler { - public typealias In = String - public typealias Out = String - - public init() {} - - public func handle(context: Lambda.Context, payload: String) -> EventLoopFuture { - context.logger.debug("hello, string!") - return context.eventLoop.makeSucceededFuture("{ \"statusCode\": 200, \"body\": \"hello, world!\" }") - } -} - -struct Request: Codable { - let requestId: String - let error: Error - - public init(requestId: String, error: Error? = nil) { - self.requestId = requestId - self.error = error ?? .none - } - - public enum Error: Codable, RawRepresentable { - case none - case managed - case unmanaged(String) - case fatal - - public init?(rawValue: String) { - switch rawValue { - case "none": - self = .none - case "managed": - self = .managed - case "fatal": - self = .fatal - default: - self = .unmanaged(rawValue) - } - } - - public var rawValue: String { - switch self { - case .none: - return "none" - case .managed: - return "managed" - case .fatal: - return "fatal" - case .unmanaged(let error): - return error - } - } - } -} - -struct Response: Codable { - let awsRequestId: String - let requestId: String - let status: Status - - public init(awsRequestId: String, requestId: String, status: Status) { - self.awsRequestId = awsRequestId - self.requestId = requestId - self.status = status - } - - public enum Status: Int, Codable { - case ok - case error - } -} - -struct UnmanagedError: Error { - let description: String -} - -struct APIGatewayRequest: Codable { - let resource: String - let path: String - let httpMethod: String? - let headers: [String: String]? - let multiValueHeaders: [String: [String]]? - let queryStringParameters: [String: String]? - let multiValueQueryStringParameters: [String: [String]]? - let pathParameters: [String: String]? - let stageVariables: [String: String]? - let requestContext: Context? - let body: String? - let isBase64Encoded: Bool? - - struct Context: Codable { - let accountId: String? - let resourceId: String? - let stage: String? - let requestId: String? - let identity: Identity? - let resourcePath: String? - let httpMethod: String? - let apiId: String - } - - struct Identity: Codable { - let cognitoIdentityPoolId: String? - let accountId: String? - let cognitoIdentityId: String? - let caller: String? - let apiKey: String? - let sourceIp: String? - let cognitoAuthenticationType: String? - let cognitoAuthenticationProvider: String? - let userArn: String? - let userAgent: String? - let user: String? - } -} - -struct APIGatewayResponse: Codable { - let statusCode: Int - let headers: [String: String]? - let multiValueHeaders: [String: [String]]? - let body: String? - let isBase64Encoded: Bool? -} diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 625ab760..d3ae6c94 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -3,7 +3,7 @@ ## ## This source file is part of the SwiftAWSLambdaRuntime open source project ## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -13,36 +13,53 @@ ## ##===----------------------------------------------------------------------===## +set -eu + lambda_name=SwiftSample s3_bucket=swift-lambda-test -executable=$(swift package dump-package | sed -e 's|: null|: ""|g' | jq '.products[] | (select(.type.executable)) | .name' | sed -e 's|"||g') +executables=( $(swift package dump-package | sed -e 's|: null|: ""|g' | jq '.products[] | (select(.type.executable)) | .name' | sed -e 's|"||g') ) + +if [[ ${#executables[@]} = 0 ]]; then + echo "no executables found" + exit 1 +elif [[ ${#executables[@]} = 1 ]]; then + executable=${executables[0]} +elif [[ ${#executables[@]} > 1 ]]; then + echo "multiple executables found:" + for executable in ${executables[@]}; do + echo " * $executable" + done + echo "" + read -p "select which executables to deploy: " executable +fi + +echo -e "\ndeploying $executable" -set -eu -echo "-------------------------------------------------------------------------" -echo "preparing docker build image" -echo "-------------------------------------------------------------------------" -docker build . -t builder echo "-------------------------------------------------------------------------" -echo "updating code" +echo "preparing docker build image" echo "-------------------------------------------------------------------------" -swift package update +docker build . -q -t builder echo "-------------------------------------------------------------------------" -echo "building lambda executable" +echo "building \"$executable\" lambda" echo "-------------------------------------------------------------------------" -docker run --rm -v `pwd`:/workspace -w /workspace builder bash -cl "swift build -c release -Xswiftc -g" +docker run --rm -v `pwd`:/workspace -w /workspace builder bash -cl "swift build --product $executable -c release -Xswiftc -g" echo "done" echo "-------------------------------------------------------------------------" -echo "packaging lambda" +echo "packaging \"$executable\" lambda" echo "-------------------------------------------------------------------------" docker run --rm -v `pwd`:/workspace -w /workspace builder bash -cl "./scripts/package.sh $executable" echo "-------------------------------------------------------------------------" -echo "uploading to s3" +echo "uploading \"$executable\" lambda to s3" echo "-------------------------------------------------------------------------" aws s3 cp .build/lambda/lambda.zip s3://$s3_bucket/ + +echo "-------------------------------------------------------------------------" +echo "updating \"$lambda_name\" to laetst \"$executable\"" +echo "-------------------------------------------------------------------------" aws lambda update-function-code --function $lambda_name --s3-bucket $s3_bucket --s3-key lambda.zip diff --git a/scripts/generate_contributors_list.sh b/scripts/generate_contributors_list.sh index d745e21e..de2275f5 100755 --- a/scripts/generate_contributors_list.sh +++ b/scripts/generate_contributors_list.sh @@ -3,7 +3,7 @@ ## ## This source file is part of the SwiftAWSLambdaRuntime open source project ## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information diff --git a/scripts/package.sh b/scripts/package.sh index ed9ac6b7..86644aaa 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -3,7 +3,7 @@ ## ## This source file is part of the SwiftAWSLambdaRuntime open source project ## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information diff --git a/scripts/sanity.sh b/scripts/sanity.sh index d5f9d4f6..d3b39e93 100755 --- a/scripts/sanity.sh +++ b/scripts/sanity.sh @@ -3,7 +3,7 @@ ## ## This source file is part of the SwiftAWSLambdaRuntime open source project ## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -19,7 +19,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" function replace_acceptable_years() { # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/2017-2018/YEARS/' -e 's/2017-2020/YEARS/' -e 's/2019/YEARS/' -e 's/2020/YEARS/' + sed -e 's/2020/YEARS/' } printf "=> Checking format... " From d3c72e93e679d77a8ddd710d502e5097b2580346 Mon Sep 17 00:00:00 2001 From: tachyonics Date: Mon, 6 Apr 2020 09:31:43 -0700 Subject: [PATCH 3/7] Add a SAM template for deploying the lambdas to AWS. (#2) motivation: demonstrate how to deploy with SAM changes: * Add a SAM template for deploying the lambdas to AWS. * Updated README with instructions on how to do this. --- .gitignore | 1 + APIGateway-template.yml | 33 +++++++++++++++++++ Benchmark-template.yml | 14 ++++++++ CurrencyExchange-template.yml | 15 +++++++++ Dockerfile | 2 +- ErrorHandling-template.yml | 14 ++++++++ HelloWorld-template.yml | 14 ++++++++ README.md | 62 ++++++++++++++++++++++++++++++++++- scripts/build-and-package.sh | 29 ++++++++++++++++ scripts/deploy.sh | 4 +-- scripts/package.sh | 2 +- scripts/sam-deploy.sh | 47 ++++++++++++++++++++++++++ 12 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 APIGateway-template.yml create mode 100644 Benchmark-template.yml create mode 100644 CurrencyExchange-template.yml create mode 100644 ErrorHandling-template.yml create mode 100644 HelloWorld-template.yml create mode 100755 scripts/build-and-package.sh create mode 100755 scripts/sam-deploy.sh diff --git a/.gitignore b/.gitignore index c4f353cd..fa94c087 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ Package.resolved .podspecs function.zip .swiftpm +samconfig.toml diff --git a/APIGateway-template.yml b/APIGateway-template.yml new file mode 100644 index 00000000..4861b60c --- /dev/null +++ b/APIGateway-template.yml @@ -0,0 +1,33 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A sample SAM template for deploying Lambda functions. + +Resources: +# APIGateway Function + apiGatewayFunction: + Type: AWS::Serverless::Function + Properties: + Handler: Provided + Runtime: provided + CodeUri: .build/lambda/APIGateway/lambda.zip +# Add an API Gateway event source for the Lambda + Events: + HttpGet: + Type: Api + Properties: + RestApiId: !Ref lambdaApiGateway + Path: '/samples/apig' + Method: get +# Instructs new versions to be published to an alias named "live". + AutoPublishAlias: live + + lambdaApiGateway: + Type: AWS::Serverless::Api + Properties: + Name: Lambda API Gateway + StageName: Beta + +Outputs: + LambdaApiGatewayEndpoint: + Description: 'API Gateway endpoint URL.' + Value: !Sub 'https://${lambdaApiGateway}.execute-api.${AWS::Region}.amazonaws.com/Beta/samples/apig' diff --git a/Benchmark-template.yml b/Benchmark-template.yml new file mode 100644 index 00000000..38941a53 --- /dev/null +++ b/Benchmark-template.yml @@ -0,0 +1,14 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A sample SAM template for deploying Lambda functions. + +Resources: +# Benchmark Function + benchmarkFunction: + Type: AWS::Serverless::Function + Properties: + Handler: Provided + Runtime: provided + CodeUri: .build/lambda/Benchmark/lambda.zip +# Instructs new versions to be published to an alias named "live". + AutoPublishAlias: live diff --git a/CurrencyExchange-template.yml b/CurrencyExchange-template.yml new file mode 100644 index 00000000..de09cd4e --- /dev/null +++ b/CurrencyExchange-template.yml @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A sample SAM template for deploying Lambda functions. + +Resources: +# CurrencyExchange Function + currencyExchangeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: Provided + Runtime: provided + CodeUri: .build/lambda/CurrencyExchange/lambda.zip + Timeout: 300 +# Instructs new versions to be published to an alias named "live". + AutoPublishAlias: live diff --git a/Dockerfile b/Dockerfile index 638fb33b..b212c320 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM fabianfett/amazonlinux-swift:5.2-branch-amazonlinux2 +FROM fabianfett/amazonlinux-swift:5.2-amazonlinux2 RUN yum -y install \ git \ diff --git a/ErrorHandling-template.yml b/ErrorHandling-template.yml new file mode 100644 index 00000000..dba76b8e --- /dev/null +++ b/ErrorHandling-template.yml @@ -0,0 +1,14 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A sample SAM template for deploying Lambda functions. + +Resources: +# ErrorHandling Function + errorHandlingFunction: + Type: AWS::Serverless::Function + Properties: + Handler: Provided + Runtime: provided + CodeUri: .build/lambda/ErrorHandling/lambda.zip +# Instructs new versions to be published to an alias named "live". + AutoPublishAlias: live diff --git a/HelloWorld-template.yml b/HelloWorld-template.yml new file mode 100644 index 00000000..7eddfbe7 --- /dev/null +++ b/HelloWorld-template.yml @@ -0,0 +1,14 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A sample SAM template for deploying Lambda functions. + +Resources: +# HelloWorld Function + helloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Handler: Provided + Runtime: provided + CodeUri: .build/lambda/HelloWorld/lambda.zip +# Instructs new versions to be published to an alias named "live". + AutoPublishAlias: live diff --git a/README.md b/README.md index 0c33c7e3..11b1875c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This repository is a deployable example demonstrating how to package and deploy a Swift based Lambda to AWS. -Steps to deploy this sample to ASW: +Steps to deploy this sample to AWS: * Login to AWS Console and create an AWS Lambda with the following settings: * Runtime: Custom runtime @@ -53,3 +53,63 @@ Steps to deploy this sample to ASW: "error": "fatal" } `` + +### Deploy to AWS using AWS Serverless Application Model + +AWS [Serverless Application Model](https://aws.amazon.com/serverless/sam/) (SAM) is an open-source framework for building serverless applications. This framework allows you to easily deploy other AWS resources and more complex deployment mechanisms such a CI pipelines. + +***Note:*** Deploying using SAM will automatically create resources within your AWS account. Charges may apply for these resources. + +To use SAM to deploy this sample to AWS- + +1. Install the AWS CLI by following the instructions at https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html + +2. Install SAM CLI by following the instructions at https://aws.amazon.com/serverless/sam/ + +3. Build, package and deploy the Lambda + + ``` + ./scripts/sam-deploy.sh --guided + ``` + +The script will ask you which sample lambda you wish to deploy. It will then guide you through the SAM setup process. + + ``` + Setting default arguments for 'sam deploy' + ========================================= + Stack Name [sam-app]: swift-aws-lambda-runtime-sample + AWS Region [us-east-1]: + #Shows you resources changes to be deployed and require a 'Y' to initiate deploy + Confirm changes before deploy [y/N]: Y + #SAM needs permission to be able to create roles to connect to the resources in your template + Allow SAM CLI IAM role creation [Y/n]: Y + Save arguments to samconfig.toml [Y/n]: Y + ``` + +If you said yes to confirm changes, SAM will ask you to accept changes to the infratructure you are setting up. For more on this, see [Cloud Formation](https://aws.amazon.com/cloudformation/). + +The sam-deploy script passes through any parameters to the SAM deploy command. + +4. Subsequent deploys can just use the command; minus the guided parameter- + + ``` + ./scripts/sam-deploy.sh + ``` + +The script will ask you which sample lambda you wish to deploy. You are deploy a different sample lambda, the deploy process will pull down the previous lambda. + +SAM will still ask you to confirm changes if you said yes to that initially. + +5. Testing + +For the API Gateway sample: + +The SAM template will provide an output labelled `LambdaApiGatewayEndpoint` which you can use to test the Lambda. For example- + + ``` + curl <> + ``` + +***Warning:*** This SAM template is only intended as a sample and creates a publically accessible http endpoint. + +For all other samples use the AWS Lambda console. diff --git a/scripts/build-and-package.sh b/scripts/build-and-package.sh new file mode 100755 index 00000000..6634539c --- /dev/null +++ b/scripts/build-and-package.sh @@ -0,0 +1,29 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +set -eu + +executable=$1 + +echo "-------------------------------------------------------------------------" +echo "building \"$executable\" lambda" +echo "-------------------------------------------------------------------------" +docker run --rm -v `pwd`:/workspace -w /workspace builder bash -cl "swift build --product $executable -c release -Xswiftc -g" +echo "done" + +echo "-------------------------------------------------------------------------" +echo "packaging \"$executable\" lambda" +echo "-------------------------------------------------------------------------" +docker run --rm -v `pwd`:/workspace -w /workspace builder bash -cl "./scripts/package.sh $executable" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index d3ae6c94..7cc00dfc 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -57,9 +57,9 @@ echo "-------------------------------------------------------------------------" echo "uploading \"$executable\" lambda to s3" echo "-------------------------------------------------------------------------" -aws s3 cp .build/lambda/lambda.zip s3://$s3_bucket/ +aws s3 cp .build/lambda/$executable/lambda.zip s3://$s3_bucket/ echo "-------------------------------------------------------------------------" -echo "updating \"$lambda_name\" to laetst \"$executable\"" +echo "updating \"$lambda_name\" to latest \"$executable\"" echo "-------------------------------------------------------------------------" aws lambda update-function-code --function $lambda_name --s3-bucket $s3_bucket --s3-key lambda.zip diff --git a/scripts/package.sh b/scripts/package.sh index 86644aaa..81fd2e4f 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -18,7 +18,7 @@ set -eu executable=$1 -target=.build/lambda +target=.build/lambda/$executable rm -rf "$target" mkdir -p "$target" cp ".build/release/$executable" "$target/" diff --git a/scripts/sam-deploy.sh b/scripts/sam-deploy.sh new file mode 100755 index 00000000..3a937828 --- /dev/null +++ b/scripts/sam-deploy.sh @@ -0,0 +1,47 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +DIR="$(cd "$(dirname "$0")" && pwd)" + +executables=( $(swift package dump-package | sed -e 's|: null|: ""|g' | jq '.products[] | (select(.type.executable)) | .name' | sed -e 's|"||g') ) + +if [[ ${#executables[@]} = 0 ]]; then + echo "no executables found" + exit 1 +elif [[ ${#executables[@]} = 1 ]]; then + executable=${executables[0]} +elif [[ ${#executables[@]} > 1 ]]; then + echo "multiple executables found:" + for executable in ${executables[@]}; do + echo " * $executable" + done + echo "" + read -p "select which executables to deploy: " executable +fi + +echo -e "\ndeploying $executable" + +echo "-------------------------------------------------------------------------" +echo "preparing docker build image" +echo "-------------------------------------------------------------------------" +docker build . -q -t builder + +$DIR/build-and-package.sh ${executable} + +echo "-------------------------------------------------------------------------" +echo "deploying using SAM" +echo "-------------------------------------------------------------------------" + +sam deploy --template "${executable}-template.yml" $@ From d5368bbe258b8503fbcefee7cf430f0cdb059692 Mon Sep 17 00:00:00 2001 From: tom doron Date: Wed, 6 May 2020 12:11:07 -0700 Subject: [PATCH 4/7] moving examples into a subdirectory --- .gitignore | 12 -- .mailmap | 3 - .swiftformat | 14 -- CODE_OF_CONDUCT.md | 55 ----- CONTRIBUTING.md | 68 ------ CONTRIBUTORS.txt | 18 -- .../LambdaFunctions/.dockerignore | 0 .../LambdaFunctions/APIGateway-template.yml | 0 .../LambdaFunctions/Benchmark-template.yml | 0 .../CurrencyExchange-template.yml | 0 .../LambdaFunctions/Dockerfile | 0 .../ErrorHandling-template.yml | 0 .../LambdaFunctions/HelloWorld-template.yml | 0 .../LambdaFunctions/Package.swift | 0 .../LambdaFunctions/README.md | 0 .../Sources}/APIGateway/main.swift | 0 .../Sources}/Benchmark/main.swift | 0 .../Sources}/CurrencyExchange/main.swift | 0 .../Sources}/ErrorHandling/main.swift | 0 .../Sources}/HelloWorld/main.swift | 0 .../scripts}/build-and-package.sh | 0 .../LambdaFunctions/scripts}/deploy.sh | 0 .../scripts}/generate_contributors_list.sh | 0 .../LambdaFunctions/scripts}/package.sh | 0 .../LambdaFunctions/scripts}/sam-deploy.sh | 0 .../LambdaFunctions/scripts}/sanity.sh | 0 LICENSE.txt | 202 ------------------ 27 files changed, 372 deletions(-) delete mode 100644 .gitignore delete mode 100644 .mailmap delete mode 100644 .swiftformat delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTING.md delete mode 100644 CONTRIBUTORS.txt rename .dockerignore => Examples/LambdaFunctions/.dockerignore (100%) rename APIGateway-template.yml => Examples/LambdaFunctions/APIGateway-template.yml (100%) rename Benchmark-template.yml => Examples/LambdaFunctions/Benchmark-template.yml (100%) rename CurrencyExchange-template.yml => Examples/LambdaFunctions/CurrencyExchange-template.yml (100%) rename Dockerfile => Examples/LambdaFunctions/Dockerfile (100%) rename ErrorHandling-template.yml => Examples/LambdaFunctions/ErrorHandling-template.yml (100%) rename HelloWorld-template.yml => Examples/LambdaFunctions/HelloWorld-template.yml (100%) rename Package.swift => Examples/LambdaFunctions/Package.swift (100%) rename README.md => Examples/LambdaFunctions/README.md (100%) rename {Sources => Examples/LambdaFunctions/Sources}/APIGateway/main.swift (100%) rename {Sources => Examples/LambdaFunctions/Sources}/Benchmark/main.swift (100%) rename {Sources => Examples/LambdaFunctions/Sources}/CurrencyExchange/main.swift (100%) rename {Sources => Examples/LambdaFunctions/Sources}/ErrorHandling/main.swift (100%) rename {Sources => Examples/LambdaFunctions/Sources}/HelloWorld/main.swift (100%) rename {scripts => Examples/LambdaFunctions/scripts}/build-and-package.sh (100%) rename {scripts => Examples/LambdaFunctions/scripts}/deploy.sh (100%) rename {scripts => Examples/LambdaFunctions/scripts}/generate_contributors_list.sh (100%) rename {scripts => Examples/LambdaFunctions/scripts}/package.sh (100%) rename {scripts => Examples/LambdaFunctions/scripts}/sam-deploy.sh (100%) rename {scripts => Examples/LambdaFunctions/scripts}/sanity.sh (100%) delete mode 100644 LICENSE.txt diff --git a/.gitignore b/.gitignore deleted file mode 100644 index fa94c087..00000000 --- a/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj -Package.pins -*.pem -/docs -Package.resolved -.podspecs -function.zip -.swiftpm -samconfig.toml diff --git a/.mailmap b/.mailmap deleted file mode 100644 index c8ad743d..00000000 --- a/.mailmap +++ /dev/null @@ -1,3 +0,0 @@ -Tomer Doron -Tomer Doron -Tomer Doron diff --git a/.swiftformat b/.swiftformat deleted file mode 100644 index 19ee383f..00000000 --- a/.swiftformat +++ /dev/null @@ -1,14 +0,0 @@ -# file options - ---swiftversion 5.1 ---exclude .build - -# format options - ---self insert ---patternlet inline ---stripunusedargs unnamed-only ---comments ignore ---ifdef no-indent - -# rules diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index ebdb302e..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,55 +0,0 @@ -# Code of Conduct -To be a truly great community, SwiftAWSLambdaRuntime needs to welcome developers from all walks of life, -with different backgrounds, and with a wide range of experience. A diverse and friendly -community will have more great ideas, more unique perspectives, and produce more great -code. We will work diligently to make the SwiftAWSLambdaRuntime community welcoming to everyone. - -To give clarity of what is expected of our members, SwiftAWSLambdaRuntime has adopted the code of conduct -defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source -communities, and we think it articulates our values well. The full text is copied below: - -### Contributor Code of Conduct v1.3 -As contributors and maintainers of this project, and in the interest of fostering an open and -welcoming community, we pledge to respect all people who contribute through reporting -issues, posting feature requests, updating documentation, submitting pull requests or patches, -and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, sexual -orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or -nationality. - -Examples of unacceptable behavior by participants include: -- The use of sexualized language or imagery -- Personal attacks -- Trolling or insulting/derogatory comments -- Public or private harassment -- Publishing other’s private information, such as physical or electronic addresses, without explicit permission -- Other unethical or unprofessional conduct - -Project maintainers have the right and responsibility to remove, edit, or reject comments, -commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of -Conduct, or to ban temporarily or permanently any contributor for other behaviors that they -deem inappropriate, threatening, offensive, or harmful. - -By adopting this Code of Conduct, project maintainers commit themselves to fairly and -consistently applying these principles to every aspect of managing this project. Project -maintainers who do not follow or enforce the Code of Conduct may be permanently removed -from the project team. - -This code of conduct applies both within project spaces and in public spaces when an -individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by -contacting a project maintainer at [swift-server-conduct@group.apple.com](mailto:swift-server-conduct@group.apple.com). All complaints will be reviewed and -investigated and will result in a response that is deemed necessary and appropriate to the -circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter -of an incident. - -*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](https://contributor-covenant.org/version/1/3/0/).* - -### Reporting -A working group of community members is committed to promptly addressing any [reported issues](mailto:swift-server-conduct@group.apple.com). -Working group members are volunteers appointed by the project lead, with a -preference for individuals with varied backgrounds and perspectives. Membership is expected -to change regularly, and may grow or shrink. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index c9062d0e..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,68 +0,0 @@ -## Legal - -By submitting a pull request, you represent that you have the right to license -your contribution to Apple and the community, and agree by submitting the patch -that your contributions are licensed under the Apache 2.0 license (see -`LICENSE.txt`). - - -## How to submit a bug report - -Please ensure to specify the following: - -* SwiftAWSLambdaRuntime commit hash -* Contextual information (e.g. what you were trying to achieve with SwiftAWSLambdaRuntime) -* Simplest possible steps to reproduce - * More complex the steps are, lower the priority will be. - * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. -* Anything that might be relevant in your opinion, such as: - * Swift version or the output of `swift --version` - * OS version and the output of `uname -a` - * Network configuration - - -### Example - -``` -SwiftAWSLambdaRuntime commit hash: 22ec043dc9d24bb011b47ece4f9ee97ee5be2757 - -Context: -While load testing my Lambda written with SwiftAWSLambdaRuntime, I noticed -that one file descriptor is leaked per request. - -Steps to reproduce: -1. ... -2. ... -3. ... -4. ... - -$ swift --version -Swift version 4.0.2 (swift-4.0.2-RELEASE) -Target: x86_64-unknown-linux-gnu - -Operating system: Ubuntu Linux 16.04 64-bit - -$ uname -a -Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux - -My system has IPv6 disabled. -``` - -## Writing a Patch - -A good SwiftAWSLambdaRuntime patch is: - -1. Concise, and contains as few changes as needed to achieve the end result. -2. Tested, ensuring that any tests provided failed before the patch and pass after it. -3. Documented, adding API documentation as needed to cover new functions and properties. -4. Accompanied by a great commit message, using our commit message template. - -### Commit Message Template - -We require that your commit messages match our template. The easiest way to do that is to get git to help you by explicitly using the template. To do that, `cd` to the root of our repository and run: - - git config commit.template dev/git.commit.template - -## How to contribute your work - -Please open a pull request at https://github.com/apple/swift-nio. Make sure the CI passes, and then wait for code review. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt deleted file mode 100644 index 8668e4d4..00000000 --- a/CONTRIBUTORS.txt +++ /dev/null @@ -1,18 +0,0 @@ -For the purpose of tracking copyright, this is the list of individuals and -organizations who have contributed source code to SwiftAWSLambdaRuntime. - -For employees of an organization/company where the copyright of work done -by employees of that company is held by the company itself, only the company -needs to be listed here. - -## COPYRIGHT HOLDERS - -- Apple Inc. (all contributors with '@apple.com') - -### Contributors - -- Tomer Doron - -**Updating this list** - -Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` diff --git a/.dockerignore b/Examples/LambdaFunctions/.dockerignore similarity index 100% rename from .dockerignore rename to Examples/LambdaFunctions/.dockerignore diff --git a/APIGateway-template.yml b/Examples/LambdaFunctions/APIGateway-template.yml similarity index 100% rename from APIGateway-template.yml rename to Examples/LambdaFunctions/APIGateway-template.yml diff --git a/Benchmark-template.yml b/Examples/LambdaFunctions/Benchmark-template.yml similarity index 100% rename from Benchmark-template.yml rename to Examples/LambdaFunctions/Benchmark-template.yml diff --git a/CurrencyExchange-template.yml b/Examples/LambdaFunctions/CurrencyExchange-template.yml similarity index 100% rename from CurrencyExchange-template.yml rename to Examples/LambdaFunctions/CurrencyExchange-template.yml diff --git a/Dockerfile b/Examples/LambdaFunctions/Dockerfile similarity index 100% rename from Dockerfile rename to Examples/LambdaFunctions/Dockerfile diff --git a/ErrorHandling-template.yml b/Examples/LambdaFunctions/ErrorHandling-template.yml similarity index 100% rename from ErrorHandling-template.yml rename to Examples/LambdaFunctions/ErrorHandling-template.yml diff --git a/HelloWorld-template.yml b/Examples/LambdaFunctions/HelloWorld-template.yml similarity index 100% rename from HelloWorld-template.yml rename to Examples/LambdaFunctions/HelloWorld-template.yml diff --git a/Package.swift b/Examples/LambdaFunctions/Package.swift similarity index 100% rename from Package.swift rename to Examples/LambdaFunctions/Package.swift diff --git a/README.md b/Examples/LambdaFunctions/README.md similarity index 100% rename from README.md rename to Examples/LambdaFunctions/README.md diff --git a/Sources/APIGateway/main.swift b/Examples/LambdaFunctions/Sources/APIGateway/main.swift similarity index 100% rename from Sources/APIGateway/main.swift rename to Examples/LambdaFunctions/Sources/APIGateway/main.swift diff --git a/Sources/Benchmark/main.swift b/Examples/LambdaFunctions/Sources/Benchmark/main.swift similarity index 100% rename from Sources/Benchmark/main.swift rename to Examples/LambdaFunctions/Sources/Benchmark/main.swift diff --git a/Sources/CurrencyExchange/main.swift b/Examples/LambdaFunctions/Sources/CurrencyExchange/main.swift similarity index 100% rename from Sources/CurrencyExchange/main.swift rename to Examples/LambdaFunctions/Sources/CurrencyExchange/main.swift diff --git a/Sources/ErrorHandling/main.swift b/Examples/LambdaFunctions/Sources/ErrorHandling/main.swift similarity index 100% rename from Sources/ErrorHandling/main.swift rename to Examples/LambdaFunctions/Sources/ErrorHandling/main.swift diff --git a/Sources/HelloWorld/main.swift b/Examples/LambdaFunctions/Sources/HelloWorld/main.swift similarity index 100% rename from Sources/HelloWorld/main.swift rename to Examples/LambdaFunctions/Sources/HelloWorld/main.swift diff --git a/scripts/build-and-package.sh b/Examples/LambdaFunctions/scripts/build-and-package.sh similarity index 100% rename from scripts/build-and-package.sh rename to Examples/LambdaFunctions/scripts/build-and-package.sh diff --git a/scripts/deploy.sh b/Examples/LambdaFunctions/scripts/deploy.sh similarity index 100% rename from scripts/deploy.sh rename to Examples/LambdaFunctions/scripts/deploy.sh diff --git a/scripts/generate_contributors_list.sh b/Examples/LambdaFunctions/scripts/generate_contributors_list.sh similarity index 100% rename from scripts/generate_contributors_list.sh rename to Examples/LambdaFunctions/scripts/generate_contributors_list.sh diff --git a/scripts/package.sh b/Examples/LambdaFunctions/scripts/package.sh similarity index 100% rename from scripts/package.sh rename to Examples/LambdaFunctions/scripts/package.sh diff --git a/scripts/sam-deploy.sh b/Examples/LambdaFunctions/scripts/sam-deploy.sh similarity index 100% rename from scripts/sam-deploy.sh rename to Examples/LambdaFunctions/scripts/sam-deploy.sh diff --git a/scripts/sanity.sh b/Examples/LambdaFunctions/scripts/sanity.sh similarity index 100% rename from scripts/sanity.sh rename to Examples/LambdaFunctions/scripts/sanity.sh diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index d6456956..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. From bf1b08cb8ec810c181fe680ec0476ca89081028e Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 7 May 2020 17:37:07 +0200 Subject: [PATCH 5/7] Rename RequestWork to GetNextInvocation (#74) --- Sources/AWSLambdaRuntime/LambdaRunner.swift | 10 ++++----- .../LambdaRuntimeClient.swift | 6 ++--- Sources/AWSLambdaRuntime/Utils.swift | 2 +- .../Lambda+CodeableTest.swift | 2 +- .../Lambda+StringTest.swift | 2 +- .../LambdaRunnerTest.swift | 4 ++-- .../LambdaRuntimeClientTest.swift | 22 +++++++++---------- Tests/AWSLambdaRuntimeTests/LambdaTest.swift | 12 +++++----- .../MockLambdaServer.swift | 8 +++---- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRunner.swift b/Sources/AWSLambdaRuntime/LambdaRunner.swift index 10e3bbe4..093f6f2e 100644 --- a/Sources/AWSLambdaRuntime/LambdaRunner.swift +++ b/Sources/AWSLambdaRuntime/LambdaRunner.swift @@ -45,13 +45,13 @@ extension Lambda { func run(logger: Logger, handler: Handler) -> EventLoopFuture { logger.debug("lambda invocation sequence starting") - // 1. request work from lambda runtime engine - return self.runtimeClient.requestWork(logger: logger).peekError { error in - logger.error("could not fetch work from lambda runtime engine: \(error)") + // 1. request invocation from lambda runtime engine + return self.runtimeClient.getNextInvocation(logger: logger).peekError { error in + logger.error("could not fetch invocation from lambda runtime engine: \(error)") }.flatMap { invocation, payload in - // 2. send work to handler + // 2. send invocation to handler let context = Context(logger: logger, eventLoop: self.eventLoop, invocation: invocation) - logger.debug("sending work to lambda handler \(handler)") + logger.debug("sending invocation to lambda handler \(handler)") return handler.handle(context: context, payload: payload) .mapResult { result in if case .failure(let error) = result { diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift index d7b8b69a..af00e22a 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift @@ -32,9 +32,9 @@ extension Lambda { self.httpClient = HTTPClient(eventLoop: eventLoop, configuration: configuration) } - /// Requests work from the Runtime Engine. - func requestWork(logger: Logger) -> EventLoopFuture<(Invocation, ByteBuffer)> { - let url = Consts.invocationURLPrefix + Consts.requestWorkURLSuffix + /// Requests invocation from the control plane. + func getNextInvocation(logger: Logger) -> EventLoopFuture<(Invocation, ByteBuffer)> { + let url = Consts.invocationURLPrefix + Consts.getNextInvocationURLSuffix logger.debug("requesting work from lambda runtime engine using \(url)") return self.httpClient.get(url: url).flatMapThrowing { response in guard response.status == .ok else { diff --git a/Sources/AWSLambdaRuntime/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift index d31c7ca5..064ccb97 100644 --- a/Sources/AWSLambdaRuntime/Utils.swift +++ b/Sources/AWSLambdaRuntime/Utils.swift @@ -18,7 +18,7 @@ import NIO internal enum Consts { private static let apiPrefix = "/2018-06-01" static let invocationURLPrefix = "\(apiPrefix)/runtime/invocation" - static let requestWorkURLSuffix = "/next" + static let getNextInvocationURLSuffix = "/next" static let postResponseURLSuffix = "/response" static let postErrorURLSuffix = "/error" static let postInitErrorURL = "\(apiPrefix)/runtime/init/error" diff --git a/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift b/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift index 1cb0553e..1d4b79dc 100644 --- a/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift +++ b/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift @@ -211,7 +211,7 @@ private struct Behavior: LambdaServerBehavior { self.result = result } - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { guard let payload = try? JSONEncoder().encode(Request(requestId: requestId)) else { XCTFail("encoding error") return .failure(.internalServerError) diff --git a/Tests/AWSLambdaRuntimeTests/Lambda+StringTest.swift b/Tests/AWSLambdaRuntimeTests/Lambda+StringTest.swift index f031af70..9eebf2d2 100644 --- a/Tests/AWSLambdaRuntimeTests/Lambda+StringTest.swift +++ b/Tests/AWSLambdaRuntimeTests/Lambda+StringTest.swift @@ -210,7 +210,7 @@ private struct Behavior: LambdaServerBehavior { self.result = result } - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { .success((requestId: self.requestId, payload: self.payload)) } diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRunnerTest.swift b/Tests/AWSLambdaRuntimeTests/LambdaRunnerTest.swift index 171850d6..71707681 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRunnerTest.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRunnerTest.swift @@ -20,7 +20,7 @@ class LambdaRunnerTest: XCTestCase { struct Behavior: LambdaServerBehavior { let requestId = UUID().uuidString let payload = "hello" - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { .success((self.requestId, self.payload)) } @@ -47,7 +47,7 @@ class LambdaRunnerTest: XCTestCase { struct Behavior: LambdaServerBehavior { static let error = "boom" let requestId = UUID().uuidString - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { .success((requestId: self.requestId, payload: "hello")) } diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift index 56bd5280..f0b7c17d 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift @@ -36,9 +36,9 @@ class LambdaRuntimeClientTest: XCTestCase { XCTAssertEqual(behavior.state, 1) } - func testGetWorkServerInternalError() { + func testGetInvocationServerInternalError() { struct Behavior: LambdaServerBehavior { - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { .failure(.internalServerError) } @@ -62,9 +62,9 @@ class LambdaRuntimeClientTest: XCTestCase { } } - func testGetWorkServerNoBodyError() { + func testGetInvocationServerNoBodyError() { struct Behavior: LambdaServerBehavior { - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { .success(("1", "")) } @@ -88,9 +88,9 @@ class LambdaRuntimeClientTest: XCTestCase { } } - func testGetWorkServerMissingHeaderRequestIDError() { + func testGetInvocationServerMissingHeaderRequestIDError() { struct Behavior: LambdaServerBehavior { - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { // no request id -> no context .success(("", "hello")) } @@ -117,7 +117,7 @@ class LambdaRuntimeClientTest: XCTestCase { func testProcessResponseInternalServerError() { struct Behavior: LambdaServerBehavior { - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { .success((requestId: "1", payload: "payload")) } @@ -142,7 +142,7 @@ class LambdaRuntimeClientTest: XCTestCase { func testProcessErrorInternalServerError() { struct Behavior: LambdaServerBehavior { - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { .success((requestId: "1", payload: "payload")) } @@ -167,8 +167,8 @@ class LambdaRuntimeClientTest: XCTestCase { func testProcessInitErrorOnBootstrapFailure() { struct Behavior: LambdaServerBehavior { - func getWork() -> GetWorkResult { - XCTFail("should not get work") + func getInvocation() -> GetInvocationResult { + XCTFail("should not get invocation") return .failure(.internalServerError) } @@ -217,7 +217,7 @@ class LambdaRuntimeClientTest: XCTestCase { return .success(()) } - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { self.state += 2 return .success(("1", "hello")) } diff --git a/Tests/AWSLambdaRuntimeTests/LambdaTest.swift b/Tests/AWSLambdaRuntimeTests/LambdaTest.swift index d8a3e4fa..6ff75fc1 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaTest.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaTest.swift @@ -100,8 +100,8 @@ class LambdaTest: XCTestCase { func testBootstrapFailureAndReportErrorFailure() { struct Behavior: LambdaServerBehavior { - func getWork() -> GetWorkResult { - XCTFail("should not get work") + func getInvocation() -> GetInvocationResult { + XCTFail("should not get invocation") return .failure(.internalServerError) } @@ -212,7 +212,7 @@ class LambdaTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } struct Behavior: LambdaServerBehavior { - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { .failure(.internalServerError) } @@ -299,7 +299,7 @@ private struct Behavior: LambdaServerBehavior { self.result = result } - func getWork() -> GetWorkResult { + func getInvocation() -> GetInvocationResult { .success((requestId: self.requestId, payload: self.payload)) } @@ -334,8 +334,8 @@ private struct Behavior: LambdaServerBehavior { } struct FailedBootstrapBehavior: LambdaServerBehavior { - func getWork() -> GetWorkResult { - XCTFail("should not get work") + func getInvocation() -> GetInvocationResult { + XCTFail("should not get invocation") return .failure(.internalServerError) } diff --git a/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift b/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift index efb84d09..595ee011 100644 --- a/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift +++ b/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift @@ -130,8 +130,8 @@ internal final class HTTPHandler: ChannelInboundHandler { case .failure(let error): responseStatus = .init(statusCode: error.rawValue) } - } else if request.head.uri.hasSuffix(Consts.requestWorkURLSuffix) { - switch self.behavior.getWork() { + } else if request.head.uri.hasSuffix(Consts.getNextInvocationURLSuffix) { + switch self.behavior.getInvocation() { case .success(let (requestId, result)): if requestId == "timeout" { usleep((UInt32(result) ?? 0) * 1000) @@ -213,13 +213,13 @@ internal final class HTTPHandler: ChannelInboundHandler { } internal protocol LambdaServerBehavior { - func getWork() -> GetWorkResult + func getInvocation() -> GetInvocationResult func processResponse(requestId: String, response: String?) -> Result func processError(requestId: String, error: ErrorResponse) -> Result func processInitError(error: ErrorResponse) -> Result } -internal typealias GetWorkResult = Result<(String, String), GetWorkError> +internal typealias GetInvocationResult = Result<(String, String), GetWorkError> internal enum GetWorkError: Int, Error { case badRequest = 400 From b7962a8213f0c14c84f6adc7e4a1b6dbb929420f Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 7 May 2020 18:05:11 +0200 Subject: [PATCH 6/7] Added DynamoDB Event (#65) motivation: support DynamoDB triggers changes: added DynamoDB Event abstractions and encoders/decoders --- Sources/AWSLambdaEvents/DynamoDB.swift | 942 ++++++++++++++++++ .../AWSLambdaEventsTests/DynamoDBTests.swift | 232 +++++ 2 files changed, 1174 insertions(+) create mode 100644 Sources/AWSLambdaEvents/DynamoDB.swift create mode 100644 Tests/AWSLambdaEventsTests/DynamoDBTests.swift diff --git a/Sources/AWSLambdaEvents/DynamoDB.swift b/Sources/AWSLambdaEvents/DynamoDB.swift new file mode 100644 index 00000000..ebc9f645 --- /dev/null +++ b/Sources/AWSLambdaEvents/DynamoDB.swift @@ -0,0 +1,942 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import struct Foundation.Date + +// https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html +public struct DynamoDB { + public struct Event: Decodable { + public let records: [EventRecord] + + public enum CodingKeys: String, CodingKey { + case records = "Records" + } + } + + public enum KeyType: String, Codable { + case hash = "HASH" + case range = "RANGE" + } + + public enum OperationType: String, Codable { + case insert = "INSERT" + case modify = "MODIFY" + case remove = "REMOVE" + } + + public enum SharedIteratorType: String, Codable { + case trimHorizon = "TRIM_HORIZON" + case latest = "LATEST" + case atSequenceNumber = "AT_SEQUENCE_NUMBER" + case afterSequenceNumber = "AFTER_SEQUENCE_NUMBER" + } + + public enum StreamStatus: String, Codable { + case enabling = "ENABLING" + case enabled = "ENABLED" + case disabling = "DISABLING" + case disabled = "DISABLED" + } + + public enum StreamViewType: String, Codable { + /// the entire item, as it appeared after it was modified. + case newImage = "NEW_IMAGE" + /// the entire item, as it appeared before it was modified. + case oldImage = "OLD_IMAGE" + /// both the new and the old item images of the item. + case newAndOldImages = "NEW_AND_OLD_IMAGES" + /// only the key attributes of the modified item. + case keysOnly = "KEYS_ONLY" + } + + public struct EventRecord: Decodable { + /// The region in which the GetRecords request was received. + public let awsRegion: AWSRegion + + /// The main body of the stream record, containing all of the DynamoDB-specific + /// fields. + public let change: StreamRecord + + /// A globally unique identifier for the event that was recorded in this stream + /// record. + public let eventId: String + + /// The type of data modification that was performed on the DynamoDB table: + /// * INSERT - a new item was added to the table. + /// * MODIFY - one or more of an existing item's attributes were modified. + /// * REMOVE - the item was deleted from the table + public let eventName: OperationType + + /// The AWS service from which the stream record originated. For DynamoDB Streams, + /// this is aws:dynamodb. + public let eventSource: String + + /// The version number of the stream record format. This number is updated whenever + /// the structure of Record is modified. + /// + /// Client applications must not assume that eventVersion will remain at a particular + /// value, as this number is subject to change at any time. In general, eventVersion + /// will only increase as the low-level DynamoDB Streams API evolves. + public let eventVersion: String + + /// The event source ARN of DynamoDB + public let eventSourceArn: String + + /// Items that are deleted by the Time to Live process after expiration have + /// the following fields: + /// * Records[].userIdentity.type + /// + /// "Service" + /// * Records[].userIdentity.principalId + /// + /// "dynamodb.amazonaws.com" + public let userIdentity: UserIdentity? + + public enum CodingKeys: String, CodingKey { + case awsRegion + case change = "dynamodb" + case eventId = "eventID" + case eventName + case eventSource + case eventVersion + case eventSourceArn = "eventSourceARN" + case userIdentity + } + } + + public struct StreamRecord { + /// The approximate date and time when the stream record was created, in UNIX + /// epoch time (http://www.epochconverter.com/) format. + public let approximateCreationDateTime: Date? + + /// The primary key attribute(s) for the DynamoDB item that was modified. + public let keys: [String: AttributeValue] + + /// The item in the DynamoDB table as it appeared after it was modified. + public let newImage: [String: AttributeValue]? + + /// The item in the DynamoDB table as it appeared before it was modified. + public let oldImage: [String: AttributeValue]? + + /// The sequence number of the stream record. + public let sequenceNumber: String + + /// The size of the stream record, in bytes. + public let sizeBytes: Int64 + + /// The type of data from the modified DynamoDB item that was captured in this + /// stream record. + public let streamViewType: StreamViewType + } + + public struct UserIdentity: Codable { + public let type: String + public let principalId: String + } +} + +extension DynamoDB.StreamRecord: Decodable { + enum CodingKeys: String, CodingKey { + case approximateCreationDateTime = "ApproximateCreationDateTime" + case keys = "Keys" + case newImage = "NewImage" + case oldImage = "OldImage" + case sequenceNumber = "SequenceNumber" + case sizeBytes = "SizeBytes" + case streamViewType = "StreamViewType" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.keys = try container.decode( + [String: DynamoDB.AttributeValue].self, + forKey: .keys + ) + + self.newImage = try container.decodeIfPresent( + [String: DynamoDB.AttributeValue].self, + forKey: .newImage + ) + self.oldImage = try container.decodeIfPresent( + [String: DynamoDB.AttributeValue].self, + forKey: .oldImage + ) + + self.sequenceNumber = try container.decode(String.self, forKey: .sequenceNumber) + self.sizeBytes = try container.decode(Int64.self, forKey: .sizeBytes) + self.streamViewType = try container.decode(DynamoDB.StreamViewType.self, forKey: .streamViewType) + + if let timestamp = try container.decodeIfPresent(Double.self, forKey: .approximateCreationDateTime) { + self.approximateCreationDateTime = Date(timeIntervalSince1970: timestamp) + } else { + self.approximateCreationDateTime = nil + } + } +} + +// MARK: - AttributeValue - + +extension DynamoDB { + public enum AttributeValue { + case boolean(Bool) + case binary([UInt8]) + case binarySet([[UInt8]]) + case string(String) + case stringSet([String]) + case null + case number(String) + case numberSet([String]) + + case list([AttributeValue]) + case map([String: AttributeValue]) + } +} + +extension DynamoDB.AttributeValue: Decodable { + enum CodingKeys: String, CodingKey { + case binary = "B" + case bool = "BOOL" + case binarySet = "BS" + case list = "L" + case map = "M" + case number = "N" + case numberSet = "NS" + case null = "NULL" + case string = "S" + case stringSet = "SS" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + guard container.allKeys.count == 1, let key = container.allKeys.first else { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Expected exactly one key, but got \(container.allKeys.count)" + ) + throw DecodingError.dataCorrupted(context) + } + + switch key { + case .binary: + let encoded = try container.decode(String.self, forKey: .binary) + self = .binary(try encoded.base64decoded()) + + case .bool: + let value = try container.decode(Bool.self, forKey: .bool) + self = .boolean(value) + + case .binarySet: + let values = try container.decode([String].self, forKey: .binarySet) + let buffers = try values.map { try $0.base64decoded() } + self = .binarySet(buffers) + + case .list: + let values = try container.decode([DynamoDB.AttributeValue].self, forKey: .list) + self = .list(values) + + case .map: + let value = try container.decode([String: DynamoDB.AttributeValue].self, forKey: .map) + self = .map(value) + + case .number: + let value = try container.decode(String.self, forKey: .number) + self = .number(value) + + case .numberSet: + let values = try container.decode([String].self, forKey: .numberSet) + self = .numberSet(values) + + case .null: + self = .null + + case .string: + let value = try container.decode(String.self, forKey: .string) + self = .string(value) + + case .stringSet: + let values = try container.decode([String].self, forKey: .stringSet) + self = .stringSet(values) + } + } +} + +extension DynamoDB.AttributeValue: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.boolean(let lhs), .boolean(let rhs)): + return lhs == rhs + case (.binary(let lhs), .binary(let rhs)): + return lhs == rhs + case (.binarySet(let lhs), .binarySet(let rhs)): + return lhs == rhs + case (.string(let lhs), .string(let rhs)): + return lhs == rhs + case (.stringSet(let lhs), .stringSet(let rhs)): + return lhs == rhs + case (.null, .null): + return true + case (.number(let lhs), .number(let rhs)): + return lhs == rhs + case (.numberSet(let lhs), .numberSet(let rhs)): + return lhs == rhs + case (.list(let lhs), .list(let rhs)): + return lhs == rhs + case (.map(let lhs), .map(let rhs)): + return lhs == rhs + default: + return false + } + } +} + +// MARK: DynamoDB AttributeValue Decoding + +extension DynamoDB { + public struct Decoder { + @usableFromInline var userInfo: [CodingUserInfoKey: Any] = [:] + + public init() {} + + @inlinable public func decode(_ type: T.Type, from image: [String: AttributeValue]) + throws -> T + { + try self.decode(type, from: .map(image)) + } + + @inlinable public func decode(_ type: T.Type, from value: AttributeValue) + throws -> T + { + let decoder = _DecoderImpl(userInfo: userInfo, from: value, codingPath: []) + return try decoder.decode(T.self) + } + } + + @usableFromInline internal struct _DecoderImpl: Swift.Decoder { + @usableFromInline let codingPath: [CodingKey] + @usableFromInline let userInfo: [CodingUserInfoKey: Any] + + @usableFromInline let value: AttributeValue + + @inlinable init(userInfo: [CodingUserInfoKey: Any], from value: AttributeValue, codingPath: [CodingKey]) { + self.userInfo = userInfo + self.codingPath = codingPath + self.value = value + } + + @inlinable public func decode(_: T.Type) throws -> T { + try T(from: self) + } + + @usableFromInline func container(keyedBy type: Key.Type) throws -> + KeyedDecodingContainer where Key: CodingKey { + guard case .map(let dictionary) = self.value else { + throw DecodingError.typeMismatch([String: AttributeValue].self, DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Expected to decode \([String: AttributeValue].self) but found \(self.value.debugDataTypeDescription) instead." + )) + } + + let container = _KeyedDecodingContainer( + impl: self, + codingPath: self.codingPath, + dictionary: dictionary + ) + return KeyedDecodingContainer(container) + } + + @usableFromInline func unkeyedContainer() throws -> UnkeyedDecodingContainer { + guard case .list(let array) = self.value else { + throw DecodingError.typeMismatch([AttributeValue].self, DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Expected to decode \([AttributeValue].self) but found \(self.value.debugDataTypeDescription) instead." + )) + } + + return _UnkeyedDecodingContainer( + impl: self, + codingPath: self.codingPath, + array: array + ) + } + + @usableFromInline func singleValueContainer() throws -> SingleValueDecodingContainer { + _SingleValueDecodingContainter( + impl: self, + codingPath: self.codingPath, + value: self.value + ) + } + } + + struct ArrayKey: CodingKey, Equatable { + init(index: Int) { + self.intValue = index + } + + init?(stringValue _: String) { + preconditionFailure("Did not expect to be initialized with a string") + } + + init?(intValue: Int) { + self.intValue = intValue + } + + var intValue: Int? + + var stringValue: String { + "Index \(self.intValue!)" + } + + static func == (lhs: ArrayKey, rhs: ArrayKey) -> Bool { + precondition(lhs.intValue != nil) + precondition(rhs.intValue != nil) + return lhs.intValue == rhs.intValue + } + } + + struct _KeyedDecodingContainer: KeyedDecodingContainerProtocol { + typealias Key = K + + let impl: _DecoderImpl + let codingPath: [CodingKey] + let dictionary: [String: AttributeValue] + + init(impl: _DecoderImpl, codingPath: [CodingKey], dictionary: [String: AttributeValue]) { + self.impl = impl + self.codingPath = codingPath + self.dictionary = dictionary + } + + var allKeys: [K] { + self.dictionary.keys.compactMap { K(stringValue: $0) } + } + + func contains(_ key: K) -> Bool { + if let _ = self.dictionary[key.stringValue] { + return true + } + return false + } + + func decodeNil(forKey key: K) throws -> Bool { + let value = try getValue(forKey: key) + return value == .null + } + + func decode(_ type: Bool.Type, forKey key: K) throws -> Bool { + let value = try getValue(forKey: key) + + guard case .boolean(let bool) = value else { + throw self.createTypeMismatchError(type: type, forKey: key, value: value) + } + + return bool + } + + func decode(_ type: String.Type, forKey key: K) throws -> String { + let value = try getValue(forKey: key) + + guard case .string(let string) = value else { + throw self.createTypeMismatchError(type: type, forKey: key, value: value) + } + + return string + } + + func decode(_ type: Double.Type, forKey key: K) throws -> Double { + try self.decodeLosslessStringConvertible(key: key) + } + + func decode(_ type: Float.Type, forKey key: K) throws -> Float { + try self.decodeLosslessStringConvertible(key: key) + } + + func decode(_ type: Int.Type, forKey key: K) throws -> Int { + try self.decodeFixedWidthInteger(key: key) + } + + func decode(_ type: Int8.Type, forKey key: K) throws -> Int8 { + try self.decodeFixedWidthInteger(key: key) + } + + func decode(_ type: Int16.Type, forKey key: K) throws -> Int16 { + try self.decodeFixedWidthInteger(key: key) + } + + func decode(_ type: Int32.Type, forKey key: K) throws -> Int32 { + try self.decodeFixedWidthInteger(key: key) + } + + func decode(_ type: Int64.Type, forKey key: K) throws -> Int64 { + try self.decodeFixedWidthInteger(key: key) + } + + func decode(_ type: UInt.Type, forKey key: K) throws -> UInt { + try self.decodeFixedWidthInteger(key: key) + } + + func decode(_ type: UInt8.Type, forKey key: K) throws -> UInt8 { + try self.decodeFixedWidthInteger(key: key) + } + + func decode(_ type: UInt16.Type, forKey key: K) throws -> UInt16 { + try self.decodeFixedWidthInteger(key: key) + } + + func decode(_ type: UInt32.Type, forKey key: K) throws -> UInt32 { + try self.decodeFixedWidthInteger(key: key) + } + + func decode(_ type: UInt64.Type, forKey key: K) throws -> UInt64 { + try self.decodeFixedWidthInteger(key: key) + } + + func decode(_ type: T.Type, forKey key: K) throws -> T where T: Decodable { + let decoder = try self.decoderForKey(key) + return try T(from: decoder) + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: K) throws + -> KeyedDecodingContainer where NestedKey: CodingKey { + return try self.decoderForKey(key).container(keyedBy: type) + } + + func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer { + try self.decoderForKey(key).unkeyedContainer() + } + + func superDecoder() throws -> Swift.Decoder { + self.impl + } + + func superDecoder(forKey key: K) throws -> Swift.Decoder { + self.impl + } + + private func decoderForKey(_ key: K) throws -> _DecoderImpl { + let value = try getValue(forKey: key) + var newPath = self.codingPath + newPath.append(key) + + return _DecoderImpl( + userInfo: self.impl.userInfo, + from: value, + codingPath: newPath + ) + } + + @inline(__always) private func getValue(forKey key: K) throws -> AttributeValue { + guard let value = self.dictionary[key.stringValue] else { + throw DecodingError.keyNotFound(key, .init( + codingPath: self.codingPath, + debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\")." + )) + } + + return value + } + + @inline(__always) private func createTypeMismatchError(type: Any.Type, forKey key: K, value: AttributeValue) -> DecodingError { + let codingPath = self.codingPath + [key] + return DecodingError.typeMismatch(type, .init( + codingPath: codingPath, debugDescription: "Expected to decode \(type) but found \(value.debugDataTypeDescription) instead." + )) + } + + @inline(__always) private func decodeFixedWidthInteger(key: Self.Key) + throws -> T + { + let value = try getValue(forKey: key) + + guard case .number(let number) = value else { + throw self.createTypeMismatchError(type: T.self, forKey: key, value: value) + } + + guard let integer = T(number) else { + throw DecodingError.dataCorruptedError( + forKey: key, + in: self, + debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)." + ) + } + + return integer + } + + @inline(__always) private func decodeLosslessStringConvertible( + key: Self.Key) throws -> T + { + let value = try getValue(forKey: key) + + guard case .number(let number) = value else { + throw self.createTypeMismatchError(type: T.self, forKey: key, value: value) + } + + guard let floatingPoint = T(number) else { + throw DecodingError.dataCorruptedError( + forKey: key, + in: self, + debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)." + ) + } + + return floatingPoint + } + } + + struct _SingleValueDecodingContainter: SingleValueDecodingContainer { + let impl: _DecoderImpl + let value: AttributeValue + let codingPath: [CodingKey] + + init(impl: _DecoderImpl, codingPath: [CodingKey], value: AttributeValue) { + self.impl = impl + self.codingPath = codingPath + self.value = value + } + + func decodeNil() -> Bool { + self.value == .null + } + + func decode(_: Bool.Type) throws -> Bool { + guard case .boolean(let bool) = self.value else { + throw self.createTypeMismatchError(type: Bool.self, value: self.value) + } + + return bool + } + + func decode(_: String.Type) throws -> String { + guard case .string(let string) = self.value else { + throw self.createTypeMismatchError(type: String.self, value: self.value) + } + + return string + } + + func decode(_: Double.Type) throws -> Double { + try self.decodeLosslessStringConvertible() + } + + func decode(_: Float.Type) throws -> Float { + try self.decodeLosslessStringConvertible() + } + + func decode(_: Int.Type) throws -> Int { + try self.decodeFixedWidthInteger() + } + + func decode(_: Int8.Type) throws -> Int8 { + try self.decodeFixedWidthInteger() + } + + func decode(_: Int16.Type) throws -> Int16 { + try self.decodeFixedWidthInteger() + } + + func decode(_: Int32.Type) throws -> Int32 { + try self.decodeFixedWidthInteger() + } + + func decode(_: Int64.Type) throws -> Int64 { + try self.decodeFixedWidthInteger() + } + + func decode(_: UInt.Type) throws -> UInt { + try self.decodeFixedWidthInteger() + } + + func decode(_: UInt8.Type) throws -> UInt8 { + try self.decodeFixedWidthInteger() + } + + func decode(_: UInt16.Type) throws -> UInt16 { + try self.decodeFixedWidthInteger() + } + + func decode(_: UInt32.Type) throws -> UInt32 { + try self.decodeFixedWidthInteger() + } + + func decode(_: UInt64.Type) throws -> UInt64 { + try self.decodeFixedWidthInteger() + } + + func decode(_: T.Type) throws -> T where T: Decodable { + return try T(from: self.impl) + } + + @inline(__always) private func createTypeMismatchError(type: Any.Type, value: AttributeValue) -> DecodingError { + DecodingError.typeMismatch(type, .init( + codingPath: self.codingPath, + debugDescription: "Expected to decode \(type) but found \(value.debugDataTypeDescription) instead." + )) + } + + @inline(__always) private func decodeFixedWidthInteger() throws + -> T + { + guard case .number(let number) = self.value else { + throw self.createTypeMismatchError(type: T.self, value: self.value) + } + + guard let integer = T(number) else { + throw DecodingError.dataCorruptedError( + in: self, + debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)." + ) + } + + return integer + } + + @inline(__always) private func decodeLosslessStringConvertible() + throws -> T + { + guard case .number(let number) = self.value else { + throw self.createTypeMismatchError(type: T.self, value: self.value) + } + + guard let floatingPoint = T(number) else { + throw DecodingError.dataCorruptedError( + in: self, + debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self)." + ) + } + + return floatingPoint + } + } + + struct _UnkeyedDecodingContainer: UnkeyedDecodingContainer { + let impl: _DecoderImpl + let codingPath: [CodingKey] + let array: [AttributeValue] + + let count: Int? // protocol requirement to be optional + var isAtEnd = false + var currentIndex = 0 + + init(impl: _DecoderImpl, codingPath: [CodingKey], array: [AttributeValue]) { + self.impl = impl + self.codingPath = codingPath + self.array = array + self.count = array.count + } + + mutating func decodeNil() throws -> Bool { + if self.array[self.currentIndex] == .null { + defer { + currentIndex += 1 + if currentIndex == count { + isAtEnd = true + } + } + return true + } + + // The protocol states: + // If the value is not null, does not increment currentIndex. + return false + } + + mutating func decode(_ type: Bool.Type) throws -> Bool { + defer { + currentIndex += 1 + if currentIndex == count { + isAtEnd = true + } + } + + guard case .boolean(let bool) = self.array[self.currentIndex] else { + throw self.createTypeMismatchError(type: type, value: self.array[self.currentIndex]) + } + + return bool + } + + mutating func decode(_ type: String.Type) throws -> String { + defer { + currentIndex += 1 + if currentIndex == count { + isAtEnd = true + } + } + + guard case .string(let string) = self.array[self.currentIndex] else { + throw self.createTypeMismatchError(type: type, value: self.array[self.currentIndex]) + } + + return string + } + + mutating func decode(_: Double.Type) throws -> Double { + try self.decodeLosslessStringConvertible() + } + + mutating func decode(_: Float.Type) throws -> Float { + try self.decodeLosslessStringConvertible() + } + + mutating func decode(_: Int.Type) throws -> Int { + try self.decodeFixedWidthInteger() + } + + mutating func decode(_: Int8.Type) throws -> Int8 { + try self.decodeFixedWidthInteger() + } + + mutating func decode(_: Int16.Type) throws -> Int16 { + try self.decodeFixedWidthInteger() + } + + mutating func decode(_: Int32.Type) throws -> Int32 { + try self.decodeFixedWidthInteger() + } + + mutating func decode(_: Int64.Type) throws -> Int64 { + try self.decodeFixedWidthInteger() + } + + mutating func decode(_: UInt.Type) throws -> UInt { + try self.decodeFixedWidthInteger() + } + + mutating func decode(_: UInt8.Type) throws -> UInt8 { + try self.decodeFixedWidthInteger() + } + + mutating func decode(_: UInt16.Type) throws -> UInt16 { + try self.decodeFixedWidthInteger() + } + + mutating func decode(_: UInt32.Type) throws -> UInt32 { + try self.decodeFixedWidthInteger() + } + + mutating func decode(_: UInt64.Type) throws -> UInt64 { + try self.decodeFixedWidthInteger() + } + + mutating func decode(_: T.Type) throws -> T where T: Decodable { + defer { + currentIndex += 1 + if currentIndex == count { + isAtEnd = true + } + } + + let json = self.array[self.currentIndex] + var newPath = self.codingPath + newPath.append(ArrayKey(index: self.currentIndex)) + let decoder = _DecoderImpl(userInfo: impl.userInfo, from: json, codingPath: newPath) + + return try T(from: decoder) + } + + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws + -> KeyedDecodingContainer where NestedKey: CodingKey { + return try self.impl.container(keyedBy: type) + } + + mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + try self.impl.unkeyedContainer() + } + + mutating func superDecoder() throws -> Swift.Decoder { + self.impl + } + + @inline(__always) private func createTypeMismatchError(type: Any.Type, value: AttributeValue) -> DecodingError { + let codingPath = self.codingPath + [ArrayKey(index: self.currentIndex)] + return DecodingError.typeMismatch(type, .init( + codingPath: codingPath, debugDescription: "Expected to decode \(type) but found \(value.debugDataTypeDescription) instead." + )) + } + + @inline(__always) private mutating func decodeFixedWidthInteger() throws + -> T + { + defer { + currentIndex += 1 + if currentIndex == count { + isAtEnd = true + } + } + + guard case .number(let number) = self.array[self.currentIndex] else { + throw self.createTypeMismatchError(type: T.self, value: self.array[self.currentIndex]) + } + + guard let integer = T(number) else { + throw DecodingError.dataCorruptedError(in: self, + debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self).") + } + + return integer + } + + @inline(__always) private mutating func decodeLosslessStringConvertible() + throws -> T + { + defer { + currentIndex += 1 + if currentIndex == count { + isAtEnd = true + } + } + + guard case .number(let number) = self.array[self.currentIndex] else { + throw self.createTypeMismatchError(type: T.self, value: self.array[self.currentIndex]) + } + + guard let float = T(number) else { + throw DecodingError.dataCorruptedError(in: self, + debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self).") + } + + return float + } + } +} + +extension DynamoDB.AttributeValue { + fileprivate var debugDataTypeDescription: String { + switch self { + case .list: + return "a list" + case .boolean: + return "boolean" + case .number: + return "a number" + case .string: + return "a string" + case .map: + return "a map" + case .null: + return "null" + case .binary: + return "bytes" + case .binarySet: + return "a set of bytes" + case .stringSet: + return "a set of strings" + case .numberSet: + return "a set of numbers" + } + } +} diff --git a/Tests/AWSLambdaEventsTests/DynamoDBTests.swift b/Tests/AWSLambdaEventsTests/DynamoDBTests.swift new file mode 100644 index 00000000..ca391da8 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/DynamoDBTests.swift @@ -0,0 +1,232 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import AWSLambdaEvents +import XCTest + +class DynamoDBTests: XCTestCase { + static let streamEventPayload = """ + { + "Records": [ + { + "eventID": "1", + "eventVersion": "1.0", + "dynamodb": { + "ApproximateCreationDateTime": 1.578648338E9, + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "StreamViewType": "NEW_AND_OLD_IMAGES", + "SequenceNumber": "111", + "SizeBytes": 26 + }, + "awsRegion": "eu-central-1", + "eventName": "INSERT", + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "eventSource": "aws:dynamodb" + }, + { + "eventID": "2", + "eventVersion": "1.0", + "dynamodb": { + "ApproximateCreationDateTime": 1.578648338E9, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "SequenceNumber": "222", + "Keys": { + "Id": { + "N": "101" + } + }, + "SizeBytes": 59, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "awsRegion": "eu-central-1", + "eventName": "MODIFY", + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "eventSource": "aws:dynamodb" + }, + { + "eventID": "3", + "eventVersion": "1.0", + "dynamodb": { + "ApproximateCreationDateTime":1.578648338E9, + "Keys": { + "Id": { + "N": "101" + } + }, + "SizeBytes": 38, + "SequenceNumber": "333", + "OldImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "awsRegion": "eu-central-1", + "eventName": "REMOVE", + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "eventSource": "aws:dynamodb" + } + ] + } + """ + + func testEventFromJSON() { + let data = DynamoDBTests.streamEventPayload.data(using: .utf8)! + var event: DynamoDB.Event? + XCTAssertNoThrow(event = try JSONDecoder().decode(DynamoDB.Event.self, from: data)) + + XCTAssertEqual(event?.records.count, 3) + } + + // MARK: - Parse Attribute Value Tests - + + func testAttributeValueBoolDecoding() { + let json = "{\"BOOL\": true}" + var value: DynamoDB.AttributeValue? + XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) + XCTAssertEqual(value, .boolean(true)) + } + + func testAttributeValueBinaryDecoding() { + let json = "{\"B\": \"YmFzZTY0\"}" + var value: DynamoDB.AttributeValue? + XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) + XCTAssertEqual(value, .binary([UInt8]("base64".utf8))) + } + + func testAttributeValueBinarySetDecoding() { + let json = "{\"BS\": [\"YmFzZTY0\", \"YWJjMTIz\"]}" + var value: DynamoDB.AttributeValue? + XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) + XCTAssertEqual(value, .binarySet([[UInt8]("base64".utf8), [UInt8]("abc123".utf8)])) + } + + func testAttributeValueStringDecoding() { + let json = "{\"S\": \"huhu\"}" + var value: DynamoDB.AttributeValue? + XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) + XCTAssertEqual(value, .string("huhu")) + } + + func testAttributeValueStringSetDecoding() { + let json = "{\"SS\": [\"huhu\", \"haha\"]}" + var value: DynamoDB.AttributeValue? + XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) + XCTAssertEqual(value, .stringSet(["huhu", "haha"])) + } + + func testAttributeValueNullDecoding() { + let json = "{\"NULL\": true}" + var value: DynamoDB.AttributeValue? + XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) + XCTAssertEqual(value, .null) + } + + func testAttributeValueNumberDecoding() { + let json = "{\"N\": \"1.2345\"}" + var value: DynamoDB.AttributeValue? + XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) + XCTAssertEqual(value, .number("1.2345")) + } + + func testAttributeValueNumberSetDecoding() { + let json = "{\"NS\": [\"1.2345\", \"-19\"]}" + var value: DynamoDB.AttributeValue? + XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) + XCTAssertEqual(value, .numberSet(["1.2345", "-19"])) + } + + func testAttributeValueListDecoding() { + let json = "{\"L\": [{\"NS\": [\"1.2345\", \"-19\"]}, {\"S\": \"huhu\"}]}" + var value: DynamoDB.AttributeValue? + XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) + XCTAssertEqual(value, .list([.numberSet(["1.2345", "-19"]), .string("huhu")])) + } + + func testAttributeValueMapDecoding() { + let json = "{\"M\": {\"numbers\": {\"NS\": [\"1.2345\", \"-19\"]}, \"string\": {\"S\": \"huhu\"}}}" + var value: DynamoDB.AttributeValue? + XCTAssertNoThrow(value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) + XCTAssertEqual(value, .map([ + "numbers": .numberSet(["1.2345", "-19"]), + "string": .string("huhu"), + ])) + } + + func testAttributeValueEmptyDecoding() { + let json = "{\"haha\": 1}" + XCTAssertThrowsError(_ = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)) { error in + guard case DecodingError.dataCorrupted = error else { + XCTFail("Unexpected error: \(String(describing: error))") + return + } + } + } + + func testAttributeValueEquatable() { + XCTAssertEqual(DynamoDB.AttributeValue.boolean(true), .boolean(true)) + XCTAssertNotEqual(DynamoDB.AttributeValue.boolean(true), .boolean(false)) + XCTAssertNotEqual(DynamoDB.AttributeValue.boolean(true), .string("haha")) + } + + // MARK: - DynamoDB Decoder Tests - + + func testDecoderSimple() { + let value: [String: DynamoDB.AttributeValue] = [ + "foo": .string("bar"), + "xyz": .number("123"), + ] + + struct Test: Codable { + let foo: String + let xyz: UInt8 + } + + var test: Test? + XCTAssertNoThrow(test = try DynamoDB.Decoder().decode(Test.self, from: value)) + XCTAssertEqual(test?.foo, "bar") + XCTAssertEqual(test?.xyz, 123) + } +} From 5361e2f64982da6ed135da54158540df4d75f3d8 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 7 May 2020 18:37:28 +0200 Subject: [PATCH 7/7] Moved Foundation to AWSLambdaRuntime; AWSLambdaRuntime renamed to AWSLambdaRuntimeCore (#41) motivation: enable non-foundation module for performance sensitive use cases changes: * rename AWSLambdaRuntime to AWSLambdaRuntimeCore * create AWSLambdaRuntime for Foundation dependent functionality * have (new) AWSLambdaRuntime export AWSLambdaRuntimeCore * adjust tests --- Package.swift | 33 ++- .../AWSLambdaRuntime/Context+Foundation.swift | 23 ++ Sources/AWSLambdaRuntime/Lambda+Codable.swift | 19 +- .../HTTPClient.swift | 0 .../Lambda+String.swift | 0 .../Lambda.swift | 0 .../LambdaConfiguration.swift | 0 .../LambdaContext.swift | 0 .../LambdaHandler.swift | 0 .../LambdaLifecycle.swift | 0 .../LambdaRunner.swift | 0 .../LambdaRuntimeClient.swift | 0 .../Utils.swift | 0 Sources/AWSLambdaTesting/Lambda+Testing.swift | 1 + Sources/CodableSample/main.swift | 2 +- Sources/StringSample/main.swift | 2 +- .../Lambda+StringTest.swift | 2 +- .../LambdaRunnerTest.swift | 2 +- .../LambdaRuntimeClientTest.swift | 2 +- .../LambdaTest.swift | 2 +- .../MockLambdaServer.swift | 2 +- .../Utils.swift | 2 +- .../Lambda+CodeableTest.swift | 275 +++--------------- 23 files changed, 105 insertions(+), 262 deletions(-) create mode 100644 Sources/AWSLambdaRuntime/Context+Foundation.swift rename Sources/{AWSLambdaRuntime => AWSLambdaRuntimeCore}/HTTPClient.swift (100%) rename Sources/{AWSLambdaRuntime => AWSLambdaRuntimeCore}/Lambda+String.swift (100%) rename Sources/{AWSLambdaRuntime => AWSLambdaRuntimeCore}/Lambda.swift (100%) rename Sources/{AWSLambdaRuntime => AWSLambdaRuntimeCore}/LambdaConfiguration.swift (100%) rename Sources/{AWSLambdaRuntime => AWSLambdaRuntimeCore}/LambdaContext.swift (100%) rename Sources/{AWSLambdaRuntime => AWSLambdaRuntimeCore}/LambdaHandler.swift (100%) rename Sources/{AWSLambdaRuntime => AWSLambdaRuntimeCore}/LambdaLifecycle.swift (100%) rename Sources/{AWSLambdaRuntime => AWSLambdaRuntimeCore}/LambdaRunner.swift (100%) rename Sources/{AWSLambdaRuntime => AWSLambdaRuntimeCore}/LambdaRuntimeClient.swift (100%) rename Sources/{AWSLambdaRuntime => AWSLambdaRuntimeCore}/Utils.swift (100%) rename Tests/{AWSLambdaRuntimeTests => AWSLambdaRuntimeCoreTests}/Lambda+StringTest.swift (99%) rename Tests/{AWSLambdaRuntimeTests => AWSLambdaRuntimeCoreTests}/LambdaRunnerTest.swift (98%) rename Tests/{AWSLambdaRuntimeTests => AWSLambdaRuntimeCoreTests}/LambdaRuntimeClientTest.swift (99%) rename Tests/{AWSLambdaRuntimeTests => AWSLambdaRuntimeCoreTests}/LambdaTest.swift (99%) rename Tests/{AWSLambdaRuntimeTests => AWSLambdaRuntimeCoreTests}/MockLambdaServer.swift (99%) rename Tests/{AWSLambdaRuntimeTests => AWSLambdaRuntimeCoreTests}/Utils.swift (98%) diff --git a/Package.swift b/Package.swift index 624cf6b5..94353929 100644 --- a/Package.swift +++ b/Package.swift @@ -8,8 +8,10 @@ let package = Package( .macOS(.v10_13), ], products: [ - // core library + // this library exports `AWSLambdaRuntimeCore` and adds Foundation convenience methods .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), + // this has all the main functionality for lambda and it does not link Foundation + .library(name: "AWSLambdaRuntimeCore", targets: ["AWSLambdaRuntimeCore"]), // common AWS events .library(name: "AWSLambdaEvents", targets: ["AWSLambdaEvents"]), // for testing only @@ -22,23 +24,40 @@ let package = Package( ], targets: [ .target(name: "AWSLambdaRuntime", dependencies: [ + .byName(name: "AWSLambdaRuntimeCore"), + .product(name: "NIO", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + ]), + .target(name: "AWSLambdaRuntimeCore", dependencies: [ .product(name: "Logging", package: "swift-log"), .product(name: "Backtrace", package: "swift-backtrace"), .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), ]), - .testTarget(name: "AWSLambdaRuntimeTests", dependencies: ["AWSLambdaRuntime"]), + .testTarget(name: "AWSLambdaRuntimeCoreTests", dependencies: [ + .byName(name: "AWSLambdaRuntimeCore"), + ]), + .testTarget(name: "AWSLambdaRuntimeTests", dependencies: [ + .byName(name: "AWSLambdaRuntimeCore"), + .byName(name: "AWSLambdaRuntime"), + ]), .target(name: "AWSLambdaEvents", dependencies: []), .testTarget(name: "AWSLambdaEventsTests", dependencies: ["AWSLambdaEvents"]), // testing helper .target(name: "AWSLambdaTesting", dependencies: [ - "AWSLambdaRuntime", + .byName(name: "AWSLambdaRuntime"), .product(name: "NIO", package: "swift-nio"), ]), - .testTarget(name: "AWSLambdaTestingTests", dependencies: ["AWSLambdaTesting"]), + .testTarget(name: "AWSLambdaTestingTests", dependencies: [ + .byName(name: "AWSLambdaTesting"), + .byName(name: "AWSLambdaRuntime"), + ]), // samples - .target(name: "StringSample", dependencies: ["AWSLambdaRuntime"]), - .target(name: "CodableSample", dependencies: ["AWSLambdaRuntime"]), + .target(name: "StringSample", dependencies: [ + .byName(name: "AWSLambdaRuntime"), + ]), + .target(name: "CodableSample", dependencies: [ + .byName(name: "AWSLambdaRuntime"), + ]), // perf tests .target(name: "MockServer", dependencies: [ .product(name: "NIOHTTP1", package: "swift-nio"), diff --git a/Sources/AWSLambdaRuntime/Context+Foundation.swift b/Sources/AWSLambdaRuntime/Context+Foundation.swift new file mode 100644 index 00000000..0aa1c019 --- /dev/null +++ b/Sources/AWSLambdaRuntime/Context+Foundation.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntimeCore +import struct Foundation.Date + +extension Lambda.Context { + var deadlineDate: Date { + let secondsSinceEpoch = Double(Int64(bitPattern: self.deadline.rawValue)) / -1_000_000_000 + return Date(timeIntervalSince1970: secondsSinceEpoch) + } +} diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index e24e736d..23743a98 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +@_exported import AWSLambdaRuntimeCore import class Foundation.JSONDecoder import class Foundation.JSONEncoder import NIO @@ -29,7 +30,7 @@ extension Lambda { /// /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. public static func run(_ closure: @escaping CodableClosure) { - self.run(closure: closure) + self.run(CodableClosureWrapper(closure)) } /// An asynchronous Lambda Closure that takes a `In: Decodable` and returns a `Result` via a completion handler. @@ -42,19 +43,7 @@ extension Lambda { /// /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. public static func run(_ closure: @escaping CodableVoidClosure) { - self.run(closure: closure) - } - - // for testing - @discardableResult - internal static func run(configuration: Configuration = .init(), closure: @escaping CodableClosure) -> Result { - self.run(configuration: configuration, handler: CodableClosureWrapper(closure)) - } - - // for testing - @discardableResult - internal static func run(configuration: Configuration = .init(), closure: @escaping CodableVoidClosure) -> Result { - self.run(configuration: configuration, handler: CodableVoidClosureWrapper(closure)) + self.run(CodableVoidClosureWrapper(closure)) } } diff --git a/Sources/AWSLambdaRuntime/HTTPClient.swift b/Sources/AWSLambdaRuntimeCore/HTTPClient.swift similarity index 100% rename from Sources/AWSLambdaRuntime/HTTPClient.swift rename to Sources/AWSLambdaRuntimeCore/HTTPClient.swift diff --git a/Sources/AWSLambdaRuntime/Lambda+String.swift b/Sources/AWSLambdaRuntimeCore/Lambda+String.swift similarity index 100% rename from Sources/AWSLambdaRuntime/Lambda+String.swift rename to Sources/AWSLambdaRuntimeCore/Lambda+String.swift diff --git a/Sources/AWSLambdaRuntime/Lambda.swift b/Sources/AWSLambdaRuntimeCore/Lambda.swift similarity index 100% rename from Sources/AWSLambdaRuntime/Lambda.swift rename to Sources/AWSLambdaRuntimeCore/Lambda.swift diff --git a/Sources/AWSLambdaRuntime/LambdaConfiguration.swift b/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift similarity index 100% rename from Sources/AWSLambdaRuntime/LambdaConfiguration.swift rename to Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift similarity index 100% rename from Sources/AWSLambdaRuntime/LambdaContext.swift rename to Sources/AWSLambdaRuntimeCore/LambdaContext.swift diff --git a/Sources/AWSLambdaRuntime/LambdaHandler.swift b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift similarity index 100% rename from Sources/AWSLambdaRuntime/LambdaHandler.swift rename to Sources/AWSLambdaRuntimeCore/LambdaHandler.swift diff --git a/Sources/AWSLambdaRuntime/LambdaLifecycle.swift b/Sources/AWSLambdaRuntimeCore/LambdaLifecycle.swift similarity index 100% rename from Sources/AWSLambdaRuntime/LambdaLifecycle.swift rename to Sources/AWSLambdaRuntimeCore/LambdaLifecycle.swift diff --git a/Sources/AWSLambdaRuntime/LambdaRunner.swift b/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift similarity index 100% rename from Sources/AWSLambdaRuntime/LambdaRunner.swift rename to Sources/AWSLambdaRuntimeCore/LambdaRunner.swift diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift similarity index 100% rename from Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift rename to Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift diff --git a/Sources/AWSLambdaRuntime/Utils.swift b/Sources/AWSLambdaRuntimeCore/Utils.swift similarity index 100% rename from Sources/AWSLambdaRuntime/Utils.swift rename to Sources/AWSLambdaRuntimeCore/Utils.swift diff --git a/Sources/AWSLambdaTesting/Lambda+Testing.swift b/Sources/AWSLambdaTesting/Lambda+Testing.swift index d8b2b125..da14faa1 100644 --- a/Sources/AWSLambdaTesting/Lambda+Testing.swift +++ b/Sources/AWSLambdaTesting/Lambda+Testing.swift @@ -17,6 +17,7 @@ // @testable is used to access of internal functions #if DEBUG @testable import AWSLambdaRuntime +@testable import AWSLambdaRuntimeCore import Dispatch import Logging import NIO diff --git a/Sources/CodableSample/main.swift b/Sources/CodableSample/main.swift index eb985f41..34e845b1 100644 --- a/Sources/CodableSample/main.swift +++ b/Sources/CodableSample/main.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/StringSample/main.swift b/Sources/StringSample/main.swift index fddfa478..c7d0434a 100644 --- a/Sources/StringSample/main.swift +++ b/Sources/StringSample/main.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import AWSLambdaRuntime +import AWSLambdaRuntimeCore import NIO // in this example we are receiving and responding with strings diff --git a/Tests/AWSLambdaRuntimeTests/Lambda+StringTest.swift b/Tests/AWSLambdaRuntimeCoreTests/Lambda+StringTest.swift similarity index 99% rename from Tests/AWSLambdaRuntimeTests/Lambda+StringTest.swift rename to Tests/AWSLambdaRuntimeCoreTests/Lambda+StringTest.swift index 9eebf2d2..a0be16d6 100644 --- a/Tests/AWSLambdaRuntimeTests/Lambda+StringTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/Lambda+StringTest.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntime +@testable import AWSLambdaRuntimeCore import NIO import XCTest diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRunnerTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift similarity index 98% rename from Tests/AWSLambdaRuntimeTests/LambdaRunnerTest.swift rename to Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift index 71707681..f91bde4a 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRunnerTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntime +@testable import AWSLambdaRuntimeCore import XCTest class LambdaRunnerTest: XCTestCase { diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift similarity index 99% rename from Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift rename to Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift index f0b7c17d..ea3e279d 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntime +@testable import AWSLambdaRuntimeCore import XCTest class LambdaRuntimeClientTest: XCTestCase { diff --git a/Tests/AWSLambdaRuntimeTests/LambdaTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift similarity index 99% rename from Tests/AWSLambdaRuntimeTests/LambdaTest.swift rename to Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift index 6ff75fc1..60cc2e9a 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntime +@testable import AWSLambdaRuntimeCore import Logging import NIO import XCTest diff --git a/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift b/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift similarity index 99% rename from Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift rename to Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift index 595ee011..b2f671a6 100644 --- a/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntime +@testable import AWSLambdaRuntimeCore import Foundation // for JSON import Logging import NIO diff --git a/Tests/AWSLambdaRuntimeTests/Utils.swift b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift similarity index 98% rename from Tests/AWSLambdaRuntimeTests/Utils.swift rename to Tests/AWSLambdaRuntimeCoreTests/Utils.swift index 8cf46b6b..11e6004e 100644 --- a/Tests/AWSLambdaRuntimeTests/Utils.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntime +@testable import AWSLambdaRuntimeCore import Logging import NIO import XCTest diff --git a/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift b/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift index 1d4b79dc..371783b5 100644 --- a/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift +++ b/Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift @@ -13,266 +13,77 @@ //===----------------------------------------------------------------------===// @testable import AWSLambdaRuntime +@testable import AWSLambdaRuntimeCore +import Logging import NIO +import NIOFoundationCompat import XCTest class CodableLambdaTest: XCTestCase { - func testCallbackSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } + var eventLoopGroup: EventLoopGroup! + let allocator = ByteBufferAllocator() - struct Handler: LambdaHandler { - typealias In = Request - typealias Out = Response - - func handle(context: Lambda.Context, payload: Request, callback: (Result) -> Void) { - callback(.success(Response(requestId: payload.requestId))) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testVoidCallbackSuccess() { - let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = Request - typealias Out = Void - - func handle(context: Lambda.Context, payload: Request, callback: (Result) -> Void) { - callback(.success(())) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testCallbackFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = Request - typealias Out = Response - - func handle(context: Lambda.Context, payload: Request, callback: (Result) -> Void) { - callback(.failure(TestError("boom"))) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testEventLoopSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: EventLoopLambdaHandler { - typealias In = Request - typealias Out = Response - - func handle(context: Lambda.Context, payload: Request) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(Response(requestId: payload.requestId)) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testVoidEventLoopSuccess() { - let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: EventLoopLambdaHandler { - typealias In = Request - typealias Out = Void - - func handle(context: Lambda.Context, payload: Request) -> EventLoopFuture { - context.eventLoop.makeSucceededFuture(()) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testEventLoopFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: EventLoopLambdaHandler { - typealias In = Request - typealias Out = Response - - func handle(context: Lambda.Context, payload: Request) -> EventLoopFuture { - context.eventLoop.makeFailedFuture(TestError("boom")) - } - } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handler: Handler()) - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testClosureSuccess() { - let server = MockLambdaServer(behavior: Behavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration) { (_, payload: Request, callback) in - callback(.success(Response(requestId: payload.requestId))) - } - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) + override func setUp() { + self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) } - func testVoidClosureSuccess() { - let server = MockLambdaServer(behavior: Behavior(result: .success(nil))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration) { (_, _: Request, callback: (Result) -> Void) in - callback(.success(())) - } - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - } - - func testClosureFailure() { - let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom")))) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - let maxTimes = Int.random(in: 1 ... 10) - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result: Result = Lambda.run(configuration: configuration) { (_, _: Request, callback: (Result) -> Void) in - callback(.failure(TestError("boom"))) - } - assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) + override func tearDown() { + try! self.eventLoopGroup.syncShutdownGracefully() } - func testBootstrapFailure() { - let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) - XCTAssertNoThrow(try server.start().wait()) - defer { XCTAssertNoThrow(try server.stop().wait()) } - - struct Handler: LambdaHandler { - typealias In = Request - typealias Out = Response + func testCodableVoidClosureWrapper() { + let request = Request(requestId: UUID().uuidString) + var inputBuffer: ByteBuffer? + var outputBuffer: ByteBuffer? - init(eventLoop: EventLoop) throws { - throw TestError("kaboom") - } - - func handle(context: Lambda.Context, payload: Request, callback: (Result) -> Void) { - callback(.failure(TestError("should not be called"))) - } + let closureWrapper = CodableVoidClosureWrapper { (_, _: Request, completion) in + XCTAssertEqual(request, request) + completion(.success(())) } - let result = Lambda.run(factory: Handler.init) - assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) - } -} - -// TODO: taking advantage of the fact we know the serialization is json -private struct Behavior: LambdaServerBehavior { - let requestId: String - let payload: String - let result: Result - - init(requestId: String = UUID().uuidString, payload: String = "hello", result: Result = .success("hello")) { - self.requestId = requestId - self.payload = payload - self.result = result + XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) + XCTAssertNoThrow(outputBuffer = try closureWrapper.handle(context: self.newContext(), payload: XCTUnwrap(inputBuffer)).wait()) + XCTAssertNil(outputBuffer) } - func getInvocation() -> GetInvocationResult { - guard let payload = try? JSONEncoder().encode(Request(requestId: requestId)) else { - XCTFail("encoding error") - return .failure(.internalServerError) - } - guard let payloadAsString = String(data: payload, encoding: .utf8) else { - XCTFail("encoding error") - return .failure(.internalServerError) - } - return .success((requestId: self.requestId, payload: payloadAsString)) - } + func testCodableClosureWrapper() { + let request = Request(requestId: UUID().uuidString) + var inputBuffer: ByteBuffer? + var outputBuffer: ByteBuffer? + var response: Response? - func processResponse(requestId: String, response: String?) -> Result { - switch self.result { - case .success(let expected) where expected != nil: - guard let data = response?.data(using: .utf8) else { - XCTFail("decoding error") - return .failure(.internalServerError) - } - guard let response = try? JSONDecoder().decode(Response.self, from: data) else { - XCTFail("decoding error") - return .failure(.internalServerError) - } - XCTAssertEqual(self.requestId, response.requestId, "expecting requestId to match") - return .success(()) - case .success(let expected) where expected == nil: - XCTAssertNil(response) - return .success(()) - case .failure: - XCTFail("unexpected to fail, but succeeded with: \(response ?? "undefined")") - return .failure(.internalServerError) - default: - preconditionFailure("invalid state") + let closureWrapper = CodableClosureWrapper { (_, req: Request, completion: (Result) -> Void) in + XCTAssertEqual(request, request) + completion(.success(Response(requestId: req.requestId))) } - } - func processError(requestId: String, error: ErrorResponse) -> Result { - XCTAssertEqual(self.requestId, requestId, "expecting requestId to match") - switch self.result { - case .success: - XCTFail("unexpected to succeed, but failed with: \(error)") - return .failure(.internalServerError) - case .failure(let expected): - XCTAssertEqual(expected.description, error.errorMessage, "expecting error to match") - return .success(()) - } + XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) + XCTAssertNoThrow(outputBuffer = try closureWrapper.handle(context: self.newContext(), payload: XCTUnwrap(inputBuffer)).wait()) + XCTAssertNoThrow(response = try JSONDecoder().decode(Response.self, from: XCTUnwrap(outputBuffer))) + XCTAssertEqual(response?.requestId, request.requestId) } - func processInitError(error: ErrorResponse) -> Result { - XCTFail("should not report init error") - return .failure(.internalServerError) + // convencience method + func newContext() -> Lambda.Context { + Lambda.Context(requestId: UUID().uuidString, + traceId: "abc123", + invokedFunctionArn: "aws:arn:", + deadline: .now() + .seconds(3), + cognitoIdentity: nil, + clientContext: nil, + logger: Logger(label: "test"), + eventLoop: self.eventLoopGroup.next()) } } -private struct Request: Codable { +private struct Request: Codable, Equatable { let requestId: String init(requestId: String) { self.requestId = requestId } } -private struct Response: Codable { +private struct Response: Codable, Equatable { let requestId: String init(requestId: String) { self.requestId = requestId