Skip to content

Commit ef2e29f

Browse files
committed
simplify code for API Gateway V2 (HTTP API)
1 parent e522a27 commit ef2e29f

File tree

5 files changed

+129
-61
lines changed

5 files changed

+129
-61
lines changed

README.md

Lines changed: 103 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
# swift-openapi-lambda
1+
# AWS Lambda transport for Swift OpenAPI
22

3-
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)
3+
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)
44

5-
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.
5+
This library allows to expose server side Swift OpenAPI implementation generated by the Swift OpenAPI generator as an AWS Lambda function.
6+
7+
The library provides two capabilities:
8+
9+
- a default implementation of an AWS Lambda function in that consumes your OpenAPI service implementation
10+
- 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.
11+
12+
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)
613

714
## Prerequisites
815

@@ -15,12 +22,16 @@ To write and deploy AWS Lambda functions based on an OpenAPI API definition, you
1522

1623
## TL;DR
1724

18-
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.
25+
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`).
26+
27+
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).
1928

20-
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).
29+
To expose your OpenAPI implementation as an AWS Lambda function:
2130

2231
1. Add the dependency to your `Package.swift`
2332

33+
The project dependencies:
34+
2435
```swift
2536
dependencies: [
2637
.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
3142
],
3243
```
3344

45+
The target dependencies:
46+
3447
```swift
3548
.executableTarget(
3649
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
4255
],
4356
```
4457

45-
2. Create a AWS Lambda function that consumes your OpenAPI implementation
58+
2. Add a protocol and a constructor to your existing OpenAPI service implementation
4659

4760
```swift
48-
import AWSLambdaEvents
49-
import OpenAPILambda
61+
import Foundation
62+
import OpenAPIRuntime
63+
import OpenAPILambda // <-- add this line
5064

