Skip to content

Add Codable helpers for FunctionURL, APIGatewayV2, and SQS #90

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

Merged
merged 4 commits into from
Aug 6, 2025
Merged
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
4 changes: 4 additions & 0 deletions Sources/AWSLambdaEvents/APIGateway+V2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ public struct APIGatewayV2Request: Encodable, Sendable {
}
}

extension APIGatewayV2Request: DecodableRequest {}

public struct APIGatewayV2Response: Codable, Sendable {
public var statusCode: HTTPResponse.Status
public var headers: HTTPHeaders?
Expand All @@ -148,6 +150,8 @@ public struct APIGatewayV2Response: Codable, Sendable {
}
}

extension APIGatewayV2Response: EncodableResponse {}

extension APIGatewayV2Request: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
Expand Down
75 changes: 75 additions & 0 deletions Sources/AWSLambdaEvents/Codable Helpers/DecodableRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2017-2022 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 HTTPTypes

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

public protocol DecodableRequest {
var body: String? { get }
var isBase64Encoded: Bool { get }

func decodeBody() throws -> Data?
func decodeBody<T>(
_ type: T.Type,
using decoder: JSONDecoder
) throws -> T where T: Decodable
}

extension DecodableRequest {
/// Decodes the body of the request into a `Data` object.
///
/// - Returns: The decoded body as `Data` or `nil` if the body is empty.
public func decodeBody() throws -> Data? {
guard let body else { return nil }

if isBase64Encoded,
let base64Decoded = Data(base64Encoded: body)
{
return base64Decoded
}

return body.data(using: .utf8)
}

/// Decodes the body of the request into a decodable object. When the
/// body is empty, an error is thrown.
///
/// - Parameters:
/// - type: The type to decode the body into.
/// - decoder: The decoder to use. Defaults to `JSONDecoder()`.
///
/// - Returns: The decoded body as `T`.
/// - Throws: An error if the body cannot be decoded.
public func decodeBody<T>(
_ type: T.Type,
using decoder: JSONDecoder = JSONDecoder()
) throws -> T where T: Decodable {
let bodyData = body?.data(using: .utf8) ?? Data()

var requestData = bodyData

if isBase64Encoded,
let base64Decoded = Data(base64Encoded: requestData)
{
requestData = base64Decoded
}

return try decoder.decode(T.self, from: requestData)
}
}
88 changes: 88 additions & 0 deletions Sources/AWSLambdaEvents/Codable Helpers/EncodableResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2017-2022 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 HTTPTypes

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

public protocol EncodableResponse {
static func encoding<T>(
_ encodable: T,
status: HTTPResponse.Status,
using encoder: JSONEncoder,
headers: HTTPHeaders?,
cookies: [String]?,
onError: ((Error) -> Self)
) -> Self where T: Encodable

init(
statusCode: HTTPResponse.Status,
headers: HTTPHeaders?,
body: String?,
isBase64Encoded: Bool?,
cookies: [String]?
)
}

extension EncodableResponse {
/// Encodes a given encodable object into a response object.
///
/// - Parameters:
/// - encodable: The object to encode.
/// - status: The status code to use. Defaults to `ok`.
/// - encoder: The encoder to use. Defaults to a new `JSONEncoder`.
/// - onError: A closure to handle errors, and transform them into a `APIGatewayV2Response`.
/// Defaults to converting the error into a 500 (Internal Server Error) response with the error message as
/// the body.
///
/// - Returns: a response object whose body is the encoded `encodable` type and with the
/// other response parameters
public static func encoding<T>(
_ encodable: T,
status: HTTPResponse.Status = .ok,
using encoder: JSONEncoder = JSONEncoder(),
headers: HTTPHeaders? = nil,
cookies: [String]? = nil,
onError: ((Error) -> Self) = Self.defaultErrorHandler
) -> Self where T: Encodable {
do {
let encodedResponse = try encoder.encode(encodable)
return Self(
statusCode: status,
headers: headers,
body: String(data: encodedResponse, encoding: .utf8),
isBase64Encoded: nil,
cookies: cookies
)
} catch {
return onError(error)
}
}

public static var defaultErrorHandler: ((Error) -> Self) {
{ error in
Self(
statusCode: .internalServerError,
headers: nil,
body: "Internal Server Error: \(String(describing: error))",
isBase64Encoded: nil,
cookies: nil
)
}
}
}
55 changes: 55 additions & 0 deletions Sources/AWSLambdaEvents/Codable Helpers/SQS+Decode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2017-2022 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
//
//===----------------------------------------------------------------------===//

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

extension SQSEvent {
/// Decodes the records included in the event into an array of decodable objects.
///
/// - Parameters:
/// - type: The type to decode the body into.
/// - decoder: The decoder to use. Defaults to a new `JSONDecoder`.
///
/// - Returns: The decoded records as `[T]`.
/// - Throws: An error if any of the records cannot be decoded.
public func decodeBody<T>(
_ type: T.Type,
using decoder: JSONDecoder = JSONDecoder()
) throws -> [T] where T: Decodable {
try records.map {
try $0.decodeBody(type, using: decoder)
}
}
}

extension SQSEvent.Message {
/// Decodes the body of the message into a decodable object.
///
/// - Parameters:
/// - type: The type to decode the body into.
/// - decoder: The decoder to use. Defaults to a new `JSONDecoder`.
///
/// - Returns: The decoded body as `T`.
/// - Throws: An error if the body cannot be decoded.
public func decodeBody<T>(
_ type: T.Type,
using decoder: JSONDecoder = JSONDecoder()
) throws -> T where T: Decodable {
try decoder.decode(T.self, from: body.data(using: .utf8) ?? Data())
}
}
4 changes: 4 additions & 0 deletions Sources/AWSLambdaEvents/FunctionURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ public struct FunctionURLRequest: Codable, Sendable {
public let stageVariables: [String: String]?
}

extension FunctionURLRequest: DecodableRequest {}

// MARK: - Response -

public struct FunctionURLResponse: Codable, Sendable {
Expand Down Expand Up @@ -121,3 +123,5 @@ public struct FunctionURLResponse: Codable, Sendable {
self.isBase64Encoded = isBase64Encoded
}
}

extension FunctionURLResponse: EncodableResponse {}
Loading