mirror of
https://github.com/swift-server/swift-openapi-lambda.git
synced 2026-05-03 07:22:26 +00:00
simplify code for API Gateway V2 (HTTP API)
This commit is contained in:
@@ -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
|
||||
|
||||
+15
-15
@@ -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
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -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,6 +1,5 @@
|
||||
import Foundation
|
||||
import AWSLambdaRuntime
|
||||
import AWSLambdaEvents
|
||||
import OpenAPIRuntime
|
||||
import HTTPTypes
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ public enum LambdaOpenAPIRouterError: Error {
|
||||
case noRouteForPath(String)
|
||||
case noHandlerForPath(String)
|
||||
case noRouteForMethod(HTTPRequest.Method)
|
||||
case invalidMethod(String)
|
||||
}
|
||||
|
||||
/// A router API
|
||||
|
||||
Reference in New Issue
Block a user