diff --git a/.gitignore b/.gitignore
index 4226f50..722a2c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,5 @@
xcuserdata
Layer
Examples/**/lambda.zip
-packaged.yaml
\ No newline at end of file
+packaged.yaml
+bootstrap
\ No newline at end of file
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AWSLambda.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AWSLambda.xcscheme
deleted file mode 100644
index 0a59884..0000000
--- a/.swiftpm/xcode/xcshareddata/xcschemes/AWSLambda.xcscheme
+++ /dev/null
@@ -1,92 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Examples/EventSources/Package.resolved b/Examples/EventSources/Package.resolved
new file mode 100644
index 0000000..cb5c554
--- /dev/null
+++ b/Examples/EventSources/Package.resolved
@@ -0,0 +1,61 @@
+{
+ "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-base64-kit",
+ "repositoryURL": "https://github.com/fabianfett/swift-base64-kit.git",
+ "state": {
+ "branch": null,
+ "revision": "3ffa48a7047fc9ac6581cd53ab1df29466d8f13b",
+ "version": "0.2.0"
+ }
+ },
+ {
+ "package": "swift-log",
+ "repositoryURL": "https://github.com/apple/swift-log.git",
+ "state": {
+ "branch": null,
+ "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa",
+ "version": "1.2.0"
+ }
+ },
+ {
+ "package": "swift-nio",
+ "repositoryURL": "https://github.com/apple/swift-nio.git",
+ "state": {
+ "branch": null,
+ "revision": "ff01888051cd7efceb1bf8319c1dd3986c4bf6fc",
+ "version": "2.10.1"
+ }
+ },
+ {
+ "package": "swift-nio-extras",
+ "repositoryURL": "https://github.com/apple/swift-nio-extras.git",
+ "state": {
+ "branch": null,
+ "revision": "53808818c2015c45247cad74dc05c7a032c96a2f",
+ "version": "1.3.2"
+ }
+ },
+ {
+ "package": "swift-nio-ssl",
+ "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
+ "state": {
+ "branch": null,
+ "revision": "ccf96bbe65ecc7c1558ab0dba7ffabdea5c1d31f",
+ "version": "2.4.4"
+ }
+ }
+ ]
+ },
+ "version": 1
+}
diff --git a/Examples/EventSources/Package.swift b/Examples/EventSources/Package.swift
new file mode 100644
index 0000000..4af465a
--- /dev/null
+++ b/Examples/EventSources/Package.swift
@@ -0,0 +1,18 @@
+// 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: "EventSources",
+ dependencies: [
+ .package(path: "../.."),
+ .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")),
+ ],
+ targets: [
+ .target(
+ name: "EventSources",
+ dependencies: ["LambdaRuntime", "Logging", "NIO", "NIOFoundationCompat", "NIOHTTP1"]),
+ ]
+)
diff --git a/Examples/EventSources/README.md b/Examples/EventSources/README.md
new file mode 100644
index 0000000..383ecc2
--- /dev/null
+++ b/Examples/EventSources/README.md
@@ -0,0 +1,3 @@
+# EventSources
+
+This package/executable can be used to test the different Event types. This especially usefull during development of new Event types. We use the sam `template.yaml` to deploy the external resources needed for testing. Please be aware, if you deploy the given `template.yaml` costs may occur (especially when using the LoadBalancer).
diff --git a/Examples/EventSources/Sources/EventSources/main.swift b/Examples/EventSources/Sources/EventSources/main.swift
new file mode 100644
index 0000000..1d9bcf1
--- /dev/null
+++ b/Examples/EventSources/Sources/EventSources/main.swift
@@ -0,0 +1,119 @@
+import LambdaRuntime
+import NIO
+import Logging
+import Foundation
+
+LoggingSystem.bootstrap(StreamLogHandler.standardError)
+
+
+let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+defer { try! group.syncShutdownGracefully() }
+let logger = Logger(label: "AWSLambda.EventSources")
+
+func handleSNS(event: SNS.Event, ctx: Context) -> EventLoopFuture {
+ ctx.logger.info("Payload: \(String(describing: event))")
+
+ return ctx.eventLoop.makeSucceededFuture(Void())
+}
+
+func handleSQS(event: SQS.Event, ctx: Context) -> EventLoopFuture {
+ ctx.logger.info("Payload: \(String(describing: event))")
+
+ return ctx.eventLoop.makeSucceededFuture(Void())
+}
+
+func handleDynamoStream(event: DynamoDB.Event, ctx: Context) -> EventLoopFuture {
+ ctx.logger.info("Payload: \(String(describing: event))")
+
+ return ctx.eventLoop.makeSucceededFuture(Void())
+}
+
+func handleCloudwatchSchedule(event: Cloudwatch.Event, ctx: Context)
+ -> EventLoopFuture
+{
+ ctx.logger.info("Payload: \(String(describing: event))")
+
+ return ctx.eventLoop.makeSucceededFuture(Void())
+}
+
+func handleAPIRequest(req: APIGateway.Request, ctx: Context) -> EventLoopFuture {
+ ctx.logger.info("Payload: \(String(describing: req))")
+
+ struct Payload: Encodable {
+ let path: String
+ let method: String
+ }
+
+ let payload = Payload(path: req.path, method: req.httpMethod.rawValue)
+ let response = try! APIGateway.Response(statusCode: .ok, payload: payload)
+
+ return ctx.eventLoop.makeSucceededFuture(response)
+}
+
+func handleLoadBalancerRequest(req: ALB.TargetGroupRequest, ctx: Context) ->
+ EventLoopFuture
+{
+ ctx.logger.info("Payload: \(String(describing: req))")
+
+ struct Payload: Encodable {
+ let path: String
+ let method: String
+ }
+
+ let payload = Payload(path: req.path, method: req.httpMethod.rawValue)
+ let response = try! ALB.TargetGroupResponse(statusCode: .ok, payload: payload)
+
+ return ctx.eventLoop.makeSucceededFuture(response)
+}
+
+func printPayload(buffer: NIO.ByteBuffer, ctx: Context) -> EventLoopFuture {
+ let payload = buffer.getString(at: 0, length: buffer.readableBytes)
+ ctx.logger.error("Payload: \(String(describing: payload))")
+
+ return ctx.eventLoop.makeSucceededFuture(nil)
+}
+
+func printOriginalPayload(_ handler: @escaping (NIO.ByteBuffer, Context) -> EventLoopFuture)
+ -> ((NIO.ByteBuffer, Context) -> EventLoopFuture)
+{
+ return { (buffer, ctx) in
+ let payload = buffer.getString(at: 0, length: buffer.readableBytes)
+ ctx.logger.info("Payload: \(String(describing: payload))")
+
+ return handler(buffer, ctx)
+ }
+}
+
+do {
+ logger.info("start runtime")
+ let environment = try Environment()
+ let handler: LambdaRuntime.Handler
+
+ switch environment.handlerName {
+ case "sns":
+ handler = printOriginalPayload(LambdaRuntime.codable(handleSNS))
+ case "sqs":
+ handler = printOriginalPayload(LambdaRuntime.codable(handleSQS))
+ case "dynamo":
+ handler = printOriginalPayload(LambdaRuntime.codable(handleDynamoStream))
+ case "schedule":
+ handler = printOriginalPayload(LambdaRuntime.codable(handleCloudwatchSchedule))
+ case "api":
+ handler = printOriginalPayload(APIGateway.handler(handleAPIRequest))
+ case "loadbalancer":
+ handler = printOriginalPayload(ALB.handler(multiValueHeadersEnabled: true, handleLoadBalancerRequest))
+ default:
+ handler = printPayload
+ }
+
+ let runtime = try LambdaRuntime.createRuntime(eventLoopGroup: group, handler: handler)
+ defer { try! runtime.syncShutdown() }
+ logger.info("starting runloop")
+
+ try runtime.start().wait()
+}
+catch {
+ logger.error("error: \(String(describing: error))")
+}
+
+
diff --git a/Examples/EventSources/template.yaml b/Examples/EventSources/template.yaml
new file mode 100644
index 0000000..e1e6261
--- /dev/null
+++ b/Examples/EventSources/template.yaml
@@ -0,0 +1,275 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Parameters:
+ SwiftLayer:
+ Type: String
+ Description: The arn of the swift layer.
+ Default: arn:aws:lambda:eu-central-1:426836788079:layer:Swift:8
+
+# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
+Globals:
+ Function:
+ Timeout: 3
+
+Resources:
+
+ # --- VPC
+
+ VPC:
+ Type: "AWS::EC2::VPC"
+ Properties:
+ CidrBlock: 10.0.0.0/16
+ EnableDnsSupport: true
+ EnableDnsHostnames: true
+ InstanceTenancy: default
+ Tags:
+ - Key: Name
+ Value: "Test-VPC"
+
+ PublicSubnetCentralA:
+ Type: "AWS::EC2::Subnet"
+ Properties:
+ AvailabilityZone: eu-central-1a
+ CidrBlock: 10.0.0.0/24
+ VpcId: !Ref VPC
+
+ PublicSubnetCentralB:
+ Type: "AWS::EC2::Subnet"
+ Properties:
+ AvailabilityZone: eu-central-1b
+ CidrBlock: 10.0.10.0/24
+ VpcId: !Ref VPC
+
+ InternetGateway:
+ Type: "AWS::EC2::InternetGateway"
+
+ InternetGatewayAttachToVPC:
+ Type: "AWS::EC2::VPCGatewayAttachment"
+ Properties:
+ InternetGatewayId: !Ref InternetGateway
+ VpcId: !Ref VPC
+
+ PublicRouteTable:
+ Type: "AWS::EC2::RouteTable"
+ Properties:
+ VpcId: !Ref VPC
+
+ InternetRouteForPublicSubnets:
+ Type: "AWS::EC2::Route"
+ Properties:
+ DestinationCidrBlock: 0.0.0.0/0
+ GatewayId: !Ref InternetGateway
+ RouteTableId: !Ref PublicRouteTable
+
+ PublicSubnetCentralARouteTableAssociation:
+ Type: "AWS::EC2::SubnetRouteTableAssociation"
+ Properties:
+ RouteTableId: !Ref PublicRouteTable
+ SubnetId: !Ref PublicSubnetCentralA
+
+ PublicSubnetCentralBRouteTableAssociation:
+ Type: "AWS::EC2::SubnetRouteTableAssociation"
+ Properties:
+ RouteTableId: !Ref PublicRouteTable
+ SubnetId: !Ref PublicSubnetCentralB
+
+ # --- cloudwatch schedule
+
+ ConsumeCloudwatchScheduleLambda:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "schedule"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Events:
+ schedule:
+ Type: Schedule
+ Properties:
+ Schedule: rate(5 minutes)
+ Enabled: True
+
+ # --- sns
+
+ SNSTopic:
+ Type: AWS::SNS::Topic
+
+ ConsumeSNSTopicLambda:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "sns"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Policies:
+ - SNSCrudPolicy:
+ TopicName: !GetAtt SNSTopic.TopicName
+ Events:
+ sns:
+ Type: SNS
+ Properties:
+ Topic: !Ref SNSTopic
+
+ # --- sqs
+
+ SQSQueue:
+ Type: AWS::SQS::Queue
+
+ ConsumeSQSQueueLambda:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "sqs"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Policies:
+ - SQSPollerPolicy:
+ QueueName: !GetAtt SQSQueue.QueueName
+ Events:
+ sqs:
+ Type: SQS
+ Properties:
+ Queue: !GetAtt SQSQueue.Arn
+ BatchSize: 10
+ Enabled: true
+
+ # --- dynamo
+
+ EventSourcesTestTable:
+ 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
+ StreamSpecification:
+ StreamViewType: NEW_AND_OLD_IMAGES
+ TableName: "EventSourcesTestTable"
+
+ ConsumeDynamoDBStreamLambda:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "dynamo"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Policies:
+ - SQSPollerPolicy:
+ QueueName: !GetAtt SQSQueue.QueueName
+ Events:
+ dynamo:
+ Type: DynamoDB
+ Properties:
+ Stream: !GetAtt EventSourcesTestTable.StreamArn
+ StartingPosition: TRIM_HORIZON
+ BatchSize: 10
+ MaximumBatchingWindowInSeconds: 10
+ Enabled: true
+ ParallelizationFactor: 8
+ MaximumRetryAttempts: 100
+ BisectBatchOnFunctionError: true
+ MaximumRecordAgeInSeconds: 86400
+
+ # --- api
+
+ HandleAPIRequestLambda:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "api"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+ Events:
+ api:
+ Type: Api
+ Properties:
+ Path: /{proxy+}
+ Method: ANY
+
+ # --- load balancer
+
+ HandleLoadBalancerLambda:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: lambda.zip
+ Handler: "loadbalancer"
+ Runtime: provided
+ Layers:
+ - !Ref SwiftLayer
+
+ HandleLoadBalancerLambdaInvokePermission:
+ Type: AWS::Lambda::Permission
+ Properties:
+ FunctionName: !GetAtt HandleLoadBalancerLambda.Arn
+ Action: lambda:InvokeFunction
+ Principal: elasticloadbalancing.amazonaws.com
+
+ TestLoadBalancerSecurityGroup:
+ Type: AWS::EC2::SecurityGroup
+ Properties:
+ GroupDescription: External ELB Security Group
+ SecurityGroupIngress:
+ - CidrIp: 0.0.0.0/0
+ FromPort: 80
+ ToPort: 80
+ IpProtocol: tcp
+ - CidrIp: 0.0.0.0/0
+ FromPort: 443
+ ToPort: 443
+ IpProtocol: tcp
+ SecurityGroupEgress:
+ - CidrIp: 0.0.0.0/0
+ IpProtocol: -1
+ VpcId: !Ref VPC
+
+ TestLoadBalancer:
+ Type: AWS::ElasticLoadBalancingV2::LoadBalancer
+ Properties:
+ Scheme: internet-facing
+ Type: application
+ Subnets:
+ - !Ref PublicSubnetCentralA
+ - !Ref PublicSubnetCentralB
+ SecurityGroups:
+ - !Ref TestLoadBalancerSecurityGroup
+
+ TestLoadBalancerListener:
+ Type: "AWS::ElasticLoadBalancingV2::Listener"
+ Properties:
+ DefaultActions:
+ - Type: forward
+ TargetGroupArn: !Ref TestLoadBalancerTargetGroup
+ LoadBalancerArn: !Ref TestLoadBalancer
+ Port: 80
+ Protocol: HTTP
+
+ TestLoadBalancerTargetGroup:
+ Type: AWS::ElasticLoadBalancingV2::TargetGroup
+ DependsOn:
+ - HandleLoadBalancerLambdaInvokePermission
+ Properties:
+ Name: EinSternDerDeinenNamenTraegt
+ Targets:
+ - Id: !GetAtt HandleLoadBalancerLambda.Arn
+ TargetGroupAttributes:
+ - Key: lambda.multi_value_headers.enabled
+ Value: true
+ TargetType: lambda
+
+
+
+
+
+
diff --git a/Examples/SquareNumber/Package.resolved b/Examples/SquareNumber/Package.resolved
index 8a8262e..225ac00 100644
--- a/Examples/SquareNumber/Package.resolved
+++ b/Examples/SquareNumber/Package.resolved
@@ -10,6 +10,15 @@
"version": "1.0.0"
}
},
+ {
+ "package": "swift-base64-kit",
+ "repositoryURL": "https://github.com/fabianfett/swift-base64-kit.git",
+ "state": {
+ "branch": null,
+ "revision": "3ffa48a7047fc9ac6581cd53ab1df29466d8f13b",
+ "version": "0.2.0"
+ }
+ },
{
"package": "swift-log",
"repositoryURL": "https://github.com/apple/swift-log.git",
diff --git a/Examples/TodoAPIGateway/Package.resolved b/Examples/TodoAPIGateway/Package.resolved
index a12c2c9..e27a38a 100644
--- a/Examples/TodoAPIGateway/Package.resolved
+++ b/Examples/TodoAPIGateway/Package.resolved
@@ -46,6 +46,15 @@
"version": "3.0.3"
}
},
+ {
+ "package": "swift-base64-kit",
+ "repositoryURL": "https://github.com/fabianfett/swift-base64-kit.git",
+ "state": {
+ "branch": null,
+ "revision": "3ffa48a7047fc9ac6581cd53ab1df29466d8f13b",
+ "version": "0.2.0"
+ }
+ },
{
"package": "swift-log",
"repositoryURL": "https://github.com/apple/swift-log.git",
diff --git a/Package.swift b/Package.swift
index 0853c61..531495c 100644
--- a/Package.swift
+++ b/Package.swift
@@ -20,7 +20,7 @@ let package = Package(
targets: [
.target(
name: "LambdaRuntime",
- dependencies: ["AsyncHTTPClient", "NIO", "NIOHTTP1", "NIOFoundationCompat", "Logging"]
+ dependencies: ["AsyncHTTPClient", "NIO", "NIOHTTP1", "NIOFoundationCompat", "Logging", "Base64Kit"]
),
.testTarget(name: "LambdaRuntimeTests", dependencies: ["LambdaRuntime", "NIOTestUtils", "Logging"])
]
diff --git a/README.md b/README.md
index 6f88f61..0a980b9 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ Learn in depth how to build Lambdas in Swift in my blog series:
- [x] Built on top of `Swift-NIO`
- [x] Integration with Swift [`Logging`](https://github.com/apple/swift-log)
-- [ ] Ready-to-use [AWS Events](https://github.com/fabianfett/swift-lambda-runtime/tree/master/Sources/LambdaRuntime/Events) structs to get started as fast as possible
+- [x] Ready-to-use [AWS Events](https://github.com/fabianfett/swift-lambda-runtime/tree/master/Sources/LambdaRuntime/Events) structs to get started as fast as possible. Currently implemented: Application Load Balancer, APIGateway, Cloudwatch Scheduled Events, DynamoDB Streams, SNS and SQS Messages. More coming soon.
- [x] [Tested integration](https://github.com/fabianfett/swift-lambda-runtime/blob/master/Examples/TodoAPIGateway/Sources/TodoAPIGateway/main.swift) with [`aws-swift-sdk`](https://github.com/swift-aws/aws-sdk-swift)
- [x] [Two examples](https://github.com/fabianfett/swift-lambda-runtime/tree/master/Examples) to get you up and running as fast as possible (including an [API-Gateway Todo-List](http://todobackend.com/client/index.html?https://mwpixnkbzj.execute-api.eu-central-1.amazonaws.com/test/todos))
- [x] Unit and end-to-end tests
diff --git a/Sources/LambdaRuntime/AWSNumber.swift b/Sources/LambdaRuntime/AWSNumber.swift
new file mode 100644
index 0000000..c7a4225
--- /dev/null
+++ b/Sources/LambdaRuntime/AWSNumber.swift
@@ -0,0 +1,32 @@
+import Foundation
+
+public struct AWSNumber: Codable, Equatable {
+
+ public let stringValue: String
+
+ public var int: Int? {
+ return Int(stringValue)
+ }
+
+ public var double: Double? {
+ return Double(stringValue)
+ }
+
+ public init(int: Int) {
+ stringValue = String(int)
+ }
+
+ public init(double: Double) {
+ stringValue = String(double)
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ stringValue = try container.decode(String.self)
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ try container.encode(stringValue)
+ }
+}
diff --git a/Sources/LambdaRuntime/Context.swift b/Sources/LambdaRuntime/Context.swift
index 71dce36..d100afc 100644
--- a/Sources/LambdaRuntime/Context.swift
+++ b/Sources/LambdaRuntime/Context.swift
@@ -18,8 +18,9 @@ public class Context {
public init(environment: Environment, invocation: Invocation, eventLoop: EventLoop) {
- var logger = Logger(label: "aws.lambda.swift.request-logger")
+ var logger = Logger(label: "AWSLambda.request-logger")
logger[metadataKey: "RequestId"] = .string(invocation.requestId)
+ logger[metadataKey: "TraceId" ] = .string(invocation.traceId)
self.environment = environment
self.invocation = invocation
diff --git a/Sources/LambdaRuntime/Events/ALB.swift b/Sources/LambdaRuntime/Events/ALB.swift
new file mode 100644
index 0000000..00446ea
--- /dev/null
+++ b/Sources/LambdaRuntime/Events/ALB.swift
@@ -0,0 +1,245 @@
+import Foundation
+import NIO
+import NIOHTTP1
+
+
+// https://github.com/aws/aws-lambda-go/blob/master/events/alb.go
+
+public struct ALB {
+
+ /// ALBTargetGroupRequest contains data originating from the ALB Lambda target group integration
+ public struct TargetGroupRequest {
+
+ /// ALBTargetGroupRequestContext contains the information to identify the load balancer invoking the lambda
+ public struct Context: Codable {
+ public let elb: ELBContext
+ }
+
+ public let httpMethod: HTTPMethod
+ public let path: String
+ public let queryStringParameters: [String: [String]]
+ public let headers: HTTPHeaders
+ public let requestContext: Context
+ public let isBase64Encoded: Bool
+ public let body: String
+ }
+
+ /// ELBContext contains the information to identify the ARN invoking the lambda
+ public struct ELBContext: Codable {
+ public let targetGroupArn: String
+ }
+
+ public struct TargetGroupResponse {
+
+ public let statusCode : HTTPResponseStatus
+ public let statusDescription: String?
+ public let headers : HTTPHeaders?
+ public let body : String
+ public let isBase64Encoded : Bool
+
+ public init(
+ statusCode: HTTPResponseStatus,
+ statusDescription: String? = nil,
+ headers: HTTPHeaders? = nil,
+ body: String = "",
+ isBase64Encoded: Bool = false)
+ {
+ self.statusCode = statusCode
+ self.statusDescription = statusDescription
+ self.headers = headers
+ self.body = body
+ self.isBase64Encoded = isBase64Encoded
+ }
+ }
+}
+
+// MARK: - Handler -
+
+extension ALB {
+
+ public static func handler(
+ multiValueHeadersEnabled: Bool = false,
+ _ handler: @escaping (ALB.TargetGroupRequest, Context) -> EventLoopFuture)
+ -> ((NIO.ByteBuffer, Context) -> EventLoopFuture)
+ {
+ // reuse as much as possible
+ let encoder = JSONEncoder()
+ let decoder = JSONDecoder()
+ encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = multiValueHeadersEnabled
+
+ return { (inputBytes: NIO.ByteBuffer, ctx: Context) -> EventLoopFuture in
+
+ let req: ALB.TargetGroupRequest
+ do {
+ req = try decoder.decode(ALB.TargetGroupRequest.self, from: inputBytes)
+ }
+ catch {
+ return ctx.eventLoop.makeFailedFuture(error)
+ }
+
+ return handler(req, ctx)
+ .flatMapErrorThrowing() { (error) -> ALB.TargetGroupResponse in
+ ctx.logger.error("Unhandled error. Responding with HTTP 500: \(error).")
+ return ALB.TargetGroupResponse(statusCode: .internalServerError)
+ }
+ .flatMapThrowing { (result: ALB.TargetGroupResponse) -> NIO.ByteBuffer in
+ return try encoder.encodeAsByteBuffer(result, allocator: ByteBufferAllocator())
+ }
+ }
+ }
+}
+
+// MARK: - Request -
+
+extension ALB.TargetGroupRequest: Decodable {
+
+ enum CodingKeys: String, CodingKey {
+ case httpMethod = "httpMethod"
+ case path = "path"
+ case queryStringParameters = "queryStringParameters"
+ case multiValueQueryStringParameters = "multiValueQueryStringParameters"
+ case headers = "headers"
+ case multiValueHeaders = "multiValueHeaders"
+ case requestContext = "requestContext"
+ case isBase64Encoded = "isBase64Encoded"
+ case body = "body"
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ let method = try container.decode(String.self, forKey: .httpMethod)
+ self.httpMethod = HTTPMethod(rawValue: method)
+
+ self.path = try container.decode(String.self, forKey: .path)
+
+ // crazy multiple headers
+ // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers
+
+ if let multiValueQueryStringParameters =
+ try container.decodeIfPresent([String: [String]].self, forKey: .multiValueQueryStringParameters)
+ {
+ self.queryStringParameters = multiValueQueryStringParameters
+ }
+ else {
+ let singleValueQueryStringParameters = try container.decode(
+ [String: String].self,
+ forKey: .queryStringParameters)
+ self.queryStringParameters = singleValueQueryStringParameters.mapValues { [$0] }
+ }
+
+ if let multiValueHeaders =
+ try container.decodeIfPresent([String: [String]].self, forKey: .multiValueHeaders)
+ {
+ self.headers = HTTPHeaders(awsHeaders: multiValueHeaders)
+ }
+ else {
+ let singleValueHeaders = try container.decode(
+ [String: String].self,
+ forKey: .headers)
+ let multiValueHeaders = singleValueHeaders.mapValues { [$0] }
+ self.headers = HTTPHeaders(awsHeaders: multiValueHeaders)
+ }
+
+ self.requestContext = try container.decode(Context.self, forKey: .requestContext)
+ self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded)
+ self.body = try container.decode(String.self, forKey: .body)
+ }
+
+}
+
+// MARK: - Response -
+
+extension ALB.TargetGroupResponse: Encodable {
+
+ internal static let MultiValueHeadersEnabledKey =
+ CodingUserInfoKey(rawValue: "ALB.TargetGroupResponse.MultiValueHeadersEnabledKey")!
+
+ enum CodingKeys: String, CodingKey {
+ case statusCode
+ case statusDescription
+ case headers
+ case multiValueHeaders
+ case body
+ case isBase64Encoded
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(statusCode.code, forKey: .statusCode)
+
+ let multiValueHeaderSupport =
+ encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] as? Bool ?? false
+
+ switch (multiValueHeaderSupport, headers) {
+ case (true, .none):
+ try container.encode([String:String](), forKey: .multiValueHeaders)
+ case (false, .none):
+ try container.encode([String:[String]](), forKey: .headers)
+ case (true, .some(let headers)):
+ var multiValueHeaders: [String: [String]] = [:]
+ headers.forEach { (name, value) in
+ var values = multiValueHeaders[name] ?? []
+ values.append(value)
+ multiValueHeaders[name] = values
+ }
+ try container.encode(multiValueHeaders, forKey: .multiValueHeaders)
+ case (false, .some(let headers)):
+ var singleValueHeaders: [String: String] = [:]
+ headers.forEach { (name, value) in
+ singleValueHeaders[name] = value
+ }
+ try container.encode(singleValueHeaders, forKey: .headers)
+ }
+
+ try container.encodeIfPresent(statusDescription, forKey: .statusDescription)
+ try container.encodeIfPresent(body, forKey: .body)
+ try container.encodeIfPresent(isBase64Encoded, forKey: .isBase64Encoded)
+ }
+
+}
+
+extension ALB.TargetGroupResponse {
+
+ public init(
+ statusCode : HTTPResponseStatus,
+ statusDescription: String? = nil,
+ 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.statusDescription = statusDescription
+ self.headers = headers
+
+ let buffer = try encoder.encodeAsByteBuffer(payload, allocator: ByteBufferAllocator())
+ self.body = buffer.getString(at: 0, length: buffer.readableBytes) ?? ""
+ self.isBase64Encoded = 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,
+ statusDescription: String? = nil,
+ headers : HTTPHeaders? = nil,
+ buffer : NIO.ByteBuffer)
+ {
+ let headers = headers ?? HTTPHeaders()
+
+ self.statusCode = statusCode
+ self.statusDescription = statusDescription
+ self.headers = headers
+ self.body = buffer.withUnsafeReadableBytes { (ptr) -> String in
+ return String(base64Encoding: ptr)
+ }
+ self.isBase64Encoded = true
+ }
+
+}
+
diff --git a/Sources/LambdaRuntime/Events/APIGateway.swift b/Sources/LambdaRuntime/Events/APIGateway.swift
index 721f014..4804a65 100644
--- a/Sources/LambdaRuntime/Events/APIGateway.swift
+++ b/Sources/LambdaRuntime/Events/APIGateway.swift
@@ -2,13 +2,14 @@ import Foundation
import NIOFoundationCompat
import NIO
import NIOHTTP1
+import Base64Kit
// 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 {
+ /// APIGatewayRequest contains data coming from the API Gateway
+ public struct Request {
public struct Context: Codable {
@@ -42,12 +43,11 @@ public struct APIGateway {
public let resource: String
public let path: String
- public let httpMethod: String
+ public let httpMethod: HTTPMethod
- public let queryStringParameters: String?
+ public let queryStringParameters: [String: String]?
public let multiValueQueryStringParameters: [String:[String]]?
- public let headers: [String: String]?
- public let multiValueHeaders: [String: [String]]?
+ public let headers: HTTPHeaders
public let pathParameters: [String:String]?
public let stageVariables: [String:String]?
@@ -82,11 +82,13 @@ public struct APIGateway {
extension APIGateway {
public static func handler(
- decoder: JSONDecoder = JSONDecoder(),
- encoder: JSONEncoder = JSONEncoder(),
_ handler: @escaping (APIGateway.Request, Context) -> EventLoopFuture)
-> ((NIO.ByteBuffer, Context) -> EventLoopFuture)
{
+ // reuse as much as possible
+ let encoder = JSONEncoder()
+ let decoder = JSONDecoder()
+
return { (inputBytes: NIO.ByteBuffer, ctx: Context) -> EventLoopFuture in
let req: APIGateway.Request
@@ -111,9 +113,56 @@ extension APIGateway {
// MARK: - Request -
+extension APIGateway.Request: Decodable {
+
+ enum CodingKeys: String, CodingKey {
+
+ case resource = "resource"
+ case path = "path"
+ case httpMethod = "httpMethod"
+
+ case queryStringParameters = "queryStringParameters"
+ case multiValueQueryStringParameters = "multiValueQueryStringParameters"
+ case headers = "headers"
+ case multiValueHeaders = "multiValueHeaders"
+ case pathParameters = "pathParameters"
+ case stageVariables = "stageVariables"
+
+ case requestContext = "requestContext"
+ case body = "body"
+ case isBase64Encoded = "isBase64Encoded"
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ let method = try container.decode(String.self, forKey: .httpMethod)
+ self.httpMethod = HTTPMethod(rawValue: method)
+ self.path = try container.decode(String.self, forKey: .path)
+ self.resource = try container.decode(String.self, forKey: .resource)
+
+ self.queryStringParameters = try container.decodeIfPresent(
+ [String: String].self,
+ forKey: .queryStringParameters)
+ self.multiValueQueryStringParameters = try container.decodeIfPresent(
+ [String: [String]].self,
+ forKey: .multiValueQueryStringParameters)
+
+ let awsHeaders = try container.decode([String: [String]].self, forKey: .multiValueHeaders)
+ self.headers = HTTPHeaders(awsHeaders: awsHeaders)
+
+ self.pathParameters = try container.decodeIfPresent([String:String].self, forKey: .pathParameters)
+ self.stageVariables = try container.decodeIfPresent([String:String].self, forKey: .stageVariables)
+
+ self.requestContext = try container.decode(Context.self, forKey: .requestContext)
+ self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded)
+ self.body = try container.decodeIfPresent(String.self, forKey: .body)
+ }
+}
+
extension APIGateway.Request {
- public func payload(decoder: JSONDecoder = JSONDecoder()) throws -> Payload {
+ public func payload(_ type: Payload.Type, decoder: JSONDecoder = JSONDecoder()) throws -> Payload {
let body = self.body ?? ""
let capacity = body.lengthOfBytes(using: .utf8)
@@ -189,7 +238,6 @@ extension APIGateway.Response {
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
@@ -199,15 +247,14 @@ extension APIGateway.Response {
headers : HTTPHeaders? = nil,
buffer : NIO.ByteBuffer)
{
- var headers = headers ?? HTTPHeaders()
- headers.add(name: "Content-Type", value: "application/json")
+ let headers = headers ?? HTTPHeaders()
self.statusCode = statusCode
self.headers = headers
-
- self.body = String(base64Encoding: buffer.getBytes(at: 0, length: buffer.readableBytes))
+ self.body = buffer.withUnsafeReadableBytes { (ptr) -> String in
+ return String(base64Encoding: ptr)
+ }
self.isBase64Encoded = true
}
- #endif
}
diff --git a/Sources/LambdaRuntime/Events/Cloudwatch.swift b/Sources/LambdaRuntime/Events/Cloudwatch.swift
new file mode 100644
index 0000000..b0e93f4
--- /dev/null
+++ b/Sources/LambdaRuntime/Events/Cloudwatch.swift
@@ -0,0 +1,60 @@
+import Foundation
+
+public struct Cloudwatch {
+
+ public struct Event {
+ public let id : String
+ public let detailType : String
+ public let source : String
+ public let accountId : String
+ public let time : Date
+ public let region : String
+ public let resources : [String]
+ public let detail : Detail
+ }
+
+ public struct ScheduledEvent: Codable {}
+
+ fileprivate static let dateFormatter: DateFormatter = Cloudwatch.createDateFormatter()
+ fileprivate static func createDateFormatter() -> DateFormatter {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
+ formatter.timeZone = TimeZone(secondsFromGMT: 0)
+ return formatter
+ }
+}
+
+extension Cloudwatch.Event: Decodable {
+
+ enum CodingKeys: String, CodingKey {
+ case id = "id"
+ case detailType = "detail-type"
+ case source = "source"
+ case accountId = "account"
+ case time = "time"
+ case region = "region"
+ case resources = "resources"
+ case detail = "detail"
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ self.id = try container.decode(String.self, forKey: .id)
+ self.detailType = try container.decode(String.self, forKey: .detailType)
+ self.source = try container.decode(String.self, forKey: .source)
+ self.accountId = try container.decode(String.self, forKey: .accountId)
+
+ let dateString = try container.decode(String.self, forKey: .time)
+ guard let time = Cloudwatch.dateFormatter.date(from: dateString) else {
+ let dateFormat = String(describing: Cloudwatch.dateFormatter.dateFormat)
+ throw DecodingError.dataCorruptedError(forKey: .time, in: container, debugDescription:
+ "Expected date to be in format `\(dateFormat)`, but `\(dateFormat) does not forfill format`")
+ }
+ self.time = time
+
+ self.region = try container.decode(String.self, forKey: .region)
+ self.resources = try container.decode([String].self, forKey: .resources)
+ self.detail = try container.decode(Detail.self, forKey: .detail)
+ }
+}
diff --git a/Sources/LambdaRuntime/Events/DynamoDB+AttributeValue.swift b/Sources/LambdaRuntime/Events/DynamoDB+AttributeValue.swift
new file mode 100644
index 0000000..25912e5
--- /dev/null
+++ b/Sources/LambdaRuntime/Events/DynamoDB+AttributeValue.swift
@@ -0,0 +1,130 @@
+import Foundation
+import NIO
+import Base64Kit
+
+extension DynamoDB {
+
+ public enum AttributeValue {
+ case boolean(Bool)
+ case binary(NIO.ByteBuffer)
+ case binarySet([NIO.ByteBuffer])
+ case string(String)
+ case stringSet([String])
+ case null
+ case number(AWSNumber)
+ case numberSet([AWSNumber])
+
+ case list([AttributeValue])
+ case map([String: AttributeValue])
+ }
+
+}
+
+extension DynamoDB.AttributeValue: Decodable {
+
+ enum CodingKeys: String, CodingKey {
+ case binary = "B"
+ case bool = "BOOL"
+ case binarySet = "BS"
+ case list = "L"
+ case map = "M"
+ case number = "N"
+ case numberSet = "NS"
+ case null = "NULL"
+ case string = "S"
+ case stringSet = "SS"
+ }
+
+ public init(from decoder: Decoder) throws {
+
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ let allocator = ByteBufferAllocator()
+
+ guard container.allKeys.count == 1, let key = container.allKeys.first else {
+ let context = DecodingError.Context(
+ codingPath: container.codingPath,
+ debugDescription: "Expected exactly one key, but got \(container.allKeys.count)")
+ throw DecodingError.dataCorrupted(context)
+ }
+
+ switch key {
+ case .binary:
+ let encoded = try container.decode(String.self, forKey: .binary)
+ let bytes = try encoded.base64decoded()
+ var buffer = allocator.buffer(capacity: bytes.count)
+ buffer.setBytes(bytes, at: 0)
+ self = .binary(buffer)
+
+ case .bool:
+ let value = try container.decode(Bool.self, forKey: .bool)
+ self = .boolean(value)
+
+ case .binarySet:
+ let values = try container.decode([String].self, forKey: .binarySet)
+ let buffers = try values.map { (encoded) -> ByteBuffer in
+ let bytes = try encoded.base64decoded()
+ var buffer = allocator.buffer(capacity: bytes.count)
+ buffer.setBytes(bytes, at: 0)
+ return buffer
+ }
+ self = .binarySet(buffers)
+
+ case .list:
+ let values = try container.decode([DynamoDB.AttributeValue].self, forKey: .list)
+ self = .list(values)
+
+ case .map:
+ let value = try container.decode([String: DynamoDB.AttributeValue].self, forKey: .map)
+ self = .map(value)
+
+ case .number:
+ let value = try container.decode(AWSNumber.self, forKey: .number)
+ self = .number(value)
+
+ case .numberSet:
+ let values = try container.decode([AWSNumber].self, forKey: .numberSet)
+ self = .numberSet(values)
+
+ case .null:
+ self = .null
+
+ case .string:
+ let value = try container.decode(String.self, forKey: .string)
+ self = .string(value)
+
+ case .stringSet:
+ let values = try container.decode([String].self, forKey: .stringSet)
+ self = .stringSet(values)
+ }
+ }
+}
+
+extension DynamoDB.AttributeValue: Equatable {
+
+ static public func == (lhs: Self, rhs: Self) -> Bool {
+ switch (lhs, rhs) {
+ case (.boolean(let lhs), .boolean(let rhs)):
+ return lhs == rhs
+ case (.binary(let lhs), .binary(let rhs)):
+ return lhs == rhs
+ case (.binarySet(let lhs), .binarySet(let rhs)):
+ return lhs == rhs
+ case (.string(let lhs), .string(let rhs)):
+ return lhs == rhs
+ case (.stringSet(let lhs), .stringSet(let rhs)):
+ return lhs == rhs
+ case (.null, .null):
+ return true
+ case (.number(let lhs), .number(let rhs)):
+ return lhs == rhs
+ case (.numberSet(let lhs), .numberSet(let rhs)):
+ return lhs == rhs
+ case (.list(let lhs), .list(let rhs)):
+ return lhs == rhs
+ case (.map(let lhs), .map(let rhs)):
+ return lhs == rhs
+ default:
+ return false
+ }
+ }
+}
diff --git a/Sources/LambdaRuntime/Events/DynamoDB.swift b/Sources/LambdaRuntime/Events/DynamoDB.swift
new file mode 100644
index 0000000..4846d37
--- /dev/null
+++ b/Sources/LambdaRuntime/Events/DynamoDB.swift
@@ -0,0 +1,175 @@
+import Foundation
+import NIO
+
+/// https://github.com/aws/aws-lambda-go/blob/master/events/dynamodb.go
+public struct DynamoDB {
+
+ public struct Event: Decodable {
+ let records: [EventRecord]
+
+ public enum CodingKeys: String, CodingKey {
+ case records = "Records"
+ }
+ }
+
+ public enum KeyType: String, Codable {
+ case hash = "HASH"
+ case range = "RANGE"
+ }
+
+ public enum OperationType: String, Codable {
+ case insert = "INSERT"
+ case modify = "MODIFY"
+ case remove = "REMOVE"
+ }
+
+ public enum SharedIteratorType: String, Codable {
+ case trimHorizon = "TRIM_HORIZON"
+ case latest = "LATEST"
+ case atSequenceNumber = "AT_SEQUENCE_NUMBER"
+ case afterSequenceNumber = "AFTER_SEQUENCE_NUMBER"
+ }
+
+ public enum StreamStatus: String, Codable {
+ case enabling = "ENABLING"
+ case enabled = "ENABLED"
+ case disabling = "DISABLING"
+ case disabled = "DISABLED"
+ }
+
+ public enum StreamViewType: String, Codable {
+ /// the entire item, as it appeared after it was modified.
+ case newImage = "NEW_IMAGE"
+ /// the entire item, as it appeared before it was modified.
+ case oldImage = "OLD_IMAGE"
+ /// both the new and the old item images of the item.
+ case newAndOldImages = "NEW_AND_OLD_IMAGES"
+ /// only the key attributes of the modified item.
+ case keysOnly = "KEYS_ONLY"
+ }
+
+ public struct EventRecord: Decodable {
+ /// The region in which the GetRecords request was received.
+ public let awsRegion: String
+
+ /// The main body of the stream record, containing all of the DynamoDB-specific
+ /// fields.
+ public let change: StreamRecord
+
+ /// A globally unique identifier for the event that was recorded in this stream
+ /// record.
+ public let eventId: String
+
+ /// The type of data modification that was performed on the DynamoDB table:
+ /// * INSERT - a new item was added to the table.
+ /// * MODIFY - one or more of an existing item's attributes were modified.
+ /// * REMOVE - the item was deleted from the table
+ public let eventName: OperationType
+
+ /// The AWS service from which the stream record originated. For DynamoDB Streams,
+ /// this is aws:dynamodb.
+ public let eventSource: String
+
+ /// The version number of the stream record format. This number is updated whenever
+ /// the structure of Record is modified.
+ ///
+ /// Client applications must not assume that eventVersion will remain at a particular
+ /// value, as this number is subject to change at any time. In general, eventVersion
+ /// will only increase as the low-level DynamoDB Streams API evolves.
+ public let eventVersion: String
+
+ /// The event source ARN of DynamoDB
+ public let eventSourceArn: String
+
+ /// Items that are deleted by the Time to Live process after expiration have
+ /// the following fields:
+ /// * Records[].userIdentity.type
+ ///
+ /// "Service"
+ /// * Records[].userIdentity.principalId
+ ///
+ /// "dynamodb.amazonaws.com"
+ public let userIdentity: UserIdentity?
+
+ public enum CodingKeys: String, CodingKey {
+ case awsRegion = "awsRegion"
+ case change = "dynamodb"
+ case eventId = "eventID"
+ case eventName = "eventName"
+ case eventSource = "eventSource"
+ case eventVersion = "eventVersion"
+ case eventSourceArn = "eventSourceARN"
+ case userIdentity = "userIdentity"
+ }
+ }
+
+ public struct StreamRecord {
+ /// The approximate date and time when the stream record was created, in UNIX
+ /// epoch time (http://www.epochconverter.com/) format.
+ public let approximateCreationDateTime: Date?
+
+ /// The primary key attribute(s) for the DynamoDB item that was modified.
+ public let keys: [String: AttributeValue]
+
+ /// The item in the DynamoDB table as it appeared after it was modified.
+ public let newImage: [String: AttributeValue]?
+
+ /// The item in the DynamoDB table as it appeared before it was modified.
+ public let oldImage: [String: AttributeValue]?
+
+ /// The sequence number of the stream record.
+ public let sequenceNumber: String
+
+ /// The size of the stream record, in bytes.
+ public let sizeBytes: Int64
+
+ /// The type of data from the modified DynamoDB item that was captured in this
+ /// stream record.
+ public let streamViewType: StreamViewType
+ }
+
+ public struct UserIdentity: Codable {
+ public let type : String
+ public let principalId: String
+ }
+
+}
+
+extension DynamoDB.StreamRecord: Decodable {
+
+ enum CodingKeys: String, CodingKey {
+ case approximateCreationDateTime = "ApproximateCreationDateTime"
+ case keys = "Keys"
+ case newImage = "NewImage"
+ case oldImage = "OldImage"
+ case sequenceNumber = "SequenceNumber"
+ case sizeBytes = "SizeBytes"
+ case streamViewType = "StreamViewType"
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ self.keys = try container.decode(
+ [String: DynamoDB.AttributeValue].self,
+ forKey: .keys)
+
+ self.newImage = try container.decodeIfPresent(
+ [String: DynamoDB.AttributeValue].self,
+ forKey: .newImage)
+ self.oldImage = try container.decodeIfPresent(
+ [String: DynamoDB.AttributeValue].self,
+ forKey: .oldImage)
+
+ self.sequenceNumber = try container.decode(String.self, forKey: .sequenceNumber)
+ self.sizeBytes = try container.decode(Int64.self, forKey: .sizeBytes)
+ self.streamViewType = try container.decode(DynamoDB.StreamViewType.self, forKey: .streamViewType)
+
+ if let timestamp = try container.decodeIfPresent(TimeInterval.self, forKey: .approximateCreationDateTime) {
+ self.approximateCreationDateTime = Date(timeIntervalSince1970: timestamp)
+ }
+ else {
+ self.approximateCreationDateTime = nil
+ }
+ }
+}
diff --git a/Sources/LambdaRuntime/Events/SNS.swift b/Sources/LambdaRuntime/Events/SNS.swift
new file mode 100644
index 0000000..1156ad0
--- /dev/null
+++ b/Sources/LambdaRuntime/Events/SNS.swift
@@ -0,0 +1,172 @@
+import Foundation
+import NIO
+import Base64Kit
+
+/// https://github.com/aws/aws-lambda-go/blob/master/events/sns.go
+public struct SNS {
+
+ public struct Event: Decodable {
+
+ public struct Record: Decodable {
+ public let eventVersion: String
+ public let eventSubscriptionArn: String
+ public let eventSource: String
+ public let sns: Message
+
+ public enum CodingKeys: String, CodingKey {
+ case eventVersion = "EventVersion"
+ case eventSubscriptionArn = "EventSubscriptionArn"
+ case eventSource = "EventSource"
+ case sns = "Sns"
+ }
+ }
+
+ public let records: [Record]
+
+ public enum CodingKeys: String, CodingKey {
+ case records = "Records"
+ }
+ }
+
+ public struct Message {
+
+ public enum Attribute {
+ case string(String)
+ case binary(ByteBuffer)
+ }
+
+ public let signature : String
+ public let messageId : String
+ public let type : String
+ public let topicArn : String
+ public let messageAttributes: [String: Attribute]
+ public let signatureVersion : String
+ public let timestamp : Date
+ public let signingCertURL : String
+ public let message : String
+ public let unsubscribeUrl : String
+ public let subject : String?
+
+ }
+}
+
+extension SNS.Message: Decodable {
+
+ enum CodingKeys: String, CodingKey {
+ case signature = "Signature"
+ case messageId = "MessageId"
+ case type = "Type"
+ case topicArn = "TopicArn"
+ case messageAttributes = "MessageAttributes"
+ case signatureVersion = "SignatureVersion"
+ case timestamp = "Timestamp"
+ case signingCertURL = "SigningCertUrl"
+ case message = "Message"
+ case unsubscribeUrl = "UnsubscribeUrl"
+ case subject = "Subject"
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ self.signature = try container.decode(String.self, forKey: .signature)
+ self.messageId = try container.decode(String.self, forKey: .messageId)
+ self.type = try container.decode(String.self, forKey: .type)
+ self.topicArn = try container.decode(String.self, forKey: .topicArn)
+ self.messageAttributes = try container.decode([String: Attribute].self, forKey: .messageAttributes)
+ self.signatureVersion = try container.decode(String.self, forKey: .signatureVersion)
+
+ let dateString = try container.decode(String.self, forKey: .timestamp)
+ guard let timestamp = SNS.Message.dateFormatter.date(from: dateString) else {
+ let dateFormat = String(describing: SNS.Message.dateFormatter.dateFormat)
+ throw DecodingError.dataCorruptedError(forKey: .timestamp, in: container, debugDescription:
+ "Expected date to be in format `\(dateFormat)`, but `\(dateFormat) does not forfill format`")
+ }
+ self.timestamp = timestamp
+
+ self.signingCertURL = try container.decode(String.self, forKey: .signingCertURL)
+ self.message = try container.decode(String.self, forKey: .message)
+ self.unsubscribeUrl = try container.decode(String.self, forKey: .unsubscribeUrl)
+ self.subject = try container.decodeIfPresent(String.self, forKey: .subject)
+ }
+
+ private static let dateFormatter: DateFormatter = SNS.Message.createDateFormatter()
+ private static func createDateFormatter() -> DateFormatter {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
+ formatter.timeZone = TimeZone(secondsFromGMT: 0)
+ return formatter
+ }
+
+}
+
+extension SNS.Message {
+
+ public func payload(decoder: JSONDecoder = JSONDecoder()) throws -> Payload {
+ let body = self.message
+
+ 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)
+ }
+}
+
+extension SNS.Message.Attribute: Equatable {}
+
+extension SNS.Message.Attribute: Codable {
+
+ enum CodingKeys: String, CodingKey {
+ case dataType = "Type"
+ case dataValue = "Value"
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ let dataType = try container.decode(String.self, forKey: .dataType)
+ // https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html#SNSMessageAttributes.DataTypes
+ switch dataType {
+ case "String":
+ let value = try container.decode(String.self, forKey: .dataValue)
+ self = .string(value)
+ case "Binary":
+ let base64encoded = try container.decode(String.self, forKey: .dataValue)
+ let bytes = try base64encoded.base64decoded()
+
+ var buffer = ByteBufferAllocator().buffer(capacity: bytes.count)
+ buffer.writeBytes(bytes)
+ buffer.moveReaderIndex(to: bytes.count)
+
+ self = .binary(buffer)
+ default:
+ throw DecodingError.dataCorruptedError(forKey: .dataType, in: container, debugDescription: """
+ Unexpected value \"\(dataType)\" for key \(CodingKeys.dataType).
+ Expected `String` or `Binary`.
+ """)
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+
+ switch self {
+ case .binary(let byteBuffer):
+ let base64 = byteBuffer.withUnsafeReadableBytes { (pointer) -> String in
+ return String(base64Encoding: pointer)
+ }
+
+ try container.encode("Binary", forKey: .dataType)
+ try container.encode(base64, forKey: .dataValue)
+ case .string(let string):
+ try container.encode("String", forKey: .dataType)
+ try container.encode(string, forKey: .dataValue)
+ }
+ }
+
+}
diff --git a/Sources/LambdaRuntime/Events/SQS.swift b/Sources/LambdaRuntime/Events/SQS.swift
new file mode 100644
index 0000000..3c6ba4e
--- /dev/null
+++ b/Sources/LambdaRuntime/Events/SQS.swift
@@ -0,0 +1,111 @@
+import Foundation
+import NIO
+
+/// https://github.com/aws/aws-lambda-go/blob/master/events/sqs.go
+public struct SQS {
+
+ public struct Event: Codable {
+ public let records: [Message]
+
+ enum CodingKeys: String, CodingKey {
+ case records = "Records"
+ }
+ }
+
+ public struct Message: Codable {
+
+ /// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html
+ public enum Attribute {
+ case string(String)
+ case binary(ByteBuffer)
+ case number(AWSNumber)
+ }
+
+ public let messageId : String
+ public let receiptHandle : String
+ public let body : String
+ public let md5OfBody : String
+ public let md5OfMessageAttributes : String?
+ public let attributes : [String: String]
+ public let messageAttributes : [String: Attribute]
+ public let eventSourceArn : String
+ public let eventSource : String
+ public let awsRegion : String
+
+ enum CodingKeys: String, CodingKey {
+ case messageId
+ case receiptHandle
+ case body
+ case md5OfBody
+ case md5OfMessageAttributes
+ case attributes
+ case messageAttributes
+ case eventSourceArn = "eventSourceARN"
+ case eventSource
+ case awsRegion
+ }
+ }
+}
+
+extension SQS.Message.Attribute: Equatable { }
+
+extension SQS.Message.Attribute: Codable {
+
+ enum CodingKeys: String, CodingKey {
+ case dataType
+ case stringValue
+ case binaryValue
+
+ // BinaryListValue and StringListValue are unimplemented since
+ // they are not implemented as discussed here:
+ // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ let dataType = try container.decode(String.self, forKey: .dataType)
+ switch dataType {
+ case "String":
+ let value = try container.decode(String.self, forKey: .stringValue)
+ self = .string(value)
+ case "Number":
+ let value = try container.decode(AWSNumber.self, forKey: .stringValue)
+ self = .number(value)
+ case "Binary":
+ let base64encoded = try container.decode(String.self, forKey: .binaryValue)
+ let bytes = try base64encoded.base64decoded()
+
+ var buffer = ByteBufferAllocator().buffer(capacity: bytes.count)
+ buffer.writeBytes(bytes)
+ buffer.moveReaderIndex(to: bytes.count)
+
+ self = .binary(buffer)
+ default:
+ throw DecodingError.dataCorruptedError(forKey: .dataType, in: container, debugDescription: """
+ Unexpected value \"\(dataType)\" for key \(CodingKeys.dataType).
+ Expected `String`, `Binary` or `Number`.
+ """)
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+
+ switch self {
+ case .binary(let byteBuffer):
+ let base64 = byteBuffer.withUnsafeReadableBytes { (pointer) -> String in
+ return String(base64Encoding: pointer)
+ }
+
+ try container.encode("Binary", forKey: .dataType)
+ try container.encode(base64, forKey: .stringValue)
+ case .string(let string):
+ try container.encode("String", forKey: .dataType)
+ try container.encode(string, forKey: .binaryValue)
+ case .number(let number):
+ try container.encode("Number", forKey: .dataType)
+ try container.encode(number, forKey: .binaryValue)
+ }
+ }
+}
diff --git a/Sources/LambdaRuntime/Runtime+Codable.swift b/Sources/LambdaRuntime/Runtime+Codable.swift
index 643eec0..92e040b 100644
--- a/Sources/LambdaRuntime/Runtime+Codable.swift
+++ b/Sources/LambdaRuntime/Runtime+Codable.swift
@@ -6,15 +6,18 @@ extension LambdaRuntime {
/// wrapper to use for the register function that wraps the encoding and decoding
public static func codable(
+ decoder: JSONDecoder = JSONDecoder(),
_ 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)
+ input = try decoder.decode(Event.self, from: inputBytes)
}
catch {
+ let payload = inputBytes.getString(at: 0, length: inputBytes.readableBytes)
+ ctx.logger.error("Could not decode to type `\(String(describing: Event.self))`: \(error), payload: \(String(describing: payload))")
return ctx.eventLoop.makeFailedFuture(error)
}
@@ -26,15 +29,18 @@ extension LambdaRuntime {
}
public static func codable(
+ decoder: JSONDecoder = JSONDecoder(),
_ 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)
+ input = try decoder.decode(Event.self, from: inputBytes)
}
catch {
+ let payload = inputBytes.getString(at: 0, length: inputBytes.readableBytes)
+ ctx.logger.error("Could not decode to type `\(String(describing: Event.self))`: \(error), payload: \(String(describing: payload))")
return ctx.eventLoop.makeFailedFuture(error)
}
diff --git a/Sources/LambdaRuntime/Runtime.swift b/Sources/LambdaRuntime/Runtime.swift
index 3c0254b..573d1ae 100644
--- a/Sources/LambdaRuntime/Runtime.swift
+++ b/Sources/LambdaRuntime/Runtime.swift
@@ -50,7 +50,6 @@ final public class LambdaRuntime {
environment: Environment,
handler: @escaping Handler)
{
-
self.eventLoopGroup = eventLoopGroup
self.runtimeLoop = eventLoopGroup.next()
@@ -93,7 +92,8 @@ final public class LambdaRuntime {
_ = self.client.getNextInvocation()
.hop(to: self.runtimeLoop)
.flatMap { (invocation, byteBuffer) -> EventLoopFuture in
-
+
+ // TBD: Does it make sense to also set this env variable?
setenv("_X_AMZN_TRACE_ID", invocation.traceId, 0)
let context = Context(
diff --git a/Sources/LambdaRuntime/Utils/HTTPHeaders+Codable.swift b/Sources/LambdaRuntime/Utils/HTTPHeaders+Codable.swift
new file mode 100644
index 0000000..1a0168a
--- /dev/null
+++ b/Sources/LambdaRuntime/Utils/HTTPHeaders+Codable.swift
@@ -0,0 +1,15 @@
+import NIOHTTP1
+
+extension HTTPHeaders {
+
+ init(awsHeaders: [String: [String]]) {
+ var nioHeaders: [(String, String)] = []
+ awsHeaders.forEach { (key, values) in
+ values.forEach { (value) in
+ nioHeaders.append((key, value))
+ }
+ }
+
+ self = HTTPHeaders(nioHeaders)
+ }
+}
diff --git a/Tests/LambdaRuntimeTests/AWSNumberTests.swift b/Tests/LambdaRuntimeTests/AWSNumberTests.swift
new file mode 100644
index 0000000..a9b5367
--- /dev/null
+++ b/Tests/LambdaRuntimeTests/AWSNumberTests.swift
@@ -0,0 +1,74 @@
+import Foundation
+import XCTest
+@testable import LambdaRuntime
+
+class AWSNumberTests: XCTestCase {
+
+ // MARK: - Int -
+
+ func testInteger() {
+ let number = AWSNumber(int: 5)
+ XCTAssertEqual(number.stringValue, "5")
+ XCTAssertEqual(number.int, 5)
+ XCTAssertEqual(number.double, 5)
+ }
+
+ func testIntCoding() {
+ do {
+ let number = AWSNumber(int: 3)
+ struct TestStruct: Codable {
+ let number: AWSNumber
+ }
+
+ // Test: Encoding
+
+ let test = TestStruct(number: number)
+ let data = try JSONEncoder().encode(test)
+ let json = String(data: data, encoding: .utf8)
+ XCTAssertEqual(json, "{\"number\":\"3\"}")
+
+ // Test: Decoding
+
+ let decoded = try JSONDecoder().decode(TestStruct.self, from: data)
+ XCTAssertEqual(decoded.number.int, 3)
+ }
+ catch {
+ XCTFail("unexpected error: \(error)")
+ }
+ }
+
+ // MARK: - Double -
+
+ func testDouble() {
+ let number = AWSNumber(double: 3.14)
+ XCTAssertEqual(number.stringValue, "3.14")
+ XCTAssertEqual(number.int, nil)
+ XCTAssertEqual(number.double, 3.14)
+ }
+
+ func testDoubleCoding() {
+ do {
+ let number = AWSNumber(double: 6.25)
+ struct TestStruct: Codable {
+ let number: AWSNumber
+ }
+
+ // Test: Encoding
+
+ let test = TestStruct(number: number)
+ let data = try JSONEncoder().encode(test)
+ let json = String(data: data, encoding: .utf8)
+ XCTAssertEqual(json, "{\"number\":\"6.25\"}")
+
+ // Test: Decoding
+
+ let decoded = try JSONDecoder().decode(TestStruct.self, from: data)
+ XCTAssertEqual(decoded.number.int, nil)
+ XCTAssertEqual(decoded.number.double, 6.25)
+ }
+ catch {
+ XCTFail("unexpected error: \(error)")
+ }
+ }
+
+}
diff --git a/Tests/LambdaRuntimeTests/Events/ALBTests.swift b/Tests/LambdaRuntimeTests/Events/ALBTests.swift
new file mode 100644
index 0000000..1a0d51b
--- /dev/null
+++ b/Tests/LambdaRuntimeTests/Events/ALBTests.swift
@@ -0,0 +1,134 @@
+import Foundation
+import XCTest
+@testable import LambdaRuntime
+
+class ALBTests: XCTestCase {
+
+ static let exampleSingleValueHeadersPayload = """
+ {
+ "requestContext":{
+ "elb":{
+ "targetGroupArn": "arn:aws:elasticloadbalancing:eu-central-1:079477498937:targetgroup/EinSternDerDeinenNamenTraegt/621febf5a44b2ce5"
+ }
+ },
+ "httpMethod": "GET",
+ "path": "/",
+ "queryStringParameters": {},
+ "headers":{
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "accept-encoding": "gzip, deflate",
+ "accept-language": "en-us",
+ "connection": "keep-alive",
+ "host": "event-testl-1wa3wrvmroilb-358275751.eu-central-1.elb.amazonaws.com",
+ "upgrade-insecure-requests": "1",
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15",
+ "x-amzn-trace-id": "Root=1-5e189143-ad18a2b0a7728cd0dac45e10",
+ "x-forwarded-for": "90.187.8.137",
+ "x-forwarded-port": "80",
+ "x-forwarded-proto": "http"
+ },
+ "body":"",
+ "isBase64Encoded":false
+ }
+ """
+
+ func testRequestWithSingleValueHeadersPayload() {
+ let data = ALBTests.exampleSingleValueHeadersPayload.data(using: .utf8)!
+ do {
+ let decoder = JSONDecoder()
+
+ let event = try decoder.decode(ALB.TargetGroupRequest.self, from: data)
+
+ XCTAssertEqual(event.httpMethod, .GET)
+ XCTAssertEqual(event.body, "")
+ XCTAssertEqual(event.isBase64Encoded, false)
+ XCTAssertEqual(event.headers.count, 11)
+ XCTAssertEqual(event.path, "/")
+ XCTAssertEqual(event.queryStringParameters, [:])
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ // MARK: - Response -
+
+ private struct TestStruct: Codable {
+ let hello: String
+ }
+
+ private struct SingleValueHeadersResponse: Codable, Equatable {
+ let statusCode: Int
+ let body: String
+ let isBase64Encoded: Bool
+ let headers: [String: String]
+ }
+
+ private struct MultiValueHeadersResponse: Codable, Equatable {
+ let statusCode: Int
+ let body: String
+ let isBase64Encoded: Bool
+ let multiValueHeaders: [String: [String]]
+ }
+
+ func testJSONResponseWithSingleValueHeaders() throws {
+ let response = try ALB.TargetGroupResponse(statusCode: .ok, payload: TestStruct(hello: "world"))
+ let encoder = JSONEncoder()
+ encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = false
+ let data = try encoder.encode(response)
+
+ let expected = SingleValueHeadersResponse(
+ statusCode: 200, body: "{\"hello\":\"world\"}",
+ isBase64Encoded: false,
+ headers: ["Content-Type": "application/json"])
+
+ let result = try JSONDecoder().decode(SingleValueHeadersResponse.self, from: data)
+ XCTAssertEqual(result, expected)
+ }
+
+ func testJSONResponseWithMultiValueHeaders() throws {
+ let response = try ALB.TargetGroupResponse(statusCode: .ok, payload: TestStruct(hello: "world"))
+ let encoder = JSONEncoder()
+ encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = true
+ let data = try encoder.encode(response)
+
+ let expected = MultiValueHeadersResponse(
+ statusCode: 200, body: "{\"hello\":\"world\"}",
+ isBase64Encoded: false,
+ multiValueHeaders: ["Content-Type": ["application/json"]])
+
+ let result = try JSONDecoder().decode(MultiValueHeadersResponse.self, from: data)
+ XCTAssertEqual(result, expected)
+ }
+
+ func testEmptyResponseWithMultiValueHeaders() throws {
+ let response = ALB.TargetGroupResponse(statusCode: .ok)
+ let encoder = JSONEncoder()
+ encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = true
+ let data = try encoder.encode(response)
+
+ let expected = MultiValueHeadersResponse(
+ statusCode: 200, body: "",
+ isBase64Encoded: false,
+ multiValueHeaders: [:])
+
+ let result = try JSONDecoder().decode(MultiValueHeadersResponse.self, from: data)
+ XCTAssertEqual(result, expected)
+ }
+
+ func testEmptyResponseWithSingleValueHeaders() throws {
+ let response = ALB.TargetGroupResponse(statusCode: .ok)
+ let encoder = JSONEncoder()
+ encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = false
+ let data = try encoder.encode(response)
+
+ let expected = SingleValueHeadersResponse(
+ statusCode: 200, body: "",
+ isBase64Encoded: false,
+ headers: [:])
+
+ let result = try JSONDecoder().decode(SingleValueHeadersResponse.self, from: data)
+ XCTAssertEqual(result, expected)
+ }
+
+}
diff --git a/Tests/LambdaRuntimeTests/Events/APIGatewayTests.swift b/Tests/LambdaRuntimeTests/Events/APIGatewayTests.swift
index 328e305..96db4c8 100644
--- a/Tests/LambdaRuntimeTests/Events/APIGatewayTests.swift
+++ b/Tests/LambdaRuntimeTests/Events/APIGatewayTests.swift
@@ -1,10 +1,3 @@
-//
-// File.swift
-//
-//
-// Created by Fabian Fett on 03.11.19.
-//
-
import Foundation
import XCTest
import NIO
@@ -71,7 +64,7 @@ class APIGatewayTests: XCTestCase {
let request = try JSONDecoder().decode(APIGateway.Request.self, from: data)
XCTAssertEqual(request.path, "/test")
- XCTAssertEqual(request.httpMethod, "GET")
+ XCTAssertEqual(request.httpMethod, .GET)
}
catch {
XCTFail("Unexpected error: \(error)")
@@ -89,9 +82,9 @@ class APIGatewayTests: XCTestCase {
let request = try JSONDecoder().decode(APIGateway.Request.self, from: data)
XCTAssertEqual(request.path, "/todos")
- XCTAssertEqual(request.httpMethod, "POST")
+ XCTAssertEqual(request.httpMethod, .POST)
- let todo: Todo = try request.payload()
+ let todo = try request.payload(Todo.self)
XCTAssertEqual(todo.title, "a todo")
}
catch {
diff --git a/Tests/LambdaRuntimeTests/Events/CloudwatchTests.swift b/Tests/LambdaRuntimeTests/Events/CloudwatchTests.swift
new file mode 100644
index 0000000..d6fc523
--- /dev/null
+++ b/Tests/LambdaRuntimeTests/Events/CloudwatchTests.swift
@@ -0,0 +1,40 @@
+import Foundation
+import XCTest
+@testable import LambdaRuntime
+
+class CloudwatchTests: XCTestCase {
+
+ static let scheduledEventPayload = """
+ {
+ "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c",
+ "detail-type": "Scheduled Event",
+ "source": "aws.events",
+ "account": "123456789012",
+ "time": "1970-01-01T00:00:00Z",
+ "region": "us-east-1",
+ "resources": [
+ "arn:aws:events:us-east-1:123456789012:rule/ExampleRule"
+ ],
+ "detail": {}
+ }
+ """
+
+ func testScheduledEventFromJSON() {
+ let data = CloudwatchTests.scheduledEventPayload.data(using: .utf8)!
+ do {
+ let decoder = JSONDecoder()
+ let event = try decoder.decode(Cloudwatch.Event.self, from: data)
+
+ XCTAssertEqual(event.id , "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c")
+ XCTAssertEqual(event.detailType, "Scheduled Event")
+ XCTAssertEqual(event.source , "aws.events")
+ XCTAssertEqual(event.accountId , "123456789012")
+ XCTAssertEqual(event.time , Date(timeIntervalSince1970: 0))
+ XCTAssertEqual(event.region , "us-east-1")
+ XCTAssertEqual(event.resources , ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"])
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+}
diff --git a/Tests/LambdaRuntimeTests/Events/DynamoDB+AttributeValueTests.swift b/Tests/LambdaRuntimeTests/Events/DynamoDB+AttributeValueTests.swift
new file mode 100644
index 0000000..312ae69
--- /dev/null
+++ b/Tests/LambdaRuntimeTests/Events/DynamoDB+AttributeValueTests.swift
@@ -0,0 +1,118 @@
+import Foundation
+import XCTest
+import NIO
+@testable import LambdaRuntime
+
+class DynamoDBAttributeValueTests: XCTestCase {
+
+ func testBoolDecoding() throws {
+
+ let json = "{\"BOOL\": true}"
+ let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+
+ XCTAssertEqual(value, .boolean(true))
+ }
+
+ func testBinaryDecoding() throws {
+
+ let json = "{\"B\": \"YmFzZTY0\"}"
+ let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+
+ var buffer = ByteBufferAllocator().buffer(capacity: 6)
+ buffer.setString("base64", at: 0)
+ XCTAssertEqual(value, .binary(buffer))
+ }
+
+ func testBinarySetDecoding() throws {
+
+ let json = "{\"BS\": [\"YmFzZTY0\", \"YWJjMTIz\"]}"
+ let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+
+ var buffer1 = ByteBufferAllocator().buffer(capacity: 6)
+ buffer1.setString("base64", at: 0)
+
+ var buffer2 = ByteBufferAllocator().buffer(capacity: 6)
+ buffer2.setString("abc123", at: 0)
+
+ XCTAssertEqual(value, .binarySet([buffer1, buffer2]))
+ }
+
+ func testStringDecoding() throws {
+
+ let json = "{\"S\": \"huhu\"}"
+ let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+
+ XCTAssertEqual(value, .string("huhu"))
+ }
+
+ func testStringSetDecoding() throws {
+
+ let json = "{\"SS\": [\"huhu\", \"haha\"]}"
+ let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+
+ XCTAssertEqual(value, .stringSet(["huhu", "haha"]))
+ }
+
+ func testNullDecoding() throws {
+ let json = "{\"NULL\": true}"
+ let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+
+ XCTAssertEqual(value, .null)
+ }
+
+ func testNumberDecoding() throws {
+ let json = "{\"N\": \"1.2345\"}"
+ let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+
+ XCTAssertEqual(value, .number(AWSNumber(double: 1.2345)))
+ }
+
+ func testNumberSetDecoding() throws {
+ let json = "{\"NS\": [\"1.2345\", \"-19\"]}"
+ let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+
+ XCTAssertEqual(value, .numberSet([AWSNumber(double: 1.2345), AWSNumber(int: -19)]))
+ }
+
+ func testListDecoding() throws {
+ let json = "{\"L\": [{\"NS\": [\"1.2345\", \"-19\"]}, {\"S\": \"huhu\"}]}"
+ let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+
+ XCTAssertEqual(value, .list([.numberSet([AWSNumber(double: 1.2345), AWSNumber(int: -19)]), .string("huhu")]))
+ }
+
+ func testMapDecoding() throws {
+ let json = "{\"M\": {\"numbers\": {\"NS\": [\"1.2345\", \"-19\"]}, \"string\": {\"S\": \"huhu\"}}}"
+ let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+
+ XCTAssertEqual(value, .map([
+ "numbers": .numberSet([AWSNumber(double: 1.2345), AWSNumber(int: -19)]),
+ "string": .string("huhu")
+ ]))
+ }
+
+ func testEmptyDecoding() throws {
+ let json = "{\"haha\": 1}"
+ do {
+ _ = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!)
+ XCTFail("Did not expect to reach this point")
+ }
+ catch {
+ switch error {
+ case DecodingError.dataCorrupted(let context):
+ // expected error
+ XCTAssertEqual(context.codingPath.count, 0)
+ default:
+ XCTFail("Unexpected error: \(String(describing: error))")
+ }
+ }
+
+ }
+
+ func testEquatable() {
+ XCTAssertEqual(DynamoDB.AttributeValue.boolean(true), .boolean(true))
+ XCTAssertNotEqual(DynamoDB.AttributeValue.boolean(true), .boolean(false))
+ XCTAssertNotEqual(DynamoDB.AttributeValue.boolean(true), .string("haha"))
+ }
+
+}
diff --git a/Tests/LambdaRuntimeTests/Events/DynamoDBTests.swift b/Tests/LambdaRuntimeTests/Events/DynamoDBTests.swift
new file mode 100644
index 0000000..c150bfe
--- /dev/null
+++ b/Tests/LambdaRuntimeTests/Events/DynamoDBTests.swift
@@ -0,0 +1,115 @@
+import Foundation
+import XCTest
+@testable import LambdaRuntime
+
+class DynamoDBTests: XCTestCase {
+
+ static let streamEventPayload = """
+ {
+ "Records": [
+ {
+ "eventID": "1",
+ "eventVersion": "1.0",
+ "dynamodb": {
+ "ApproximateCreationDateTime": 1.578648338E9,
+ "Keys": {
+ "Id": {
+ "N": "101"
+ }
+ },
+ "NewImage": {
+ "Message": {
+ "S": "New item!"
+ },
+ "Id": {
+ "N": "101"
+ }
+ },
+ "StreamViewType": "NEW_AND_OLD_IMAGES",
+ "SequenceNumber": "111",
+ "SizeBytes": 26
+ },
+ "awsRegion": "eu-central-1",
+ "eventName": "INSERT",
+ "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899",
+ "eventSource": "aws:dynamodb"
+ },
+ {
+ "eventID": "2",
+ "eventVersion": "1.0",
+ "dynamodb": {
+ "ApproximateCreationDateTime": 1.578648338E9,
+ "OldImage": {
+ "Message": {
+ "S": "New item!"
+ },
+ "Id": {
+ "N": "101"
+ }
+ },
+ "SequenceNumber": "222",
+ "Keys": {
+ "Id": {
+ "N": "101"
+ }
+ },
+ "SizeBytes": 59,
+ "NewImage": {
+ "Message": {
+ "S": "This item has changed"
+ },
+ "Id": {
+ "N": "101"
+ }
+ },
+ "StreamViewType": "NEW_AND_OLD_IMAGES"
+ },
+ "awsRegion": "eu-central-1",
+ "eventName": "MODIFY",
+ "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899",
+ "eventSource": "aws:dynamodb"
+ },
+ {
+ "eventID": "3",
+ "eventVersion": "1.0",
+ "dynamodb": {
+ "ApproximateCreationDateTime":1.578648338E9,
+ "Keys": {
+ "Id": {
+ "N": "101"
+ }
+ },
+ "SizeBytes": 38,
+ "SequenceNumber": "333",
+ "OldImage": {
+ "Message": {
+ "S": "This item has changed"
+ },
+ "Id": {
+ "N": "101"
+ }
+ },
+ "StreamViewType": "NEW_AND_OLD_IMAGES"
+ },
+ "awsRegion": "eu-central-1",
+ "eventName": "REMOVE",
+ "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899",
+ "eventSource": "aws:dynamodb"
+ }
+ ]
+ }
+ """
+
+ func testScheduledEventFromJSON() {
+ let data = DynamoDBTests.streamEventPayload.data(using: .utf8)!
+ do {
+ let event = try JSONDecoder().decode(DynamoDB.Event.self, from: data)
+
+ XCTAssertEqual(event.records.count, 3)
+
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+}
diff --git a/Tests/LambdaRuntimeTests/Events/SNSTests.swift b/Tests/LambdaRuntimeTests/Events/SNSTests.swift
new file mode 100644
index 0000000..04198d6
--- /dev/null
+++ b/Tests/LambdaRuntimeTests/Events/SNSTests.swift
@@ -0,0 +1,83 @@
+import Foundation
+import XCTest
+import NIO
+@testable import LambdaRuntime
+
+class SNSTests: XCTestCase {
+
+ static let eventPayload = """
+ {
+ "Records": [
+ {
+ "EventSource": "aws:sns",
+ "EventVersion": "1.0",
+ "EventSubscriptionArn": "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c",
+ "Sns": {
+ "Type": "Notification",
+ "MessageId": "bdb6900e-1ae9-5b4b-b7fc-c681fde222e3",
+ "TopicArn": "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5",
+ "Subject": null,
+ "Message": "{\\\"hello\\\": \\\"world\\\"}",
+ "Timestamp": "2020-01-08T14:18:51.203Z",
+ "SignatureVersion": "1",
+ "Signature": "LJMF/xmMH7A1gNy2unLA3hmzyf6Be+zS/Yeiiz9tZbu6OG8fwvWZeNOcEZardhSiIStc0TF7h9I+4Qz3omCntaEfayzTGmWN8itGkn2mfn/hMFmPbGM8gEUz3+jp1n6p+iqP3XTx92R0LBIFrU3ylOxSo8+SCOjA015M93wfZzwj0WPtynji9iAvvtf15d8JxPUu1T05BRitpFd5s6ZXDHtVQ4x/mUoLUN8lOVp+rs281/ZdYNUG/V5CwlyUDTOERdryTkBJ/GO1NNPa+6m04ywJFa5d+BC8mDcUcHhhXXjpTEbt8AHBmswK3nudHrVMRO/G4zmssxU2P7ii5+gCfA==",
+ "SigningCertUrl": "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem",
+ "UnsubscribeUrl": "https://sns.eu-central-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c",
+ "MessageAttributes": {
+ "binary":{
+ "Type": "Binary",
+ "Value": "YmFzZTY0"
+ },
+ "string":{
+ "Type": "String",
+ "Value": "abc123"
+ }
+ }
+ }
+ }
+ ]
+ }
+ """
+
+ struct TestStruct: Decodable {
+ let hello: String
+ }
+
+ func testSimpleEventFromJSON() {
+ let data = SNSTests.eventPayload.data(using: .utf8)!
+ do {
+ let event = try JSONDecoder().decode(SNS.Event.self, from: data)
+
+ guard let record = event.records.first else {
+ XCTFail("Expected to have one record"); return
+ }
+
+ XCTAssertEqual(record.eventSource, "aws:sns")
+ XCTAssertEqual(record.eventVersion, "1.0")
+ XCTAssertEqual(record.eventSubscriptionArn, "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c")
+
+ XCTAssertEqual(record.sns.type, "Notification")
+ XCTAssertEqual(record.sns.messageId, "bdb6900e-1ae9-5b4b-b7fc-c681fde222e3")
+ XCTAssertEqual(record.sns.topicArn, "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5")
+ XCTAssertEqual(record.sns.message, "{\"hello\": \"world\"}")
+ XCTAssertEqual(record.sns.timestamp, Date(timeIntervalSince1970: 1578493131.203))
+ XCTAssertEqual(record.sns.signatureVersion, "1")
+ XCTAssertEqual(record.sns.signature, "LJMF/xmMH7A1gNy2unLA3hmzyf6Be+zS/Yeiiz9tZbu6OG8fwvWZeNOcEZardhSiIStc0TF7h9I+4Qz3omCntaEfayzTGmWN8itGkn2mfn/hMFmPbGM8gEUz3+jp1n6p+iqP3XTx92R0LBIFrU3ylOxSo8+SCOjA015M93wfZzwj0WPtynji9iAvvtf15d8JxPUu1T05BRitpFd5s6ZXDHtVQ4x/mUoLUN8lOVp+rs281/ZdYNUG/V5CwlyUDTOERdryTkBJ/GO1NNPa+6m04ywJFa5d+BC8mDcUcHhhXXjpTEbt8AHBmswK3nudHrVMRO/G4zmssxU2P7ii5+gCfA==")
+ XCTAssertEqual(record.sns.signingCertURL, "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem")
+ XCTAssertEqual(record.sns.unsubscribeUrl, "https://sns.eu-central-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c")
+
+ XCTAssertEqual(record.sns.messageAttributes.count, 2)
+
+ var binaryBuffer = ByteBufferAllocator().buffer(capacity: 6)
+ binaryBuffer.setString("base64", at: 0)
+ XCTAssertEqual(record.sns.messageAttributes["binary"], .binary(binaryBuffer))
+ XCTAssertEqual(record.sns.messageAttributes["string"], .string("abc123"))
+
+ let payload: TestStruct = try record.sns.payload()
+ XCTAssertEqual(payload.hello, "world")
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+}
diff --git a/Tests/LambdaRuntimeTests/Events/SQSTests.swift b/Tests/LambdaRuntimeTests/Events/SQSTests.swift
new file mode 100644
index 0000000..86d98ff
--- /dev/null
+++ b/Tests/LambdaRuntimeTests/Events/SQSTests.swift
@@ -0,0 +1,86 @@
+import Foundation
+import XCTest
+import NIO
+@testable import LambdaRuntime
+
+class SQSTests: XCTestCase {
+
+ static let testPayload = """
+ {
+ "Records": [
+ {
+ "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
+ "receiptHandle": "MessageReceiptHandle",
+ "body": "Hello from SQS!",
+ "attributes": {
+ "ApproximateReceiveCount": "1",
+ "SentTimestamp": "1523232000000",
+ "SenderId": "123456789012",
+ "ApproximateFirstReceiveTimestamp": "1523232000001"
+ },
+ "messageAttributes": {
+ "number":{
+ "stringValue":"123",
+ "stringListValues":[],
+ "binaryListValues":[],
+ "dataType":"Number"
+ },
+ "string":{
+ "stringValue":"abc123",
+ "stringListValues":[],
+ "binaryListValues":[],
+ "dataType":"String"
+ },
+ "binary":{
+ "dataType": "Binary",
+ "stringListValues":[],
+ "binaryListValues":[],
+ "binaryValue":"YmFzZTY0"
+ },
+
+ },
+ "md5OfBody": "7b270e59b47ff90a553787216d55d91d",
+ "eventSource": "aws:sqs",
+ "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue",
+ "awsRegion": "us-east-1"
+ }
+ ]
+ }
+ """
+
+
+ func testSimpleEventFromJSON() {
+ let data = SQSTests.testPayload.data(using: .utf8)!
+ do {
+ let decoder = JSONDecoder()
+ let event = try decoder.decode(SQS.Event.self, from: data)
+
+ XCTAssertEqual(event.records.count, 1)
+
+ guard let message = event.records.first else {
+ XCTFail("Expected to have one message in the event")
+ return
+ }
+
+ XCTAssertEqual(message.messageId , "19dd0b57-b21e-4ac1-bd88-01bbb068cb78")
+ XCTAssertEqual(message.receiptHandle , "MessageReceiptHandle")
+ XCTAssertEqual(message.body , "Hello from SQS!")
+ XCTAssertEqual(message.attributes.count, 4)
+
+ var binaryBuffer = ByteBufferAllocator().buffer(capacity: 6)
+ binaryBuffer.setString("base64", at: 0)
+ XCTAssertEqual(message.messageAttributes, [
+ "number": .number(AWSNumber(int: 123)),
+ "string": .string("abc123"),
+ "binary": .binary(binaryBuffer)
+ ])
+ XCTAssertEqual(message.md5OfBody , "7b270e59b47ff90a553787216d55d91d")
+ XCTAssertEqual(message.eventSource , "aws:sqs")
+ XCTAssertEqual(message.eventSourceArn , "arn:aws:sqs:us-east-1:123456789012:MyQueue")
+ XCTAssertEqual(message.awsRegion , "us-east-1")
+ }
+ catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+}
diff --git a/Tests/LambdaRuntimeTests/Utils/MockLambdaRuntimeAPI.swift b/Tests/LambdaRuntimeTests/Utils/MockLambdaRuntimeAPI.swift
index 9f7f292..1ece753 100644
--- a/Tests/LambdaRuntimeTests/Utils/MockLambdaRuntimeAPI.swift
+++ b/Tests/LambdaRuntimeTests/Utils/MockLambdaRuntimeAPI.swift
@@ -1,10 +1,3 @@
-//
-// File.swift
-//
-//
-// Created by Fabian Fett on 11.11.19.
-//
-
import Foundation
import NIO
@testable import LambdaRuntime