simplify code for API Gateway V2 (HTTP API)

This commit is contained in:
Sebastien Stormacq
2023-12-13 16:46:45 -05:00
parent e522a27ac1
commit ef2e29fc26
5 changed files with 129 additions and 61 deletions
+103 -41
View File
@@ -1,8 +1,15 @@
# swift-openapi-lambda
# AWS Lambda transport for Swift OpenAPI
An [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) Transport for [Swift OpenAPI generator](https://github.com/apple/swift-openapi-generator)
This library provides an [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) transport for [Swift OpenAPI generator](https://github.com/apple/swift-openapi-generator)
This library allows to expose server side Swift OpenAPI implementation generated by the Swift OpenAPI generator as an AWS Lambda function and an AWS API Gateway.
This library allows to expose server side Swift OpenAPI implementation generated by the Swift OpenAPI generator as an AWS Lambda function.
The library provides two capabilities:
- a default implementation of an AWS Lambda function in that consumes your OpenAPI service implementation
- a binding with the [Amazon API Gateway (HTTP API mode)](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) (aka `APIGatewayV2`) event type.
Other Lambda function bindings (event types) are supported as well, depending on your needs. [We include instructions](#implement-your-own-openapilambda-to-support-other-event-types) to create a binding with an [Amazon API Gateway (REST API mode)](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html)
## Prerequisites
@@ -15,12 +22,16 @@ To write and deploy AWS Lambda functions based on an OpenAPI API definition, you
## TL;DR
Assuming you already have an OpenAPI definition and you already generated the server stubs. Here are the additional steps to expose your service implementation as a AWS Lambda function.
If you already have an OpenAPI definition, you already generated the server stubs, and wrote an implementation, here are the additional steps to expose your OpenAPI service implementation as a AWS Lambda function and an Amazon API Gateway HTTP API (aka `APIGatewayV2`).
If you don't know how to do that, read on, there is [a tutorial with step-by-step instructions](#tutorial-a-quick-start-with-a-stock-quote-api-service-example).
If you don't know how to start, read the next section, there is [a tutorial with step-by-step instructions](#tutorial-a-quick-start-with-a-stock-quote-api-service-example).
To expose your OpenAPI implementation as an AWS Lambda function:
1. Add the dependency to your `Package.swift`
The project dependencies:
```swift
dependencies: [
.package(url: "https://github.com/apple/swift-openapi-generator.git", .upToNextMinor(from: "1.0.0-alpha.1")),
@@ -31,6 +42,8 @@ If you don't know how to do that, read on, there is [a tutorial with step-by-ste
],
```
The target dependencies:
```swift
.executableTarget(
name: "YourOpenAPIService",
@@ -42,23 +55,38 @@ If you don't know how to do that, read on, there is [a tutorial with step-by-ste
],
```
2. Create a AWS Lambda function that consumes your OpenAPI implementation
2. Add a protocol and a constructor to your existing OpenAPI service implementation
```swift
import AWSLambdaEvents
import OpenAPILambda
import Foundation
import OpenAPIRuntime
import OpenAPILambda // <-- add this line
@main
struct QuoteServiceLambda: OpenAPILambda {
typealias Event = APIGatewayV2Request
typealias Output = APIGatewayV2Response
struct QuoteServiceImpl: APIProtocol, OpenAPILambdaHttpApi { // <-- add the OpenAPILambdaHttpApi protocol
public init(transport: LambdaOpenAPITransport) throws {
let openAPIHandler = QuoteServiceImpl()
try openAPIHandler.registerHandlers(on: transport)
init(transport: LambdaOpenAPITransport) throws { // <-- add this constructor (don't remove the call to `registerHandlers(on:)`)
try self.registerHandlers(on: transport)
}
// the rest below is unmodified
func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output {
let symbol = input.path.symbol
let price = Components.Schemas.quote(
symbol: symbol,
price: Double.random(in: 100..<150).rounded(),
change: Double.random(in: -5..<5).rounded(),
changePercent: Double.random(in: -0.05..<0.05),
volume: Double.random(in: 10000..<100000).rounded(),
timestamp: Date())
return .ok(.init(body: .json(price)))
}
}
```
3. Package and deploy your Lambda function + create an HTTP API Gateway (aka `APIGatewayV2`)
@@ -67,7 +95,7 @@ struct QuoteServiceLambda: OpenAPILambda {
## Tutorial (a Quick Start with a Stock Quote API service example)
### Part 1 - the Swift OpenAPI part
### Part 1 - the code
1. Create a Swift executable project
@@ -204,13 +232,19 @@ rm Sources/main.swift
cat << EOF > Sources/QuoteService.swift
import Foundation
import OpenAPIRuntime
import OpenAPILambda
@main
struct QuoteServiceImpl: APIProtocol, OpenAPILambdaHttpApi {
init(transport: LambdaOpenAPITransport) throws {
try self.registerHandlers(on: transport)
}
struct QuoteServiceImpl: APIProtocol {
func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output {
let symbol = input.path.symbol
// in real life, don't use random values 🤣
let price = Components.Schemas.quote(
symbol: symbol,
price: Double.random(in: 100..<150).rounded(),
@@ -225,30 +259,15 @@ struct QuoteServiceImpl: APIProtocol {
EOF
```
### Part 2 - the Lambda part
1. Add a Lambda function that consumes your OpenAPI service implementation. The Lambda is invoked by an AWS API Gateway.
7. Build the project to ensure everything works
```sh
cat << EOF > Sources/Lambda.swift
import AWSLambdaEvents
import OpenAPILambda
@main
struct QuoteServiceLambda: OpenAPILambda {
typealias Event = APIGatewayV2Request
typealias Output = APIGatewayV2Response
public init(transport: LambdaOpenAPITransport) throws {
let openAPIHandler = QuoteServiceImpl()
try openAPIHandler.registerHandlers(on: transport)
}
}
swift build
```
2. Add the build instructions as a Docker file and a Makefile. We build for Swift 5.9 on Amazon Linux 2
### Part 2 - the deployment
1. Add the Lambda build instructions as a Docker file and a Makefile. We build for Swift 5.9 on Amazon Linux 2
```sh
cat << EOF > Dockerfile
@@ -299,16 +318,55 @@ builder-bot:
cp $($@STAGE)/* $($@ARTIFACTS_DIR)
EOF
```
3. Build the executable
2. Add a SAM template to deploy the Lambda function and the API Gateway
```sh
cat << EOF > template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM Template for QuoteService
Globals:
Function:
Timeout: 60
CodeUri: .
Handler: swift.bootstrap
Runtime: provided.al2
MemorySize: 512
Architectures:
- arm64
Resources:
# Lambda function
QuoteService:
Type: AWS::Serverless::Function
Properties:
Events:
# handles all GET / method of the REST API
Api:
Type: HttpApi
Metadata:
BuildMethod: makefile
# print API endpoint and name of database table
Outputs:
SwiftAPIEndpoint:
Description: "API Gateway endpoint URL for your application"
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
EOF
```
3. Build the Lambda function executable for Amazon Linux 2
```sh
sam build
```
4. Deploy the Lambda function and creates an API Gateway in front of it
4. Deploy the Lambda function and create an API Gateway in front of it
```sh
# use --guided for the first deployment. SAM cli collects a few parameters and store them in `samconfig.toml`
# use --guided for the first deployment only. SAM cli collects a few parameters and store them in `samconfig.toml`
sam deploy --guided
```
@@ -348,6 +406,10 @@ LOCAL_LAMBDA_SERVER_ENABLED=true swift run
curl -v -X POST --header "Content-Type: application/json" --data @events/GetQuote.json http://127.0.0.1:7000/invoke
```
## Implement your own `OpenAPILambda` to support other event types
TBD
## References
### Swift OpenAPI generator
@@ -1,27 +1,13 @@
import Foundation
import AWSLambdaEvents
import HTTPTypes
import OpenAPIRuntime
extension HTTPHeaders {
func httpFields() -> HTTPFields {
HTTPFields(self.map { key, value in HTTPField(name: .init(key)!, value: value) })
}
/// Create HTTPHeaders from HTTPFields
public init(from fields: HTTPFields) {
var headers: HTTPHeaders = [:]
fields.forEach { headers[$0.name.rawName] = $0.value }
self = headers
}
}
extension APIGatewayV2Request {
/// Return an `HTTPRequest.Method` for this `APIGatewayV2Request`
public func httpRequestMethod() throws -> HTTPRequest.Method {
guard let method = HTTPRequest.Method(rawValue: self.context.http.method.rawValue) else {
throw LambdaOpenAPIRouterError.invalidMethod(self.context.http.method.rawValue)
throw LambdaOpenAPIHttpError.invalidMethod(self.context.http.method.rawValue)
}
return method
}
@@ -50,3 +36,17 @@ extension APIGatewayV2Response {
)
}
}
public extension HTTPHeaders {
/// Create an `HTTPFields` (from `HTTPTypes` library) from this APIGateway `HTTPHeader`
func httpFields() -> HTTPFields {
HTTPFields(self.map { key, value in HTTPField(name: .init(key)!, value: value) })
}
/// Create HTTPHeaders from HTTPFields
init(from fields: HTTPFields) {
var headers: HTTPHeaders = [:]
fields.forEach { headers[$0.name.rawName] = $0.value }
self = headers
}
}
@@ -1,15 +1,23 @@
import Foundation
import AWSLambdaRuntime
import AWSLambdaEvents
import OpenAPIRuntime
import HTTPTypes
extension OpenAPILambda where Event == APIGatewayV2Request {
public enum LambdaOpenAPIHttpError: Error {
case invalidMethod(String)
}
public protocol OpenAPILambdaHttpApi: OpenAPILambda where Event == APIGatewayV2Request,
Output == APIGatewayV2Response {}
extension OpenAPILambdaHttpApi {
/// Transform a Lambda input (`APIGatewayV2Request` and `LambdaContext`) to an OpenAPILambdaRequest (`HTTPRequest`, `String?`)
public func request(context: LambdaContext, from request: Event) throws -> OpenAPILambdaRequest {
(try request.httpRequest(), request.body)
}
}
extension OpenAPILambda where Output == APIGatewayV2Response {
/// Transform an OpenAPI response (`HTTPResponse`, `String?`) to a Lambda Output (`APIGatewayV2Response`)
public func output(from response: OpenAPILambdaResponse) -> Output {
var apiResponse = APIGatewayV2Response(from: response.0)
-1
View File
@@ -1,6 +1,5 @@
import Foundation
import AWSLambdaRuntime
import AWSLambdaEvents
import OpenAPIRuntime
import HTTPTypes
-1
View File
@@ -7,7 +7,6 @@ public enum LambdaOpenAPIRouterError: Error {
case noRouteForPath(String)
case noHandlerForPath(String)
case noRouteForMethod(HTTPRequest.Method)
case invalidMethod(String)
}
/// A router API