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

Conversation

natanrolnik
Copy link
Contributor

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 Decodables 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:

let decodedBody = try functionURLRequest.decode(MyPayload.self)

Instead of:

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:

func handle(...) -> FunctionURLResponse {
    // business logic
    let responsePayload = MyResponse(text: "Hello Lambda", count: 613)
    return .encoding(responsePayload)
}

Instead of:

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)"
        }
    }
}

@sebsto sebsto self-assigned this Aug 6, 2025
@sebsto sebsto added the 🆕 semver/minor Adds new public API. label Aug 6, 2025
@sebsto sebsto self-requested a review August 6, 2025 05:35
@sebsto
Copy link
Contributor

sebsto commented Aug 6, 2025

Thank you @natanrolnik for this welcome change.
Can you add the license header to all Swift files (except Package.swift) and be sure to run the ./scripts/format.sh on your staged files to apply formatting rules.

Copy link
Contributor

@sebsto sebsto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for this addition. I left one question and one suggestion. Also, be sure to include the license header on all source files and apply the swift formatting (there is a script to help you in ./scriptsformat.sh`)

@natanrolnik
Copy link
Contributor Author

@sebsto done! I've unified them into two protocols: DecodableRequest and EncodableResponse. Moved tests to a single file accordingly.
About the non-throwing and throwing methods, as we spoke offline, having the two options makes the compiler confused, so I preferred to use the simpler one (non-throwing, with the onError closure that returns a response).

Copy link
Contributor

@sebsto sebsto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you or the changes. I still have two quetsions.

}
}

extension APIGatewayV2Response: EncodableResponse {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like the only difference between APIGAteway's init() and the protocol is the order of the parameters. Unless I missed something else, is there a reason we can't use the existing order for the parameters and delete this piece of code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct. I thought about changing the order, but it would be a breaking change.

Copy link
Contributor

@sebsto sebsto Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we can't change the order on the APIGatewayResponse, but can you change the order on the EncodableResponse protocol ? That way APIGatewayResponse will automatically implement the EncodableResponse protocol.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this init() function to allow this change

@sebsto sebsto self-requested a review August 6, 2025 14:31
Copy link
Contributor

@sebsto sebsto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@sebsto sebsto merged commit 89f1172 into swift-server:main Aug 6, 2025
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants