Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom Decodable conformance #76

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions Sources/AWSLambdaEvents/APIGateway+V2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import HTTPTypes

/// `APIGatewayV2Request` contains data coming from the new HTTP API Gateway.
public struct APIGatewayV2Request: Codable, Sendable {
public struct APIGatewayV2Request: Encodable, Sendable {
/// `Context` contains information to identify the AWS account and resources invoking the Lambda function.
public struct Context: Codable, Sendable {
public struct HTTP: Codable, Sendable {
Expand Down Expand Up @@ -96,13 +96,13 @@ public struct APIGatewayV2Request: Codable, Sendable {
public let rawPath: String
public let rawQueryString: String

public let cookies: [String]?
public let cookies: [String]
public let headers: HTTPHeaders
public let queryStringParameters: [String: String]?
public let pathParameters: [String: String]?
public let queryStringParameters: [String: String]
public let pathParameters: [String: String]

public let context: Context
public let stageVariables: [String: String]?
public let stageVariables: [String: String]

public let body: String?
public let isBase64Encoded: Bool
Expand Down Expand Up @@ -147,3 +147,25 @@ public struct APIGatewayV2Response: Codable, Sendable {
self.cookies = cookies
}
}

extension APIGatewayV2Request: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.version = try container.decode(String.self, forKey: .version)
self.routeKey = try container.decode(String.self, forKey: .routeKey)
self.rawPath = try container.decode(String.self, forKey: .rawPath)
self.rawQueryString = try container.decode(String.self, forKey: .rawQueryString)

self.cookies = try container.decodeIfPresent([String].self, forKey: .cookies) ?? []
self.headers = try container.decodeIfPresent(HTTPHeaders.self, forKey: .headers) ?? HTTPHeaders()
self.queryStringParameters = try container.decodeIfPresent([String: String].self, forKey: .queryStringParameters) ?? [:]
self.pathParameters = try container.decodeIfPresent([String: String].self, forKey: .pathParameters) ?? [:]

self.context = try container.decode(Context.self, forKey: .context)
self.stageVariables = try container.decodeIfPresent([String: String].self, forKey: .stageVariables) ?? [:]

self.body = try container.decodeIfPresent(String.self, forKey: .body)
self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded)
}
}
31 changes: 26 additions & 5 deletions Sources/AWSLambdaEvents/APIGateway.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Foundation
// https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html

/// `APIGatewayRequest` contains data coming from the API Gateway.
public struct APIGatewayRequest: Codable, Sendable {
public struct APIGatewayRequest: Encodable, Sendable {
public struct Context: Codable, Sendable {
public struct Identity: Codable, Sendable {
public let cognitoIdentityPoolId: String?
Expand Down Expand Up @@ -64,12 +64,12 @@ public struct APIGatewayRequest: Codable, Sendable {
public let path: String
public let httpMethod: HTTPRequest.Method

public let queryStringParameters: [String: String]?
public let multiValueQueryStringParameters: [String: [String]]?
public let queryStringParameters: [String: String]
public let multiValueQueryStringParameters: [String: [String]]
public let headers: HTTPHeaders
public let multiValueHeaders: HTTPMultiValueHeaders
public let pathParameters: [String: String]?
public let stageVariables: [String: String]?
public let pathParameters: [String: String]
public let stageVariables: [String: String]

public let requestContext: Context
public let body: String?
Expand Down Expand Up @@ -99,3 +99,24 @@ public struct APIGatewayResponse: Codable, Sendable {
self.isBase64Encoded = isBase64Encoded
}
}

extension APIGatewayRequest: Decodable {
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.resource = try container.decode(String.self, forKey: .resource)
self.path = try container.decode(String.self, forKey: .path)
self.httpMethod = try container.decode(HTTPRequest.Method.self, forKey: .httpMethod)

self.queryStringParameters = try container.decodeIfPresent([String: String].self, forKey: .queryStringParameters) ?? [:]
self.multiValueQueryStringParameters = try container.decodeIfPresent([String: [String]].self, forKey: .multiValueQueryStringParameters) ?? [:]
self.headers = try container.decodeIfPresent(HTTPHeaders.self, forKey: .headers) ?? HTTPHeaders()
self.multiValueHeaders = try container.decodeIfPresent(HTTPMultiValueHeaders.self, forKey: .multiValueHeaders) ?? HTTPMultiValueHeaders()
self.pathParameters = try container.decodeIfPresent([String: String].self, forKey: .pathParameters) ?? [:]
self.stageVariables = try container.decodeIfPresent([String: String].self, forKey: .stageVariables) ?? [:]

self.requestContext = try container.decode(Context.self, forKey: .requestContext)
self.body = try container.decodeIfPresent(String.self, forKey: .body)
self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded)
}
}
47 changes: 46 additions & 1 deletion Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,46 @@ class APIGatewayV2Tests: XCTestCase {
}
"""

static let exampleGetEventBodyNilHeaders = """
{
"routeKey":"GET /hello",
"version":"2.0",
"rawPath":"/hello",
"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"
}
"""

static let fullExamplePayload = """
{
"version": "2.0",
Expand Down Expand Up @@ -156,7 +196,7 @@ class APIGatewayV2Tests: XCTestCase {

XCTAssertEqual(req?.rawPath, "/hello")
XCTAssertEqual(req?.context.http.method, .get)
XCTAssertEqual(req?.queryStringParameters?.count, 1)
XCTAssertEqual(req?.queryStringParameters.count, 1)
XCTAssertEqual(req?.rawQueryString, "foo=bar")
XCTAssertEqual(req?.headers.count, 8)
XCTAssertEqual(req?.context.authorizer?.jwt?.claims?["aud"], "customers")
Expand All @@ -176,4 +216,9 @@ class APIGatewayV2Tests: XCTestCase {
XCTAssertEqual(clientCert?.validity.notBefore, "May 28 12:30:02 2019 GMT")
XCTAssertEqual(clientCert?.validity.notAfter, "Aug 5 09:36:04 2021 GMT")
}

func testDecodingNilCollections() {
let data = APIGatewayV2Tests.exampleGetEventBodyNilHeaders.data(using: .utf8)!
XCTAssertNoThrow(_ = try JSONDecoder().decode(APIGatewayV2Request.self, from: data))
}
}
9 changes: 9 additions & 0 deletions Tests/AWSLambdaEventsTests/APIGatewayTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class APIGatewayTests: XCTestCase {
{"httpMethod": "POST", "body": "{\\"title\\":\\"a todo\\"}", "resource": "/todos", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "domainName": "1234567890.execute-api.us-east-1.amazonaws.com", "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}
"""

static let postEventBodyNilHeaders = """
{"httpMethod": "POST", "body": "{\\"title\\":\\"a todo\\"}", "resource":"/todos", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "domainName": "1234567890.execute-api.us-east-1.amazonaws.com", "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"}, "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"]}, "path": "/todos", "isBase64Encoded": false}
"""

// MARK: - Request -

// MARK: Decoding
Expand Down Expand Up @@ -108,4 +112,9 @@ class APIGatewayTests: XCTestCase {
XCTAssertEqual(json?.isBase64Encoded, resp.isBase64Encoded)
XCTAssertEqual(json?.headers?["Server"], "Test")
}

func testDecodingNilCollections() {
let data = APIGatewayTests.postEventBodyNilHeaders.data(using: .utf8)!
XCTAssertNoThrow(_ = try JSONDecoder().decode(APIGatewayRequest.self, from: data))
}
}