Skip to content

Commit 8a09adc

Browse files
feat(event_handler): add ability to expose a Swagger UI (#3254)
Co-authored-by: Leandro Damascena <[email protected]>
1 parent 8329153 commit 8a09adc

17 files changed

+671
-63
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

+198-27
Large diffs are not rendered by default.

aws_lambda_powertools/event_handler/lambda_function_url.py

+6
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,9 @@ def __init__(
6262
strip_prefixes,
6363
enable_validation,
6464
)
65+
66+
def _get_base_path(self) -> str:
67+
stage = self.current_event.request_context.stage
68+
if stage and stage != "$default" and self.current_event.request_context.http.method.startswith(f"/{stage}"):
69+
return f"/{stage}"
70+
return ""

aws_lambda_powertools/event_handler/middlewares/openapi_validation.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -94,20 +94,31 @@ def handler(self, app: EventHandlerInstance, next_middleware: NextMiddleware) ->
9494
else:
9595
# Re-write the route_args with the validated values, and call the next middleware
9696
app.context["_route_args"] = values
97-
response = next_middleware(app)
9897

99-
# Process the response body if it exists
100-
raw_response = jsonable_encoder(response.body)
98+
# Call the handler by calling the next middleware
99+
response = next_middleware(app)
101100

102-
# Validate and serialize the response
103-
return self._serialize_response(field=route.dependant.return_param, response_content=raw_response)
101+
# Process the response
102+
return self._handle_response(route=route, response=response)
104103
except RequestValidationError as e:
105104
return Response(
106105
status_code=422,
107106
content_type="application/json",
108107
body=json.dumps({"detail": e.errors()}),
109108
)
110109

110+
def _handle_response(self, *, route: Route, response: Response):
111+
# Process the response body if it exists
112+
if response.body:
113+
# Validate and serialize the response, if it's JSON
114+
if response.is_json():
115+
response.body = json.dumps(
116+
self._serialize_response(field=route.dependant.return_param, response_content=response.body),
117+
sort_keys=True,
118+
)
119+
120+
return response
121+
111122
def _serialize_response(
112123
self,
113124
*,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DEFAULT_API_VERSION = "1.0.0"
2+
DEFAULT_OPENAPI_VERSION = "3.1.0"

aws_lambda_powertools/event_handler/openapi/models.py

+16-16
Original file line numberDiff line numberDiff line change
@@ -363,9 +363,24 @@ class Config:
363363
extra = "allow"
364364

365365

366+
# https://swagger.io/specification/#tag-object
367+
class Tag(BaseModel):
368+
name: str
369+
description: Optional[str] = None
370+
externalDocs: Optional[ExternalDocumentation] = None
371+
372+
if PYDANTIC_V2:
373+
model_config = {"extra": "allow"}
374+
375+
else:
376+
377+
class Config:
378+
extra = "allow"
379+
380+
366381
# https://swagger.io/specification/#operation-object
367382
class Operation(BaseModel):
368-
tags: Optional[List[str]] = None
383+
tags: Optional[List[Tag]] = None
369384
summary: Optional[str] = None
370385
description: Optional[str] = None
371386
externalDocs: Optional[ExternalDocumentation] = None
@@ -540,21 +555,6 @@ class Config:
540555
extra = "allow"
541556

542557

543-
# https://swagger.io/specification/#tag-object
544-
class Tag(BaseModel):
545-
name: str
546-
description: Optional[str] = None
547-
externalDocs: Optional[ExternalDocumentation] = None
548-
549-
if PYDANTIC_V2:
550-
model_config = {"extra": "allow"}
551-
552-
else:
553-
554-
class Config:
555-
extra = "allow"
556-
557-
558558
# https://swagger.io/specification/#openapi-object
559559
class OpenAPI(BaseModel):
560560
openapi: str

aws_lambda_powertools/event_handler/openapi/swagger_ui/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
def generate_swagger_html(spec: str, js_url: str, css_url: str) -> str:
2+
"""
3+
Generate Swagger UI HTML page
4+
5+
Parameters
6+
----------
7+
spec: str
8+
The OpenAPI spec in the JSON format
9+
js_url: str
10+
The URL to the Swagger UI JavaScript file
11+
css_url: str
12+
The URL to the Swagger UI CSS file
13+
"""
14+
return f"""
15+
<!DOCTYPE html>
16+
<html>
17+
<head>
18+
<meta charset="UTF-8">
19+
<title>Swagger UI</title>
20+
<link rel="stylesheet" type="text/css" href="{css_url}">
21+
</head>
22+
23+
<body>
24+
<div id="swagger-ui">
25+
Loading...
26+
</div>
27+
</body>
28+
29+
<script src="{js_url}"></script>
30+
31+
<script>
32+
var swaggerUIOptions = {{
33+
dom_id: "#swagger-ui",
34+
docExpansion: "list",
35+
deepLinking: true,
36+
filter: true,
37+
spec: JSON.parse(`
38+
{spec}
39+
`.trim()),
40+
presets: [
41+
SwaggerUIBundle.presets.apis,
42+
SwaggerUIBundle.SwaggerUIStandalonePreset
43+
],
44+
plugins: [
45+
SwaggerUIBundle.plugins.DownloadUrl
46+
]
47+
}}
48+
49+
var ui = SwaggerUIBundle(swaggerUIOptions)
50+
</script>
51+
</html>
52+
""".strip()

aws_lambda_powertools/event_handler/openapi/swagger_ui/swagger-ui-bundle.min.js

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws_lambda_powertools/event_handler/openapi/swagger_ui/swagger-ui.min.css

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws_lambda_powertools/event_handler/vpc_lattice.py

+6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ def __init__(
5353
"""Amazon VPC Lattice resolver"""
5454
super().__init__(ProxyEventType.VPCLatticeEvent, cors, debug, serializer, strip_prefixes, enable_validation)
5555

56+
def _get_base_path(self) -> str:
57+
return ""
58+
5659

5760
class VPCLatticeV2Resolver(ApiGatewayResolver):
5861
"""VPC Lattice resolver
@@ -98,3 +101,6 @@ def __init__(
98101
):
99102
"""Amazon VPC Lattice resolver"""
100103
super().__init__(ProxyEventType.VPCLatticeEventV2, cors, debug, serializer, strip_prefixes, enable_validation)
104+
105+
def _get_base_path(self) -> str:
106+
return ""

aws_lambda_powertools/utilities/parameters/ssm.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,13 @@ def _get(self, name: str, decrypt: bool = False, **sdk_options) -> str:
188188

189189
return self.client.get_parameter(**sdk_options)["Parameter"]["Value"]
190190

191-
def _get_multiple(self, path: str, decrypt: Optional[bool] = None, recursive: bool = False, **sdk_options) -> Dict[str, str]:
191+
def _get_multiple(
192+
self,
193+
path: str,
194+
decrypt: Optional[bool] = None,
195+
recursive: bool = False,
196+
**sdk_options,
197+
) -> Dict[str, str]:
192198
"""
193199
Retrieve multiple parameter values from AWS Systems Manager Parameter Store
194200

tests/functional/event_handler/test_api_gateway.py

+23
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,29 @@ def handler(event, context):
413413
assert headers["Content-Encoding"] == ["gzip"]
414414

415415

416+
def test_response_is_json_without_content_type():
417+
response = Response(200, None, "")
418+
419+
assert response.is_json() is False
420+
421+
422+
def test_response_is_json_with_json_content_type():
423+
response = Response(200, content_types.APPLICATION_JSON, "")
424+
assert response.is_json() is True
425+
426+
427+
def test_response_is_json_with_multiple_json_content_types():
428+
response = Response(
429+
200,
430+
None,
431+
"",
432+
{
433+
"Content-Type": [content_types.APPLICATION_JSON, content_types.APPLICATION_JSON],
434+
},
435+
)
436+
assert response.is_json() is True
437+
438+
416439
def test_compress():
417440
# GIVEN a function that has compress=True
418441
# AND an event with a "Accept-Encoding" that include gzip
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from aws_lambda_powertools.event_handler import (
2+
ALBResolver,
3+
APIGatewayHttpResolver,
4+
APIGatewayRestResolver,
5+
LambdaFunctionUrlResolver,
6+
VPCLatticeResolver,
7+
VPCLatticeV2Resolver,
8+
)
9+
from tests.functional.utils import load_event
10+
11+
12+
def test_base_path_api_gateway_rest():
13+
app = APIGatewayRestResolver(enable_validation=True)
14+
15+
@app.get("/")
16+
def handle():
17+
return app._get_base_path()
18+
19+
event = load_event("apiGatewayProxyEvent.json")
20+
event["path"] = "/"
21+
22+
result = app(event, {})
23+
assert result["statusCode"] == 200
24+
assert result["body"] == ""
25+
26+
27+
def test_base_path_api_gateway_http():
28+
app = APIGatewayHttpResolver(enable_validation=True)
29+
30+
@app.get("/")
31+
def handle():
32+
return app._get_base_path()
33+
34+
event = load_event("apiGatewayProxyV2Event.json")
35+
event["rawPath"] = "/"
36+
event["requestContext"]["http"]["path"] = "/"
37+
event["requestContext"]["http"]["method"] = "GET"
38+
39+
result = app(event, {})
40+
assert result["statusCode"] == 200
41+
assert result["body"] == ""
42+
43+
44+
def test_base_path_alb():
45+
app = ALBResolver(enable_validation=True)
46+
47+
@app.get("/")
48+
def handle():
49+
return app._get_base_path()
50+
51+
event = load_event("albEvent.json")
52+
event["path"] = "/"
53+
54+
result = app(event, {})
55+
assert result["statusCode"] == 200
56+
assert result["body"] == ""
57+
58+
59+
def test_base_path_lambda_function_url():
60+
app = LambdaFunctionUrlResolver(enable_validation=True)
61+
62+
@app.get("/")
63+
def handle():
64+
return app._get_base_path()
65+
66+
event = load_event("lambdaFunctionUrlIAMEvent.json")
67+
event["rawPath"] = "/"
68+
event["requestContext"]["http"]["path"] = "/"
69+
event["requestContext"]["http"]["method"] = "GET"
70+
71+
result = app(event, {})
72+
assert result["statusCode"] == 200
73+
assert result["body"] == ""
74+
75+
76+
def test_vpc_lattice():
77+
app = VPCLatticeResolver(enable_validation=True)
78+
79+
@app.get("/")
80+
def handle():
81+
return app._get_base_path()
82+
83+
event = load_event("vpcLatticeEvent.json")
84+
event["raw_path"] = "/"
85+
86+
result = app(event, {})
87+
assert result["statusCode"] == 200
88+
assert result["body"] == ""
89+
90+
91+
def test_vpc_latticev2():
92+
app = VPCLatticeV2Resolver(enable_validation=True)
93+
94+
@app.get("/")
95+
def handle():
96+
return app._get_base_path()
97+
98+
event = load_event("vpcLatticeV2Event.json")
99+
event["path"] = "/"
100+
101+
result = app(event, {})
102+
assert result["statusCode"] == 200
103+
assert result["body"] == ""

0 commit comments

Comments
 (0)