Skip to content

Commit e34f344

Browse files
committed
Add custom Decodable conformance
1 parent 7bb1b04 commit e34f344

File tree

4 files changed

+108
-11
lines changed

4 files changed

+108
-11
lines changed

Sources/AWSLambdaEvents/APIGateway+V2.swift

+27-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import HTTPTypes
1616

1717
/// `APIGatewayV2Request` contains data coming from the new HTTP API Gateway.
18-
public struct APIGatewayV2Request: Codable, Sendable {
18+
public struct APIGatewayV2Request: Encodable, Sendable {
1919
/// `Context` contains information to identify the AWS account and resources invoking the Lambda function.
2020
public struct Context: Codable, Sendable {
2121
public struct HTTP: Codable, Sendable {
@@ -96,13 +96,13 @@ public struct APIGatewayV2Request: Codable, Sendable {
9696
public let rawPath: String
9797
public let rawQueryString: String
9898

99-
public let cookies: [String]?
99+
public let cookies: [String]
100100
public let headers: HTTPHeaders
101-
public let queryStringParameters: [String: String]?
102-
public let pathParameters: [String: String]?
101+
public let queryStringParameters: [String: String]
102+
public let pathParameters: [String: String]
103103

104104
public let context: Context
105-
public let stageVariables: [String: String]?
105+
public let stageVariables: [String: String]
106106

107107
public let body: String?
108108
public let isBase64Encoded: Bool
@@ -147,3 +147,25 @@ public struct APIGatewayV2Response: Codable, Sendable {
147147
self.cookies = cookies
148148
}
149149
}
150+
151+
extension APIGatewayV2Request: Decodable {
152+
public init(from decoder: Decoder) throws {
153+
let container = try decoder.container(keyedBy: CodingKeys.self)
154+
155+
self.version = try container.decode(String.self, forKey: .version)
156+
self.routeKey = try container.decode(String.self, forKey: .routeKey)
157+
self.rawPath = try container.decode(String.self, forKey: .rawPath)
158+
self.rawQueryString = try container.decode(String.self, forKey: .rawQueryString)
159+
160+
self.cookies = try container.decodeIfPresent([String].self, forKey: .cookies) ?? []
161+
self.headers = try container.decodeIfPresent(HTTPHeaders.self, forKey: .headers) ?? HTTPHeaders()
162+
self.queryStringParameters = try container.decodeIfPresent([String: String].self, forKey: .queryStringParameters) ?? [:]
163+
self.pathParameters = try container.decodeIfPresent([String: String].self, forKey: .pathParameters) ?? [:]
164+
165+
self.context = try container.decode(Context.self, forKey: .context)
166+
self.stageVariables = try container.decodeIfPresent([String: String].self, forKey: .stageVariables) ?? [:]
167+
168+
self.body = try container.decodeIfPresent(String.self, forKey: .body)
169+
self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded)
170+
}
171+
}

Sources/AWSLambdaEvents/APIGateway.swift

+26-5
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import Foundation
2424
// https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
2525

2626
/// `APIGatewayRequest` contains data coming from the API Gateway.
27-
public struct APIGatewayRequest: Codable, Sendable {
27+
public struct APIGatewayRequest: Encodable, Sendable {
2828
public struct Context: Codable, Sendable {
2929
public struct Identity: Codable, Sendable {
3030
public let cognitoIdentityPoolId: String?
@@ -64,12 +64,12 @@ public struct APIGatewayRequest: Codable, Sendable {
6464
public let path: String
6565
public let httpMethod: HTTPRequest.Method
6666

67-
public let queryStringParameters: [String: String]?
68-
public let multiValueQueryStringParameters: [String: [String]]?
67+
public let queryStringParameters: [String: String]
68+
public let multiValueQueryStringParameters: [String: [String]]
6969
public let headers: HTTPHeaders
7070
public let multiValueHeaders: HTTPMultiValueHeaders
71-
public let pathParameters: [String: String]?
72-
public let stageVariables: [String: String]?
71+
public let pathParameters: [String: String]
72+
public let stageVariables: [String: String]
7373

7474
public let requestContext: Context
7575
public let body: String?
@@ -99,3 +99,24 @@ public struct APIGatewayResponse: Codable, Sendable {
9999
self.isBase64Encoded = isBase64Encoded
100100
}
101101
}
102+
103+
extension APIGatewayRequest: Decodable {
104+
public init(from decoder: any Decoder) throws {
105+
let container = try decoder.container(keyedBy: CodingKeys.self)
106+
107+
self.resource = try container.decode(String.self, forKey: .resource)
108+
self.path = try container.decode(String.self, forKey: .path)
109+
self.httpMethod = try container.decode(HTTPRequest.Method.self, forKey: .httpMethod)
110+
111+
self.queryStringParameters = try container.decodeIfPresent([String: String].self, forKey: .queryStringParameters) ?? [:]
112+
self.multiValueQueryStringParameters = try container.decodeIfPresent([String: [String]].self, forKey: .multiValueQueryStringParameters) ?? [:]
113+
self.headers = try container.decodeIfPresent(HTTPHeaders.self, forKey: .headers) ?? HTTPHeaders()
114+
self.multiValueHeaders = try container.decodeIfPresent(HTTPMultiValueHeaders.self, forKey: .multiValueHeaders) ?? HTTPMultiValueHeaders()
115+
self.pathParameters = try container.decodeIfPresent([String: String].self, forKey: .pathParameters) ?? [:]
116+
self.stageVariables = try container.decodeIfPresent([String: String].self, forKey: .stageVariables) ?? [:]
117+
118+
self.requestContext = try container.decode(Context.self, forKey: .requestContext)
119+
self.body = try container.decodeIfPresent(String.self, forKey: .body)
120+
self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded)
121+
}
122+
}

Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift

+46-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,46 @@ class APIGatewayV2Tests: XCTestCase {
7373
}
7474
"""
7575

76+
static let exampleGetEventBodyNilHeaders = """
77+
{
78+
"routeKey":"GET /hello",
79+
"version":"2.0",
80+
"rawPath":"/hello",
81+
"requestContext":{
82+
"timeEpoch":1587750461466,
83+
"domainPrefix":"hello",
84+
"authorizer":{
85+
"jwt":{
86+
"scopes":[
87+
"hello"
88+
],
89+
"claims":{
90+
"aud":"customers",
91+
"iss":"https://hello.test.com/",
92+
"iat":"1587749276",
93+
"exp":"1587756476"
94+
}
95+
}
96+
},
97+
"accountId":"0123456789",
98+
"stage":"$default",
99+
"domainName":"hello.test.com",
100+
"apiId":"pb5dg6g3rg",
101+
"requestId":"LgLpnibOFiAEPCA=",
102+
"http":{
103+
"path":"/hello",
104+
"userAgent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest",
105+
"method":"GET",
106+
"protocol":"HTTP/1.1",
107+
"sourceIp":"91.64.117.86"
108+
},
109+
"time":"24/Apr/2020:17:47:41 +0000"
110+
},
111+
"isBase64Encoded":false,
112+
"rawQueryString":"foo=bar"
113+
}
114+
"""
115+
76116
static let fullExamplePayload = """
77117
{
78118
"version": "2.0",
@@ -156,7 +196,7 @@ class APIGatewayV2Tests: XCTestCase {
156196

157197
XCTAssertEqual(req?.rawPath, "/hello")
158198
XCTAssertEqual(req?.context.http.method, .get)
159-
XCTAssertEqual(req?.queryStringParameters?.count, 1)
199+
XCTAssertEqual(req?.queryStringParameters.count, 1)
160200
XCTAssertEqual(req?.rawQueryString, "foo=bar")
161201
XCTAssertEqual(req?.headers.count, 8)
162202
XCTAssertEqual(req?.context.authorizer?.jwt?.claims?["aud"], "customers")
@@ -176,4 +216,9 @@ class APIGatewayV2Tests: XCTestCase {
176216
XCTAssertEqual(clientCert?.validity.notBefore, "May 28 12:30:02 2019 GMT")
177217
XCTAssertEqual(clientCert?.validity.notAfter, "Aug 5 09:36:04 2021 GMT")
178218
}
219+
220+
func testDecodingNilCollections() {
221+
let data = APIGatewayV2Tests.exampleGetEventBodyNilHeaders.data(using: .utf8)!
222+
XCTAssertNoThrow(_ = try JSONDecoder().decode(APIGatewayV2Request.self, from: data))
223+
}
179224
}

Tests/AWSLambdaEventsTests/APIGatewayTests.swift

+9
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class APIGatewayTests: XCTestCase {
3434
{"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}
3535
"""
3636

37+
static let postEventBodyNilHeaders = """
38+
{"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}
39+
"""
40+
3741
// MARK: - Request -
3842

3943
// MARK: Decoding
@@ -108,4 +112,9 @@ class APIGatewayTests: XCTestCase {
108112
XCTAssertEqual(json?.isBase64Encoded, resp.isBase64Encoded)
109113
XCTAssertEqual(json?.headers?["Server"], "Test")
110114
}
115+
116+
func testDecodingNilCollections() {
117+
let data = APIGatewayTests.postEventBodyNilHeaders.data(using: .utf8)!
118+
XCTAssertNoThrow(_ = try JSONDecoder().decode(APIGatewayRequest.self, from: data))
119+
}
111120
}

0 commit comments

Comments
 (0)