Skip to content

Commit db6f62f

Browse files
authored
feature: add swagger and other minor fixes(#784)
1. add swagger 2. fix appconfig breaking changes
1 parent e1a6ad9 commit db6f62f

24 files changed

+476
-202
lines changed

.github/workflows/main-serverless-service.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ jobs:
7373
- name: Codecov
7474
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
7575
with:
76+
token: ${{ secrets.CODECOV_TOKEN }}
7677
files: ./coverage.xml
77-
fail_ci_if_error: false # optional (default = false)
78-
verbose: false # optional (default = false)
78+
fail_ci_if_error: yes # optional (default = false)
79+
verbose: yes # optional (default = false)
7980
- name: Run E2E tests
8081
run: make e2e
8182
env:

.github/workflows/pr-serverless-service.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,10 @@ jobs:
9494
- name: Codecov
9595
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
9696
with:
97+
token: ${{ secrets.CODECOV_TOKEN }}
9798
files: ./coverage.xml
98-
fail_ci_if_error: false # optional (default = false)
99-
verbose: false # optional (default = false)
99+
fail_ci_if_error: yes # optional (default = false)
100+
verbose: yes # optional (default = false)
100101
- name: Run E2E tests
101102
run: make e2e
102103
- name: Destroy stack

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ repos:
2626
exclude: "^(?!helpers/)"
2727
- repo: https://github.com/astral-sh/ruff-pre-commit
2828
# Ruff version.
29-
rev: v0.1.13
29+
rev: v0.1.14
3030
hooks:
3131
# Run the Ruff linter.
3232
- id: ruff

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ integration:
5252
e2e:
5353
poetry run pytest tests/e2e --cov-config=.coveragerc --cov=service --cov-report xml
5454

55-
pr: deps pre-commit complex lint lint-docs unit deploy coverage-tests e2e
55+
pr: deps format pre-commit complex lint lint-docs unit deploy coverage-tests e2e
5656

5757
coverage-tests:
5858
poetry run pytest tests/unit tests/integration --cov-config=.coveragerc --cov=service --cov-report xml

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ This project aims to reduce cognitive load and answer these questions for you by
9393
- REST API protected by WAF with four AWS managed rules in production deployment
9494
- CloudWatch dashboards - High level and low level including CloudWatch alarms
9595
- Unit, infrastructure, security, integration and end to end tests.
96+
- Automatically generated OpenAPI endpoint: /swagger with Pydnatic schemas for both requests and responses
9697

9798

9899
## CDK Deployment

cdk/service/api_construct.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,31 @@ def __init__(self, scope: Construct, id_: str, appconfig_app_name: str, is_produ
2020
self.lambda_role = self._build_lambda_role(self.api_db.db, self.api_db.idempotency_db)
2121
self.common_layer = self._build_common_layer()
2222
self.rest_api = self._build_api_gw()
23-
api_resource: aws_apigateway.Resource = self.rest_api.root.add_resource('api').add_resource(constants.GW_RESOURCE)
23+
api_resource: aws_apigateway.Resource = self.rest_api.root.add_resource('api')
24+
orders_resource = api_resource.add_resource(constants.GW_RESOURCE)
2425
self.create_order_func = self._add_post_lambda_integration(
25-
api_resource, self.lambda_role, self.api_db.db, appconfig_app_name, self.api_db.idempotency_db
26+
orders_resource, self.lambda_role, self.api_db.db, appconfig_app_name, self.api_db.idempotency_db
2627
)
28+
self._build_swagger_endpoints(rest_api=self.rest_api, dest_func=self.create_order_func)
2729
self.monitoring = CrudMonitoring(self, id_, self.rest_api, self.api_db.db, self.api_db.idempotency_db, [self.create_order_func])
2830

2931
if is_production_env:
3032
# add WAF
3133
self.waf = WafToApiGatewayConstruct(self, f'{id_}waf', self.rest_api)
3234

35+
def _build_swagger_endpoints(self, rest_api: aws_apigateway.RestApi, dest_func: _lambda.Function) -> None:
36+
# GET /swagger
37+
swagger_resource: aws_apigateway.Resource = rest_api.root.add_resource(constants.SWAGGER_RESOURCE)
38+
swagger_resource.add_method(http_method='GET', integration=aws_apigateway.LambdaIntegration(handler=dest_func))
39+
# GET /swagger.css
40+
swagger_resource_css = rest_api.root.add_resource(constants.SWAGGER_CSS_RESOURCE)
41+
swagger_resource_css.add_method(http_method='GET', integration=aws_apigateway.LambdaIntegration(handler=dest_func))
42+
# GET /swagger.js
43+
swagger_resource_js = rest_api.root.add_resource(constants.SWAGGER_JS_RESOURCE)
44+
swagger_resource_js.add_method(http_method='GET', integration=aws_apigateway.LambdaIntegration(handler=dest_func))
45+
46+
CfnOutput(self, id=constants.SWAGGER_URL, value=f'{rest_api.url}swagger').override_logical_id(constants.SWAGGER_URL)
47+
3348
def _build_api_gw(self) -> aws_apigateway.RestApi:
3449
rest_api: aws_apigateway.RestApi = aws_apigateway.RestApi(
3550
self,
@@ -92,7 +107,7 @@ def _build_common_layer(self) -> PythonLayerVersion:
92107
)
93108

94109
def _add_post_lambda_integration(
95-
self, api_name: aws_apigateway.Resource, role: iam.Role, db: dynamodb.Table, appconfig_app_name: str, idempotency_table: dynamodb.Table
110+
self, api_resource: aws_apigateway.Resource, role: iam.Role, db: dynamodb.Table, appconfig_app_name: str, idempotency_table: dynamodb.Table
96111
) -> _lambda.Function:
97112
lambda_function = _lambda.Function(
98113
self,
@@ -102,7 +117,7 @@ def _add_post_lambda_integration(
102117
handler='service.handlers.handle_create_order.lambda_handler',
103118
environment={
104119
constants.POWERTOOLS_SERVICE_NAME: constants.SERVICE_NAME, # for logger, tracer and metrics
105-
constants.POWER_TOOLS_LOG_LEVEL: 'DEBUG', # for logger
120+
constants.POWER_TOOLS_LOG_LEVEL: 'INFO', # for logger
106121
'CONFIGURATION_APP': appconfig_app_name, # for feature flags
107122
'CONFIGURATION_ENV': constants.ENVIRONMENT, # for feature flags
108123
'CONFIGURATION_NAME': constants.CONFIGURATION_NAME, # for feature flags
@@ -124,5 +139,5 @@ def _add_post_lambda_integration(
124139
)
125140

126141
# POST /api/orders/
127-
api_name.add_method(http_method='POST', integration=aws_apigateway.LambdaIntegration(handler=lambda_function))
142+
api_resource.add_method(http_method='POST', integration=aws_apigateway.LambdaIntegration(handler=lambda_function))
128143
return lambda_function

cdk/service/configuration/configuration_construct.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ def __init__(self, scope: Construct, id_: str, environment: str, service_name: s
2929
self.config_app = appconfig.Application(
3030
self,
3131
id=self.app_name,
32-
name=self.app_name,
32+
application_name=self.app_name,
3333
)
3434

3535
self.config_env = appconfig.Environment(
3636
self,
3737
id=f'{id_}env',
3838
application=self.config_app,
39-
name=environment,
39+
environment_name=environment,
4040
)
4141

4242
# zero minutes, zero bake, 100 growth all at once

cdk/service/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
APIGATEWAY = 'Apigateway'
1010
MONITORING_TOPIC = 'MonitoringTopic'
1111
GW_RESOURCE = 'orders'
12+
SWAGGER_RESOURCE = 'swagger'
13+
SWAGGER_CSS_RESOURCE = 'swagger.css'
14+
SWAGGER_JS_RESOURCE = 'swagger.js'
15+
SWAGGER_URL = 'SwaggerURL'
1216
LAMBDA_LAYER_NAME = 'common'
1317
API_HANDLER_LAMBDA_MEMORY_SIZE = 128 # MB
1418
API_HANDLER_LAMBDA_TIMEOUT = 10 # seconds

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ This project aims to reduce cognitive load and answer these questions for you by
4646
- Idempotent API
4747
- REST API protected by WAF with four AWS managed rules in production deployment
4848
- Unit, infrastructure, security, integration and E2E tests.
49+
- Automatically generated OpenAPI endpoint: /swagger with Pydnatic schemas for both requests and responses
4950

5051
The GitHub template project can be found at [https://github.com/ran-isenberg/aws-lambda-handler-cookbook](https://github.com/ran-isenberg/aws-lambda-handler-cookbook){:target="_blank" rel="noopener"}.
5152

docs/swagger/openapi.json

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
{
2+
"openapi": "3.0.0",
3+
"info": {
4+
"title": "AWS Lambda Handler Cookbook - Orders Service",
5+
"version": "1.0.0"
6+
},
7+
"servers": [
8+
{
9+
"url": "/prod"
10+
}
11+
],
12+
"paths": {
13+
"/api/orders": {
14+
"post": {
15+
"tags": [
16+
"CRUD"
17+
],
18+
"summary": "Create an order",
19+
"description": "Create an order identified by the body payload",
20+
"operationId": "handle_create_order_api_orders_post",
21+
"requestBody": {
22+
"content": {
23+
"application/json": {
24+
"schema": {
25+
"$ref": "#/components/schemas/CreateOrderRequest"
26+
}
27+
}
28+
},
29+
"required": true
30+
},
31+
"responses": {
32+
"200": {
33+
"description": "The created order",
34+
"content": {
35+
"application/json": {
36+
"schema": {
37+
"$ref": "#/components/schemas/CreateOrderOutput"
38+
}
39+
}
40+
}
41+
},
42+
"442": {
43+
"description": "Invalid create order request",
44+
"content": {
45+
"application/json": {
46+
"schema": {
47+
"$ref": "#/components/schemas/InvalidRestApiRequest"
48+
}
49+
}
50+
}
51+
},
52+
"501": {
53+
"description": "Internal server error",
54+
"content": {
55+
"application/json": {
56+
"schema": {
57+
"$ref": "#/components/schemas/InternalServerErrorOutput"
58+
}
59+
}
60+
}
61+
}
62+
}
63+
}
64+
}
65+
},
66+
"components": {
67+
"schemas": {
68+
"CreateOrderOutput": {
69+
"properties": {
70+
"name": {
71+
"type": "string",
72+
"maxLength": 20,
73+
"minLength": 1,
74+
"title": "Name",
75+
"description": "Customer name"
76+
},
77+
"item_count": {
78+
"type": "integer",
79+
"exclusiveMinimum": 0.0,
80+
"title": "Item Count",
81+
"description": "Amount of items in order"
82+
},
83+
"id": {
84+
"type": "string",
85+
"maxLength": 36,
86+
"minLength": 36,
87+
"title": "Id",
88+
"description": "Order ID as UUID"
89+
}
90+
},
91+
"type": "object",
92+
"required": [
93+
"name",
94+
"item_count",
95+
"id"
96+
],
97+
"title": "CreateOrderOutput"
98+
},
99+
"CreateOrderRequest": {
100+
"properties": {
101+
"customer_name": {
102+
"type": "string",
103+
"maxLength": 20,
104+
"minLength": 1,
105+
"title": "Customer Name",
106+
"description": "Customer name"
107+
},
108+
"order_item_count": {
109+
"type": "integer",
110+
"exclusiveMinimum": 0.0,
111+
"title": "Order Item Count",
112+
"description": "Amount of items in order"
113+
}
114+
},
115+
"type": "object",
116+
"required": [
117+
"customer_name",
118+
"order_item_count"
119+
],
120+
"title": "CreateOrderRequest"
121+
},
122+
"InternalServerErrorOutput": {
123+
"properties": {
124+
"error": {
125+
"type": "string",
126+
"title": "Error",
127+
"description": "Error description",
128+
"default": "internal server error"
129+
}
130+
},
131+
"type": "object",
132+
"title": "InternalServerErrorOutput"
133+
},
134+
"InvalidRestApiRequest": {
135+
"properties": {
136+
"details": {
137+
"items": {
138+
"$ref": "#/components/schemas/PydanticError"
139+
},
140+
"type": "array",
141+
"title": "Details",
142+
"description": "Error details"
143+
}
144+
},
145+
"type": "object",
146+
"required": [
147+
"details"
148+
],
149+
"title": "InvalidRestApiRequest"
150+
},
151+
"PydanticError": {
152+
"properties": {
153+
"loc": {
154+
"items": {
155+
"anyOf": [
156+
{
157+
"type": "string"
158+
},
159+
{
160+
"type": "integer"
161+
}
162+
]
163+
},
164+
"type": "array",
165+
"title": "Loc",
166+
"description": "Error location"
167+
},
168+
"type": {
169+
"type": "string",
170+
"title": "Type",
171+
"description": "Error type"
172+
},
173+
"msg": {
174+
"anyOf": [
175+
{
176+
"type": "string"
177+
},
178+
{
179+
"type": "null"
180+
}
181+
],
182+
"title": "Msg",
183+
"description": "Error message"
184+
}
185+
},
186+
"type": "object",
187+
"required": [
188+
"loc",
189+
"type",
190+
"msg"
191+
],
192+
"title": "PydanticError"
193+
}
194+
}
195+
}
196+
}

docs/swagger/swagger.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
title: Swagger
3+
description: Swagger Documentation
4+
---
5+
6+
!!swagger openapi.json!!

mkdocs.yml

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ nav:
99
- Getting Started: getting_started.md
1010
- CDK: cdk.md
1111
- Pipeline: pipeline.md
12+
- Swagger: swagger/swagger.md
1213
- Best Practices:
1314
- best_practices/logger.md
1415
- best_practices/tracer.md
@@ -80,19 +81,21 @@ copyright: Copyright © 2023 Ran Isenberg
8081
plugins:
8182
- git-revision-date
8283
- search
84+
- render_swagger
8385
- glightbox:
84-
touchNavigation: true
85-
loop: false
86-
effect: zoom
87-
slide_effect: slide
88-
width: 100%
89-
height: auto
90-
zoomable: true
91-
draggable: true
92-
skip_classes:
93-
- custom-skip-class-name
94-
auto_caption: false
95-
caption_position: bottom
86+
touchNavigation: true
87+
loop: false
88+
effect: zoom
89+
slide_effect: slide
90+
width: 100%
91+
height: auto
92+
zoomable: true
93+
draggable: true
94+
skip_classes:
95+
- custom-skip-class-name
96+
auto_caption: false
97+
caption_position: bottom
98+
9699
extra_css:
97100
- stylesheets/extra.css
98101

0 commit comments

Comments
 (0)