From 9abd4f98ce809015a05cde4e89c0a204b35f001d Mon Sep 17 00:00:00 2001 From: Eneko Alonso Date: Sun, 14 Jun 2020 08:33:35 -0700 Subject: [PATCH 1/5] Add APIGateway+V2+JSON --- .../AWSLambdaEvents/APIGateway+V2+JSON.swift | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 Sources/AWSLambdaEvents/APIGateway+V2+JSON.swift diff --git a/Sources/AWSLambdaEvents/APIGateway+V2+JSON.swift b/Sources/AWSLambdaEvents/APIGateway+V2+JSON.swift new file mode 100644 index 00000000..564e3e3c --- /dev/null +++ b/Sources/AWSLambdaEvents/APIGateway+V2+JSON.swift @@ -0,0 +1,115 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension APIGateway.V2 { + /// APIGateway.V2.Request contains data coming from the new HTTP API Gateway + public struct JSONRequest: Codable { + /// Context contains the information to identify the AWS account and resources invoking the Lambda function. + public struct Context: Codable { + public struct HTTP: Codable { + public let method: HTTPMethod + public let path: String + public let `protocol`: String + public let sourceIp: String + public let userAgent: String + } + + /// Authorizer contains authorizer information for the request context. + public struct Authorizer: Codable { + /// JWT contains JWT authorizer information for the request context. + public struct JWT: Codable { + public let claims: [String: String] + public let scopes: [String]? + } + + let jwt: JWT + } + + public let accountId: String + public let apiId: String + public let domainName: String + public let domainPrefix: String + public let stage: String + public let requestId: String + + public let http: HTTP + public let authorizer: Authorizer? + + /// The request time in format: 23/Apr/2020:11:08:18 +0000 + public let time: String + public let timeEpoch: UInt64 + } + + public let version: String + public let routeKey: String + public let rawPath: String + public let rawQueryString: String + + public let cookies: [String]? + public let headers: HTTPHeaders + public let queryStringParameters: [String: String]? + public let pathParameters: [String: String]? + + public let context: Context + public let stageVariables: [String: String]? + + public let body: Payload? + public let isBase64Encoded: Bool + + enum CodingKeys: String, CodingKey { + case version + case routeKey + case rawPath + case rawQueryString + + case cookies + case headers + case queryStringParameters + case pathParameters + + case context = "requestContext" + case stageVariables + + case body + case isBase64Encoded + } + } +} + +extension APIGateway.V2 { + public struct JSONResponse: Codable { + public let statusCode: HTTPResponseStatus + public let headers: HTTPHeaders? + public let multiValueHeaders: HTTPMultiValueHeaders? + public let body: Payload? + public let isBase64Encoded: Bool? + public let cookies: [String]? + + public init( + statusCode: HTTPResponseStatus, + headers: HTTPHeaders? = nil, + multiValueHeaders: HTTPMultiValueHeaders? = nil, + body: Payload? = nil, + isBase64Encoded: Bool? = nil, + cookies: [String]? = nil + ) { + self.statusCode = statusCode + self.headers = headers + self.multiValueHeaders = multiValueHeaders + self.body = body + self.isBase64Encoded = isBase64Encoded + self.cookies = cookies + } + } +} From b01b5e654e5b35a87119508cf3058f442143a134 Mon Sep 17 00:00:00 2001 From: Eneko Alonso Date: Sun, 14 Jun 2020 15:30:35 -0700 Subject: [PATCH 2/5] Provide extensions to decode and encode API Gateway V2 payloads --- .../AWSLambdaEvents/APIGateway+V2+JSON.swift | 115 ------------------ Sources/AWSLambdaEvents/APIGateway+V2.swift | 72 +++++++++++ 2 files changed, 72 insertions(+), 115 deletions(-) delete mode 100644 Sources/AWSLambdaEvents/APIGateway+V2+JSON.swift diff --git a/Sources/AWSLambdaEvents/APIGateway+V2+JSON.swift b/Sources/AWSLambdaEvents/APIGateway+V2+JSON.swift deleted file mode 100644 index 564e3e3c..00000000 --- a/Sources/AWSLambdaEvents/APIGateway+V2+JSON.swift +++ /dev/null @@ -1,115 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -extension APIGateway.V2 { - /// APIGateway.V2.Request contains data coming from the new HTTP API Gateway - public struct JSONRequest: Codable { - /// Context contains the information to identify the AWS account and resources invoking the Lambda function. - public struct Context: Codable { - public struct HTTP: Codable { - public let method: HTTPMethod - public let path: String - public let `protocol`: String - public let sourceIp: String - public let userAgent: String - } - - /// Authorizer contains authorizer information for the request context. - public struct Authorizer: Codable { - /// JWT contains JWT authorizer information for the request context. - public struct JWT: Codable { - public let claims: [String: String] - public let scopes: [String]? - } - - let jwt: JWT - } - - public let accountId: String - public let apiId: String - public let domainName: String - public let domainPrefix: String - public let stage: String - public let requestId: String - - public let http: HTTP - public let authorizer: Authorizer? - - /// The request time in format: 23/Apr/2020:11:08:18 +0000 - public let time: String - public let timeEpoch: UInt64 - } - - public let version: String - public let routeKey: String - public let rawPath: String - public let rawQueryString: String - - public let cookies: [String]? - public let headers: HTTPHeaders - public let queryStringParameters: [String: String]? - public let pathParameters: [String: String]? - - public let context: Context - public let stageVariables: [String: String]? - - public let body: Payload? - public let isBase64Encoded: Bool - - enum CodingKeys: String, CodingKey { - case version - case routeKey - case rawPath - case rawQueryString - - case cookies - case headers - case queryStringParameters - case pathParameters - - case context = "requestContext" - case stageVariables - - case body - case isBase64Encoded - } - } -} - -extension APIGateway.V2 { - public struct JSONResponse: Codable { - public let statusCode: HTTPResponseStatus - public let headers: HTTPHeaders? - public let multiValueHeaders: HTTPMultiValueHeaders? - public let body: Payload? - public let isBase64Encoded: Bool? - public let cookies: [String]? - - public init( - statusCode: HTTPResponseStatus, - headers: HTTPHeaders? = nil, - multiValueHeaders: HTTPMultiValueHeaders? = nil, - body: Payload? = nil, - isBase64Encoded: Bool? = nil, - cookies: [String]? = nil - ) { - self.statusCode = statusCode - self.headers = headers - self.multiValueHeaders = multiValueHeaders - self.body = body - self.isBase64Encoded = isBase64Encoded - self.cookies = cookies - } - } -} diff --git a/Sources/AWSLambdaEvents/APIGateway+V2.swift b/Sources/AWSLambdaEvents/APIGateway+V2.swift index 12c4c2ce..c06a95c1 100644 --- a/Sources/AWSLambdaEvents/APIGateway+V2.swift +++ b/Sources/AWSLambdaEvents/APIGateway+V2.swift @@ -117,3 +117,75 @@ extension APIGateway.V2 { } } } + +// MARK: - Codable Request body + +extension APIGateway.V2.Request { + /// Generic body decoder for JSON payloads + /// Example: + /// ``` + /// struct Request: Codable { + /// let value: String + /// } + /// + /// func handle(context: Context, event: APIGateway.V2.Request, callback: @escaping (Result) -> Void) { + /// do { + /// let request: Request? = try event.decodedBody() + /// // Do something with `request` + /// callback(.success(APIGateway.V2.Response(statusCode: .ok, body:""))) + /// } + /// catch { + /// callback(.failure(error)) + /// } + /// } + /// ``` + /// - Throws: `DecodingError` if body contains a value that couldn't be decoded + /// - Returns: Decoded payload. Returns `nil` if body property is `nil`. + public func decodedBody() throws -> Payload? { + guard let bodyString = body else { + return nil + } + let data = Data(bodyString.utf8) + return try JSONDecoder().decode(Payload.self, from: data) + } +} + +// MARK: - Codable Response body + +extension APIGateway.V2.Response { + /// Codable initializer for Response payload + /// Example use: + /// ``` + /// struct Response: Codable { + /// let message: String + /// } + /// + /// func handle(context: Context, event: APIGateway.V2.Request, callback: @escaping (Result) -> Void) { + /// ... + /// callback(.success(APIGateway.V2.Response(statusCode: .ok, body: Response(message: "Hello, World!"))) + /// } + /// ``` + /// - Parameters: + /// - statusCode: Response HTTP status code + /// - headers: Response HTTP headers + /// - multiValueHeaders: Resposne multi-value headers + /// - body: `Codable` response payload + /// - cookies: Response cookies + /// - Throws: `EncodingError` if payload could not be encoded into a JSON string + public init( + statusCode: HTTPResponseStatus, + headers: HTTPHeaders? = nil, + multiValueHeaders: HTTPMultiValueHeaders? = nil, + body: Payload? = nil, + cookies: [String]? = nil + ) throws { + let data = try JSONEncoder().encode(body) + let bodyString = String(data: data, encoding: .utf8) + self.init(statusCode: statusCode, + headers: headers, + multiValueHeaders: multiValueHeaders, + body: bodyString, + isBase64Encoded: false, + cookies: cookies) + } +} From ae03de0eebcc5aa8555aebc2144c31a9d5a40d0d Mon Sep 17 00:00:00 2001 From: Eneko Alonso Date: Sun, 14 Jun 2020 15:54:13 -0700 Subject: [PATCH 3/5] Unit tests for payload coding/decoding --- Sources/AWSLambdaEvents/APIGateway+V2.swift | 8 ++++- .../APIGateway+V2Tests.swift | 36 +++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Sources/AWSLambdaEvents/APIGateway+V2.swift b/Sources/AWSLambdaEvents/APIGateway+V2.swift index c06a95c1..4add5629 100644 --- a/Sources/AWSLambdaEvents/APIGateway+V2.swift +++ b/Sources/AWSLambdaEvents/APIGateway+V2.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import Foundation + extension APIGateway { public struct V2 {} } @@ -122,6 +124,7 @@ extension APIGateway.V2 { extension APIGateway.V2.Request { /// Generic body decoder for JSON payloads + /// /// Example: /// ``` /// struct Request: Codable { @@ -139,6 +142,7 @@ extension APIGateway.V2.Request { /// } /// } /// ``` + /// /// - Throws: `DecodingError` if body contains a value that couldn't be decoded /// - Returns: Decoded payload. Returns `nil` if body property is `nil`. public func decodedBody() throws -> Payload? { @@ -154,7 +158,8 @@ extension APIGateway.V2.Request { extension APIGateway.V2.Response { /// Codable initializer for Response payload - /// Example use: + /// + /// Example: /// ``` /// struct Response: Codable { /// let message: String @@ -165,6 +170,7 @@ extension APIGateway.V2.Response { /// callback(.success(APIGateway.V2.Response(statusCode: .ok, body: Response(message: "Hello, World!"))) /// } /// ``` + /// /// - Parameters: /// - statusCode: Response HTTP status code /// - headers: Response HTTP headers diff --git a/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift index 9d682c94..a0de2049 100644 --- a/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift +++ b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift @@ -68,10 +68,15 @@ class APIGatewayV2Tests: XCTestCase { "x-amzn-trace-id":"Root=1-5ea3263d-07c5d5ddfd0788bed7dad831", "user-agent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", "content-length":"0" - } + }, + "body": "{\\"some\\":\\"json\\",\\"number\\":42}" } """ + static let exampleResponse = """ + {"isBase64Encoded":false,"statusCode":200,"body":"{\\"message\\":\\"Foo Bar\\",\\"code\\":42}"} + """ + // MARK: - Request - // MARK: Decoding @@ -86,6 +91,33 @@ class APIGatewayV2Tests: XCTestCase { XCTAssertEqual(req?.queryStringParameters?.count, 1) XCTAssertEqual(req?.rawQueryString, "foo=bar") XCTAssertEqual(req?.headers.count, 8) - XCTAssertNil(req?.body) + XCTAssertNotNil(req?.body) } + + func testRquestPayloadDecoding() throws { + struct Payload: Codable { + let some: String + let number: Int + } + + let data = APIGatewayV2Tests.exampleGetEventBody.data(using: .utf8)! + let request = try JSONDecoder().decode(APIGateway.V2.Request.self, from: data) + + let payload: Payload? = try request.decodedBody() + XCTAssertEqual(payload?.some, "json") + XCTAssertEqual(payload?.number, 42) + } + + func testResponsePayloadEncoding() throws { + struct Payload: Codable { + let code: Int + let message: String + } + + let response = try APIGateway.V2.Response(statusCode: .ok, body: Payload(code: 42, message: "Foo Bar")) + let data = try JSONEncoder().encode(response) + let json = String(data: data, encoding: .utf8) + XCTAssertEqual(json, APIGatewayV2Tests.exampleResponse) + } + } From bb688c4223b4c13abb50b19578982245be4b8e37 Mon Sep 17 00:00:00 2001 From: Eneko Alonso Date: Sun, 14 Jun 2020 19:36:03 -0700 Subject: [PATCH 4/5] Inject encoder and decoder --- Sources/AWSLambdaEvents/APIGateway+V2.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/AWSLambdaEvents/APIGateway+V2.swift b/Sources/AWSLambdaEvents/APIGateway+V2.swift index 4add5629..713dff7c 100644 --- a/Sources/AWSLambdaEvents/APIGateway+V2.swift +++ b/Sources/AWSLambdaEvents/APIGateway+V2.swift @@ -145,12 +145,12 @@ extension APIGateway.V2.Request { /// /// - Throws: `DecodingError` if body contains a value that couldn't be decoded /// - Returns: Decoded payload. Returns `nil` if body property is `nil`. - public func decodedBody() throws -> Payload? { + public func decodedBody(decoder: JSONDecoder = JSONDecoder()) throws -> Payload? { guard let bodyString = body else { return nil } let data = Data(bodyString.utf8) - return try JSONDecoder().decode(Payload.self, from: data) + return try decoder.decode(Payload.self, from: data) } } @@ -183,9 +183,10 @@ extension APIGateway.V2.Response { headers: HTTPHeaders? = nil, multiValueHeaders: HTTPMultiValueHeaders? = nil, body: Payload? = nil, - cookies: [String]? = nil + cookies: [String]? = nil, + encoder: JSONEncoder = JSONEncoder() ) throws { - let data = try JSONEncoder().encode(body) + let data = try encoder.encode(body) let bodyString = String(data: data, encoding: .utf8) self.init(statusCode: statusCode, headers: headers, From f24463b71eac5785c878ed2f5c82cb4e2e01ddb4 Mon Sep 17 00:00:00 2001 From: Eneko Alonso Date: Mon, 15 Jun 2020 20:23:01 -0700 Subject: [PATCH 5/5] PR Feedback. Fix tests. --- Sources/AWSLambdaEvents/APIGateway+V2.swift | 26 +++++++++--------- .../APIGateway+V2Tests.swift | 27 ++++++++++--------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Sources/AWSLambdaEvents/APIGateway+V2.swift b/Sources/AWSLambdaEvents/APIGateway+V2.swift index 713dff7c..b4a5bed2 100644 --- a/Sources/AWSLambdaEvents/APIGateway+V2.swift +++ b/Sources/AWSLambdaEvents/APIGateway+V2.swift @@ -123,19 +123,19 @@ extension APIGateway.V2 { // MARK: - Codable Request body extension APIGateway.V2.Request { - /// Generic body decoder for JSON payloads + /// Generic decoder for JSON body /// /// Example: /// ``` - /// struct Request: Codable { + /// struct Body: Codable { /// let value: String /// } /// /// func handle(context: Context, event: APIGateway.V2.Request, callback: @escaping (Result) -> Void) { /// do { - /// let request: Request? = try event.decodedBody() - /// // Do something with `request` - /// callback(.success(APIGateway.V2.Response(statusCode: .ok, body:""))) + /// let body: Body? = try event.decodedBody() + /// // Do something with `body` + /// callback(.success(APIGateway.V2.Response(statusCode: .ok, body: ""))) /// } /// catch { /// callback(.failure(error)) @@ -144,20 +144,20 @@ extension APIGateway.V2.Request { /// ``` /// /// - Throws: `DecodingError` if body contains a value that couldn't be decoded - /// - Returns: Decoded payload. Returns `nil` if body property is `nil`. - public func decodedBody(decoder: JSONDecoder = JSONDecoder()) throws -> Payload? { + /// - Returns: Decoded body. Returns `nil` if body property is `nil`. + public func decodedBody(decoder: JSONDecoder = JSONDecoder()) throws -> Body? { guard let bodyString = body else { return nil } let data = Data(bodyString.utf8) - return try decoder.decode(Payload.self, from: data) + return try decoder.decode(Body.self, from: data) } } // MARK: - Codable Response body extension APIGateway.V2.Response { - /// Codable initializer for Response payload + /// Codable initializer for Response body /// /// Example: /// ``` @@ -175,14 +175,14 @@ extension APIGateway.V2.Response { /// - statusCode: Response HTTP status code /// - headers: Response HTTP headers /// - multiValueHeaders: Resposne multi-value headers - /// - body: `Codable` response payload + /// - body: `Codable` response body /// - cookies: Response cookies - /// - Throws: `EncodingError` if payload could not be encoded into a JSON string - public init( + /// - Throws: `EncodingError` if body could not be encoded into a JSON string + public init( statusCode: HTTPResponseStatus, headers: HTTPHeaders? = nil, multiValueHeaders: HTTPMultiValueHeaders? = nil, - body: Payload? = nil, + body: Body? = nil, cookies: [String]? = nil, encoder: JSONEncoder = JSONEncoder() ) throws { diff --git a/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift index a0de2049..fd7ddc3e 100644 --- a/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift +++ b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift @@ -73,8 +73,8 @@ class APIGatewayV2Tests: XCTestCase { } """ - static let exampleResponse = """ - {"isBase64Encoded":false,"statusCode":200,"body":"{\\"message\\":\\"Foo Bar\\",\\"code\\":42}"} + static let exampleResponseBody = """ + {\"code\":42,\"message\":\"Foo Bar\"} """ // MARK: - Request - @@ -94,8 +94,8 @@ class APIGatewayV2Tests: XCTestCase { XCTAssertNotNil(req?.body) } - func testRquestPayloadDecoding() throws { - struct Payload: Codable { + func testRequestBodyDecoding() throws { + struct Body: Codable { let some: String let number: Int } @@ -103,21 +103,22 @@ class APIGatewayV2Tests: XCTestCase { let data = APIGatewayV2Tests.exampleGetEventBody.data(using: .utf8)! let request = try JSONDecoder().decode(APIGateway.V2.Request.self, from: data) - let payload: Payload? = try request.decodedBody() - XCTAssertEqual(payload?.some, "json") - XCTAssertEqual(payload?.number, 42) + let body: Body? = try request.decodedBody() + XCTAssertEqual(body?.some, "json") + XCTAssertEqual(body?.number, 42) } - func testResponsePayloadEncoding() throws { - struct Payload: Codable { + func testResponseBodyEncoding() throws { + struct Body: Codable { let code: Int let message: String } - let response = try APIGateway.V2.Response(statusCode: .ok, body: Payload(code: 42, message: "Foo Bar")) - let data = try JSONEncoder().encode(response) - let json = String(data: data, encoding: .utf8) - XCTAssertEqual(json, APIGatewayV2Tests.exampleResponse) + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + let body = Body(code: 42, message: "Foo Bar") + let response = try APIGateway.V2.Response(statusCode: .ok, body: body, encoder: encoder) + XCTAssertEqual(response.body, APIGatewayV2Tests.exampleResponseBody) } }