diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..5d05cb9
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,77 @@
+name: CI
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ "Integration-Tests":
+ runs-on: ubuntu-18.04
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ - name: Install ruby
+ uses: actions/setup-ruby@v1
+ - name: Install aws-sam-cli
+ run: sudo pip install aws-sam-cli
+ - name: Build Docker Swift Dev Image
+ run: docker build -t swift-dev:5.1.2 .
+ - name: Build local layer
+ run: cd Layer && make create_layer
+ - name: test local lambda
+ run: make test_lambda
+ env:
+ EXAMPLE_LAMBDA: SquareNumber
+
+ "tuxOS-Tests":
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ tag: ['5.1']
+ container:
+ image: swift:${{ matrix.tag }}
+ volumes:
+ - $GITHUB_WORKSPACE:/src
+ options: --workdir /src
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ - name: Install dependencies
+ run: apt-get update && apt-get install -y zlib1g-dev zip openssl libssl-dev
+ - name: Test
+ run: swift test --enable-code-coverage --enable-test-discovery
+ - name: Convert coverage files
+ run: llvm-cov export -format="lcov" .build/debug/swift-aws-lambdaPackageTests.xctest -instr-profile .build/debug/codecov/default.profdata > info.lcov
+ - name: Upload to codecov.io
+ uses: codecov/codecov-action@v1.0.3
+ with:
+ token: ${{secrets.CODECOV_TOKEN}}
+
+ "macOS-Tests":
+ runs-on: macOS-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ - name: Show all Xcode versions
+ run: ls -an /Applications/ | grep Xcode*
+ - name: Change Xcode command line tools
+ run: sudo xcode-select -s /Applications/Xcode_11.2.app/Contents/Developer
+ - name: SPM Build
+ run: swift build
+ - name: SPM Tests
+ run: swift test --parallel -Xswiftc -DDEBUG
+ - name: Xcode Tests
+ run: |
+ swift package generate-xcodeproj
+ xcodebuild -quiet -parallel-testing-enabled YES -scheme swift-aws-lambda-Package -enableCodeCoverage YES build test
+ - name: Codecov
+ run: bash <(curl -s https://codecov.io/bash) -J 'AWSLambda' -t ${{secrets.CODECOV_TOKEN}}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e31ca2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+.build
+/*.xcodeproj
+xcuserdata
+Layer/swift-lambda-runtime
+Layer/swift-lambda-runtime.zip
+Examples/**/lambda.zip
\ No newline at end of file
diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AWSLambda.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AWSLambda.xcscheme
new file mode 100644
index 0000000..0a59884
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/AWSLambda.xcscheme
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ea26f11
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,6 @@
+# This Dockerfile is used to compile our examples, by just adding some dev
+# dependencies.
+
+FROM swift:5.1.2
+
+RUN apt-get update && apt-get install -y zlib1g-dev zip openssl libssl-dev
\ No newline at end of file
diff --git a/Docs/Add-Layer-to-Function.png b/Docs/Add-Layer-to-Function.png
new file mode 100644
index 0000000..5646397
Binary files /dev/null and b/Docs/Add-Layer-to-Function.png differ
diff --git a/Docs/Develop.md b/Docs/Develop.md
new file mode 100644
index 0000000..165a4e2
--- /dev/null
+++ b/Docs/Develop.md
@@ -0,0 +1,11 @@
+
+
+build
+```bash
+$ docker build -t swift-dev:5.1.2 .
+```
+
+run docker in interactive mode
+```
+$ docker run -it --rm -v $(pwd):"/src" --workdir "/src" swift-dev:5.1.2
+```
\ No newline at end of file
diff --git a/Docs/Function-Create.png b/Docs/Function-Create.png
new file mode 100644
index 0000000..603ab4c
Binary files /dev/null and b/Docs/Function-Create.png differ
diff --git a/Docs/Invocation-Success.png b/Docs/Invocation-Success.png
new file mode 100644
index 0000000..de2eef0
Binary files /dev/null and b/Docs/Invocation-Success.png differ
diff --git a/Docs/Layer-Copy-Arn.png b/Docs/Layer-Copy-Arn.png
new file mode 100644
index 0000000..c6078e9
Binary files /dev/null and b/Docs/Layer-Copy-Arn.png differ
diff --git a/Docs/Upload-Lambda-zip.png b/Docs/Upload-Lambda-zip.png
new file mode 100644
index 0000000..ab5e079
Binary files /dev/null and b/Docs/Upload-Lambda-zip.png differ
diff --git a/Examples/SquareNumber/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/Examples/SquareNumber/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/Examples/SquareNumber/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Examples/SquareNumber/Package.resolved b/Examples/SquareNumber/Package.resolved
new file mode 100644
index 0000000..8a8262e
--- /dev/null
+++ b/Examples/SquareNumber/Package.resolved
@@ -0,0 +1,52 @@
+{
+ "object": {
+ "pins": [
+ {
+ "package": "async-http-client",
+ "repositoryURL": "https://github.com/swift-server/async-http-client.git",
+ "state": {
+ "branch": null,
+ "revision": "51dc885a30ca704b02fa803099b0a9b5b38067b6",
+ "version": "1.0.0"
+ }
+ },
+ {
+ "package": "swift-log",
+ "repositoryURL": "https://github.com/apple/swift-log.git",
+ "state": {
+ "branch": null,
+ "revision": "e8aabbe95db22e064ad42f1a4a9f8982664c70ed",
+ "version": "1.1.1"
+ }
+ },
+ {
+ "package": "swift-nio",
+ "repositoryURL": "https://github.com/apple/swift-nio.git",
+ "state": {
+ "branch": null,
+ "revision": "8066b0f581604e3711979307a4377457e2b0f007",
+ "version": "2.9.0"
+ }
+ },
+ {
+ "package": "swift-nio-extras",
+ "repositoryURL": "https://github.com/apple/swift-nio-extras.git",
+ "state": {
+ "branch": null,
+ "revision": "ed97628fa310c314c4a5cd8038445054b2991f07",
+ "version": "1.3.1"
+ }
+ },
+ {
+ "package": "swift-nio-ssl",
+ "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
+ "state": {
+ "branch": null,
+ "revision": "e5c1af45ac934ac0a6117b2927a51d845cf4f705",
+ "version": "2.4.3"
+ }
+ }
+ ]
+ },
+ "version": 1
+}
diff --git a/Examples/SquareNumber/Package.swift b/Examples/SquareNumber/Package.swift
new file mode 100644
index 0000000..8a11aa4
--- /dev/null
+++ b/Examples/SquareNumber/Package.swift
@@ -0,0 +1,17 @@
+// swift-tools-version:5.1
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "SquareNumber",
+ dependencies: [
+ .package(path: "../../"),
+ ],
+ targets: [
+ .target(
+ name: "SquareNumber",
+ dependencies: ["AWSLambda"]
+ ),
+ ]
+)
diff --git a/Examples/SquareNumber/Sources/SquareNumber/main.swift b/Examples/SquareNumber/Sources/SquareNumber/main.swift
new file mode 100644
index 0000000..9bc9add
--- /dev/null
+++ b/Examples/SquareNumber/Sources/SquareNumber/main.swift
@@ -0,0 +1,33 @@
+import AWSLambda
+import NIO
+
+struct Input: Codable {
+ let number: Double
+}
+
+struct Output: Codable {
+ let result: Double
+}
+
+func squareNumber(input: Input, context: Context) -> Output {
+ let squaredNumber = input.number * input.number
+ return Output(result: squaredNumber)
+}
+
+let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+defer {
+ try! group.syncShutdownGracefully()
+}
+
+do {
+ let runtime = try Runtime.createRuntime(eventLoopGroup: group)
+ defer { try! runtime.syncShutdown() }
+
+ runtime.register(for: "squareNumber", handler: Runtime.codable(squareNumber))
+ try runtime.start().wait()
+}
+catch {
+ print(String(describing: error))
+}
+
+
diff --git a/Examples/SquareNumber/template.yaml b/Examples/SquareNumber/template.yaml
new file mode 100644
index 0000000..62f45c1
--- /dev/null
+++ b/Examples/SquareNumber/template.yaml
@@ -0,0 +1,37 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Description: >
+ sam-app
+
+ Sample SAM Template for sam-app
+
+# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
+Globals:
+ Function:
+ Timeout: 3
+
+Resources:
+
+ SwiftLayer:
+ Type: AWS::Serverless::LayerVersion
+ Properties:
+ ContentUri: Layer/swift-lambda-runtime/
+ # ContentUri:
+ # Bucket: de.fabianfett.denkan.app.sam
+ # Key: swift-lambda-runtime.zip
+
+ SquareNumberFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: Examples/SquareNumber/lambda.zip
+ Handler: "SquareNumber.squareNumber"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Events:
+ HelloWorld:
+ Type: Api
+ Properties:
+ Path: /hello
+ Method: get
+
diff --git a/Examples/TodoAPIGateway/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/Examples/TodoAPIGateway/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/Examples/TodoAPIGateway/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Examples/TodoAPIGateway/Package.resolved b/Examples/TodoAPIGateway/Package.resolved
new file mode 100644
index 0000000..bc4cc43
--- /dev/null
+++ b/Examples/TodoAPIGateway/Package.resolved
@@ -0,0 +1,106 @@
+{
+ "object": {
+ "pins": [
+ {
+ "package": "async-http-client",
+ "repositoryURL": "https://github.com/swift-server/async-http-client.git",
+ "state": {
+ "branch": null,
+ "revision": "51dc885a30ca704b02fa803099b0a9b5b38067b6",
+ "version": "1.0.0"
+ }
+ },
+ {
+ "package": "AWSSDKSwift",
+ "repositoryURL": "https://github.com/swift-aws/aws-sdk-swift.git",
+ "state": {
+ "branch": "master",
+ "revision": "00f9cca4c05bcc65572f78a66437fd764a3384e8",
+ "version": null
+ }
+ },
+ {
+ "package": "AWSSDKSwiftCore",
+ "repositoryURL": "https://github.com/swift-aws/aws-sdk-swift-core.git",
+ "state": {
+ "branch": null,
+ "revision": "a538c99d210de5f8713e42c2754119b45c79db6b",
+ "version": "4.0.0-rc2"
+ }
+ },
+ {
+ "package": "HypertextApplicationLanguage",
+ "repositoryURL": "https://github.com/swift-aws/HypertextApplicationLanguage.git",
+ "state": {
+ "branch": null,
+ "revision": "aa2c9141d491682f17b2310aed17b9adfc006256",
+ "version": "1.1.1"
+ }
+ },
+ {
+ "package": "INIParser",
+ "repositoryURL": "https://github.com/swift-aws/Perfect-INIParser.git",
+ "state": {
+ "branch": null,
+ "revision": "42de0efc7a01105e19b80d533d3d282a98277f6c",
+ "version": "3.0.3"
+ }
+ },
+ {
+ "package": "swift-log",
+ "repositoryURL": "https://github.com/apple/swift-log.git",
+ "state": {
+ "branch": null,
+ "revision": "e8aabbe95db22e064ad42f1a4a9f8982664c70ed",
+ "version": "1.1.1"
+ }
+ },
+ {
+ "package": "swift-nio",
+ "repositoryURL": "https://github.com/apple/swift-nio.git",
+ "state": {
+ "branch": null,
+ "revision": "8066b0f581604e3711979307a4377457e2b0f007",
+ "version": "2.9.0"
+ }
+ },
+ {
+ "package": "swift-nio-extras",
+ "repositoryURL": "https://github.com/apple/swift-nio-extras.git",
+ "state": {
+ "branch": null,
+ "revision": "ed97628fa310c314c4a5cd8038445054b2991f07",
+ "version": "1.3.1"
+ }
+ },
+ {
+ "package": "swift-nio-ssl",
+ "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
+ "state": {
+ "branch": null,
+ "revision": "e5c1af45ac934ac0a6117b2927a51d845cf4f705",
+ "version": "2.4.3"
+ }
+ },
+ {
+ "package": "swift-nio-ssl-support",
+ "repositoryURL": "https://github.com/apple/swift-nio-ssl-support.git",
+ "state": {
+ "branch": null,
+ "revision": "c02eec4e0e6d351cd092938cf44195a8e669f555",
+ "version": "1.0.0"
+ }
+ },
+ {
+ "package": "swift-nio-transport-services",
+ "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git",
+ "state": {
+ "branch": null,
+ "revision": "80b11dc13261e0e52f75ea3a0b2e04f24e925019",
+ "version": "1.2.1"
+ }
+ }
+ ]
+ },
+ "version": 1
+}
diff --git a/Examples/TodoAPIGateway/Package.swift b/Examples/TodoAPIGateway/Package.swift
new file mode 100644
index 0000000..1998d67
--- /dev/null
+++ b/Examples/TodoAPIGateway/Package.swift
@@ -0,0 +1,32 @@
+// swift-tools-version:5.1
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "TodoAPIGateway",
+ products: [
+ .executable(name: "TodoAPIGateway", targets: ["TodoAPIGateway"]),
+ .library(name: "TodoService", targets: ["TodoService"])
+ ],
+ dependencies: [
+ .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.9.0")),
+ .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.1.1")),
+ .package(url: "https://github.com/swift-aws/aws-sdk-swift.git", .branch("master")),
+ .package(path: "../../"),
+ ],
+ targets: [
+ .target(
+ name: "TodoAPIGateway",
+ dependencies: ["AWSLambda", "Logging", "TodoService", "NIO", "NIOHTTP1", "DynamoDB"]),
+ .testTarget(
+ name: "TodoAPIGatewayTests",
+ dependencies: ["TodoAPIGateway"]),
+ .target(
+ name: "TodoService",
+ dependencies: ["DynamoDB"]),
+ .testTarget(
+ name: "TodoServiceTests",
+ dependencies: ["TodoService"])
+ ]
+)
diff --git a/Examples/TodoAPIGateway/README.md b/Examples/TodoAPIGateway/README.md
new file mode 100644
index 0000000..782a665
--- /dev/null
+++ b/Examples/TodoAPIGateway/README.md
@@ -0,0 +1,7 @@
+# TodoAPIGateway
+
+This package demonstrates how swift-aws-lambda can be used with aws-sdk-swift to
+implement a very simple Todo-Backend. As the persistent store we use a DynamoDB.
+
+https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html
+
diff --git a/Examples/TodoAPIGateway/Sources/TodoAPIGateway/TodoController.swift b/Examples/TodoAPIGateway/Sources/TodoAPIGateway/TodoController.swift
new file mode 100644
index 0000000..31e65a6
--- /dev/null
+++ b/Examples/TodoAPIGateway/Sources/TodoAPIGateway/TodoController.swift
@@ -0,0 +1,159 @@
+//
+// File.swift
+//
+//
+// Created by Fabian Fett on 19.11.19.
+//
+
+import Foundation
+import NIO
+import NIOHTTP1
+import TodoService
+import AWSLambda
+
+class TodoController {
+
+ let store : TodoStore
+
+ static let sharedHeader = HTTPHeaders([
+ ("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE"),
+ ("Access-Control-Allow-Origin" , "*"),
+ ("Access-Control-Allow-Headers", "Content-Type"),
+ ("Server", "Swift on AWS Lambda"),
+ ])
+
+ init(store: TodoStore) {
+ self.store = store
+ }
+
+ func listTodos(request: APIGateway.Request, context: Context) -> EventLoopFuture {
+ return self.store.getTodos()
+ .flatMapThrowing { (items) -> APIGateway.Response in
+ return try APIGateway.Response(
+ statusCode: .ok,
+ headers: TodoController.sharedHeader,
+ payload: items,
+ encoder: self.createResponseEncoder(request))
+ }
+ }
+
+ struct NewTodo: Decodable {
+ let title: String
+ let order: Int?
+ let completed: Bool?
+ }
+
+ func createTodo(request: APIGateway.Request, context: Context) -> EventLoopFuture {
+ let newTodo: TodoItem
+ do {
+ let payload: NewTodo = try request.payload()
+ newTodo = TodoItem(
+ id: UUID().uuidString.lowercased(),
+ order: payload.order,
+ title: payload.title,
+ completed: payload.completed ?? false)
+ }
+ catch {
+ return context.eventLoop.makeFailedFuture(error)
+ }
+
+ return self.store.createTodo(newTodo)
+ .flatMapThrowing { (todo) -> APIGateway.Response in
+ return try APIGateway.Response(
+ statusCode: .created,
+ headers: TodoController.sharedHeader,
+ payload: todo,
+ encoder: self.createResponseEncoder(request))
+ }
+ }
+
+ func deleteAll(request: APIGateway.Request, context: Context) -> EventLoopFuture {
+ return self.store.deleteAllTodos()
+ .flatMapThrowing { _ -> APIGateway.Response in
+ return try APIGateway.Response(
+ statusCode: .ok,
+ headers: TodoController.sharedHeader,
+ payload: [TodoItem](),
+ encoder: self.createResponseEncoder(request))
+ }
+ }
+
+ func getTodo(request: APIGateway.Request, context: Context) -> EventLoopFuture {
+ guard let id = request.pathParameters?["id"] else {
+ return context.eventLoop.makeSucceededFuture(APIGateway.Response(statusCode: .badRequest))
+ }
+
+ return self.store.getTodo(id: id)
+ .flatMapThrowing { (todo) -> APIGateway.Response in
+ return try APIGateway.Response(
+ statusCode: .ok,
+ headers: TodoController.sharedHeader,
+ payload: todo,
+ encoder: self.createResponseEncoder(request))
+ }
+ .flatMapErrorThrowing { (error) -> APIGateway.Response in
+ switch error {
+ case TodoError.notFound:
+ return APIGateway.Response(statusCode: .notFound)
+ default:
+ throw error
+ }
+ }
+ }
+
+ func deleteTodo(request: APIGateway.Request, context: Context) -> EventLoopFuture {
+ guard let id = request.pathParameters?["id"] else {
+ return context.eventLoop.makeSucceededFuture(APIGateway.Response(statusCode: .badRequest))
+ }
+
+ return self.store.deleteTodos(ids: [id])
+ .flatMapThrowing { _ -> APIGateway.Response in
+ return try APIGateway.Response(
+ statusCode: .ok,
+ headers: TodoController.sharedHeader,
+ payload: [TodoItem](),
+ encoder: self.createResponseEncoder(request))
+ }
+ }
+
+ func patchTodo(request: APIGateway.Request, context: Context) -> EventLoopFuture {
+ guard let id = request.pathParameters?["id"] else {
+ return context.eventLoop.makeSucceededFuture(APIGateway.Response(statusCode: .badRequest))
+ }
+
+ let patchTodo: PatchTodo
+ do {
+ patchTodo = try request.payload()
+ }
+ catch {
+ return context.eventLoop.makeFailedFuture(error)
+ }
+
+ return self.store.patchTodo(id: id, patch: patchTodo)
+ .flatMapThrowing { (todo) -> APIGateway.Response in
+ return try APIGateway.Response(
+ statusCode: .ok,
+ headers: TodoController.sharedHeader,
+ payload: todo,
+ encoder: self.createResponseEncoder(request))
+ }
+ }
+
+ private func createResponseEncoder(_ request: APIGateway.Request) -> JSONEncoder {
+ let encoder = JSONEncoder()
+
+ guard let proto = request.headers?["X-Forwarded-Proto"], let host = request.headers?["Host"] else {
+ return encoder
+ }
+
+ if request.requestContext.apiId != "1234567890" {
+ encoder.userInfo[.baseUrl] = URL(string: "\(proto)://\(host)/\(request.requestContext.stage)")!
+ }
+ else { //local
+ encoder.userInfo[.baseUrl] = URL(string: "\(proto)://\(host)/")!
+ }
+
+ return encoder
+ }
+
+}
diff --git a/Examples/TodoAPIGateway/Sources/TodoAPIGateway/main.swift b/Examples/TodoAPIGateway/Sources/TodoAPIGateway/main.swift
new file mode 100644
index 0000000..ef2601a
--- /dev/null
+++ b/Examples/TodoAPIGateway/Sources/TodoAPIGateway/main.swift
@@ -0,0 +1,47 @@
+import AWSLambda
+import NIO
+import Logging
+import Foundation
+import TodoService
+import AWSSDKSwiftCore
+
+LoggingSystem.bootstrap(StreamLogHandler.standardError)
+
+
+let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+defer { try! group.syncShutdownGracefully() }
+let logger = Logger(label: "AWSLambda.TodoAPIGateway")
+
+do {
+ logger.info("start runtime")
+ let runtime = try Runtime.createRuntime(eventLoopGroup: group)
+ let env = runtime.environment
+ let store = DynamoTodoStore(
+ eventLoopGroup: group,
+ tableName: "SwiftLambdaTodos",
+ accessKeyId: env.accessKeyId,
+ secretAccessKey: env.secretAccessKey,
+ sessionToken: env.sessionToken,
+ region: Region(rawValue: env.region)!)
+ let controller = TodoController(store: store)
+
+ defer { try! runtime.syncShutdown() }
+
+ logger.info("register functions")
+
+ runtime.register(for: "list", handler: APIGateway.handler(controller.listTodos))
+ runtime.register(for: "create", handler: APIGateway.handler(controller.createTodo))
+ runtime.register(for: "deleteAll", handler: APIGateway.handler(controller.deleteAll))
+ runtime.register(for: "getTodo", handler: APIGateway.handler(controller.getTodo))
+ runtime.register(for: "deleteTodo", handler: APIGateway.handler(controller.deleteTodo))
+ runtime.register(for: "patchTodo", handler: APIGateway.handler(controller.patchTodo))
+
+ logger.info("starting runloop")
+
+ try runtime.start().wait()
+}
+catch {
+ logger.error("error: \(String(describing: error))")
+}
+
+
diff --git a/Examples/TodoAPIGateway/Sources/TodoService/DynamoTodoStore.swift b/Examples/TodoAPIGateway/Sources/TodoService/DynamoTodoStore.swift
new file mode 100644
index 0000000..78439be
--- /dev/null
+++ b/Examples/TodoAPIGateway/Sources/TodoService/DynamoTodoStore.swift
@@ -0,0 +1,146 @@
+import NIO
+import DynamoDB
+import AWSSDKSwiftCore
+
+public class DynamoTodoStore {
+
+ let dynamo : DynamoDB
+ let tableName: String
+ let listName : String = "list"
+
+ public init(
+ eventLoopGroup: EventLoopGroup,
+ tableName: String,
+ accessKeyId: String,
+ secretAccessKey: String,
+ sessionToken: String,
+ region: Region)
+ {
+
+ self.dynamo = DynamoDB(
+ accessKeyId: accessKeyId,
+ secretAccessKey: secretAccessKey,
+ sessionToken: sessionToken,
+ region: region,
+ eventLoopGroupProvider: .shared(eventLoopGroup))
+ self.tableName = tableName
+
+ }
+
+}
+
+extension DynamoTodoStore: TodoStore {
+
+ public func getTodos() -> EventLoopFuture<[TodoItem]> {
+ return self.dynamo.query(.init(
+ expressionAttributeValues: [":id" : .init(s: listName)],
+ keyConditionExpression: "ListId = :id",
+ tableName: tableName))
+ .map { (output) -> ([TodoItem]) in
+ return output.items!
+ .compactMap { (attributes) -> TodoItem? in
+ return TodoItem(attributes: attributes)
+ }
+ .sorted { (t1, t2) -> Bool in
+ switch (t1.order, t2.order) {
+ case (.none, .none):
+ return false
+ case (.some(_), .none):
+ return true
+ case (.none, .some(_)):
+ return false
+ case (.some(let o1), .some(let o2)):
+ return o1 < o2
+ }
+ }
+ }
+ }
+
+ public func getTodo(id: String) -> EventLoopFuture {
+ return self.dynamo.getItem(.init(key: ["ListId": .init(s: listName), "TodoId": .init(s: id)], tableName: tableName))
+ .flatMapThrowing { (output) throws -> TodoItem in
+ guard let attributes = output.item else {
+ throw TodoError.notFound
+ }
+ guard let todo = TodoItem(attributes: attributes) else {
+ throw TodoError.missingAttributes
+ }
+ return todo
+ }
+ }
+
+ public func createTodo(_ todo: TodoItem) -> EventLoopFuture {
+ var attributes = todo.toDynamoItem()
+ attributes["ListId"] = .init(s: self.listName)
+
+ return self.dynamo.putItem(.init(item: attributes, tableName: tableName))
+ .map { _ in
+ return todo
+ }
+ }
+
+ public func patchTodo(id: String, patch: PatchTodo) -> EventLoopFuture {
+
+ var updates: [String : DynamoDB.AttributeValueUpdate] = [:]
+ if let title = patch.title {
+ updates["Title"] = .init(action: .put, value: .init(s: title))
+ }
+
+ if let order = patch.order {
+ updates["Order"] = .init(action: .put, value: .init(n: String(order)))
+ }
+
+ if let completed = patch.completed {
+ updates["Completed"] = .init(action: .put, value: .init(bool: completed))
+ }
+
+ guard updates.count > 0 else {
+ return self.getTodo(id: id)
+ }
+
+ let update = DynamoDB.UpdateItemInput(
+ attributeUpdates: updates,
+ key: ["ListId": .init(s: listName), "TodoId": .init(s: id)],
+ returnValues: .allNew,
+ tableName: tableName)
+
+ return self.dynamo.updateItem(update)
+ .flatMapThrowing { (output) -> TodoItem in
+ guard let attributes = output.attributes else {
+ throw TodoError.notFound
+ }
+ guard let todo = TodoItem(attributes: attributes) else {
+ throw TodoError.missingAttributes
+ }
+ return todo
+ }
+ }
+
+ public func deleteTodos(ids: [String]) -> EventLoopFuture {
+
+ guard ids.count > 0 else {
+ return self.dynamo.client.eventLoopGroup.next().makeSucceededFuture(Void())
+ }
+
+ let writeRequests = ids.map { (id) in
+ DynamoDB.WriteRequest(deleteRequest: .init(key: ["ListId": .init(s: listName), "TodoId": .init(s: id)]))
+ }
+
+ return self.dynamo.batchWriteItem(.init(requestItems: [tableName : writeRequests]))
+ .map { _ in }
+ }
+
+// func updateTodo() -> EventLoopFuture {
+//
+// }
+
+ public func deleteAllTodos() -> EventLoopFuture {
+
+ return self.getTodos()
+ .flatMap { (todos) -> EventLoopFuture in
+ let ids = todos.map() { $0.id }
+ return self.deleteTodos(ids: ids)
+ }
+
+ }
+}
diff --git a/Examples/TodoAPIGateway/Sources/TodoService/TodoError.swift b/Examples/TodoAPIGateway/Sources/TodoService/TodoError.swift
new file mode 100644
index 0000000..d5d5190
--- /dev/null
+++ b/Examples/TodoAPIGateway/Sources/TodoService/TodoError.swift
@@ -0,0 +1,16 @@
+//
+// File.swift
+//
+//
+// Created by Fabian Fett on 19.11.19.
+//
+
+import Foundation
+
+public enum TodoError: Error {
+
+ case notFound
+ case missingAttributes
+ case invalidRequest
+
+}
diff --git a/Examples/TodoAPIGateway/Sources/TodoService/TodoItem.swift b/Examples/TodoAPIGateway/Sources/TodoService/TodoItem.swift
new file mode 100644
index 0000000..fd09acb
--- /dev/null
+++ b/Examples/TodoAPIGateway/Sources/TodoService/TodoItem.swift
@@ -0,0 +1,109 @@
+import Foundation
+import DynamoDB
+
+public struct TodoItem {
+
+ public let id: String
+ public let order: Int?
+
+ /// Text to display
+ public let title: String
+
+ /// Whether completed or not
+ public let completed: Bool
+
+ public init(id: String, order: Int?, title: String, completed: Bool) {
+ self.id = id
+ self.order = order
+ self.title = title
+ self.completed = completed
+ }
+}
+
+extension CodingUserInfoKey {
+ public static let baseUrl = CodingUserInfoKey(rawValue: "de.fabianfett.TodoBackend.BaseURL")!
+}
+
+extension TodoItem : Codable {
+
+ enum CodingKeys: String, CodingKey {
+ case id
+ case order
+ case title
+ case completed
+ case url
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.id = try container.decode(String.self, forKey: .id)
+ self.title = try container.decode(String.self, forKey: .title)
+ self.completed = try container.decode(Bool.self, forKey: .completed)
+ self.order = try container.decodeIfPresent(Int.self, forKey: .order)
+ }
+
+ public func encode(to encoder: Encoder) throws {
+
+ var container = encoder.container(keyedBy: CodingKeys.self)
+
+ try container.encode(id, forKey: .id)
+ try container.encode(order, forKey: .order)
+ try container.encode(title, forKey: .title)
+ try container.encode(completed, forKey: .completed)
+
+ if let url = encoder.userInfo[.baseUrl] as? URL {
+ let todoUrl = url.appendingPathComponent("/todos/\(id)")
+ try container.encode(todoUrl, forKey: .url)
+ }
+ }
+
+}
+extension TodoItem : Equatable { }
+
+public func == (lhs: TodoItem, rhs: TodoItem) -> Bool {
+ return lhs.id == rhs.id
+ && lhs.order == rhs.order
+ && lhs.title == rhs.title
+ && lhs.completed == rhs.completed
+}
+
+extension TodoItem {
+
+ func toDynamoItem() -> [String: DynamoDB.AttributeValue] {
+ var result: [String: DynamoDB.AttributeValue] = [
+ "TodoId" : .init(s: self.id),
+ "Title" : .init(s: self.title),
+ "Completed": .init(bool: self.completed),
+ ]
+
+ if let order = order {
+ result["Order"] = DynamoDB.AttributeValue(n: String(order))
+ }
+
+ return result
+ }
+
+ init?(attributes: [String: DynamoDB.AttributeValue]) {
+ guard let id = attributes["TodoId"]?.s,
+ let title = attributes["Title"]?.s,
+ let completed = attributes["Completed"]?.bool
+ else
+ {
+ return nil
+ }
+
+ var order: Int? = nil
+ if let orderString = attributes["Order"]?.n, let number = Int(orderString) {
+ order = number
+ }
+
+ self.init(id: id, order: order, title: title, completed: completed)
+ }
+
+}
+
+public struct PatchTodo: Codable {
+ public let order : Int?
+ public let title : String?
+ public let completed: Bool?
+}
diff --git a/Examples/TodoAPIGateway/Sources/TodoService/TodoStore.swift b/Examples/TodoAPIGateway/Sources/TodoService/TodoStore.swift
new file mode 100644
index 0000000..ab61eb3
--- /dev/null
+++ b/Examples/TodoAPIGateway/Sources/TodoService/TodoStore.swift
@@ -0,0 +1,16 @@
+import Foundation
+import NIO
+
+public protocol TodoStore {
+
+ func getTodos() -> EventLoopFuture<[TodoItem]>
+ func getTodo(id: String) -> EventLoopFuture
+
+ func createTodo(_ todo: TodoItem) -> EventLoopFuture
+
+ func patchTodo(id: String, patch: PatchTodo) -> EventLoopFuture
+
+ func deleteTodos(ids: [String]) -> EventLoopFuture
+ func deleteAllTodos() -> EventLoopFuture
+
+}
diff --git a/Examples/TodoAPIGateway/Tests/LinuxMain.swift b/Examples/TodoAPIGateway/Tests/LinuxMain.swift
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/Examples/TodoAPIGateway/Tests/LinuxMain.swift
@@ -0,0 +1 @@
+
diff --git a/Examples/TodoAPIGateway/Tests/TodoAPIGatewayTests/TodoAPIGatewayTests.swift b/Examples/TodoAPIGateway/Tests/TodoAPIGatewayTests/TodoAPIGatewayTests.swift
new file mode 100644
index 0000000..4c1d727
--- /dev/null
+++ b/Examples/TodoAPIGateway/Tests/TodoAPIGatewayTests/TodoAPIGatewayTests.swift
@@ -0,0 +1,5 @@
+import XCTest
+
+final class TodoAPIGatewayTests: XCTestCase {
+
+}
diff --git a/Examples/TodoAPIGateway/Tests/TodoServiceTests/TodoServiceTests.swift b/Examples/TodoAPIGateway/Tests/TodoServiceTests/TodoServiceTests.swift
new file mode 100644
index 0000000..b0407b0
--- /dev/null
+++ b/Examples/TodoAPIGateway/Tests/TodoServiceTests/TodoServiceTests.swift
@@ -0,0 +1,8 @@
+import Foundation
+import NIO
+import XCTest
+@testable import TodoService
+
+final class TodoAPIGatewayTests: XCTestCase {
+
+}
diff --git a/Examples/TodoAPIGateway/template.yaml b/Examples/TodoAPIGateway/template.yaml
new file mode 100644
index 0000000..7302ced
--- /dev/null
+++ b/Examples/TodoAPIGateway/template.yaml
@@ -0,0 +1,161 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Description: >
+ sam-app
+
+ Sample SAM Template for sam-app
+
+# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
+Globals:
+ Function:
+ Timeout: 3
+
+Resources:
+
+ SwiftLayer:
+ Type: AWS::Serverless::LayerVersion
+ Properties:
+ ContentUri: ../../Layer/swift-lambda-runtime/
+ # ContentUri:
+ # Bucket: de.fabianfett.swift-lambda-runtimes
+ # Key: swift-lambda-runtime.zip
+
+ DynamoDbFailedLoginsTable:
+ Type: "AWS::DynamoDB::Table"
+ Properties:
+ BillingMode: PAY_PER_REQUEST
+ AttributeDefinitions:
+ - AttributeName: ListId
+ AttributeType: S
+ - AttributeName: TodoId
+ AttributeType: S
+ KeySchema:
+ - AttributeName: ListId
+ KeyType: HASH
+ - AttributeName: TodoId
+ KeyType: RANGE
+ TableName: "SwiftLambdaTodos"
+
+ APIGateway:
+ Type: AWS::Serverless::Api
+ Properties:
+ StageName: test
+ Cors:
+ AllowMethods: "'OPTIONS,GET,POST,DELETE,PATCH'"
+ AllowHeaders: "'Content-Type'"
+ AllowOrigin : "'*'"
+ AllowCredentials: "'*'"
+
+ TodoAPIGatewayListFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "TodoAPIGateway.list"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Policies:
+ - DynamoDBReadPolicy:
+ TableName: "SwiftLambdaTodos"
+ Events:
+ Api:
+ Type: Api
+ Properties:
+ RestApiId: !Ref APIGateway
+ Path: /todos
+ Method: GET
+
+ TodoAPIGatewayCreateFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "TodoAPIGateway.create"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Policies:
+ - DynamoDBCrudPolicy:
+ TableName: "SwiftLambdaTodos"
+ Events:
+ Api:
+ Type: Api
+ Properties:
+ RestApiId: !Ref APIGateway
+ Path: /todos
+ Method: POST
+
+ TodoAPIGatewayDeleteAllFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "TodoAPIGateway.deleteAll"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Policies:
+ - DynamoDBCrudPolicy:
+ TableName: "SwiftLambdaTodos"
+ Events:
+ Api:
+ Type: Api
+ Properties:
+ RestApiId: !Ref APIGateway
+ Path: /todos
+ Method: DELETE
+
+ TodoAPIGatewayGetTodo:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "TodoAPIGateway.getTodo"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Policies:
+ - DynamoDBReadPolicy:
+ TableName: "SwiftLambdaTodos"
+ Events:
+ Api:
+ Type: Api
+ Properties:
+ RestApiId: !Ref APIGateway
+ Path: /todos/{id}
+ Method: GET
+
+ TodoAPIGatewayDeleteTodo:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "TodoAPIGateway.deleteTodo"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Policies:
+ - DynamoDBCrudPolicy:
+ TableName: "SwiftLambdaTodos"
+ Events:
+ Api:
+ Type: Api
+ Properties:
+ RestApiId: !Ref APIGateway
+ Path: /todos/{id}
+ Method: DELETE
+
+ TodoAPIGatewayPatchTodo:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "TodoAPIGateway.patchTodo"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Policies:
+ - DynamoDBCrudPolicy:
+ TableName: "SwiftLambdaTodos"
+ Events:
+ Api:
+ Type: Api
+ Properties:
+ RestApiId: !Ref APIGateway
+ Path: /todos/{id}
+ Method: PATCH
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /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/Layer/bootstrap b/Layer/bootstrap
new file mode 100755
index 0000000..212a2d6
--- /dev/null
+++ b/Layer/bootstrap
@@ -0,0 +1,3 @@
+#!/bin/sh
+EXECUTABLE=$LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1)"
+/opt/swift-shared-libs/ld-linux-x86-64.so.2 --library-path /opt/swift-shared-libs/lib $EXECUTABLE
\ No newline at end of file
diff --git a/Layer/makefile b/Layer/makefile
new file mode 100644
index 0000000..cec0657
--- /dev/null
+++ b/Layer/makefile
@@ -0,0 +1,93 @@
+SWIFT_DOCKER_IMAGE=swift:5.1.2
+
+LAYER_FOLDER=swift-lambda-runtime
+LAYER_ZIP=$(LAYER_FOLDER).zip
+SHARED_LIBS_FOLDER=$(LAYER_FOLDER)/swift-shared-libs
+
+clean_layer:
+ rm $(LAYER_ZIP) || true
+ rm -r $(SHARED_LIBS_FOLDER) || true
+
+create_layer: clean_layer
+ mkdir -p $(LAYER_FOLDER)
+ mkdir -p $(SHARED_LIBS_FOLDER)/lib
+ cp ./bootstrap "$(LAYER_FOLDER)/bootstrap"
+ chmod 755 "$(LAYER_FOLDER)/bootstrap"
+ docker run \
+ --rm \
+ --volume "$(shell pwd)/:/src" \
+ --workdir "/src" \
+ $(SWIFT_DOCKER_IMAGE) \
+ cp /lib64/ld-linux-x86-64.so.2 $(SHARED_LIBS_FOLDER)
+ docker run \
+ --rm \
+ --volume "$(shell pwd)/:/src" \
+ --workdir "/src" \
+ $(SWIFT_DOCKER_IMAGE) \
+ cp -t $(SHARED_LIBS_FOLDER)/lib \
+ /lib/x86_64-linux-gnu/libbsd.so.0 \
+ /lib/x86_64-linux-gnu/libc.so.6 \
+ /lib/x86_64-linux-gnu/libcom_err.so.2 \
+ /lib/x86_64-linux-gnu/libcrypt.so.1 \
+ /lib/x86_64-linux-gnu/libdl.so.2 \
+ /lib/x86_64-linux-gnu/libgcc_s.so.1 \
+ /lib/x86_64-linux-gnu/libkeyutils.so.1 \
+ /lib/x86_64-linux-gnu/liblzma.so.5 \
+ /lib/x86_64-linux-gnu/libm.so.6 \
+ /lib/x86_64-linux-gnu/libpthread.so.0 \
+ /lib/x86_64-linux-gnu/libresolv.so.2 \
+ /lib/x86_64-linux-gnu/librt.so.1 \
+ /lib/x86_64-linux-gnu/libutil.so.1 \
+ /lib/x86_64-linux-gnu/libz.so.1 \
+ /usr/lib/swift/linux/libBlocksRuntime.so \
+ /usr/lib/swift/linux/libFoundation.so \
+ /usr/lib/swift/linux/libFoundationXML.so \
+ /usr/lib/swift/linux/libFoundationNetworking.so \
+ /usr/lib/swift/linux/libdispatch.so \
+ /usr/lib/swift/linux/libicudataswift.so.61 \
+ /usr/lib/swift/linux/libicui18nswift.so.61 \
+ /usr/lib/swift/linux/libicuucswift.so.61 \
+ /usr/lib/swift/linux/libswiftCore.so \
+ /usr/lib/swift/linux/libswiftDispatch.so \
+ /usr/lib/swift/linux/libswiftGlibc.so \
+ /usr/lib/swift/linux/libswiftSwiftOnoneSupport.so \
+ /usr/lib/x86_64-linux-gnu/libasn1.so.8 \
+ /usr/lib/x86_64-linux-gnu/libatomic.so.1 \
+ /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 \
+ /usr/lib/x86_64-linux-gnu/libcurl.so.4 \
+ /usr/lib/x86_64-linux-gnu/libffi.so.6 \
+ /usr/lib/x86_64-linux-gnu/libgmp.so.10 \
+ /usr/lib/x86_64-linux-gnu/libgnutls.so.30 \
+ /usr/lib/x86_64-linux-gnu/libgssapi.so.3 \
+ /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2 \
+ /usr/lib/x86_64-linux-gnu/libhcrypto.so.4 \
+ /usr/lib/x86_64-linux-gnu/libheimbase.so.1 \
+ /usr/lib/x86_64-linux-gnu/libheimntlm.so.0 \
+ /usr/lib/x86_64-linux-gnu/libhogweed.so.4 \
+ /usr/lib/x86_64-linux-gnu/libhx509.so.5 \
+ /usr/lib/x86_64-linux-gnu/libicudata.so.60 \
+ /usr/lib/x86_64-linux-gnu/libicuuc.so.60 \
+ /usr/lib/x86_64-linux-gnu/libidn2.so.0 \
+ /usr/lib/x86_64-linux-gnu/libk5crypto.so.3 \
+ /usr/lib/x86_64-linux-gnu/libkrb5.so.26 \
+ /usr/lib/x86_64-linux-gnu/libkrb5.so.3 \
+ /usr/lib/x86_64-linux-gnu/libkrb5support.so.0 \
+ /usr/lib/x86_64-linux-gnu/liblber-2.4.so.2 \
+ /usr/lib/x86_64-linux-gnu/libldap_r-2.4.so.2 \
+ /usr/lib/x86_64-linux-gnu/libnettle.so.6 \
+ /usr/lib/x86_64-linux-gnu/libnghttp2.so.14 \
+ /usr/lib/x86_64-linux-gnu/libp11-kit.so.0 \
+ /usr/lib/x86_64-linux-gnu/libpsl.so.5 \
+ /usr/lib/x86_64-linux-gnu/libroken.so.18 \
+ /usr/lib/x86_64-linux-gnu/librtmp.so.1 \
+ /usr/lib/x86_64-linux-gnu/libsasl2.so.2 \
+ /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 \
+ /usr/lib/x86_64-linux-gnu/libssl.so.1.1 \
+ /usr/lib/x86_64-linux-gnu/libstdc++.so.6 \
+ /usr/lib/x86_64-linux-gnu/libtasn1.so.6 \
+ /usr/lib/x86_64-linux-gnu/libunistring.so.2 \
+ /usr/lib/x86_64-linux-gnu/libwind.so.0 \
+ /usr/lib/x86_64-linux-gnu/libxml2.so.2
+
+package_layer: create_layer
+ zip -r $(LAYER_ZIP) bootstrap $(SHARED_LIBS_FOLDER)
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..8a8262e
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,52 @@
+{
+ "object": {
+ "pins": [
+ {
+ "package": "async-http-client",
+ "repositoryURL": "https://github.com/swift-server/async-http-client.git",
+ "state": {
+ "branch": null,
+ "revision": "51dc885a30ca704b02fa803099b0a9b5b38067b6",
+ "version": "1.0.0"
+ }
+ },
+ {
+ "package": "swift-log",
+ "repositoryURL": "https://github.com/apple/swift-log.git",
+ "state": {
+ "branch": null,
+ "revision": "e8aabbe95db22e064ad42f1a4a9f8982664c70ed",
+ "version": "1.1.1"
+ }
+ },
+ {
+ "package": "swift-nio",
+ "repositoryURL": "https://github.com/apple/swift-nio.git",
+ "state": {
+ "branch": null,
+ "revision": "8066b0f581604e3711979307a4377457e2b0f007",
+ "version": "2.9.0"
+ }
+ },
+ {
+ "package": "swift-nio-extras",
+ "repositoryURL": "https://github.com/apple/swift-nio-extras.git",
+ "state": {
+ "branch": null,
+ "revision": "ed97628fa310c314c4a5cd8038445054b2991f07",
+ "version": "1.3.1"
+ }
+ },
+ {
+ "package": "swift-nio-ssl",
+ "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
+ "state": {
+ "branch": null,
+ "revision": "e5c1af45ac934ac0a6117b2927a51d845cf4f705",
+ "version": "2.4.3"
+ }
+ }
+ ]
+ },
+ "version": 1
+}
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..799b636
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,26 @@
+// swift-tools-version:5.1
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "swift-aws-lambda",
+ products: [
+ .library(
+ name: "AWSLambda",
+ targets: ["AWSLambda"]
+ ),
+ ],
+ dependencies: [
+ .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.9.0")),
+ .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.1.1")),
+ .package(url: "https://github.com/swift-server/async-http-client.git", .upToNextMajor(from: "1.0.0"))
+ ],
+ targets: [
+ .target(
+ name: "AWSLambda",
+ dependencies: ["AsyncHTTPClient", "NIO", "NIOHTTP1", "NIOFoundationCompat", "Logging"]
+ ),
+ .testTarget(name: "AWSLambdaTests", dependencies: ["AWSLambda", "NIOTestUtils", "Logging"])
+ ]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6d29aa3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,167 @@
+# swift-aws-lambda
+
+[![Swift 5.1.2](https://img.shields.io/badge/Swift-5.1.2-blue.svg)](https://swift.org/download/)
+[![codecov](https://codecov.io/gh/fabianfett/swift-aws-lambda/branch/master/graph/badge.svg?token=ebzMRn2TqY)](https://codecov.io/gh/fabianfett/swift-aws-lambda)
+![Linux](https://img.shields.io/badge/os-tuxOS-green.svg?style=flat)
+![macOS](https://img.shields.io/badge/os-macOS-green.svg?style=flat)
+
+AWS unfortunately does not provide an official swift runtime for its [Lambda](https://en.wikipedia.org/wiki/AWS_Lambda) offering. Therefore someone needs to do this, if we want to use the benefits of lambda and swift on the server. That's why this project exists. Hopefully this is sherlocked by AWS sooner rather than later. re:Invent is just around the corner. 🙃
+
+In order to achieve our goal this project consists out ot two parts:
+
+1. A swift layer to run swift code within lambda. In other words: Without this your swift code won't run.
+1. A swift package that you can use to get your next invocation from the lambda runtime and to post your lambda result back to lambda. In other words: Without this your swift code will not be able to process any tasks.
+
+This project uses [Swift-NIO](https://github.com/apple/swift-nio). Therefore the developer facing API of this package expects to be initialized with an EventLoopGroup, exposes an `EventLoop` during an invocation as a property on the [`Context`](https://github.com/fabianfett/swift-aws-lambda/blob/5405bd30737a7347a95c7024bcb6f0a8fafb3931/Sources/AWSLambda/Context.swift#L16) and expects an `EventLoopFuture` to be returned. If you are not familiar with Swift-NIO, I highly encourage you to learn about [this first](https://github.com/apple/swift-nio#basic-architecture).
+
+Your lambda needs to be build in the linux environment. Therefore we need [Docker](https://en.wikipedia.org/wiki/Docker_(software)) to compile your lambda. If you don't have Docker yet, now is a good time to go look for that.
+
+## Status
+
+- [x] A number of examples to get you on the run as fast as possible (including an [API-Gateway TodoList](http://todobackend.com/client/index.html?https://mwpixnkbzj.execute-api.eu-central-1.amazonaws.com/test/todos) example)
+- [x] Tested integration with aws-swift-sdk
+- [x] Unit and End-to-end Tests
+- [x] CI Workflow with GitHub Actions
+- [x] Wrapper functions to build a synchronous lambda
+- [ ] Ready to use AWS Events structs to get you started as fast as possible
+
+Alternatives: There is another project to get Swift to work within AWS-Lambda: [Swift-Sprinter](https://github.com/swift-sprinter/aws-lambda-swift-sprinter) If you don't like this project, maybe your needs get better addressed over there.
+
+## Create and run your first Swift Lambda
+
+This should help you to get started with the swift on lambda in AWS. We focus primarily on the AWS console. Of course you can use the aws-cli, sam-cli or cloudformation. At every step of your way.
+
+### Step 1: Setup your layer (Prepare AWS Lambda to run swift)
+
+Check out this repo and create layer yourself within this project:
+
+```bash
+$ cd Layer
+$ make package_layer
+```
+
+The makefile uses Docker under the hood. So, you need to have this installed by now. You can change the swift version of your layer by using the enviornment variable `SWIFT_DOCKER_IMAGE`. Example:
+
+```bash
+$ SWIFT_DOCKER_IMAGE=5.1.2 make package_layer
+```
+
+In the `Layer` directory you will now have an `swift-lambda-runtime.zip`.
+
+Open your AWS Console and navigate to lambda. Select "Layers" in the side navigation and click on "Create Layer" in the upper right corner. Give your runtime a name. I suggest: "Swift [Version]". Upload your zip layer from the `Layer` folder and click "Create".
+
+Next you will see this screen. Note the selected `Version ARN`. Please copy & paste that somewhere. You'll need this later.
+
+![Layer created; Copy the arn](Docs/Layer-Copy-Arn.png)
+
+
+### Step 2: Develop your lambda
+
+Create a new SPM project and add "swift-aws-lambda" as a dependency. Your `Package.swift` should look something like this.
+
+```swift
+// swift-tools-version:5.1
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "SquareNumber",
+ dependencies: [
+ .package(url: "https://github.com/fabianfett/swift-aws-lambda.git", .upToNextMajor(from: "0.1.0")),
+ ],
+ targets: [
+ .target(
+ name: "SquareNumber",
+ dependencies: ["AWSLambda"]
+ ),
+ ]
+)
+```
+
+Then open your `main.swift` and create your function. Your function can do whatever you like. In this example we just want to square numbers.
+
+```swift
+import AWSLambda
+import NIO
+
+struct Input: Codable {
+ let number: Double
+}
+
+struct Output: Codable {
+ let result: Double
+}
+
+func squareNumber(input: Input, context: Context) -> Output {
+ let squaredNumber = input.number * input.number
+ return Output(result: squaredNumber)
+}
+
+let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+defer { try! group.syncShutdownGracefully() }
+
+do {
+ let runtime = try Runtime.createRuntime(eventLoopGroup: group)
+ defer { try! runtime.syncShutdown() }
+
+ runtime.register(for: "squareNumber", handler: Runtime.codable(squareNumber))
+ try runtime.start().wait()
+}
+catch {
+ print("\(error)")
+}
+```
+
+### Step 3: Build your lambda
+
+Your lambda needs to be compiled in the linux environment. That's why we use Docker to compile your lambda.
+
+```bash
+# build your lambda in the linux environment
+$ docker run --rm --volume "$(pwd)/:/src" --workdir "/src/" swift:5.1.2 swift build -c release
+
+# zip your build product.
+$ zip -j lambda.zip .build/release/$(EXAMPLE_EXECUTABLE)
+```
+
+### Step 4: Create your lambda on AWS
+
+Open your AWS Console and navigate to lambda. Select "Functions" in the side navigation and click on "Create function" in the upper right corner. Give your function a name! I'll choose "SquareNumbers". And select the runtime "Provide your own bootstrap".
+
+You'll see a screen that looks like this.
+
+![Create your function](Docs/Function-Create.png)
+
+First we need to select our lambda swift runtime. We do so by clicking "Layers" below the function name in the center of the screen. The lower part of the screen changes and we can see an "Add Layer" button in the center. On the next screen we need to select "Provide a layer version ARN" and then we enter the ARN that we saved, when we created the layer. Next we click "Add".
+
+![Add the swift layer to your Function](Docs/Add-Layer-to-Function.png)
+
+Now we should see a layer below our function. Next we click our function and select, in the lower screen we have the section "Function Code". Select "Upload a zip file" in the "Code entry type". Click on "Upload" and select your `lambda.zip`. In the Handler fill in your `ExecutableName.FunctionName`. In my case this is `SquareNumber.squareNumber`. Next hit "Save".
+
+![Upload your lambda code](Docs/Upload-Lambda-zip.png)
+
+### Step 5: Invoke your lambda
+
+Now the only thing left, is to invoke your lambda. Select "Test" (in the upper right corner) and change your test payload to whatever json you want to supply to your function. Since I want numbers squared mine is.
+
+```json
+{
+ "number": 3
+}
+```
+
+Also you need to give your Event a name. Mine is "Number3". Click "Save" and you can click "Test" again, and this time your lambda will execute. If everything went well you should see a scren like this:
+
+![The lambda invocation is a success!](Docs/Invocation-Success.png)
+
+## Contributing
+
+All developers should feel welcome and encouraged to contribute to swift-aws-lambda. The current version of swift-aws-lambda has a long way to go before being ready for production use and help is always welcome.
+
+If you've found a bug, have a suggestion or need help getting started, please open an Issue or a PR. If you use this package, please reach out and share your experience.
+
+## Credits
+
+- [Toni Suter](https://github.com/tonisuter/aws-lambda-swift) created the original makefile that creates the layer to run the lambda.
+
diff --git a/Sources/AWSLambda/Context.swift b/Sources/AWSLambda/Context.swift
new file mode 100644
index 0000000..71dce36
--- /dev/null
+++ b/Sources/AWSLambda/Context.swift
@@ -0,0 +1,36 @@
+import Foundation
+import NIO
+import NIOHTTP1
+import Logging
+
+/// TBD: What shall the context be? A struct? A class?
+public class Context {
+
+ public let environment : Environment
+ public let invocation : Invocation
+
+ public let traceId : String
+ public let requestId : String
+
+ public let logger : Logger
+ public let eventLoop : EventLoop
+ public let deadlineDate: Date
+
+ public init(environment: Environment, invocation: Invocation, eventLoop: EventLoop) {
+
+ var logger = Logger(label: "aws.lambda.swift.request-logger")
+ logger[metadataKey: "RequestId"] = .string(invocation.requestId)
+
+ self.environment = environment
+ self.invocation = invocation
+ self.eventLoop = eventLoop
+ self.logger = logger
+ self.requestId = invocation.requestId
+ self.traceId = invocation.traceId
+ self.deadlineDate = invocation.deadlineDate
+ }
+
+ public func getRemainingTime() -> TimeInterval {
+ return deadlineDate.timeIntervalSinceNow
+ }
+}
diff --git a/Sources/AWSLambda/Environment.swift b/Sources/AWSLambda/Environment.swift
new file mode 100644
index 0000000..e6996a6
--- /dev/null
+++ b/Sources/AWSLambda/Environment.swift
@@ -0,0 +1,49 @@
+import Foundation
+
+public struct Environment {
+
+ public let lambdaRuntimeAPI: String
+ public let handlerName : String
+
+ public let functionName : String
+ public let functionVersion : String
+ public let logGroupName : String
+ public let logStreamName : String
+ public let memoryLimitInMB : String
+ public let accessKeyId : String
+ public let secretAccessKey : String
+ public let sessionToken : String
+ public let region : String
+
+ init(_ env: [String: String]) throws {
+
+ guard let awsLambdaRuntimeAPI = env["AWS_LAMBDA_RUNTIME_API"] else {
+ throw RuntimeError.missingEnvironmentVariable("AWS_LAMBDA_RUNTIME_API")
+ }
+
+ guard let handler = env["_HANDLER"] else {
+ throw RuntimeError.missingEnvironmentVariable("_HANDLER")
+ }
+
+ guard let periodIndex = handler.firstIndex(of: ".") else {
+ throw RuntimeError.invalidHandlerName
+ }
+
+ let handlerName = String(handler[handler.index(after: periodIndex)...])
+
+ self.lambdaRuntimeAPI = awsLambdaRuntimeAPI
+ self.handlerName = handlerName
+
+ self.functionName = env["AWS_LAMBDA_FUNCTION_NAME"] ?? ""
+ self.functionVersion = env["AWS_LAMBDA_FUNCTION_VERSION"] ?? ""
+ self.logGroupName = env["AWS_LAMBDA_LOG_GROUP_NAME"] ?? ""
+ self.logStreamName = env["AWS_LAMBDA_LOG_STREAM_NAME"] ?? ""
+ self.memoryLimitInMB = env["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"] ?? ""
+
+ self.accessKeyId = env["AWS_ACCESS_KEY_ID"] ?? ""
+ self.secretAccessKey = env["AWS_SECRET_ACCESS_KEY"] ?? ""
+ self.sessionToken = env["AWS_SESSION_TOKEN"] ?? ""
+
+ self.region = env["AWS_REGION"] ?? "us-east-1"
+ }
+}
diff --git a/Sources/AWSLambda/Events/APIGateway.swift b/Sources/AWSLambda/Events/APIGateway.swift
new file mode 100644
index 0000000..97f6a1e
--- /dev/null
+++ b/Sources/AWSLambda/Events/APIGateway.swift
@@ -0,0 +1,213 @@
+import Foundation
+import NIOFoundationCompat
+import NIO
+import NIOHTTP1
+
+// https://github.com/aws/aws-lambda-go/blob/master/events/apigw.go
+
+public struct APIGateway {
+
+ // https://github.com/aws/aws-lambda-go/blob/master/events/apigw.go
+ public struct Request: Codable {
+
+ public struct Context: Codable {
+
+ public struct Identity: Codable {
+ public let cognitoIdentityPoolId: String?
+
+ public let apiKey: String?
+ public let userArn: String?
+ public let cognitoAuthenticationType: String?
+ public let caller: String?
+ public let userAgent: String?
+ public let user: String?
+
+ public let cognitoAuthenticationProvider: String?
+ public let sourceIp: String?
+ public let accountId: String?
+ }
+
+ public let resourceId: String
+ public let apiId: String
+ public let resourcePath: String
+ public let httpMethod: String
+ public let requestId: String
+ public let accountId: String
+ public let stage: String
+
+ public let identity: Identity
+ public let extendedRequestId: String?
+ public let path: String
+ }
+
+ public let resource: String
+ public let path: String
+ public let httpMethod: String
+
+ public let queryStringParameters: String?
+ public let multiValueQueryStringParameters: [String:[String]]?
+ public let headers: [String: String]?
+ public let multiValueHeaders: [String: [String]]?
+ public let pathParameters: [String:String]?
+ public let stageVariables: [String:String]?
+
+ public let requestContext: Request.Context
+ public let body: String?
+ public let isBase64Encoded: Bool
+ }
+
+ public struct Response {
+
+ public let statusCode : HTTPResponseStatus
+ public let headers : HTTPHeaders?
+ public let body : String?
+ public let isBase64Encoded: Bool?
+
+ public init(
+ statusCode: HTTPResponseStatus,
+ headers: HTTPHeaders? = nil,
+ body: String? = nil,
+ isBase64Encoded: Bool? = nil)
+ {
+ self.statusCode = statusCode
+ self.headers = headers
+ self.body = body
+ self.isBase64Encoded = isBase64Encoded
+ }
+ }
+}
+
+// MARK: - Handler -
+
+extension APIGateway {
+
+ public static func handler(
+ decoder: JSONDecoder = JSONDecoder(),
+ encoder: JSONEncoder = JSONEncoder(),
+ _ handler: @escaping (APIGateway.Request, Context) -> EventLoopFuture)
+ -> ((NIO.ByteBuffer, Context) -> EventLoopFuture)
+ {
+ return { (inputBytes: NIO.ByteBuffer, ctx: Context) -> EventLoopFuture in
+
+ let req: APIGateway.Request
+ do {
+ req = try decoder.decode(APIGateway.Request.self, from: inputBytes)
+ }
+ catch {
+ return ctx.eventLoop.makeFailedFuture(error)
+ }
+
+ return handler(req, ctx)
+ .flatMapErrorThrowing() { (error) -> APIGateway.Response in
+ ctx.logger.error("Unhandled error. Responding with HTTP 500: \(error).")
+ return APIGateway.Response(statusCode: .internalServerError)
+ }
+ .flatMapThrowing { (result: Response) -> NIO.ByteBuffer in
+ return try encoder.encodeAsByteBuffer(result, allocator: ByteBufferAllocator())
+ }
+ }
+ }
+}
+
+// MARK: - Request -
+
+extension APIGateway.Request {
+
+ public func payload(decoder: JSONDecoder = JSONDecoder()) throws -> Payload {
+ let body = self.body ?? ""
+
+ let capacity = body.lengthOfBytes(using: .utf8)
+
+ // TBD: I am pretty sure, we don't need this buffer copy here.
+ // Access the strings buffer directly to get to the data.
+ var buffer = ByteBufferAllocator().buffer(capacity: capacity)
+ buffer.setString(body, at: 0)
+ buffer.moveWriterIndex(to: capacity)
+
+ return try decoder.decode(Payload.self, from: buffer)
+ }
+}
+
+// MARK: - Response -
+
+extension APIGateway.Response: Encodable {
+
+ enum CodingKeys: String, CodingKey {
+ case statusCode
+ case headers
+ case body
+ case isBase64Encoded
+ }
+
+ private struct HeaderKeys: CodingKey {
+ var stringValue: String
+
+ init?(stringValue: String) {
+ self.stringValue = stringValue
+ }
+ var intValue: Int? {
+ fatalError("unexpected use")
+ }
+ init?(intValue: Int) {
+ fatalError("unexpected use")
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(statusCode.code, forKey: .statusCode)
+
+ if let headers = headers {
+ var headerContainer = container.nestedContainer(keyedBy: HeaderKeys.self, forKey: .headers)
+ try headers.forEach { (name, value) in
+ try headerContainer.encode(value, forKey: HeaderKeys(stringValue: name)!)
+ }
+ }
+
+ try container.encodeIfPresent(body, forKey: .body)
+ try container.encodeIfPresent(isBase64Encoded, forKey: .isBase64Encoded)
+ }
+
+}
+
+extension APIGateway.Response {
+
+ public init(
+ statusCode: HTTPResponseStatus,
+ headers : HTTPHeaders? = nil,
+ payload : Payload,
+ encoder : JSONEncoder = JSONEncoder()) throws
+ {
+ var headers = headers ?? HTTPHeaders()
+ headers.add(name: "Content-Type", value: "application/json")
+
+ self.statusCode = statusCode
+ self.headers = headers
+
+ let buffer = try encoder.encodeAsByteBuffer(payload, allocator: ByteBufferAllocator())
+ self.body = buffer.getString(at: 0, length: buffer.readableBytes)
+ self.isBase64Encoded = false
+ }
+
+ #if false
+ /// Use this method to send any arbitrary byte buffer back to the API Gateway.
+ /// Sadly Apple currently doesn't seem to be confident enough to advertise
+ /// their base64 implementation publically. SAD. SO SAD. Therefore no
+ /// ByteBuffer for you my friend.
+ public init(
+ statusCode: HTTPResponseStatus,
+ headers : HTTPHeaders? = nil,
+ buffer : NIO.ByteBuffer)
+ {
+ var headers = headers ?? HTTPHeaders()
+ headers.add(name: "Content-Type", value: "application/json")
+
+ self.statusCode = statusCode
+ self.headers = headers
+
+ self.body = String(base64Encoding: buffer.getBytes(at: 0, length: buffer.readableBytes))
+ self.isBase64Encoded = true
+ }
+ #endif
+
+}
diff --git a/Sources/AWSLambda/Runtime+Codable.swift b/Sources/AWSLambda/Runtime+Codable.swift
new file mode 100644
index 0000000..5d2352e
--- /dev/null
+++ b/Sources/AWSLambda/Runtime+Codable.swift
@@ -0,0 +1,50 @@
+import Foundation
+import NIO
+import NIOFoundationCompat
+
+extension Runtime {
+
+ /// wrapper to use for the register function that wraps the encoding and decoding
+ public static func codable(
+ _ handler: @escaping (Event, Context) -> EventLoopFuture)
+ -> ((NIO.ByteBuffer, Context) -> EventLoopFuture)
+ {
+ return { (inputBytes: NIO.ByteBuffer, ctx: Context) -> EventLoopFuture in
+ let input: Event
+ do {
+ input = try JSONDecoder().decode(Event.self, from: inputBytes)
+ }
+ catch {
+ return ctx.eventLoop.makeFailedFuture(error)
+ }
+
+ return handler(input, ctx)
+ .flatMapThrowing { (encodable) -> NIO.ByteBuffer in
+ return try JSONEncoder().encodeAsByteBuffer(encodable, allocator: ByteBufferAllocator())
+ }
+ }
+ }
+
+ /// synchronous interface to use with codable
+ public static func codable(
+ _ handler: @escaping (Event, Context) throws -> (Result))
+ -> ((NIO.ByteBuffer, Context) -> EventLoopFuture)
+ {
+ return Runtime.codable { (event: Event, context) -> EventLoopFuture in
+ let promise = context.eventLoop.makePromise(of: Result.self)
+
+ // TBD: What is the best queue to jump to here?
+ DispatchQueue.global(qos: .userInteractive).async {
+ do {
+ let result = try handler(event, context)
+ promise.succeed(result)
+ }
+ catch {
+ promise.fail(error)
+ }
+ }
+
+ return promise.futureResult
+ }
+ }
+}
diff --git a/Sources/AWSLambda/Runtime.swift b/Sources/AWSLambda/Runtime.swift
new file mode 100644
index 0000000..b8e53b3
--- /dev/null
+++ b/Sources/AWSLambda/Runtime.swift
@@ -0,0 +1,147 @@
+import Foundation
+import AsyncHTTPClient
+import NIO
+import NIOHTTP1
+import NIOFoundationCompat
+
+struct InvocationError: Codable {
+ let errorMessage: String
+}
+
+final public class Runtime {
+
+ public let eventLoopGroup: EventLoopGroup
+ public let runtimeLoop : EventLoop
+
+ /// the name of the function to invoke
+ public let handlerName : String
+ public let environment : Environment
+
+ // MARK: - Private Properties -
+
+ private let client: LambdaRuntimeAPI
+
+ /// The functions that can be invoked by the runtime by name.
+ private var handlers: [String: Handler]
+
+ private var shutdownPromise: EventLoopPromise?
+ private var isShutdown: Bool = false
+
+ // MARK: - Public Methods -
+
+ /// the runtime shall be initialised with an EventLoopGroup, that is used throughout the lambda
+ public static func createRuntime(eventLoopGroup: EventLoopGroup) throws -> Runtime {
+
+ let environment = try Environment(ProcessInfo.processInfo.environment)
+
+ let client = RuntimeAPIClient(
+ eventLoopGroup: eventLoopGroup,
+ lambdaRuntimeAPI: environment.lambdaRuntimeAPI)
+ let runtime = Runtime(
+ eventLoopGroup: eventLoopGroup,
+ client: client,
+ environment: environment)
+
+ return runtime
+ }
+
+ init(eventLoopGroup: EventLoopGroup,
+ client: LambdaRuntimeAPI,
+ environment: Environment)
+ {
+
+ self.eventLoopGroup = eventLoopGroup
+ self.runtimeLoop = eventLoopGroup.next()
+
+ self.client = client
+
+ self.handlerName = environment.handlerName
+ self.handlers = [:]
+
+
+ self.environment = environment
+
+ // TODO: post init error
+ // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-initerror
+ }
+
+
+ public typealias Handler = (NIO.ByteBuffer, Context) -> EventLoopFuture
+
+ /// Registers a handler function for execution by the runtime. This method is
+ /// not thread safe. Therefore it is only safe to invoke this function before
+ /// the `start` method is called.
+ public func register(for name: String, handler: @escaping Handler) {
+ self.runtimeLoop.execute {
+ self.handlers[name] = handler
+ }
+ }
+
+ // MARK: Runtime loop
+
+ public func start() -> EventLoopFuture {
+ precondition(self.shutdownPromise == nil)
+
+ self.shutdownPromise = self.runtimeLoop.makePromise(of: Void.self)
+ self.runtimeLoop.execute {
+ self.runner()
+ }
+
+ return self.shutdownPromise!.futureResult
+ }
+
+ public func syncShutdown() throws {
+
+ self.runtimeLoop.execute {
+ self.isShutdown = true
+ }
+
+ try self.shutdownPromise?.futureResult.wait()
+ try self.client.syncShutdown()
+ }
+
+ // MARK: - Private Methods -
+
+ private func runner() {
+ precondition(self.runtimeLoop.inEventLoop)
+
+ _ = self.client.getNextInvocation()
+ .hop(to: self.runtimeLoop)
+ .flatMap { (invocation, byteBuffer) -> EventLoopFuture in
+
+ setenv("_X_AMZN_TRACE_ID", invocation.traceId, 0)
+
+ let context = Context(
+ environment: self.environment,
+ invocation: invocation,
+ eventLoop: self.runtimeLoop)
+
+ guard let handler = self.handlers[self.handlerName] else {
+ return self.runtimeLoop.makeFailedFuture(RuntimeError.unknownLambdaHandler(self.handlerName))
+ }
+
+ return handler(byteBuffer, context)
+ .flatMap { (byteBuffer) -> EventLoopFuture in
+ return self.client.postInvocationResponse(for: context.requestId, httpBody: byteBuffer)
+ }
+ .flatMapError { (error) -> EventLoopFuture in
+ return self.client.postInvocationError(for: context.requestId, error: error)
+ }
+ .flatMapErrorThrowing { (error) in
+ context.logger.error("Could not post lambda result to runtime. error: \(error)")
+ }
+ }
+ .hop(to: self.runtimeLoop)
+ .whenComplete() { (_) in
+ precondition(self.runtimeLoop.inEventLoop)
+
+ if !self.isShutdown {
+ self.runner()
+ }
+ else {
+ self.shutdownPromise?.succeed(Void())
+ }
+ }
+ }
+}
+
diff --git a/Sources/AWSLambda/RuntimeAPIClient.swift b/Sources/AWSLambda/RuntimeAPIClient.swift
new file mode 100644
index 0000000..b26af50
--- /dev/null
+++ b/Sources/AWSLambda/RuntimeAPIClient.swift
@@ -0,0 +1,116 @@
+import NIO
+import NIOHTTP1
+import NIOFoundationCompat
+import AsyncHTTPClient
+import Foundation
+
+public struct Invocation {
+ public let requestId : String
+ public let deadlineDate : Date
+ public let invokedFunctionArn: String
+ public let traceId : String
+ public let clientContext : String?
+ public let cognitoIdentity : String?
+
+ init(headers: HTTPHeaders) throws {
+
+ guard let requestId = headers["Lambda-Runtime-Aws-Request-Id"].first else {
+ throw RuntimeError.invocationMissingHeader("Lambda-Runtime-Aws-Request-Id")
+ }
+
+ guard let unixTimeMilliseconds = headers["Lambda-Runtime-Deadline-Ms"].first,
+ let timeInterval = TimeInterval(unixTimeMilliseconds)
+ else
+ {
+ throw RuntimeError.invocationMissingHeader("Lambda-Runtime-Deadline-Ms")
+ }
+
+ guard let invokedFunctionArn = headers["Lambda-Runtime-Invoked-Function-Arn"].first else {
+ throw RuntimeError.invocationMissingHeader("Lambda-Runtime-Invoked-Function-Arn")
+ }
+
+ guard let traceId = headers["Lambda-Runtime-Trace-Id"].first else {
+ throw RuntimeError.invocationMissingHeader("Lambda-Runtime-Trace-Id")
+ }
+
+ self.requestId = requestId
+ self.deadlineDate = Date(timeIntervalSince1970: timeInterval / 1000)
+ self.invokedFunctionArn = invokedFunctionArn
+ self.traceId = traceId
+ self.clientContext = headers["Lambda-Runtime-Client-Context"].first
+ self.cognitoIdentity = headers["Lambda-Runtime-Cognito-Identity"].first
+ }
+
+}
+
+/// This protocol defines the Lambda Runtime API as defined here.
+/// The sole purpose of this protocol is to define stubs to make
+/// testing easier.
+/// Therefore use is internal only.
+/// https://docs.aws.amazon.com/en_pv/lambda/latest/dg/runtimes-api.html
+protocol LambdaRuntimeAPI {
+
+ func getNextInvocation() -> EventLoopFuture<(Invocation, NIO.ByteBuffer)>
+ func postInvocationResponse(for requestId: String, httpBody: NIO.ByteBuffer) -> EventLoopFuture
+ func postInvocationError(for requestId: String, error: Error) -> EventLoopFuture
+
+ func syncShutdown() throws
+
+}
+
+final class RuntimeAPIClient {
+
+ let httpClient : HTTPClient
+
+ /// the local domain to call, to get the next task/invocation
+ /// as defined here: https://docs.aws.amazon.com/en_pv/lambda/latest/dg/runtimes-api.html#runtimes-api-next
+ let lambdaRuntimeAPI: String
+
+ init(eventLoopGroup: EventLoopGroup, lambdaRuntimeAPI: String) {
+
+ self.httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup))
+ self.lambdaRuntimeAPI = lambdaRuntimeAPI
+
+ }
+}
+
+extension RuntimeAPIClient: LambdaRuntimeAPI {
+
+ func getNextInvocation() -> EventLoopFuture<(Invocation, NIO.ByteBuffer)> {
+ return self.httpClient
+ .get(url: "http://\(lambdaRuntimeAPI)/2018-06-01/runtime/invocation/next")
+ .flatMapErrorThrowing { (error) -> HTTPClient.Response in
+ throw RuntimeError.endpointError(error.localizedDescription)
+ }
+ .flatMapThrowing { (response) -> (Invocation, NIO.ByteBuffer) in
+ guard let data = response.body else {
+ throw RuntimeError.invocationMissingData
+ }
+
+ return (try Invocation(headers: response.headers), data)
+ }
+ }
+
+ func postInvocationResponse(for requestId: String, httpBody: NIO.ByteBuffer) -> EventLoopFuture {
+ let url = "http://\(lambdaRuntimeAPI)/2018-06-01/runtime/invocation/\(requestId)/response"
+ return self.httpClient.post(url: url, body: .byteBuffer(httpBody))
+ .map { (_) -> Void in }
+ }
+
+ func postInvocationError(for requestId: String, error: Error) -> EventLoopFuture {
+ let errorMessage = String(describing: error)
+ let invocationError = InvocationError(errorMessage: errorMessage)
+ let jsonEncoder = JSONEncoder()
+ let httpBody = try! jsonEncoder.encodeAsByteBuffer(invocationError, allocator: ByteBufferAllocator())
+
+ let url = "http://\(lambdaRuntimeAPI)/2018-06-01/runtime/invocation/\(requestId)/error"
+
+ return self.httpClient.post(url: url, body: .byteBuffer(httpBody))
+ .map { (_) -> Void in }
+ }
+
+ func syncShutdown() throws {
+ try self.httpClient.syncShutdown()
+ }
+
+}
diff --git a/Sources/AWSLambda/RuntimeError.swift b/Sources/AWSLambda/RuntimeError.swift
new file mode 100644
index 0000000..5fe6214
--- /dev/null
+++ b/Sources/AWSLambda/RuntimeError.swift
@@ -0,0 +1,15 @@
+enum RuntimeError: Error, Equatable {
+ case unknown
+
+ case missingEnvironmentVariable(String)
+ case invalidHandlerName
+
+ case invocationMissingHeader(String)
+ case invocationMissingData
+
+ case unknownLambdaHandler(String)
+
+ case endpointError(String)
+
+
+}
diff --git a/Tests/AWSLambdaTests/ContextTests.swift b/Tests/AWSLambdaTests/ContextTests.swift
new file mode 100644
index 0000000..866c124
--- /dev/null
+++ b/Tests/AWSLambdaTests/ContextTests.swift
@@ -0,0 +1,30 @@
+import Foundation
+import XCTest
+import NIO
+@testable import AWSLambda
+
+class ContextTests: XCTestCase {
+
+ public func testDeadline() {
+ let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer {
+ XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
+ }
+
+ do {
+ let timeout: TimeInterval = 3
+ let context = try Context(
+ environment: .forTesting(),
+ invocation: .forTesting(timeout: timeout),
+ eventLoop: eventLoopGroup.next())
+
+ let remaining = context.getRemainingTime()
+
+ XCTAssert(timeout > remaining && remaining > timeout * 0.99, "Expected the remaining time to be within 99%")
+ }
+ catch {
+ XCTFail("unexpected error: \(error)")
+ }
+ }
+
+}
diff --git a/Tests/AWSLambdaTests/Events/APIGatewayTests.swift b/Tests/AWSLambdaTests/Events/APIGatewayTests.swift
new file mode 100644
index 0000000..b901eea
--- /dev/null
+++ b/Tests/AWSLambdaTests/Events/APIGatewayTests.swift
@@ -0,0 +1,132 @@
+//
+// File.swift
+//
+//
+// Created by Fabian Fett on 03.11.19.
+//
+
+import Foundation
+import XCTest
+import NIO
+import NIOHTTP1
+import NIOFoundationCompat
+@testable import AWSLambda
+
+class APIGatewayTests: XCTestCase {
+
+ static let exampleGetPayload = """
+ {"httpMethod": "GET", "body": null, "resource": "/test", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/test", "httpMethod": "GET", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "Prod", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/test"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Cache-Control": "max-age=0", "Dnt": "1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24", "Sec-Fetch-User": "?1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Sec-Fetch-Site": "none", "Sec-Fetch-Mode": "navigate", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Cache-Control": ["max-age=0"], "Dnt": ["1"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24"], "Sec-Fetch-User": ["?1"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"], "Sec-Fetch-Site": ["none"], "Sec-Fetch-Mode": ["navigate"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "/test", "isBase64Encoded": false}
+ """
+
+ static let todoPostPayload = """
+ {"httpMethod": "POST", "body": "{\\"title\\":\\"a todo\\"}", "resource": "/todos", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/todos", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "test", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/todos"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Content-Length": "18", "Pragma": "no-cache", "Cache-Control": "no-cache", "Accept": "text/plain, */*; q=0.01", "Origin": "http://todobackend.com", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25", "Dnt": "1", "Content-Type": "application/json", "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Mode": "cors", "Referer": "http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Content-Length": ["18"], "Pragma": ["no-cache"], "Cache-Control": ["no-cache"], "Accept": ["text/plain, */*; q=0.01"], "Origin": ["http://todobackend.com"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25"], "Dnt": ["1"], "Content-Type": ["application/json"], "Sec-Fetch-Site": ["cross-site"], "Sec-Fetch-Mode": ["cors"], "Referer": ["http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "/todos", "isBase64Encoded": false}
+ """
+
+ // MARK: - Handler -
+
+ func testHandlerSuccess() {
+ let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer {
+ XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
+ }
+
+ do {
+ let timeout: TimeInterval = 3
+ let context = try Context(
+ environment: .forTesting(),
+ invocation: .forTesting(timeout: timeout),
+ eventLoop: eventLoopGroup.next())
+
+ let payload = APIGatewayTests.exampleGetPayload
+ let length = payload.lengthOfBytes(using: .utf8)
+ var testPayload = ByteBufferAllocator().buffer(capacity: length)
+ testPayload.setString(payload, at: 0)
+ testPayload.moveWriterIndex(forwardBy: length)
+
+ let handler = APIGateway.handler { (request, context) -> EventLoopFuture in
+ return context.eventLoop.makeSucceededFuture(APIGateway.Response(statusCode: .ok))
+ }
+
+ let result = try handler(testPayload, context).wait()
+
+ let response = try JSONDecoder().decode(JSONResponse.self, from: result)
+ XCTAssertEqual(response.statusCode, 200)
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+
+ }
+
+ // MARK: - Request -
+
+ // MARK: Decoding
+
+ func testRequestDecodingExampleGetRequest() {
+ do {
+ let data = APIGatewayTests.exampleGetPayload.data(using: .utf8)!
+ let request = try JSONDecoder().decode(APIGateway.Request.self, from: data)
+
+ XCTAssertEqual(request.path, "/test")
+ XCTAssertEqual(request.httpMethod, "GET")
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ func testRequestDecodingTodoPostRequest() {
+
+ struct Todo: Decodable {
+ let title: String
+ }
+
+ do {
+ let data = APIGatewayTests.todoPostPayload.data(using: .utf8)!
+ let request = try JSONDecoder().decode(APIGateway.Request.self, from: data)
+
+ XCTAssertEqual(request.path, "/todos")
+ XCTAssertEqual(request.httpMethod, "POST")
+
+ let todo: Todo = try request.payload()
+ XCTAssertEqual(todo.title, "a todo")
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+
+
+ // MARK: - Response -
+
+ // MARK: Encoding
+
+ struct JSONResponse: Codable {
+ let statusCode: UInt
+ let headers: [String: String]?
+ let body: String?
+ let isBase64Encoded: Bool?
+ }
+
+ func testResponseEncoding() {
+
+ let resp = APIGateway.Response(
+ statusCode: .ok,
+ headers: HTTPHeaders([("Server", "Test")]),
+ body: "abc123")
+
+ do {
+ let data = try JSONEncoder().encodeAsByteBuffer(resp, allocator: ByteBufferAllocator())
+ let json = try JSONDecoder().decode(JSONResponse.self, from: data)
+
+ XCTAssertEqual(json.statusCode, resp.statusCode.code)
+ XCTAssertEqual(json.body, resp.body)
+ XCTAssertEqual(json.isBase64Encoded, resp.isBase64Encoded)
+ }
+ catch {
+ XCTFail("unexpected error: \(error)")
+ }
+
+ }
+}
diff --git a/Tests/AWSLambdaTests/Runtime+CodableTests.swift b/Tests/AWSLambdaTests/Runtime+CodableTests.swift
new file mode 100644
index 0000000..3f72a6a
--- /dev/null
+++ b/Tests/AWSLambdaTests/Runtime+CodableTests.swift
@@ -0,0 +1,102 @@
+import Foundation
+import XCTest
+import NIO
+import NIOHTTP1
+@testable import AWSLambda
+
+class RuntimeCodableTests: XCTestCase {
+
+ override func setUp() {
+
+ }
+
+ override func tearDown() {
+
+ }
+
+ struct TestRequest: Codable {
+ let name: String
+ }
+
+ struct TestResponse: Codable, Equatable {
+ let greeting: String
+ }
+
+ func testHappyPath() {
+ let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer {
+ XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
+ }
+ let handler = Runtime.codable { (req: TestRequest, ctx) -> EventLoopFuture in
+ return ctx.eventLoop.makeSucceededFuture(TestResponse(greeting: "Hello \(req.name)!"))
+ }
+
+ do {
+ let inputBytes = try JSONEncoder().encodeAsByteBuffer(TestRequest(name: "world"), allocator: ByteBufferAllocator())
+ let ctx = try Context(environment: .forTesting(), invocation: .forTesting(), eventLoop: eventLoopGroup.next())
+
+ let response = try handler(inputBytes, ctx).flatMapThrowing { (outputBytes) -> TestResponse in
+ return try JSONDecoder().decode(TestResponse.self, from: outputBytes)
+ }.wait()
+
+ XCTAssertEqual(response, TestResponse(greeting: "Hello world!"))
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ func testInvalidJSONInput() {
+ let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer {
+ XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
+ }
+ let handler = Runtime.codable { (req: TestRequest, ctx) -> EventLoopFuture in
+ return ctx.eventLoop.makeSucceededFuture(TestResponse(greeting: "Hello \(req.name)!"))
+ }
+
+ do {
+ var inputBytes = try JSONEncoder().encodeAsByteBuffer(TestRequest(name: "world"), allocator: ByteBufferAllocator())
+ inputBytes.setString("asd", at: 0) // destroy the json
+ let ctx = try Context(environment: .forTesting(), invocation: .forTesting(), eventLoop: eventLoopGroup.next())
+
+ _ = try handler(inputBytes, ctx).flatMapThrowing { (outputBytes) -> TestResponse in
+ XCTFail("The function should not be invoked.")
+ return try JSONDecoder().decode(TestResponse.self, from: outputBytes)
+ }.wait()
+
+ XCTFail("Did not expect to succeed.")
+ }
+ catch DecodingError.dataCorrupted(_) {
+ // this is our expected case
+ }
+ catch {
+ XCTFail("Expected to have an data corrupted error")
+ }
+ }
+
+ func testSynchronousCodableInterface() {
+ let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer {
+ XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
+ }
+ let handler = Runtime.codable { (req: TestRequest, ctx) -> TestResponse in
+ return TestResponse(greeting: "Hello \(req.name)!")
+ }
+
+ do {
+ let inputBytes = try JSONEncoder().encodeAsByteBuffer(TestRequest(name: "world"), allocator: ByteBufferAllocator())
+ let ctx = try Context(environment: .forTesting(), invocation: .forTesting(), eventLoop: eventLoopGroup.next())
+
+ let response = try handler(inputBytes, ctx).flatMapThrowing { (outputBytes) -> TestResponse in
+ return try JSONDecoder().decode(TestResponse.self, from: outputBytes)
+ }.wait()
+
+ XCTAssertEqual(response, TestResponse(greeting: "Hello world!"))
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+}
diff --git a/Tests/AWSLambdaTests/RuntimeAPIClientTests.swift b/Tests/AWSLambdaTests/RuntimeAPIClientTests.swift
new file mode 100644
index 0000000..47aa126
--- /dev/null
+++ b/Tests/AWSLambdaTests/RuntimeAPIClientTests.swift
@@ -0,0 +1,144 @@
+import Foundation
+import XCTest
+import NIO
+import NIOHTTP1
+import NIOTestUtils
+@testable import AWSLambda
+
+class RuntimeAPIClientTests: XCTestCase {
+
+ struct InvocationBody: Codable {
+ let test: String
+ }
+
+ func testGetNextInvocationHappyPathTest() {
+ let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ let web = NIOHTTP1TestServer(group: group)
+ let client = RuntimeAPIClient(eventLoopGroup: group, lambdaRuntimeAPI: "localhost:\(web.serverPort)")
+
+ defer {
+ XCTAssertNoThrow(try client.syncShutdown())
+ XCTAssertNoThrow(try web.stop())
+ XCTAssertNoThrow(try group.syncShutdownGracefully())
+ }
+
+ let result = client.getNextInvocation()
+
+ XCTAssertNoThrow(try XCTAssertEqual(
+ web.readInbound(),
+ HTTPServerRequestPart.head(.init(version: .init(major: 1, minor: 1), method: .GET, uri: "/2018-06-01/runtime/invocation/next", headers:
+ HTTPHeaders([("Host", "localhost"), ("Connection", "close"), ("Content-Length", "0")])))))
+ XCTAssertNoThrow(try XCTAssertEqual(
+ web.readInbound(),
+ HTTPServerRequestPart.end(nil)))
+
+ let now = UInt(Date().timeIntervalSinceNow * 1000 + 1000)
+
+ XCTAssertNoThrow(try web.writeOutbound(
+ .head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: HTTPHeaders([
+ ("Lambda-Runtime-Aws-Request-Id", UUID().uuidString),
+ ("Lambda-Runtime-Deadline-Ms", "\(now)"),
+ ("Lambda-Runtime-Invoked-Function-Arn", "fancy:arn"),
+ ("Lambda-Runtime-Trace-Id", "aTraceId"),
+ ("Lambda-Runtime-Client-Context", "someContext"),
+ ("Lambda-Runtime-Cognito-Identity", "someIdentity"),
+ ])))))
+
+ XCTAssertNoThrow(try web.writeOutbound(
+ .body(.byteBuffer(try JSONEncoder().encodeAsByteBuffer(InvocationBody(test: "abc"), allocator: ByteBufferAllocator())))))
+ XCTAssertNoThrow(try web.writeOutbound(.end(nil)))
+
+ XCTAssertNoThrow(try result.wait())
+
+ }
+
+ struct InvocationResponse: Codable {
+ let msg: String
+ }
+
+ func testPostInvocationResponseHappyPath() {
+ let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ let web = NIOHTTP1TestServer(group: group)
+ let client = RuntimeAPIClient(eventLoopGroup: group, lambdaRuntimeAPI: "localhost:\(web.serverPort)")
+ defer {
+ XCTAssertNoThrow(try web.stop())
+ XCTAssertNoThrow(try client.syncShutdown())
+ XCTAssertNoThrow(try group.syncShutdownGracefully())
+ }
+
+ do {
+ let invocationId = "abc"
+ let resp = InvocationResponse(msg: "hello world!")
+ let body = try JSONEncoder().encodeAsByteBuffer(resp, allocator: ByteBufferAllocator())
+ let result = client.postInvocationResponse(for: invocationId, httpBody: body)
+
+ XCTAssertNoThrow(try XCTAssertEqual(
+ web.readInbound(),
+ HTTPServerRequestPart.head(.init(version: .init(major: 1, minor: 1), method: .POST,
+ uri: "/2018-06-01/runtime/invocation/\(invocationId)/response",
+ headers: HTTPHeaders([("Host", "localhost"), ("Connection", "close"), ("Content-Length", "\(body.readableBytes)")])))))
+ XCTAssertNoThrow(try XCTAssertEqual(
+ web.readInbound(),
+ HTTPServerRequestPart.body(body)))
+ XCTAssertNoThrow(try XCTAssertEqual(
+ web.readInbound(),
+ HTTPServerRequestPart.end(nil)))
+
+ XCTAssertNoThrow(try web.writeOutbound(
+ .head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: HTTPHeaders([])))))
+ XCTAssertNoThrow(try web.writeOutbound(.end(nil)))
+
+ XCTAssertNoThrow(try result.wait())
+
+ }
+ catch {
+ XCTFail("unexpected error: \(error)")
+ }
+ }
+
+ enum InvocationError: Error {
+ case unknown
+ }
+
+ func testPostInvocationErrorHappyPath() {
+ let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ let web = NIOHTTP1TestServer(group: group)
+ let client = RuntimeAPIClient(eventLoopGroup: group, lambdaRuntimeAPI: "localhost:\(web.serverPort)")
+ defer {
+ XCTAssertNoThrow(try web.stop())
+ XCTAssertNoThrow(try client.syncShutdown())
+ XCTAssertNoThrow(try group.syncShutdownGracefully())
+ }
+
+ do {
+ let invocationId = "abc"
+ let error = InvocationError.unknown
+ let result = client.postInvocationError(for: invocationId, error: error)
+
+ let respError = AWSLambda.InvocationError(errorMessage: String(describing: error))
+ let body = try JSONEncoder().encodeAsByteBuffer(respError, allocator: ByteBufferAllocator())
+
+ XCTAssertNoThrow(try XCTAssertEqual(
+ web.readInbound(),
+ HTTPServerRequestPart.head(.init(version: .init(major: 1, minor: 1), method: .POST,
+ uri: "/2018-06-01/runtime/invocation/\(invocationId)/error",
+ headers: HTTPHeaders([("Host", "localhost"), ("Connection", "close"), ("Content-Length", "\(body.readableBytes)")])))))
+ XCTAssertNoThrow(try XCTAssertEqual(
+ web.readInbound(),
+ HTTPServerRequestPart.body(body)))
+ XCTAssertNoThrow(try XCTAssertEqual(
+ web.readInbound(),
+ HTTPServerRequestPart.end(nil)))
+
+ XCTAssertNoThrow(try web.writeOutbound(
+ .head(.init(version: .init(major: 1, minor: 1), status: .ok, headers: HTTPHeaders([])))))
+ XCTAssertNoThrow(try web.writeOutbound(.end(nil)))
+
+ XCTAssertNoThrow(try result.wait())
+
+ }
+ catch {
+ XCTFail("unexpected error: \(error)")
+ }
+ }
+}
diff --git a/Tests/AWSLambdaTests/RuntimeTests.swift b/Tests/AWSLambdaTests/RuntimeTests.swift
new file mode 100644
index 0000000..38ef615
--- /dev/null
+++ b/Tests/AWSLambdaTests/RuntimeTests.swift
@@ -0,0 +1,124 @@
+import Foundation
+import XCTest
+import NIO
+@testable import AWSLambda
+
+class RuntimeTests: XCTestCase {
+
+ // MARK: - Test Setup -
+
+ func testCreateRuntimeHappyPath() {
+
+ setenv("AWS_LAMBDA_RUNTIME_API", "localhost", 1)
+ setenv("_HANDLER", "BlaBla.testHandler", 1)
+
+ let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
+
+ do {
+ let runtime = try Runtime.createRuntime(eventLoopGroup: group)
+ defer { XCTAssertNoThrow(try runtime.syncShutdown()) }
+ XCTAssertEqual(runtime.handlerName, "testHandler")
+ XCTAssert(runtime.eventLoopGroup === group)
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ func testCreateRuntimeInvalidHandlerName() {
+ setenv("AWS_LAMBDA_RUNTIME_API", "localhost", 1)
+ setenv("_HANDLER", "testHandler", 1)
+
+ let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
+
+ do {
+ let runtime = try Runtime.createRuntime(eventLoopGroup: group)
+ defer { XCTAssertNoThrow(try runtime.syncShutdown()) }
+ XCTFail("Did not expect to succeed")
+ }
+ catch let error as RuntimeError {
+ XCTAssertEqual(error, RuntimeError.invalidHandlerName)
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ func testCreateRuntimeMissingLambdaRuntimeAPI() {
+ unsetenv("AWS_LAMBDA_RUNTIME_API")
+ setenv("_HANDLER", "BlaBla.testHandler", 1)
+
+ let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
+
+ do {
+ let runtime = try Runtime.createRuntime(eventLoopGroup: group)
+ defer { XCTAssertNoThrow(try runtime.syncShutdown()) }
+ XCTFail("Did not expect to succeed")
+ }
+ catch let error as RuntimeError {
+ XCTAssertEqual(error, RuntimeError.missingEnvironmentVariable("AWS_LAMBDA_RUNTIME_API"))
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ func testCreateRuntimeMissingHanlder() {
+ setenv("AWS_LAMBDA_RUNTIME_API", "localhost", 1)
+ unsetenv("_HANDLER")
+
+ let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+ defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
+
+ do {
+ let runtime = try Runtime.createRuntime(eventLoopGroup: group)
+ defer { XCTAssertNoThrow(try runtime.syncShutdown()) }
+ XCTFail("Did not expect to succeed")
+ }
+ catch let error as RuntimeError {
+ XCTAssertEqual(error, RuntimeError.missingEnvironmentVariable("_HANDLER"))
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ // MARK: - Test Running -
+
+// func testRegisterAFunction() {
+// let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+// let client = MockLambdaRuntimeAPI(eventLoopGroup: group)
+// defer {
+// XCTAssertNoThrow(try client.syncShutdown())
+// XCTAssertNoThrow(try group.syncShutdownGracefully())
+// }
+//
+// do {
+// let env = try Environment.forTesting(handler: "lambda.testFunction")
+//
+// let runtime = Runtime(eventLoopGroup: group, client: client, environment: env)
+// let expectation = self.expectation(description: "test function is hit")
+// var hits = 0
+// runtime.register(for: "testFunction") { (req, ctx) -> EventLoopFuture in
+// expectation.fulfill()
+// hits += 1
+// return ctx.eventLoop.makeSucceededFuture(ByteBufferAllocator().buffer(capacity: 0))
+// }
+//
+// _ = runtime.start()
+//
+// self.wait(for: [expectation], timeout: 3)
+//
+// XCTAssertNoThrow(try runtime.syncShutdown())
+// XCTAssertEqual(hits, 1)
+// }
+// catch {
+// XCTFail("Unexpected error: \(error)")
+// }
+// }
+
+}
+
diff --git a/Tests/AWSLambdaTests/Utils/Environment+TestUtils.swift b/Tests/AWSLambdaTests/Utils/Environment+TestUtils.swift
new file mode 100644
index 0000000..9bc8d00
--- /dev/null
+++ b/Tests/AWSLambdaTests/Utils/Environment+TestUtils.swift
@@ -0,0 +1,38 @@
+import NIO
+import NIOHTTP1
+@testable import AWSLambda
+
+extension Environment {
+
+ static func forTesting(
+ lambdaRuntimeAPI: String? = nil,
+ handler : String? = nil,
+ functionName : String? = nil,
+ functionVersion : String? = nil,
+ logGroupName : String? = nil,
+ logStreamName : String? = nil,
+ memoryLimitInMB : String? = nil,
+ accessKeyId : String? = nil,
+ secretAccessKey : String? = nil,
+ sessionToken : String? = nil)
+ throws -> Environment
+ {
+ var env = [String: String]()
+
+ env["AWS_LAMBDA_RUNTIME_API"] = lambdaRuntimeAPI ?? "localhost"
+ env["_HANDLER"] = handler ?? "lambda.handler"
+
+ env["AWS_LAMBDA_FUNCTION_NAME"] = functionName ?? "TestFunction"
+ env["AWS_LAMBDA_FUNCTION_VERSION"] = functionVersion ?? "1"
+ env["AWS_LAMBDA_LOG_GROUP_NAME"] = logGroupName ?? "TestFunctionLogGroupName"
+ env["AWS_LAMBDA_LOG_STREAM_NAME"] = logStreamName ?? "TestFunctionLogStreamName"
+ env["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"] = memoryLimitInMB ?? "512"
+ env["AWS_ACCESS_KEY_ID"] = accessKeyId ?? ""
+ env["AWS_SECRET_ACCESS_KEY"] = secretAccessKey ?? ""
+ env["AWS_SESSION_TOKEN"] = sessionToken ?? ""
+
+ return try Environment(env)
+ }
+
+
+}
diff --git a/Tests/AWSLambdaTests/Utils/Invocation+TestUtils.swift b/Tests/AWSLambdaTests/Utils/Invocation+TestUtils.swift
new file mode 100644
index 0000000..ab129ed
--- /dev/null
+++ b/Tests/AWSLambdaTests/Utils/Invocation+TestUtils.swift
@@ -0,0 +1,27 @@
+import Foundation
+import NIO
+import NIOHTTP1
+@testable import AWSLambda
+
+extension Invocation {
+
+ static func forTesting(
+ requestId : String = UUID().uuidString.lowercased(),
+ timeout : TimeInterval = 1,
+ functionArn: String = "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime",
+ traceId : String = "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1")
+ throws -> Invocation
+ {
+ let deadline = String(Int(Date(timeIntervalSinceNow: timeout).timeIntervalSince1970 * 1000))
+
+ let headers = HTTPHeaders([
+ ("Lambda-Runtime-Aws-Request-Id" , requestId),
+ ("Lambda-Runtime-Deadline-Ms" , deadline),
+ ("Lambda-Runtime-Invoked-Function-Arn", functionArn),
+ ("Lambda-Runtime-Trace-Id" , traceId),
+ ])
+
+ return try Invocation(headers: headers)
+ }
+
+}
diff --git a/Tests/AWSLambdaTests/Utils/MockLambdaRuntimeAPI.swift b/Tests/AWSLambdaTests/Utils/MockLambdaRuntimeAPI.swift
new file mode 100644
index 0000000..19c69d4
--- /dev/null
+++ b/Tests/AWSLambdaTests/Utils/MockLambdaRuntimeAPI.swift
@@ -0,0 +1,71 @@
+//
+// File.swift
+//
+//
+// Created by Fabian Fett on 11.11.19.
+//
+
+import Foundation
+import NIO
+@testable import AWSLambda
+
+class MockLambdaRuntimeAPI {
+
+ let eventLoopGroup : EventLoopGroup
+ let runLoop : EventLoop
+ let maxInvocations : Int
+ var invocationCount: Int = 0
+
+ private var isShutdown = false
+
+ init(eventLoopGroup: EventLoopGroup, maxInvocations: Int) {
+ self.eventLoopGroup = eventLoopGroup
+ self.runLoop = eventLoopGroup.next()
+ self.maxInvocations = maxInvocations
+ }
+}
+
+struct TestRequest: Codable {
+ let name: String
+}
+
+struct TestResponse: Codable {
+ let greeting: String
+}
+
+extension MockLambdaRuntimeAPI: LambdaRuntimeAPI {
+
+ func getNextInvocation() -> EventLoopFuture<(Invocation, ByteBuffer)> {
+ do {
+ let invocation = try Invocation.forTesting()
+ let payload = try JSONEncoder().encodeAsByteBuffer(
+ TestRequest(name: "world"),
+ allocator: ByteBufferAllocator())
+ return self.runLoop.makeSucceededFuture((invocation, payload))
+ }
+ catch {
+ return self.runLoop.makeFailedFuture(error)
+ }
+ }
+
+ func postInvocationResponse(for requestId: String, httpBody: ByteBuffer) -> EventLoopFuture {
+ return self.runLoop.makeSucceededFuture(Void())
+ }
+
+ func postInvocationError(for requestId: String, error: Error) -> EventLoopFuture {
+ return self.runLoop.makeSucceededFuture(Void())
+ }
+
+ func syncShutdown() throws {
+ self.runLoop.execute {
+ self.isShutdown = true
+ }
+ }
+
+}
+
+extension Invocation {
+
+
+
+}
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
new file mode 100644
index 0000000..e69de29
diff --git a/makefile b/makefile
new file mode 100644
index 0000000..8967f78
--- /dev/null
+++ b/makefile
@@ -0,0 +1,29 @@
+# Example settings
+EXAMPLE_LAMBDA=SquareNumber
+EXAMPLE_EXECUTABLE=$(EXAMPLE_LAMBDA)
+EXAMPLE_PROJECT_PATH=Examples/$(EXAMPLE_LAMBDA)
+LAMBDA_ZIP=$(EXAMPLE_PROJECT_PATH)/lambda.zip
+
+SWIFT_DOCKER_IMAGE=swift-dev:5.1.2
+
+clean_lambda:
+ rm $(EXAMPLE_PROJECT_PATH)/$(LAMBDA_ZIP) || true
+ rm -rf $(EXAMPLE_PROJECT_PATH)/.build || true
+
+build_lambda:
+ docker run \
+ --rm \
+ --volume "$(shell pwd)/:/src" \
+ --workdir "/src/$(EXAMPLE_PROJECT_PATH)" \
+ $(SWIFT_DOCKER_IMAGE) \
+ swift build -c release
+
+package_lambda: build_lambda
+ zip -r -j $(LAMBDA_ZIP) $(EXAMPLE_PROJECT_PATH)/.build/release/$(EXAMPLE_EXECUTABLE)
+
+deploy_lambda: package_lambda
+ aws lambda update-function-code --function-name $(EXAMPLE_LAMBDA) --zip-file fileb://$(LAMBDA_ZIP)
+
+test_lambda: package_lambda
+ echo '{"number": 9 }' | sam local invoke --template $(EXAMPLE_PROJECT_PATH)/template.yaml --force-image-build -v . "SquareNumberFunction"
+