5165
@main
52-
struct QuoteServiceLambda: OpenAPILambda {
53-
54-
typealias Event = APIGatewayV2Request
55-
typealias Output = APIGatewayV2Response
66+
struct QuoteServiceImpl: APIProtocol, OpenAPILambdaHttpApi { // <-- add the OpenAPILambdaHttpApi protocol
67+
68+
init(transport: LambdaOpenAPITransport) throws { // <-- add this constructor (don't remove the call to `registerHandlers(on:)`)
69+
try self.registerHandlers(on: transport)
70+
}
71+
72+
// the rest below is unmodified
5673

57-
public init(transport: LambdaOpenAPITransport) throws {
58-
let openAPIHandler = QuoteServiceImpl()
59-
try openAPIHandler.registerHandlers(on: transport)
74+
func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output {
75+
76+
let symbol = input.path.symbol
77+
78+
let price = Components.Schemas.quote(
79+
symbol: symbol,
80+
price: Double.random(in: 100..<150).rounded(),
81+
change: Double.random(in: -5..<5).rounded(),
82+
changePercent: Double.random(in: -0.05..<0.05),
83+
volume: Double.random(in: 10000..<100000).rounded(),
84+
timestamp: Date())
85+
86+
return .ok(.init(body: .json(price)))
6087
}
6188
}
89+
6290
```
6391

6492
3. Package and deploy your Lambda function + create an HTTP API Gateway (aka `APIGatewayV2`)
@@ -67,7 +95,7 @@ struct QuoteServiceLambda: OpenAPILambda {
6795

6896
## Tutorial (a Quick Start with a Stock Quote API service example)
6997

70-
### Part 1 - the Swift OpenAPI part
98+
### Part 1 - the code
7199

72100
1. Create a Swift executable project
73101

@@ -204,13 +232,19 @@ rm Sources/main.swift
204232
cat << EOF > Sources/QuoteService.swift
205233
import Foundation
206234
import OpenAPIRuntime
235+
import OpenAPILambda
236+
237+
@main
238+
struct QuoteServiceImpl: APIProtocol, OpenAPILambdaHttpApi {
239+
240+
init(transport: LambdaOpenAPITransport) throws {
241+
try self.registerHandlers(on: transport)
242+
}
207243

208-
struct QuoteServiceImpl: APIProtocol {
209244
func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output {
210245

211246
let symbol = input.path.symbol
212247

213-
// in real life, don't use random values 🤣
214248
let price = Components.Schemas.quote(
215249
symbol: symbol,
216250
price: Double.random(in: 100..<150).rounded(),
@@ -225,30 +259,15 @@ struct QuoteServiceImpl: APIProtocol {
225259
EOF
226260
```
227261

228-
### Part 2 - the Lambda part
229-
230-
231-
1. Add a Lambda function that consumes your OpenAPI service implementation. The Lambda is invoked by an AWS API Gateway.
262+
7. Build the project to ensure everything works
232263

233264
```sh
234-
cat << EOF > Sources/Lambda.swift
235-
import AWSLambdaEvents
236-
import OpenAPILambda
237-
238-
@main
239-
struct QuoteServiceLambda: OpenAPILambda {
240-
241-
typealias Event = APIGatewayV2Request
242-
typealias Output = APIGatewayV2Response
243-
244-
public init(transport: LambdaOpenAPITransport) throws {
245-
let openAPIHandler = QuoteServiceImpl()
246-
try openAPIHandler.registerHandlers(on: transport)
247-
}
248-
}
265+
swift build
249266
```
250267

251-
2. Add the build instructions as a Docker file and a Makefile. We build for Swift 5.9 on Amazon Linux 2
268+
### Part 2 - the deployment
269+
270+
1. Add the Lambda build instructions as a Docker file and a Makefile. We build for Swift 5.9 on Amazon Linux 2
252271

253272
```sh
254273
cat << EOF > Dockerfile
@@ -299,16 +318,55 @@ builder-bot:
299318
cp $($@STAGE)/* $($@ARTIFACTS_DIR)
300319
EOF
301320
```
302-
3. Build the executable
321+
322+
2. Add a SAM template to deploy the Lambda function and the API Gateway
323+
324+
```sh
325+
cat << EOF > template.yml
326+
AWSTemplateFormatVersion: '2010-09-09'
327+
Transform: AWS::Serverless-2016-10-31
328+
Description: SAM Template for QuoteService
329+
330+
Globals:
331+
Function:
332+
Timeout: 60
333+
CodeUri: .
334+
Handler: swift.bootstrap
335+
Runtime: provided.al2
336+
MemorySize: 512
337+
Architectures:
338+
- arm64
339+
340+
Resources:
341+
# Lambda function
342+
QuoteService:
343+
Type: AWS::Serverless::Function
344+
Properties:
345+
Events:
346+
# handles all GET / method of the REST API
347+
Api:
348+
Type: HttpApi
349+
Metadata:
350+
BuildMethod: makefile
351+
352+
# print API endpoint and name of database table
353+
Outputs:
354+
SwiftAPIEndpoint:
355+
Description: "API Gateway endpoint URL for your application"
356+
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
357+
EOF
358+
```
359+
360+
3. Build the Lambda function executable for Amazon Linux 2
303361
304362
```sh
305363
sam build
306364
```
307365
308-
4. Deploy the Lambda function and creates an API Gateway in front of it
366+
4. Deploy the Lambda function and create an API Gateway in front of it
309367
310368
```sh
311-
# use --guided for the first deployment. SAM cli collects a few parameters and store them in `samconfig.toml`
369+
# use --guided for the first deployment only. SAM cli collects a few parameters and store them in `samconfig.toml`
312370
sam deploy --guided
313371
```
314372
@@ -348,6 +406,10 @@ LOCAL_LAMBDA_SERVER_ENABLED=true swift run
348406
curl -v -X POST --header "Content-Type: application/json" --data @events/GetQuote.json http://127.0.0.1:7000/invoke
349407
```
350408
409+
## Implement your own `OpenAPILambda` to support other event types
410+
411+
TBD
412+
351413
## References
352414
353415
### Swift OpenAPI generator

Sources/APIGatewayV2+HTTPRequest.swift renamed to Sources/HttpApi/APIGatewayV2+HTTPRequest.swift

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,13 @@
1-
import Foundation
21
import AWSLambdaEvents
32
import HTTPTypes
43
import OpenAPIRuntime
54

6-
extension HTTPHeaders {
7-
func httpFields() -> HTTPFields {
8-
HTTPFields(self.map { key, value in HTTPField(name: .init(key)!, value: value) })
9-
}
10-
11-
/// Create HTTPHeaders from HTTPFields
12-
public init(from fields: HTTPFields) {
13-
var headers: HTTPHeaders = [:]
14-
fields.forEach { headers[$0.name.rawName] = $0.value }
15-
self = headers
16-
}
17-
}
18-
195
extension APIGatewayV2Request {
206

217
/// Return an `HTTPRequest.Method` for this `APIGatewayV2Request`
228
public func httpRequestMethod() throws -> HTTPRequest.Method {
239
guard let method = HTTPRequest.Method(rawValue: self.context.http.method.rawValue) else {
24-
throw LambdaOpenAPIRouterError.invalidMethod(self.context.http.method.rawValue)
10+
throw LambdaOpenAPIHttpError.invalidMethod(self.context.http.method.rawValue)
2511
}
2612
return method
2713
}
@@ -50,3 +36,17 @@ extension APIGatewayV2Response {
5036
)
5137
}
5238
}
39+
40+
public extension HTTPHeaders {
41+
/// Create an `HTTPFields` (from `HTTPTypes` library) from this APIGateway `HTTPHeader`
42+
func httpFields() -> HTTPFields {
43+
HTTPFields(self.map { key, value in HTTPField(name: .init(key)!, value: value) })
44+
}
45+
46+
/// Create HTTPHeaders from HTTPFields
47+
init(from fields: HTTPFields) {
48+
var headers: HTTPHeaders = [:]
49+
fields.forEach { headers[$0.name.rawName] = $0.value }
50+
self = headers
51+
}
52+
}

Sources/OpenAPILambda+APIGatewayV2.swift renamed to Sources/HttpApi/OpenAPILambdaHttpApi.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1+
import Foundation
12
import AWSLambdaRuntime
23
import AWSLambdaEvents
4+
import OpenAPIRuntime
35
import HTTPTypes
46

5-
extension OpenAPILambda where Event == APIGatewayV2Request {
7+
public enum LambdaOpenAPIHttpError: Error {
8+
case invalidMethod(String)
9+
}
10+
11+
public protocol OpenAPILambdaHttpApi: OpenAPILambda where Event == APIGatewayV2Request,
12+
Output == APIGatewayV2Response {}
13+
14+
15+
extension OpenAPILambdaHttpApi {
616
/// Transform a Lambda input (`APIGatewayV2Request` and `LambdaContext`) to an OpenAPILambdaRequest (`HTTPRequest`, `String?`)
717
public func request(context: LambdaContext, from request: Event) throws -> OpenAPILambdaRequest {
818
(try request.httpRequest(), request.body)
919
}
10-
}
1120

12-
extension OpenAPILambda where Output == APIGatewayV2Response {
1321
/// Transform an OpenAPI response (`HTTPResponse`, `String?`) to a Lambda Output (`APIGatewayV2Response`)
1422
public func output(from response: OpenAPILambdaResponse) -> Output {
1523
var apiResponse = APIGatewayV2Response(from: response.0)

Sources/OpenAPILambda.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Foundation
22
import AWSLambdaRuntime
3-
import AWSLambdaEvents
43
import OpenAPIRuntime
54
import HTTPTypes
65

Sources/Router/OpenAPILambdaRouter.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ public enum LambdaOpenAPIRouterError: Error {
77
case noRouteForPath(String)
88
case noHandlerForPath(String)
99
case noRouteForMethod(HTTPRequest.Method)
10-
case invalidMethod(String)
1110
}
1211

1312
/// A router API

0 commit comments

Comments
 (0)