Skip to content

Commit 89f1172

Browse files
authored
Add Codable helpers for FunctionURL, APIGatewayV2, and SQS (#90)
Add helpers for FunctionURL, APIGatewayV2, and SQS events to ease usage of `Decodable` and `Encodable` types ### Motivation: When using Function URL (or API Gateway) events and outputs, and SQS events, it is very common to decode a `Decodable` body, or encode a `Codable` type into a function URL response. This PR adds convenient methods that are easy to use, but also maintain a level of customization by allowing custom JSON encoders/decoders. ### Modifications: * Add helper method to encode `Encodable` types into a `FunctionURLReponse` or `APIGatewayV2Response` * Add helper method do decode `Decodable` types from a `FunctionURLRequest` or `APIGatewayV2Request` * Add helper method to decode multiple `Decodable`s from `SQSEvent` records, or from a single `SQSEvent.Message` * Add test cases (generated with the help of LLM. Thanks Anthropic. Verified tests pass after many tries, and went over the final changes to make sure they're correct) ### Result: The changes allow the following usage: ```swift let decodedBody = try functionURLRequest.decode(MyPayload.self) ``` Instead of: ```swift var bodyData = functionURLRequest.body?.data(using: .utf8) ?? Data() // check for base 64 encoding of the body, and if that's the case, decode that into bodyData if isBase64Encoded, let base64Decoded = ... { bodyData = base64Decoded } let decodedBody = JSONDecoder().decode(MyPayload.self, from: bodyData) ``` The same is similar when decoding SQS event messages. And when encoding a response: ```swift func handle(...) -> FunctionURLResponse { // business logic let responsePayload = MyResponse(text: "Hello Lambda", count: 613) return .encoding(responsePayload) } ``` Instead of: ```swift func handle(...) -> FunctionURLResponse { // business logic let responsePayload = MyResponse(text: "Hello Lambda", count: 613) do { let data = try JSONEncoder().encode(responsePayload) return .init( status: .ok, body: String(data: data, encoding: .utf8) } } catch { return .init( status: .internalServerError, body: "Internal server error: \(error)" } } } ```
1 parent 9edb1f8 commit 89f1172

File tree

9 files changed

+1143
-0
lines changed

9 files changed

+1143
-0
lines changed

Sources/AWSLambdaEvents/APIGateway+V2.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ public struct APIGatewayV2Request: Encodable, Sendable {
126126
}
127127
}
128128

129+
extension APIGatewayV2Request: DecodableRequest {}
130+
129131
public struct APIGatewayV2Response: Codable, Sendable {
130132
public var statusCode: HTTPResponse.Status
131133
public var headers: HTTPHeaders?
@@ -148,6 +150,8 @@ public struct APIGatewayV2Response: Codable, Sendable {
148150
}
149151
}
150152

153+
extension APIGatewayV2Response: EncodableResponse {}
154+
151155
extension APIGatewayV2Request: Decodable {
152156
public init(from decoder: Decoder) throws {
153157
let container = try decoder.container(keyedBy: CodingKeys.self)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import HTTPTypes
16+
17+
#if canImport(FoundationEssentials)
18+
import FoundationEssentials
19+
#else
20+
import Foundation
21+
#endif
22+
23+
public protocol DecodableRequest {
24+
var body: String? { get }
25+
var isBase64Encoded: Bool { get }
26+
27+
func decodeBody() throws -> Data?
28+
func decodeBody<T>(
29+
_ type: T.Type,
30+
using decoder: JSONDecoder
31+
) throws -> T where T: Decodable
32+
}
33+
34+
extension DecodableRequest {
35+
/// Decodes the body of the request into a `Data` object.
36+
///
37+
/// - Returns: The decoded body as `Data` or `nil` if the body is empty.
38+
public func decodeBody() throws -> Data? {
39+
guard let body else { return nil }
40+
41+
if isBase64Encoded,
42+
let base64Decoded = Data(base64Encoded: body)
43+
{
44+
return base64Decoded
45+
}
46+
47+
return body.data(using: .utf8)
48+
}
49+
50+
/// Decodes the body of the request into a decodable object. When the
51+
/// body is empty, an error is thrown.
52+
///
53+
/// - Parameters:
54+
/// - type: The type to decode the body into.
55+
/// - decoder: The decoder to use. Defaults to `JSONDecoder()`.
56+
///
57+
/// - Returns: The decoded body as `T`.
58+
/// - Throws: An error if the body cannot be decoded.
59+
public func decodeBody<T>(
60+
_ type: T.Type,
61+
using decoder: JSONDecoder = JSONDecoder()
62+
) throws -> T where T: Decodable {
63+
let bodyData = body?.data(using: .utf8) ?? Data()
64+
65+
var requestData = bodyData
66+
67+
if isBase64Encoded,
68+
let base64Decoded = Data(base64Encoded: requestData)
69+
{
70+
requestData = base64Decoded
71+
}
72+
73+
return try decoder.decode(T.self, from: requestData)
74+
}
75+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import HTTPTypes
16+
17+
#if canImport(FoundationEssentials)
18+
import FoundationEssentials
19+
#else
20+
import Foundation
21+
#endif
22+
23+
public protocol EncodableResponse {
24+
static func encoding<T>(
25+
_ encodable: T,
26+
status: HTTPResponse.Status,
27+
using encoder: JSONEncoder,
28+
headers: HTTPHeaders?,
29+
cookies: [String]?,
30+
onError: ((Error) -> Self)
31+
) -> Self where T: Encodable
32+
33+
init(
34+
statusCode: HTTPResponse.Status,
35+
headers: HTTPHeaders?,
36+
body: String?,
37+
isBase64Encoded: Bool?,
38+
cookies: [String]?
39+
)
40+
}
41+
42+
extension EncodableResponse {
43+
/// Encodes a given encodable object into a response object.
44+
///
45+
/// - Parameters:
46+
/// - encodable: The object to encode.
47+
/// - status: The status code to use. Defaults to `ok`.
48+
/// - encoder: The encoder to use. Defaults to a new `JSONEncoder`.
49+
/// - onError: A closure to handle errors, and transform them into a `APIGatewayV2Response`.
50+
/// Defaults to converting the error into a 500 (Internal Server Error) response with the error message as
51+
/// the body.
52+
///
53+
/// - Returns: a response object whose body is the encoded `encodable` type and with the
54+
/// other response parameters
55+
public static func encoding<T>(
56+
_ encodable: T,
57+
status: HTTPResponse.Status = .ok,
58+
using encoder: JSONEncoder = JSONEncoder(),
59+
headers: HTTPHeaders? = nil,
60+
cookies: [String]? = nil,
61+
onError: ((Error) -> Self) = Self.defaultErrorHandler
62+
) -> Self where T: Encodable {
63+
do {
64+
let encodedResponse = try encoder.encode(encodable)
65+
return Self(
66+
statusCode: status,
67+
headers: headers,
68+
body: String(data: encodedResponse, encoding: .utf8),
69+
isBase64Encoded: nil,
70+
cookies: cookies
71+
)
72+
} catch {
73+
return onError(error)
74+
}
75+
}
76+
77+
public static var defaultErrorHandler: ((Error) -> Self) {
78+
{ error in
79+
Self(
80+
statusCode: .internalServerError,
81+
headers: nil,
82+
body: "Internal Server Error: \(String(describing: error))",
83+
isBase64Encoded: nil,
84+
cookies: nil
85+
)
86+
}
87+
}
88+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#if canImport(FoundationEssentials)
16+
import FoundationEssentials
17+
#else
18+
import Foundation
19+
#endif
20+
21+
extension SQSEvent {
22+
/// Decodes the records included in the event into an array of decodable objects.
23+
///
24+
/// - Parameters:
25+
/// - type: The type to decode the body into.
26+
/// - decoder: The decoder to use. Defaults to a new `JSONDecoder`.
27+
///
28+
/// - Returns: The decoded records as `[T]`.
29+
/// - Throws: An error if any of the records cannot be decoded.
30+
public func decodeBody<T>(
31+
_ type: T.Type,
32+
using decoder: JSONDecoder = JSONDecoder()
33+
) throws -> [T] where T: Decodable {
34+
try records.map {
35+
try $0.decodeBody(type, using: decoder)
36+
}
37+
}
38+
}
39+
40+
extension SQSEvent.Message {
41+
/// Decodes the body of the message into a decodable object.
42+
///
43+
/// - Parameters:
44+
/// - type: The type to decode the body into.
45+
/// - decoder: The decoder to use. Defaults to a new `JSONDecoder`.
46+
///
47+
/// - Returns: The decoded body as `T`.
48+
/// - Throws: An error if the body cannot be decoded.
49+
public func decodeBody<T>(
50+
_ type: T.Type,
51+
using decoder: JSONDecoder = JSONDecoder()
52+
) throws -> T where T: Decodable {
53+
try decoder.decode(T.self, from: body.data(using: .utf8) ?? Data())
54+
}
55+
}

Sources/AWSLambdaEvents/FunctionURL.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ public struct FunctionURLRequest: Codable, Sendable {
8383
public let stageVariables: [String: String]?
8484
}
8585

86+
extension FunctionURLRequest: DecodableRequest {}
87+
8688
// MARK: - Response -
8789

8890
public struct FunctionURLResponse: Codable, Sendable {
@@ -121,3 +123,5 @@ public struct FunctionURLResponse: Codable, Sendable {
121123
self.isBase64Encoded = isBase64Encoded
122124
}
123125
}
126+
127+
extension FunctionURLResponse: EncodableResponse {}

0 commit comments

Comments
 (0)