From 85568a91f33cc9e0924c1e472f09c1e1c54fe900 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 25 Mar 2020 10:22:46 +0100 Subject: [PATCH 1/4] Added ALB and APIGateway Events --- Sources/AWSLambdaEvents/ALB.swift | 210 +++++++++++ Sources/AWSLambdaEvents/APIGateway.swift | 117 ++++++ Sources/AWSLambdaEvents/Utils/HTTP.swift | 343 ++++++++++++++++++ Tests/AWSLambdaEventsTests/ALBTests.swift | 148 ++++++++ .../APIGatewayTests.swift | 91 +++++ 5 files changed, 909 insertions(+) create mode 100644 Sources/AWSLambdaEvents/ALB.swift create mode 100644 Sources/AWSLambdaEvents/APIGateway.swift create mode 100644 Sources/AWSLambdaEvents/Utils/HTTP.swift create mode 100644 Tests/AWSLambdaEventsTests/ALBTests.swift create mode 100644 Tests/AWSLambdaEventsTests/APIGatewayTests.swift diff --git a/Sources/AWSLambdaEvents/ALB.swift b/Sources/AWSLambdaEvents/ALB.swift new file mode 100644 index 00000000..f955447d --- /dev/null +++ b/Sources/AWSLambdaEvents/ALB.swift @@ -0,0 +1,210 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import class Foundation.JSONEncoder + +// https://github.com/aws/aws-lambda-go/blob/master/events/alb.go +public enum 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: - Request - + +extension ALB.TargetGroupRequest: Decodable { + enum CodingKeys: String, CodingKey { + case httpMethod + case path + case queryStringParameters + case multiValueQueryStringParameters + case headers + case multiValueHeaders + case requestContext + case isBase64Encoded + case body + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let rawMethod = try container.decode(String.self, forKey: .httpMethod) + guard let method = HTTPMethod(rawValue: rawMethod) else { + throw DecodingError.dataCorruptedError( + forKey: .httpMethod, + in: container, + debugDescription: #"Method "\#(rawMethod)" does not conform to allowed http method syntax defined in RFC 7230 Section 3.2.6"# + ) + } + self.httpMethod = 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(multiValueHeaders) + } else { + let singleValueHeaders = try container.decode( + [String: String].self, + forKey: .headers + ) + let multiValueHeaders = singleValueHeaders.mapValues { [$0] } + self.headers = HTTPHeaders(multiValueHeaders) + } + + self.requestContext = try container.decode(Context.self, forKey: .requestContext) + self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded) + + let body = try container.decode(String.self, forKey: .body) + self.body = body != "" ? body : nil + } +} + +// MARK: - Response - + +extension ALB.TargetGroupResponse: Encodable { + 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)): + try container.encode(headers.headers, forKey: .multiValueHeaders) + case (false, .some(let headers)): + let singleValueHeaders = headers.headers.mapValues { (values) -> String in + #warning("Is this correct?") + return values.joined(separator: ", ") + } + 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() + if !headers.contains(name: "Content-Type") { + headers.add(name: "Content-Type", value: "application/json") + } + + self.statusCode = statusCode + self.statusDescription = statusDescription + self.headers = headers + + let data = try encoder.encode(payload) + self.body = String(decoding: data, as: Unicode.UTF8.self) + self.isBase64Encoded = false + } + + public init( + statusCode: HTTPResponseStatus, + statusDescription: String? = nil, + headers: HTTPHeaders? = nil, + bytes: [UInt8]? + ) { + let headers = headers ?? HTTPHeaders() + + self.statusCode = statusCode + self.statusDescription = statusDescription + self.headers = headers + if let bytes = bytes { + self.body = String(base64Encoding: bytes) + } else { + self.body = "" + } + self.isBase64Encoded = true + } +} diff --git a/Sources/AWSLambdaEvents/APIGateway.swift b/Sources/AWSLambdaEvents/APIGateway.swift new file mode 100644 index 00000000..ed28909d --- /dev/null +++ b/Sources/AWSLambdaEvents/APIGateway.swift @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import class Foundation.JSONEncoder + +// https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + +public enum APIGateway { + /// APIGatewayRequest contains data coming from the API Gateway + public struct Request { + public struct Context: Codable { + public struct Identity: Codable { + public let cognitoIdentityPoolId: String? + + public let apiKey: String? + public let userArn: String? + public let cognitoAuthenticationType: String? + public let caller: String? + public let userAgent: String? + public let user: String? + + public let cognitoAuthenticationProvider: String? + public let sourceIp: String? + public let accountId: String? + } + + public let resourceId: String + public let apiId: String + public let resourcePath: String + public let httpMethod: String + public let requestId: String + public let accountId: String + public let stage: String + + public let identity: Identity + public let extendedRequestId: String? + public let path: String + } + + public let resource: String + public let path: String + public let httpMethod: HTTPMethod + + public let queryStringParameters: [String: String]? + public let multiValueQueryStringParameters: [String: [String]]? + public let headers: HTTPHeaders + public let pathParameters: [String: String]? + public let stageVariables: [String: String]? + + public let requestContext: Context + public let body: String? + public let isBase64Encoded: Bool + } +} + +// MARK: - Request - + +extension APIGateway.Request: Decodable { + enum CodingKeys: String, CodingKey { + case resource + case path + case httpMethod + + case queryStringParameters + case multiValueQueryStringParameters + case headers = "multiValueHeaders" + case pathParameters + case stageVariables + + case requestContext + case body + case isBase64Encoded + } +} + +// MARK: - Response - + +extension APIGateway { + public struct Response { + public let statusCode: HTTPResponseStatus + public let headers: HTTPHeaders? + public let body: String? + public let isBase64Encoded: Bool? + + public init( + statusCode: HTTPResponseStatus, + headers: HTTPHeaders? = nil, + body: String? = nil, + isBase64Encoded: Bool? = nil + ) { + self.statusCode = statusCode + self.headers = headers + self.body = body + self.isBase64Encoded = isBase64Encoded + } + } +} + +extension APIGateway.Response: Encodable { + enum CodingKeys: String, CodingKey { + case statusCode + case headers = "multiValueHeaders" + case body + case isBase64Encoded + } +} diff --git a/Sources/AWSLambdaEvents/Utils/HTTP.swift b/Sources/AWSLambdaEvents/Utils/HTTP.swift new file mode 100644 index 00000000..c9aa7c91 --- /dev/null +++ b/Sources/AWSLambdaEvents/Utils/HTTP.swift @@ -0,0 +1,343 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// MARK: HTTPHeaders + +public struct HTTPHeaders { + internal var headers: [String: [String]] + + init() { + self.headers = [:] + } + + init(_ headers: [String: [String]]) { + self.headers = headers + } + + /// Add a header name/value pair to the block. + /// + /// This method is strictly additive: if there are other values for the given header name + /// already in the block, this will add a new entry. + /// + /// - Parameter name: The header field name. For maximum compatibility this should be an + /// ASCII string. For future-proofing with HTTP/2 lowercase header names are strongly + /// recommended. + /// - Parameter value: The header field value to add for the given name. + public mutating func add(name: String, value: String) { + precondition(name.isValidHTTPToken, "name must be a valid RFC 7230 Section 3.2.6 compliant token") + var values = self.headers[name] ?? [] + values.append(value) + self.headers[name] = values + } + + /// Add a sequence of header name/value pairs to the block. + /// + /// This method is strictly additive: if there are other entries with the same header + /// name already in the block, this will add new entries. + /// + /// - Parameter contentsOf: The sequence of header name/value pairs. For maximum compatibility + /// the header should be an ASCII string. For future-proofing with HTTP/2 lowercase header + /// names are strongly recommended. + // @inlinable + // public mutating func add(contentsOf other: S) where S.Element == (String, String) { +// self.headers.reserveCapacity(self.headers.count + other.underestimatedCount) +// for (name, value) in other { +// self.add(name: name, value: value) +// } + // } + + /// Add another block of headers to the block. + /// + /// - Parameter contentsOf: The block of headers to add to these headers. + // public mutating func add(contentsOf other: HTTPHeaders) { +// self.headers.append(contentsOf: other.headers) +// if other.keepAliveState == .unknown { +// self.keepAliveState = .unknown +// } + // } + + /// Add a header name/value pair to the block, replacing any previous values for the + /// same header name that are already in the block. + /// + /// This is a supplemental method to `add` that essentially combines `remove` and `add` + /// in a single function. It can be used to ensure that a header block is in a + /// well-defined form without having to check whether the value was previously there. + /// Like `add`, this method performs case-insensitive comparisons of the header field + /// names. + /// + /// - Parameter name: The header field name. For maximum compatibility this should be an + /// ASCII string. For future-proofing with HTTP/2 lowercase header names are strongly + // recommended. + /// - Parameter value: The header field value to add for the given name. + public mutating func replaceOrAdd(name: String, value: String) { + precondition(name.isValidHTTPToken, "name must be a valid RFC 7230 Section 3.2.6 compliant token") + self.headers[name] = [value] + } + + /// Remove all values for a given header name from the block. + /// + /// This method uses case-insensitive comparisons for the header field name. + /// + /// - Parameter name: The name of the header field to remove from the block. + public mutating func remove(name nameToRemove: String) { + self.headers[nameToRemove] = nil + } + + /// Retrieve all of the values for a give header field name from the block. + /// + /// This method uses case-insensitive comparisons for the header field name. It + /// does not return a maximally-decomposed list of the header fields, but instead + /// returns them in their original representation: that means that a comma-separated + /// header field list may contain more than one entry, some of which contain commas + /// and some do not. If you want a representation of the header fields suitable for + /// performing computation on, consider `subscript(canonicalForm:)`. + /// + /// - Parameter name: The header field name whose values are to be retrieved. + /// - Returns: A list of the values for that header field name. + public subscript(name: String) -> [String] { + self.headers[name] ?? [] + } + + /// Retrieves the first value for a given header field name from the block. + /// + /// This method uses case-insensitive comparisons for the header field name. It + /// does not return the first value from a maximally-decomposed list of the header fields, + /// but instead returns the first value from the original representation: that means + /// that a comma-separated header field list may contain more than one entry, some of + /// which contain commas and some do not. If you want a representation of the header fields + /// suitable for performing computation on, consider `subscript(canonicalForm:)`. + /// + /// - Parameter name: The header field name whose first value should be retrieved. + /// - Returns: The first value for the header field name. + public func first(name: String) -> String? { + self.headers[name]?.first + } + + /// Checks if a header is present + /// + /// - parameters: + /// - name: The name of the header + // - returns: `true` if a header with the name (and value) exists, `false` otherwise. + public func contains(name: String) -> Bool { + guard let values = self.headers[name], values.count > 0 else { + return false + } + return true + } + + /// Retrieves the header values for the given header field in "canonical form": that is, + /// splitting them on commas as extensively as possible such that multiple values received on the + /// one line are returned as separate entries. Also respects the fact that Set-Cookie should not + /// be split in this way. + /// + /// - Parameter name: The header field name whose values are to be retrieved. + /// - Returns: A list of the values for that header field name. + // public subscript(canonicalForm name: String) -> [Substring] { +// let result = self[name] +// +// guard result.count > 0 else { +// return [] +// } +// +// // It's not safe to split Set-Cookie on comma. +// guard name.lowercased() != "set-cookie" else { +// return result.map { $0[...] } +// } +// +// return result.flatMap { $0.split(separator: ",").map { $0.trimWhitespace() } } + // } +} + +extension HTTPHeaders: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.headers = try container.decode([String: [String]].self) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.headers) + } +} + +// MARK: HTTPMethod + +public struct HTTPMethod: RawRepresentable, Equatable { + public var rawValue: String + + public init?(rawValue: String) { + guard rawValue.isValidHTTPToken else { + return nil + } + self.rawValue = rawValue + } + + public static var GET: HTTPMethod { HTTPMethod(rawValue: "GET")! } + public static var POST: HTTPMethod { HTTPMethod(rawValue: "POST")! } + public static var PUT: HTTPMethod { HTTPMethod(rawValue: "PUT")! } + public static var PATCH: HTTPMethod { HTTPMethod(rawValue: "PATCH")! } + public static var DELETE: HTTPMethod { HTTPMethod(rawValue: "DELETE")! } + public static var OPTIONS: HTTPMethod { HTTPMethod(rawValue: "OPTIONS")! } + public static var HEAD: HTTPMethod { HTTPMethod(rawValue: "HEAD")! } + + public static func RAW(value: String) -> HTTPMethod? { HTTPMethod(rawValue: value) } +} + +extension HTTPMethod: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawMethod = try container.decode(String.self) + + guard let method = HTTPMethod(rawValue: rawMethod) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: #"Method "\#(rawMethod)" does not conform to allowed http method syntax defined in RFC 7230 Section 3.2.6"# + ) + } + + self = method + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawValue) + } +} + +// MARK: HTTPResponseStatus + +public struct HTTPResponseStatus { + public let code: UInt + public let reasonPhrase: String? + + public init(code: UInt, reasonPhrase: String? = nil) { + self.code = code + self.reasonPhrase = reasonPhrase + } + + public static var `continue`: HTTPResponseStatus { HTTPResponseStatus(code: 100) } + public static var switchingProtocols: HTTPResponseStatus { HTTPResponseStatus(code: 101) } + public static var processing: HTTPResponseStatus { HTTPResponseStatus(code: 102) } + public static var earlyHints: HTTPResponseStatus { HTTPResponseStatus(code: 103) } + + public static var ok: HTTPResponseStatus { HTTPResponseStatus(code: 200) } + public static var created: HTTPResponseStatus { HTTPResponseStatus(code: 201) } + public static var accepted: HTTPResponseStatus { HTTPResponseStatus(code: 202) } + public static var nonAuthoritativeInformation: HTTPResponseStatus { HTTPResponseStatus(code: 203) } + public static var noContent: HTTPResponseStatus { HTTPResponseStatus(code: 204) } + public static var resetContent: HTTPResponseStatus { HTTPResponseStatus(code: 205) } + public static var partialContent: HTTPResponseStatus { HTTPResponseStatus(code: 206) } + public static var multiStatus: HTTPResponseStatus { HTTPResponseStatus(code: 207) } + public static var alreadyReported: HTTPResponseStatus { HTTPResponseStatus(code: 208) } + public static var imUsed: HTTPResponseStatus { HTTPResponseStatus(code: 226) } + + public static var multipleChoices: HTTPResponseStatus { HTTPResponseStatus(code: 300) } + public static var movedPermanently: HTTPResponseStatus { HTTPResponseStatus(code: 301) } + public static var found: HTTPResponseStatus { HTTPResponseStatus(code: 302) } + public static var seeOther: HTTPResponseStatus { HTTPResponseStatus(code: 303) } + public static var notModified: HTTPResponseStatus { HTTPResponseStatus(code: 304) } + public static var useProxy: HTTPResponseStatus { HTTPResponseStatus(code: 305) } + public static var temporaryRedirect: HTTPResponseStatus { HTTPResponseStatus(code: 307) } + public static var permanentRedirect: HTTPResponseStatus { HTTPResponseStatus(code: 308) } + + public static var badRequest: HTTPResponseStatus { HTTPResponseStatus(code: 400) } + public static var unauthorized: HTTPResponseStatus { HTTPResponseStatus(code: 401) } + public static var paymentRequired: HTTPResponseStatus { HTTPResponseStatus(code: 402) } + public static var forbidden: HTTPResponseStatus { HTTPResponseStatus(code: 403) } + public static var notFound: HTTPResponseStatus { HTTPResponseStatus(code: 404) } + public static var methodNotAllowed: HTTPResponseStatus { HTTPResponseStatus(code: 405) } + public static var notAcceptable: HTTPResponseStatus { HTTPResponseStatus(code: 406) } + public static var proxyAuthenticationRequired: HTTPResponseStatus { HTTPResponseStatus(code: 407) } + public static var requestTimeout: HTTPResponseStatus { HTTPResponseStatus(code: 408) } + public static var conflict: HTTPResponseStatus { HTTPResponseStatus(code: 409) } + public static var gone: HTTPResponseStatus { HTTPResponseStatus(code: 410) } + public static var lengthRequired: HTTPResponseStatus { HTTPResponseStatus(code: 411) } + public static var preconditionFailed: HTTPResponseStatus { HTTPResponseStatus(code: 412) } + public static var payloadTooLarge: HTTPResponseStatus { HTTPResponseStatus(code: 413) } + public static var uriTooLong: HTTPResponseStatus { HTTPResponseStatus(code: 414) } + public static var unsupportedMediaType: HTTPResponseStatus { HTTPResponseStatus(code: 415) } + public static var rangeNotSatisfiable: HTTPResponseStatus { HTTPResponseStatus(code: 416) } + public static var expectationFailed: HTTPResponseStatus { HTTPResponseStatus(code: 417) } + public static var imATeapot: HTTPResponseStatus { HTTPResponseStatus(code: 418) } + public static var misdirectedRequest: HTTPResponseStatus { HTTPResponseStatus(code: 421) } + public static var unprocessableEntity: HTTPResponseStatus { HTTPResponseStatus(code: 422) } + public static var locked: HTTPResponseStatus { HTTPResponseStatus(code: 423) } + public static var failedDependency: HTTPResponseStatus { HTTPResponseStatus(code: 424) } + public static var upgradeRequired: HTTPResponseStatus { HTTPResponseStatus(code: 426) } + public static var preconditionRequired: HTTPResponseStatus { HTTPResponseStatus(code: 428) } + public static var tooManyRequests: HTTPResponseStatus { HTTPResponseStatus(code: 429) } + public static var requestHeaderFieldsTooLarge: HTTPResponseStatus { HTTPResponseStatus(code: 431) } + public static var unavailableForLegalReasons: HTTPResponseStatus { HTTPResponseStatus(code: 451) } + + public static var internalServerError: HTTPResponseStatus { HTTPResponseStatus(code: 500) } + public static var notImplemented: HTTPResponseStatus { HTTPResponseStatus(code: 501) } + public static var badGateway: HTTPResponseStatus { HTTPResponseStatus(code: 502) } + public static var serviceUnavailable: HTTPResponseStatus { HTTPResponseStatus(code: 503) } + public static var gatewayTimeout: HTTPResponseStatus { HTTPResponseStatus(code: 504) } + public static var httpVersionNotSupported: HTTPResponseStatus { HTTPResponseStatus(code: 505) } + public static var variantAlsoNegotiates: HTTPResponseStatus { HTTPResponseStatus(code: 506) } + public static var insufficientStorage: HTTPResponseStatus { HTTPResponseStatus(code: 507) } + public static var loopDetected: HTTPResponseStatus { HTTPResponseStatus(code: 508) } + public static var notExtended: HTTPResponseStatus { HTTPResponseStatus(code: 510) } + public static var networkAuthenticationRequired: HTTPResponseStatus { HTTPResponseStatus(code: 511) } +} + +extension HTTPResponseStatus: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.code == rhs.code + } +} + +extension HTTPResponseStatus: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.code = try container.decode(UInt.self) + self.reasonPhrase = nil + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.code) + } +} + +extension String { + internal var isValidHTTPToken: Bool { + self.utf8.allSatisfy { (char) -> Bool in + switch char { + case UInt8(ascii: "a") ... UInt8(ascii: "z"), + UInt8(ascii: "A") ... UInt8(ascii: "Z"), + UInt8(ascii: "0") ... UInt8(ascii: "9"), + UInt8(ascii: "!"), + UInt8(ascii: "#"), + UInt8(ascii: "$"), + UInt8(ascii: "%"), + UInt8(ascii: "&"), + UInt8(ascii: "'"), + UInt8(ascii: "*"), + UInt8(ascii: "+"), + UInt8(ascii: "-"), + UInt8(ascii: "."), + UInt8(ascii: "^"), + UInt8(ascii: "_"), + UInt8(ascii: "`"), + UInt8(ascii: "|"), + UInt8(ascii: "~"): + return true + default: + return false + } + } + } +} diff --git a/Tests/AWSLambdaEventsTests/ALBTests.swift b/Tests/AWSLambdaEventsTests/ALBTests.swift new file mode 100644 index 00000000..3b35013c --- /dev/null +++ b/Tests/AWSLambdaEventsTests/ALBTests.swift @@ -0,0 +1,148 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import AWSLambdaEvents +import XCTest + +class 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, nil) + 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/AWSLambdaEventsTests/APIGatewayTests.swift b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift new file mode 100644 index 00000000..d4d41264 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import AWSLambdaEvents +import XCTest + +class APIGatewayTests: XCTestCase { + static let exampleGetPayload = """ + {"httpMethod": "GET", "body": null, "resource": "/test", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/test", "httpMethod": "GET", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "Prod", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/test"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Cache-Control": "max-age=0", "Dnt": "1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24", "Sec-Fetch-User": "?1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Sec-Fetch-Site": "none", "Sec-Fetch-Mode": "navigate", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Cache-Control": ["max-age=0"], "Dnt": ["1"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24"], "Sec-Fetch-User": ["?1"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"], "Sec-Fetch-Site": ["none"], "Sec-Fetch-Mode": ["navigate"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "/test", "isBase64Encoded": false} + """ + + static let todoPostPayload = """ + {"httpMethod": "POST", "body": "{\\"title\\":\\"a todo\\"}", "resource": "/todos", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/todos", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "test", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/todos"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Content-Length": "18", "Pragma": "no-cache", "Cache-Control": "no-cache", "Accept": "text/plain, */*; q=0.01", "Origin": "http://todobackend.com", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25", "Dnt": "1", "Content-Type": "application/json", "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Mode": "cors", "Referer": "http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Content-Length": ["18"], "Pragma": ["no-cache"], "Cache-Control": ["no-cache"], "Accept": ["text/plain, */*; q=0.01"], "Origin": ["http://todobackend.com"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25"], "Dnt": ["1"], "Content-Type": ["application/json"], "Sec-Fetch-Site": ["cross-site"], "Sec-Fetch-Mode": ["cors"], "Referer": ["http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "/todos", "isBase64Encoded": false} + """ + + // MARK: - Request - + + // MARK: Decoding + + func testRequestDecodingExampleGetRequest() { + do { + let data = APIGatewayTests.exampleGetPayload.data(using: .utf8)! + let request = try JSONDecoder().decode(APIGateway.Request.self, from: data) + + XCTAssertEqual(request.path, "/test") + XCTAssertEqual(request.httpMethod, .GET) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testRequestDecodingTodoPostRequest() { + struct Todo: Decodable { + let title: String + } + + do { + let data = APIGatewayTests.todoPostPayload.data(using: .utf8)! + let request = try JSONDecoder().decode(APIGateway.Request.self, from: data) + + XCTAssertEqual(request.path, "/todos") + XCTAssertEqual(request.httpMethod, .POST) + +// let todo = try request.decodeBody(Todo.self) +// XCTAssertEqual(todo.title, "a todo") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + // MARK: - Response - + + // MARK: Encoding + + struct JSONResponse: Codable { + let statusCode: UInt + let headers: [String: String]? + let body: String? + let isBase64Encoded: Bool? + } + + func testResponseEncoding() { + let resp = APIGateway.Response( + statusCode: .ok, + headers: HTTPHeaders(["Server": ["Test"]]), + body: "abc123" + ) + + do { + let data = try JSONEncoder().encode(resp) + let json = try JSONDecoder().decode(JSONResponse.self, from: data) + + XCTAssertEqual(json.statusCode, resp.statusCode.code) + XCTAssertEqual(json.body, resp.body) + XCTAssertEqual(json.isBase64Encoded, resp.isBase64Encoded) + } catch { + XCTFail("unexpected error: \(error)") + } + } +} From 39845768de1f93725ab86842996453bce986a7f7 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 22 Apr 2020 21:35:03 +0200 Subject: [PATCH 2/4] Removed all high level stuff. --- Sources/AWSLambdaEvents/ALB.swift | 175 +++--------------- Sources/AWSLambdaEvents/APIGateway.swift | 43 +---- Sources/AWSLambdaEvents/Utils/Base64.swift | 135 -------------- Sources/AWSLambdaEvents/Utils/HTTP.swift | 159 ---------------- Tests/AWSLambdaEventsTests/ALBTests.swift | 88 +-------- .../APIGatewayTests.swift | 3 +- .../Utils/Base64Tests.swift | 26 --- 7 files changed, 34 insertions(+), 595 deletions(-) diff --git a/Sources/AWSLambdaEvents/ALB.swift b/Sources/AWSLambdaEvents/ALB.swift index f955447d..853a76c2 100644 --- a/Sources/AWSLambdaEvents/ALB.swift +++ b/Sources/AWSLambdaEvents/ALB.swift @@ -17,7 +17,7 @@ import class Foundation.JSONEncoder // https://github.com/aws/aws-lambda-go/blob/master/events/alb.go public enum ALB { /// ALBTargetGroupRequest contains data originating from the ALB Lambda target group integration - public struct TargetGroupRequest { + public struct TargetGroupRequest: Codable { /// ALBTargetGroupRequestContext contains the information to identify the load balancer invoking the lambda public struct Context: Codable { public let elb: ELBContext @@ -26,7 +26,20 @@ public enum ALB { public let httpMethod: HTTPMethod public let path: String public let queryStringParameters: [String: [String]] - public let headers: HTTPHeaders + + /// Depending on your configuration of your target group either `headers` or `multiValueHeaders` + /// are set. + /// + /// For more information visit: + /// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers + public let headers: [String: String]? + + /// Depending on your configuration of your target group either `headers` or `multiValueHeaders` + /// are set. + /// + /// For more information visit: + /// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers + public let multiValueHeaders: [String: [String]]? public let requestContext: Context public let isBase64Encoded: Bool public let body: String? @@ -37,174 +50,28 @@ public enum ALB { public let targetGroupArn: String } - public struct TargetGroupResponse { + public struct TargetGroupResponse: Codable { public let statusCode: HTTPResponseStatus public let statusDescription: String? - public let headers: HTTPHeaders? + public let headers: [String: String]? + public let multiValueHeaders: [String: [String]]? public let body: String public let isBase64Encoded: Bool public init( statusCode: HTTPResponseStatus, statusDescription: String? = nil, - headers: HTTPHeaders? = nil, + headers: [String: String]? = nil, + multiValueHeaders: [String: [String]]? = nil, body: String = "", isBase64Encoded: Bool = false ) { self.statusCode = statusCode self.statusDescription = statusDescription self.headers = headers + self.multiValueHeaders = multiValueHeaders self.body = body self.isBase64Encoded = isBase64Encoded } } } - -// MARK: - Request - - -extension ALB.TargetGroupRequest: Decodable { - enum CodingKeys: String, CodingKey { - case httpMethod - case path - case queryStringParameters - case multiValueQueryStringParameters - case headers - case multiValueHeaders - case requestContext - case isBase64Encoded - case body - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let rawMethod = try container.decode(String.self, forKey: .httpMethod) - guard let method = HTTPMethod(rawValue: rawMethod) else { - throw DecodingError.dataCorruptedError( - forKey: .httpMethod, - in: container, - debugDescription: #"Method "\#(rawMethod)" does not conform to allowed http method syntax defined in RFC 7230 Section 3.2.6"# - ) - } - self.httpMethod = 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(multiValueHeaders) - } else { - let singleValueHeaders = try container.decode( - [String: String].self, - forKey: .headers - ) - let multiValueHeaders = singleValueHeaders.mapValues { [$0] } - self.headers = HTTPHeaders(multiValueHeaders) - } - - self.requestContext = try container.decode(Context.self, forKey: .requestContext) - self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded) - - let body = try container.decode(String.self, forKey: .body) - self.body = body != "" ? body : nil - } -} - -// MARK: - Response - - -extension ALB.TargetGroupResponse: Encodable { - 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)): - try container.encode(headers.headers, forKey: .multiValueHeaders) - case (false, .some(let headers)): - let singleValueHeaders = headers.headers.mapValues { (values) -> String in - #warning("Is this correct?") - return values.joined(separator: ", ") - } - 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() - if !headers.contains(name: "Content-Type") { - headers.add(name: "Content-Type", value: "application/json") - } - - self.statusCode = statusCode - self.statusDescription = statusDescription - self.headers = headers - - let data = try encoder.encode(payload) - self.body = String(decoding: data, as: Unicode.UTF8.self) - self.isBase64Encoded = false - } - - public init( - statusCode: HTTPResponseStatus, - statusDescription: String? = nil, - headers: HTTPHeaders? = nil, - bytes: [UInt8]? - ) { - let headers = headers ?? HTTPHeaders() - - self.statusCode = statusCode - self.statusDescription = statusDescription - self.headers = headers - if let bytes = bytes { - self.body = String(base64Encoding: bytes) - } else { - self.body = "" - } - self.isBase64Encoded = true - } -} diff --git a/Sources/AWSLambdaEvents/APIGateway.swift b/Sources/AWSLambdaEvents/APIGateway.swift index ed28909d..e866c747 100644 --- a/Sources/AWSLambdaEvents/APIGateway.swift +++ b/Sources/AWSLambdaEvents/APIGateway.swift @@ -18,7 +18,7 @@ import class Foundation.JSONEncoder public enum APIGateway { /// APIGatewayRequest contains data coming from the API Gateway - public struct Request { + public struct Request: Codable { public struct Context: Codable { public struct Identity: Codable { public let cognitoIdentityPoolId: String? @@ -54,7 +54,8 @@ public enum APIGateway { public let queryStringParameters: [String: String]? public let multiValueQueryStringParameters: [String: [String]]? - public let headers: HTTPHeaders + public let headers: [String: String] + public let multiValueHeaders: [String: [String]] public let pathParameters: [String: String]? public let stageVariables: [String: String]? @@ -64,54 +65,28 @@ public enum APIGateway { } } -// MARK: - Request - - -extension APIGateway.Request: Decodable { - enum CodingKeys: String, CodingKey { - case resource - case path - case httpMethod - - case queryStringParameters - case multiValueQueryStringParameters - case headers = "multiValueHeaders" - case pathParameters - case stageVariables - - case requestContext - case body - case isBase64Encoded - } -} - // MARK: - Response - extension APIGateway { - public struct Response { + public struct Response: Codable { public let statusCode: HTTPResponseStatus - public let headers: HTTPHeaders? + public let headers: [String: String]? + public let multiValueHeaders: [String: [String]]? public let body: String? public let isBase64Encoded: Bool? public init( statusCode: HTTPResponseStatus, - headers: HTTPHeaders? = nil, + headers: [String: String]? = nil, + multiValueHeaders: [String: [String]]? = nil, body: String? = nil, isBase64Encoded: Bool? = nil ) { self.statusCode = statusCode self.headers = headers + self.multiValueHeaders = multiValueHeaders self.body = body self.isBase64Encoded = isBase64Encoded } } } - -extension APIGateway.Response: Encodable { - enum CodingKeys: String, CodingKey { - case statusCode - case headers = "multiValueHeaders" - case body - case isBase64Encoded - } -} diff --git a/Sources/AWSLambdaEvents/Utils/Base64.swift b/Sources/AWSLambdaEvents/Utils/Base64.swift index 66f16080..310a6aa5 100644 --- a/Sources/AWSLambdaEvents/Utils/Base64.swift +++ b/Sources/AWSLambdaEvents/Utils/Base64.swift @@ -18,136 +18,6 @@ struct Base64 {} -// MARK: Encoding - -extension Base64 { - struct EncodingOptions: OptionSet { - let rawValue: UInt - init(rawValue: UInt) { self.rawValue = rawValue } - - static let base64UrlAlphabet = EncodingOptions(rawValue: UInt(1 << 0)) - } - - /// Base64 encode a collection of UInt8 to a string, without the use of Foundation. - /// - /// This function performs the world's most naive Base64 encoding: no attempts to use a larger - /// lookup table or anything intelligent like that, just shifts and masks. This works fine, for - /// now: the purpose of this encoding is to avoid round-tripping through Data, and the perf gain - /// from avoiding that is more than enough to outweigh the silliness of this code. - @inline(__always) - static func encode(bytes: Buffer, options: EncodingOptions = []) - -> String where Buffer.Element == UInt8 { - // In Base64, 3 bytes become 4 output characters, and we pad to the - // nearest multiple of four. - let newCapacity = ((bytes.count + 2) / 3) * 4 - let alphabet = options.contains(.base64UrlAlphabet) - ? Base64.encodeBase64Url - : Base64.encodeBase64 - - var outputBytes = [UInt8]() - outputBytes.reserveCapacity(newCapacity) - - var input = bytes.makeIterator() - - while let firstByte = input.next() { - let secondByte = input.next() - let thirdByte = input.next() - - let firstChar = Base64.encode(alphabet: alphabet, firstByte: firstByte) - let secondChar = Base64.encode(alphabet: alphabet, firstByte: firstByte, secondByte: secondByte) - let thirdChar = Base64.encode(alphabet: alphabet, secondByte: secondByte, thirdByte: thirdByte) - let forthChar = Base64.encode(alphabet: alphabet, thirdByte: thirdByte) - - outputBytes.append(firstChar) - outputBytes.append(secondChar) - outputBytes.append(thirdChar) - outputBytes.append(forthChar) - } - - return String(decoding: outputBytes, as: Unicode.UTF8.self) - } - - // MARK: Internal - - // The base64 unicode table. - @usableFromInline - static let encodeBase64: [UInt8] = [ - UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), - UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), - UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"), - UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"), - UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"), - UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"), - UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"), - UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"), - UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"), - UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"), - UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"), - UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"), - UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"), - UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), - UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), - UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "+"), UInt8(ascii: "/"), - ] - - @usableFromInline - static let encodeBase64Url: [UInt8] = [ - UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), - UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), - UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"), - UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"), - UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"), - UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"), - UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"), - UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"), - UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"), - UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"), - UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"), - UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"), - UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"), - UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), - UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), - UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "-"), UInt8(ascii: "_"), - ] - - static let encodePaddingCharacter: UInt8 = 61 - - static func encode(alphabet: [UInt8], firstByte: UInt8) -> UInt8 { - let index = firstByte >> 2 - return alphabet[Int(index)] - } - - static func encode(alphabet: [UInt8], firstByte: UInt8, secondByte: UInt8?) -> UInt8 { - var index = (firstByte & 0b0000_0011) << 4 - if let secondByte = secondByte { - index += (secondByte & 0b1111_0000) >> 4 - } - return alphabet[Int(index)] - } - - static func encode(alphabet: [UInt8], secondByte: UInt8?, thirdByte: UInt8?) -> UInt8 { - guard let secondByte = secondByte else { - // No second byte means we are just emitting padding. - return Base64.encodePaddingCharacter - } - var index = (secondByte & 0b0000_1111) << 2 - if let thirdByte = thirdByte { - index += (thirdByte & 0b1100_0000) >> 6 - } - return alphabet[Int(index)] - } - - @inlinable - static func encode(alphabet: [UInt8], thirdByte: UInt8?) -> UInt8 { - guard let thirdByte = thirdByte else { - // No third byte means just padding. - return Base64.encodePaddingCharacter - } - let index = thirdByte & 0b0011_1111 - return alphabet[Int(index)] - } -} - // MARK: - Decode - extension Base64 { @@ -341,11 +211,6 @@ extension IteratorProtocol where Self.Element == UInt8 { // MARK: - Extensions - extension String { - init(base64Encoding bytes: Buffer, options: Base64.EncodingOptions = []) - where Buffer.Element == UInt8 { - self = Base64.encode(bytes: bytes, options: options) - } - func base64decoded(options: Base64.DecodingOptions = []) throws -> [UInt8] { // In Base64, 3 bytes become 4 output characters, and we pad to the nearest multiple // of four. diff --git a/Sources/AWSLambdaEvents/Utils/HTTP.swift b/Sources/AWSLambdaEvents/Utils/HTTP.swift index c9aa7c91..455324f5 100644 --- a/Sources/AWSLambdaEvents/Utils/HTTP.swift +++ b/Sources/AWSLambdaEvents/Utils/HTTP.swift @@ -12,165 +12,6 @@ // //===----------------------------------------------------------------------===// -// MARK: HTTPHeaders - -public struct HTTPHeaders { - internal var headers: [String: [String]] - - init() { - self.headers = [:] - } - - init(_ headers: [String: [String]]) { - self.headers = headers - } - - /// Add a header name/value pair to the block. - /// - /// This method is strictly additive: if there are other values for the given header name - /// already in the block, this will add a new entry. - /// - /// - Parameter name: The header field name. For maximum compatibility this should be an - /// ASCII string. For future-proofing with HTTP/2 lowercase header names are strongly - /// recommended. - /// - Parameter value: The header field value to add for the given name. - public mutating func add(name: String, value: String) { - precondition(name.isValidHTTPToken, "name must be a valid RFC 7230 Section 3.2.6 compliant token") - var values = self.headers[name] ?? [] - values.append(value) - self.headers[name] = values - } - - /// Add a sequence of header name/value pairs to the block. - /// - /// This method is strictly additive: if there are other entries with the same header - /// name already in the block, this will add new entries. - /// - /// - Parameter contentsOf: The sequence of header name/value pairs. For maximum compatibility - /// the header should be an ASCII string. For future-proofing with HTTP/2 lowercase header - /// names are strongly recommended. - // @inlinable - // public mutating func add(contentsOf other: S) where S.Element == (String, String) { -// self.headers.reserveCapacity(self.headers.count + other.underestimatedCount) -// for (name, value) in other { -// self.add(name: name, value: value) -// } - // } - - /// Add another block of headers to the block. - /// - /// - Parameter contentsOf: The block of headers to add to these headers. - // public mutating func add(contentsOf other: HTTPHeaders) { -// self.headers.append(contentsOf: other.headers) -// if other.keepAliveState == .unknown { -// self.keepAliveState = .unknown -// } - // } - - /// Add a header name/value pair to the block, replacing any previous values for the - /// same header name that are already in the block. - /// - /// This is a supplemental method to `add` that essentially combines `remove` and `add` - /// in a single function. It can be used to ensure that a header block is in a - /// well-defined form without having to check whether the value was previously there. - /// Like `add`, this method performs case-insensitive comparisons of the header field - /// names. - /// - /// - Parameter name: The header field name. For maximum compatibility this should be an - /// ASCII string. For future-proofing with HTTP/2 lowercase header names are strongly - // recommended. - /// - Parameter value: The header field value to add for the given name. - public mutating func replaceOrAdd(name: String, value: String) { - precondition(name.isValidHTTPToken, "name must be a valid RFC 7230 Section 3.2.6 compliant token") - self.headers[name] = [value] - } - - /// Remove all values for a given header name from the block. - /// - /// This method uses case-insensitive comparisons for the header field name. - /// - /// - Parameter name: The name of the header field to remove from the block. - public mutating func remove(name nameToRemove: String) { - self.headers[nameToRemove] = nil - } - - /// Retrieve all of the values for a give header field name from the block. - /// - /// This method uses case-insensitive comparisons for the header field name. It - /// does not return a maximally-decomposed list of the header fields, but instead - /// returns them in their original representation: that means that a comma-separated - /// header field list may contain more than one entry, some of which contain commas - /// and some do not. If you want a representation of the header fields suitable for - /// performing computation on, consider `subscript(canonicalForm:)`. - /// - /// - Parameter name: The header field name whose values are to be retrieved. - /// - Returns: A list of the values for that header field name. - public subscript(name: String) -> [String] { - self.headers[name] ?? [] - } - - /// Retrieves the first value for a given header field name from the block. - /// - /// This method uses case-insensitive comparisons for the header field name. It - /// does not return the first value from a maximally-decomposed list of the header fields, - /// but instead returns the first value from the original representation: that means - /// that a comma-separated header field list may contain more than one entry, some of - /// which contain commas and some do not. If you want a representation of the header fields - /// suitable for performing computation on, consider `subscript(canonicalForm:)`. - /// - /// - Parameter name: The header field name whose first value should be retrieved. - /// - Returns: The first value for the header field name. - public func first(name: String) -> String? { - self.headers[name]?.first - } - - /// Checks if a header is present - /// - /// - parameters: - /// - name: The name of the header - // - returns: `true` if a header with the name (and value) exists, `false` otherwise. - public func contains(name: String) -> Bool { - guard let values = self.headers[name], values.count > 0 else { - return false - } - return true - } - - /// Retrieves the header values for the given header field in "canonical form": that is, - /// splitting them on commas as extensively as possible such that multiple values received on the - /// one line are returned as separate entries. Also respects the fact that Set-Cookie should not - /// be split in this way. - /// - /// - Parameter name: The header field name whose values are to be retrieved. - /// - Returns: A list of the values for that header field name. - // public subscript(canonicalForm name: String) -> [Substring] { -// let result = self[name] -// -// guard result.count > 0 else { -// return [] -// } -// -// // It's not safe to split Set-Cookie on comma. -// guard name.lowercased() != "set-cookie" else { -// return result.map { $0[...] } -// } -// -// return result.flatMap { $0.split(separator: ",").map { $0.trimWhitespace() } } - // } -} - -extension HTTPHeaders: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - self.headers = try container.decode([String: [String]].self) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(self.headers) - } -} - // MARK: HTTPMethod public struct HTTPMethod: RawRepresentable, Equatable { diff --git a/Tests/AWSLambdaEventsTests/ALBTests.swift b/Tests/AWSLambdaEventsTests/ALBTests.swift index 3b35013c..398e32ed 100644 --- a/Tests/AWSLambdaEventsTests/ALBTests.swift +++ b/Tests/AWSLambdaEventsTests/ALBTests.swift @@ -52,97 +52,13 @@ class ALBTests: XCTestCase { let event = try decoder.decode(ALB.TargetGroupRequest.self, from: data) XCTAssertEqual(event.httpMethod, .GET) - XCTAssertEqual(event.body, nil) + XCTAssertEqual(event.body, "") XCTAssertEqual(event.isBase64Encoded, false) -// XCTAssertEqual(event.headers.count, 11) + 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/AWSLambdaEventsTests/APIGatewayTests.swift b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift index d4d41264..19e7d46a 100644 --- a/Tests/AWSLambdaEventsTests/APIGatewayTests.swift +++ b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift @@ -73,7 +73,7 @@ class APIGatewayTests: XCTestCase { func testResponseEncoding() { let resp = APIGateway.Response( statusCode: .ok, - headers: HTTPHeaders(["Server": ["Test"]]), + headers: ["Server": "Test"], body: "abc123" ) @@ -84,6 +84,7 @@ class APIGatewayTests: XCTestCase { XCTAssertEqual(json.statusCode, resp.statusCode.code) XCTAssertEqual(json.body, resp.body) XCTAssertEqual(json.isBase64Encoded, resp.isBase64Encoded) + XCTAssertEqual(json.headers?["Server"], "Test") } catch { XCTFail("unexpected error: \(error)") } diff --git a/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift b/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift index 8e3c8695..59e300c1 100644 --- a/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift +++ b/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift @@ -16,32 +16,6 @@ import XCTest class Base64Tests: XCTestCase { - // MARK: - Encoding - - - func testEncodeEmptyData() throws { - let data = [UInt8]() - let encodedData = String(base64Encoding: data) - XCTAssertEqual(encodedData.count, 0) - } - - func testBase64EncodingArrayOfNulls() throws { - let data = Array(repeating: UInt8(0), count: 10) - let encodedData = String(base64Encoding: data) - XCTAssertEqual(encodedData, "AAAAAAAAAAAAAA==") - } - - func testBase64EncodingAllTheBytesSequentially() throws { - let data = Array(UInt8(0) ... UInt8(255)) - let encodedData = String(base64Encoding: data) - XCTAssertEqual(encodedData, "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==") - } - - func testBase64UrlEncodingAllTheBytesSequentially() throws { - let data = Array(UInt8(0) ... UInt8(255)) - let encodedData = String(base64Encoding: data, options: .base64UrlAlphabet) - XCTAssertEqual(encodedData, "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w==") - } - // MARK: - Decoding - func testDecodeEmptyString() throws { From 59e9d8b133b8f715d3c6a54e91c385260d62a639 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 22 Apr 2020 23:46:11 +0200 Subject: [PATCH 3/4] MultiValueHeaders and Headers tyealias --- Sources/AWSLambdaEvents/ALB.swift | 12 ++++++------ Sources/AWSLambdaEvents/APIGateway.swift | 12 ++++++------ Sources/AWSLambdaEvents/Utils/HTTP.swift | 3 +++ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Sources/AWSLambdaEvents/ALB.swift b/Sources/AWSLambdaEvents/ALB.swift index 853a76c2..121531a6 100644 --- a/Sources/AWSLambdaEvents/ALB.swift +++ b/Sources/AWSLambdaEvents/ALB.swift @@ -32,14 +32,14 @@ public enum ALB { /// /// For more information visit: /// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers - public let headers: [String: String]? + public let headers: HTTPHeaders? /// Depending on your configuration of your target group either `headers` or `multiValueHeaders` /// are set. /// /// For more information visit: /// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers - public let multiValueHeaders: [String: [String]]? + public let multiValueHeaders: HTTPMultiValueHeaders? public let requestContext: Context public let isBase64Encoded: Bool public let body: String? @@ -53,16 +53,16 @@ public enum ALB { public struct TargetGroupResponse: Codable { public let statusCode: HTTPResponseStatus public let statusDescription: String? - public let headers: [String: String]? - public let multiValueHeaders: [String: [String]]? + public let headers: HTTPHeaders? + public let multiValueHeaders: HTTPMultiValueHeaders? public let body: String public let isBase64Encoded: Bool public init( statusCode: HTTPResponseStatus, statusDescription: String? = nil, - headers: [String: String]? = nil, - multiValueHeaders: [String: [String]]? = nil, + headers: HTTPHeaders? = nil, + multiValueHeaders: HTTPMultiValueHeaders? = nil, body: String = "", isBase64Encoded: Bool = false ) { diff --git a/Sources/AWSLambdaEvents/APIGateway.swift b/Sources/AWSLambdaEvents/APIGateway.swift index e866c747..948aca25 100644 --- a/Sources/AWSLambdaEvents/APIGateway.swift +++ b/Sources/AWSLambdaEvents/APIGateway.swift @@ -54,8 +54,8 @@ public enum APIGateway { 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 multiValueHeaders: HTTPMultiValueHeaders public let pathParameters: [String: String]? public let stageVariables: [String: String]? @@ -70,15 +70,15 @@ public enum APIGateway { extension APIGateway { public struct Response: Codable { public let statusCode: HTTPResponseStatus - public let headers: [String: String]? - public let multiValueHeaders: [String: [String]]? + public let headers: HTTPHeaders? + public let multiValueHeaders: HTTPMultiValueHeaders? public let body: String? public let isBase64Encoded: Bool? public init( statusCode: HTTPResponseStatus, - headers: [String: String]? = nil, - multiValueHeaders: [String: [String]]? = nil, + headers: HTTPHeaders? = nil, + multiValueHeaders: HTTPMultiValueHeaders? = nil, body: String? = nil, isBase64Encoded: Bool? = nil ) { diff --git a/Sources/AWSLambdaEvents/Utils/HTTP.swift b/Sources/AWSLambdaEvents/Utils/HTTP.swift index 455324f5..9e0d8f2d 100644 --- a/Sources/AWSLambdaEvents/Utils/HTTP.swift +++ b/Sources/AWSLambdaEvents/Utils/HTTP.swift @@ -14,6 +14,9 @@ // MARK: HTTPMethod +public typealias HTTPHeaders = [String: String] +public typealias HTTPMultiValueHeaders = [String: [String]] + public struct HTTPMethod: RawRepresentable, Equatable { public var rawValue: String From cf2bc9a6062948c314ae6509e78c64b7c611726a Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 23 Apr 2020 13:15:59 +0200 Subject: [PATCH 4/4] Added APIGateway V2 --- Sources/AWSLambdaEvents/APIGateway+V2.swift | 119 ++++++++++++++++++ .../APIGateway+V2Tests.swift | 91 ++++++++++++++ .../APIGatewayTests.swift | 51 +++----- 3 files changed, 228 insertions(+), 33 deletions(-) create mode 100644 Sources/AWSLambdaEvents/APIGateway+V2.swift create mode 100644 Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift diff --git a/Sources/AWSLambdaEvents/APIGateway+V2.swift b/Sources/AWSLambdaEvents/APIGateway+V2.swift new file mode 100644 index 00000000..12c4c2ce --- /dev/null +++ b/Sources/AWSLambdaEvents/APIGateway+V2.swift @@ -0,0 +1,119 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + public struct V2 {} +} + +extension APIGateway.V2 { + /// APIGateway.V2.Request contains data coming from the new HTTP API Gateway + public struct Request: 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: String? + 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 Response: Codable { + public let statusCode: HTTPResponseStatus + public let headers: HTTPHeaders? + public let multiValueHeaders: HTTPMultiValueHeaders? + public let body: String? + public let isBase64Encoded: Bool? + public let cookies: [String]? + + public init( + statusCode: HTTPResponseStatus, + headers: HTTPHeaders? = nil, + multiValueHeaders: HTTPMultiValueHeaders? = nil, + body: String? = 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/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift new file mode 100644 index 00000000..c48d9dc0 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import AWSLambdaEvents +import XCTest + +class APIGatewayV2Tests: XCTestCase { + static let exampleGetPayload = """ + { + "routeKey":"GET /hello", + "version":"2.0", + "rawPath":"/hello", + "stageVariables":{ + "foo":"bar" + }, + "requestContext":{ + "timeEpoch":1587750461466, + "domainPrefix":"hello", + "authorizer":{ + "jwt":{ + "scopes":[ + "hello" + ], + "claims":{ + "aud":"customers", + "iss":"https://hello.test.com/", + "iat":"1587749276", + "exp":"1587756476" + } + } + }, + "accountId":"0123456789", + "stage":"$default", + "domainName":"hello.test.com", + "apiId":"pb5dg6g3rg", + "requestId":"LgLpnibOFiAEPCA=", + "http":{ + "path":"/hello", + "userAgent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", + "method":"GET", + "protocol":"HTTP/1.1", + "sourceIp":"91.64.117.86" + }, + "time":"24/Apr/2020:17:47:41 +0000" + }, + "isBase64Encoded":false, + "rawQueryString":"foo=bar", + "queryStringParameters":{ + "foo":"bar" + }, + "headers":{ + "x-forwarded-proto":"https", + "x-forwarded-for":"91.64.117.86", + "x-forwarded-port":"443", + "authorization":"Bearer abc123", + "host":"hello.test.com", + "x-amzn-trace-id":"Root=1-5ea3263d-07c5d5ddfd0788bed7dad831", + "user-agent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", + "content-length":"0" + } + } + """ + + // MARK: - Request - + + // MARK: Decoding + + func testRequestDecodingExampleGetRequest() { + let data = APIGatewayV2Tests.exampleGetPayload.data(using: .utf8)! + var req: APIGateway.V2.Request? + XCTAssertNoThrow(req = try JSONDecoder().decode(APIGateway.V2.Request.self, from: data)) + + XCTAssertEqual(req?.rawPath, "/hello") + XCTAssertEqual(req?.context.http.method, .GET) + XCTAssertEqual(req?.queryStringParameters?.count, 1) + XCTAssertEqual(req?.rawQueryString, "foo=bar") + XCTAssertEqual(req?.headers.count, 8) + XCTAssertNil(req?.body) + } +} diff --git a/Tests/AWSLambdaEventsTests/APIGatewayTests.swift b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift index 19e7d46a..7a1a7d9e 100644 --- a/Tests/AWSLambdaEventsTests/APIGatewayTests.swift +++ b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift @@ -29,34 +29,21 @@ class APIGatewayTests: XCTestCase { // MARK: Decoding func testRequestDecodingExampleGetRequest() { - do { - let data = APIGatewayTests.exampleGetPayload.data(using: .utf8)! - let request = try JSONDecoder().decode(APIGateway.Request.self, from: data) + let data = APIGatewayTests.exampleGetPayload.data(using: .utf8)! + var req: APIGateway.Request? + XCTAssertNoThrow(req = try JSONDecoder().decode(APIGateway.Request.self, from: data)) - XCTAssertEqual(request.path, "/test") - XCTAssertEqual(request.httpMethod, .GET) - } catch { - XCTFail("Unexpected error: \(error)") - } + XCTAssertEqual(req?.path, "/test") + XCTAssertEqual(req?.httpMethod, .GET) } func testRequestDecodingTodoPostRequest() { - struct Todo: Decodable { - let title: String - } + let data = APIGatewayTests.todoPostPayload.data(using: .utf8)! + var req: APIGateway.Request? + XCTAssertNoThrow(req = try JSONDecoder().decode(APIGateway.Request.self, from: data)) - do { - let data = APIGatewayTests.todoPostPayload.data(using: .utf8)! - let request = try JSONDecoder().decode(APIGateway.Request.self, from: data) - - XCTAssertEqual(request.path, "/todos") - XCTAssertEqual(request.httpMethod, .POST) - -// let todo = try request.decodeBody(Todo.self) -// XCTAssertEqual(todo.title, "a todo") - } catch { - XCTFail("Unexpected error: \(error)") - } + XCTAssertEqual(req?.path, "/todos") + XCTAssertEqual(req?.httpMethod, .POST) } // MARK: - Response - @@ -77,16 +64,14 @@ class APIGatewayTests: XCTestCase { body: "abc123" ) - do { - let data = try JSONEncoder().encode(resp) - let json = try JSONDecoder().decode(JSONResponse.self, from: data) + var data: Data? + XCTAssertNoThrow(data = try JSONEncoder().encode(resp)) + var json: JSONResponse? + XCTAssertNoThrow(json = try JSONDecoder().decode(JSONResponse.self, from: XCTUnwrap(data))) - XCTAssertEqual(json.statusCode, resp.statusCode.code) - XCTAssertEqual(json.body, resp.body) - XCTAssertEqual(json.isBase64Encoded, resp.isBase64Encoded) - XCTAssertEqual(json.headers?["Server"], "Test") - } catch { - XCTFail("unexpected error: \(error)") - } + XCTAssertEqual(json?.statusCode, resp.statusCode.code) + XCTAssertEqual(json?.body, resp.body) + XCTAssertEqual(json?.isBase64Encoded, resp.isBase64Encoded) + XCTAssertEqual(json?.headers?["Server"], "Test") } }