Skip to content

Codable support for APIGateway V2 request and response payloads #129

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

Closed
Closed
Show file tree
Hide file tree
Changes from 3 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
78 changes: 78 additions & 0 deletions Sources/AWSLambdaEvents/APIGateway+V2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

import Foundation

extension APIGateway {
public struct V2 {}
}
Expand Down Expand Up @@ -117,3 +119,79 @@ extension APIGateway.V2 {
}
}
}

// MARK: - Codable Request body

extension APIGateway.V2.Request {
/// Generic body decoder for JSON payloads
///
/// Example:
/// ```
/// struct Request: Codable {
/// let value: String
/// }
///
/// func handle(context: Context, event: APIGateway.V2.Request, callback: @escaping (Result<APIGateway.V2.Response, Error>) -> Void) {
/// do {
/// let request: Request? = try event.decodedBody()
/// // Do something with `request`
/// callback(.success(APIGateway.V2.Response(statusCode: .ok, body:"")))
/// }
/// catch {
/// callback(.failure(error))
/// }
/// }
/// ```
///
/// - Throws: `DecodingError` if body contains a value that couldn't be decoded
/// - Returns: Decoded payload. Returns `nil` if body property is `nil`.
public func decodedBody<Payload: Codable>() throws -> Payload? {
guard let bodyString = body else {
return nil
}
let data = Data(bodyString.utf8)
return try JSONDecoder().decode(Payload.self, from: data)

Choose a reason for hiding this comment

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

how about:
public func decodedBody<Payload: Codable>(decoder:JSONDecoder = JSONDecoder()) throws -> Payload? {

This way the user has can customize the decoder and provide their own. For example, to change the dateDecodingStrategy.

Choose a reason for hiding this comment

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

(Same suggestion for the JSONEncoder usage below)

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a good point but also yet another instance where "TopLevelDecoder" types would be the right thing to go with... Since we'd like to allow using various coder implementations, not marry the API to Foundation's impl only.

We could do them ad-hoc here in the lambda lib again as Combine does, but that's growing the problem a bit; Or we ignore and go along with this for now, but eventually try to resolve it once we get to it.

// fyi @tomerd

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback. I've updated the PR to inject both JSONEncoder and JSONDecoder.

I agree a "TopLevelDecoder" type would be ideal, let me know if I should do any further changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Another idea would be to see if it's possible to push the functions out into an "AWSLambdaFoundationCompat" module or something similar?

Copy link
Contributor Author

@eneko eneko Jun 15, 2020

Choose a reason for hiding this comment

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

@ktoso I was looking at Lambda+Codable.swift from AWSLambdaRuntime and it seems like encoding/decoding work could be moved to a separate module, like you suggest. I haven't tried it, though.

Would you be open for that work being done on a separate pull request? While I think it is valuable, it feels out of the initial scope of this work.

Choose a reason for hiding this comment

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

Sorry, I don't know the internals here, based on a github search I don't see too many instances of "import Foundation". Would that mean that the lambda runtime wouldn't depend on Foundation? That might be nice for performance, right?

Copy link
Contributor Author

@eneko eneko Jun 15, 2020

Choose a reason for hiding this comment

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

@calcohen, at the moment, yes, AWSLambdaRuntime depends on Foundation for Codable support:

It might be possible to remove this dependency, like @ktoso suggested, but to me that feels like work for a different pull request.

}
}

// MARK: - Codable Response body

extension APIGateway.V2.Response {
/// Codable initializer for Response payload
///
/// Example:
/// ```
/// struct Response: Codable {
/// let message: String
/// }
///
/// func handle(context: Context, event: APIGateway.V2.Request, callback: @escaping (Result<APIGateway.V2.Response, Error>) -> Void) {
/// ...
/// callback(.success(APIGateway.V2.Response(statusCode: .ok, body: Response(message: "Hello, World!")))
/// }
/// ```
///
/// - Parameters:
/// - statusCode: Response HTTP status code
/// - headers: Response HTTP headers
/// - multiValueHeaders: Resposne multi-value headers
/// - body: `Codable` response payload
/// - cookies: Response cookies
/// - Throws: `EncodingError` if payload could not be encoded into a JSON string
public init<Payload: Codable>(
Copy link
Contributor

Choose a reason for hiding this comment

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

rename Payload -> Body

statusCode: HTTPResponseStatus,
headers: HTTPHeaders? = nil,
multiValueHeaders: HTTPMultiValueHeaders? = nil,
body: Payload? = nil,
cookies: [String]? = nil
) throws {
let data = try JSONEncoder().encode(body)
let bodyString = String(data: data, encoding: .utf8)
self.init(statusCode: statusCode,
headers: headers,
multiValueHeaders: multiValueHeaders,
body: bodyString,
isBase64Encoded: false,
cookies: cookies)
}
}
36 changes: 34 additions & 2 deletions Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,15 @@ class APIGatewayV2Tests: XCTestCase {
"x-amzn-trace-id":"Root=1-5ea3263d-07c5d5ddfd0788bed7dad831",
"user-agent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest",
"content-length":"0"
}
},
"body": "{\\"some\\":\\"json\\",\\"number\\":42}"
}
"""

static let exampleResponse = """
{"isBase64Encoded":false,"statusCode":200,"body":"{\\"message\\":\\"Foo Bar\\",\\"code\\":42}"}
"""

// MARK: - Request -

// MARK: Decoding
Expand All @@ -86,6 +91,33 @@ class APIGatewayV2Tests: XCTestCase {
XCTAssertEqual(req?.queryStringParameters?.count, 1)
XCTAssertEqual(req?.rawQueryString, "foo=bar")
XCTAssertEqual(req?.headers.count, 8)
XCTAssertNil(req?.body)
XCTAssertNotNil(req?.body)
}

func testRquestPayloadDecoding() throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

typo

struct Payload: Codable {
let some: String
let number: Int
}

let data = APIGatewayV2Tests.exampleGetEventBody.data(using: .utf8)!
let request = try JSONDecoder().decode(APIGateway.V2.Request.self, from: data)

let payload: Payload? = try request.decodedBody()
XCTAssertEqual(payload?.some, "json")
XCTAssertEqual(payload?.number, 42)
}

func testResponsePayloadEncoding() throws {
struct Payload: Codable {
let code: Int
let message: String
}

let response = try APIGateway.V2.Response(statusCode: .ok, body: Payload(code: 42, message: "Foo Bar"))
let data = try JSONEncoder().encode(response)
let json = String(data: data, encoding: .utf8)
XCTAssertEqual(json, APIGatewayV2Tests.exampleResponse)
}

}