Skip to content

Commit 9c13ba5

Browse files
committed
Add Codable helpers for FunctionURL, APIGatewayV2, and SQS
1 parent 9edb1f8 commit 9c13ba5

File tree

5 files changed

+219
-0
lines changed

5 files changed

+219
-0
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#if canImport(FoundationEssentials)
2+
import FoundationEssentials
3+
#else
4+
import Foundation
5+
#endif
6+
7+
import HTTPTypes
8+
9+
public extension APIGatewayV2Request {
10+
/// Decodes the body of the request into a `Data` object.
11+
///
12+
/// - Returns: The decoded body as `Data` or `nil` if the body is empty.
13+
func decodeBody() throws -> Data? {
14+
guard let body else { return nil }
15+
16+
if isBase64Encoded,
17+
let base64Decoded = Data(base64Encoded: body) {
18+
return base64Decoded
19+
}
20+
21+
return body.data(using: .utf8)
22+
}
23+
24+
/// Decodes the body of the request into a decodable object. When the
25+
/// body is empty, an error is thrown.
26+
///
27+
/// - Parameters:
28+
/// - type: The type to decode the body into.
29+
/// - decoder: The decoder to use. Defaults to `JSONDecoder()`.
30+
///
31+
/// - Returns: The decoded body as `T`.
32+
/// - Throws: An error if the body cannot be decoded.
33+
func decodeBody<T>(
34+
_ type: T.Type,
35+
using decoder: JSONDecoder = JSONDecoder()
36+
) throws -> T where T: Decodable {
37+
let bodyData = body?.data(using: .utf8) ?? Data()
38+
39+
var requestData = bodyData
40+
41+
if isBase64Encoded,
42+
let base64Decoded = Data(base64Encoded: requestData) {
43+
requestData = base64Decoded
44+
}
45+
46+
return try decoder.decode(T.self, from: requestData)
47+
}
48+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#if canImport(FoundationEssentials)
2+
import FoundationEssentials
3+
#else
4+
import Foundation
5+
#endif
6+
7+
import HTTPTypes
8+
9+
public extension APIGatewayV2Response {
10+
/// Encodes a given encodable object into a `APIGatewayV2Response` object.
11+
///
12+
/// - Parameters:
13+
/// - encodable: The object to encode.
14+
/// - status: The status code to use. Defaults to `ok`.
15+
/// - encoder: The encoder to use. Defaults to a new `JSONEncoder`.
16+
/// - onError: A closure to handle errors, and transform them into a `APIGatewayV2Response`. Defaults
17+
/// to converting the error into a 500 (Internal Server Error) response with the error message as the body.
18+
static func encoding<T>(
19+
_ encodable: T,
20+
status: HTTPResponse.Status = .ok,
21+
using encoder: JSONEncoder = JSONEncoder(),
22+
onError: ((Error) -> Self)? = nil
23+
) -> Self where T: Encodable {
24+
do {
25+
let encodedResponse = try encoder.encode(encodable)
26+
return APIGatewayV2Response(
27+
statusCode: status,
28+
body: String(data: encodedResponse, encoding: .utf8)
29+
)
30+
} catch {
31+
return (onError ?? defaultErrorHandler)(error)
32+
}
33+
}
34+
}
35+
36+
private func defaultErrorHandler(_ error: Error) -> APIGatewayV2Response {
37+
APIGatewayV2Response(
38+
statusCode: .internalServerError,
39+
body: "Internal Server Error: \(String(describing: error))"
40+
)
41+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#if canImport(FoundationEssentials)
2+
import FoundationEssentials
3+
#else
4+
import Foundation
5+
#endif
6+
7+
import HTTPTypes
8+
9+
public extension FunctionURLRequest {
10+
/// Decodes the body of the request into a `Data` object.
11+
///
12+
/// - Returns: The decoded body as `Data` or `nil` if the body is empty.
13+
func decodeBody() throws -> Data? {
14+
guard let body else { return nil }
15+
16+
if isBase64Encoded,
17+
let base64Decoded = Data(base64Encoded: body) {
18+
return base64Decoded
19+
}
20+
21+
return body.data(using: .utf8)
22+
}
23+
24+
/// Decodes the body of the request into a decodable object. When the
25+
/// body is empty, an error is thrown.
26+
///
27+
/// - Parameters:
28+
/// - type: The type to decode the body into.
29+
/// - decoder: The decoder to use. Defaults to `JSONDecoder()`.
30+
///
31+
/// - Returns: The decoded body as `T`.
32+
/// - Throws: An error if the body cannot be decoded.
33+
func decodeBody<T>(
34+
_ type: T.Type,
35+
using decoder: JSONDecoder = JSONDecoder()
36+
) throws -> T where T: Decodable {
37+
let bodyData = body?.data(using: .utf8) ?? Data()
38+
39+
var requestData = bodyData
40+
41+
if isBase64Encoded,
42+
let base64Decoded = Data(base64Encoded: requestData) {
43+
requestData = base64Decoded
44+
}
45+
46+
return try decoder.decode(T.self, from: requestData)
47+
}
48+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#if canImport(FoundationEssentials)
2+
import FoundationEssentials
3+
#else
4+
import Foundation
5+
#endif
6+
7+
import HTTPTypes
8+
9+
public extension FunctionURLResponse {
10+
/// Encodes a given encodable object into a `FunctionURLResponse` object.
11+
///
12+
/// - Parameters:
13+
/// - encodable: The object to encode.
14+
/// - status: The status code to use. Defaults to `ok`.
15+
/// - encoder: The encoder to use. Defaults to a new `JSONEncoder`.
16+
/// - onError: A closure to handle errors, and transform them into a `FunctionURLResponse`. Defaults
17+
/// to converting the error into a 500 (Internal Server Error) response with the error message as the body.
18+
static func encoding<T>(
19+
_ encodable: T,
20+
status: HTTPResponse.Status = .ok,
21+
using encoder: JSONEncoder = JSONEncoder(),
22+
onError: ((Error) -> Self)? = nil
23+
) -> Self where T: Encodable {
24+
do {
25+
let encodedResponse = try encoder.encode(encodable)
26+
return FunctionURLResponse(
27+
statusCode: status,
28+
body: String(data: encodedResponse, encoding: .utf8)
29+
)
30+
} catch {
31+
return (onError ?? defaultErrorHandler)(error)
32+
}
33+
}
34+
}
35+
36+
private func defaultErrorHandler(_ error: Error) -> FunctionURLResponse {
37+
FunctionURLResponse(
38+
statusCode: .internalServerError,
39+
body: "Internal Server Error: \(String(describing: error))"
40+
)
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#if canImport(FoundationEssentials)
2+
import FoundationEssentials
3+
#else
4+
import Foundation
5+
#endif
6+
7+
public extension SQSEvent {
8+
/// Decodes the records included in the event into an array of decodable objects.
9+
///
10+
/// - Parameters:
11+
/// - type: The type to decode the body into.
12+
/// - decoder: The decoder to use. Defaults to a new `JSONDecoder`.
13+
///
14+
/// - Returns: The decoded records as `[T]`.
15+
/// - Throws: An error if any of the records cannot be decoded.
16+
func decodeBody<T>(
17+
_ type: T.Type,
18+
using decoder: JSONDecoder = JSONDecoder()
19+
) throws -> [T] where T: Decodable {
20+
try records.map {
21+
try $0.decodeBody(type, using: decoder)
22+
}
23+
}
24+
}
25+
26+
public extension SQSEvent.Message {
27+
/// Decodes the body of the message into a decodable object.
28+
///
29+
/// - Parameters:
30+
/// - type: The type to decode the body into.
31+
/// - decoder: The decoder to use. Defaults to a new `JSONDecoder`.
32+
///
33+
/// - Returns: The decoded body as `T`.
34+
/// - Throws: An error if the body cannot be decoded.
35+
func decodeBody<T>(
36+
_ type: T.Type,
37+
using decoder: JSONDecoder = JSONDecoder()
38+
) throws -> T where T: Decodable {
39+
try decoder.decode(T.self, from: body.data(using: .utf8) ?? Data())
40+
}
41+
}

0 commit comments

Comments
 (0)