Improvements to schema generation (#336)

* Include mounted paths in schemas (part of #172)

* Remove unnecessary indirection

* Refactor: cleaner interface, return a dict always
This commit is contained in:
Jose Eduardo 2019-02-19 12:49:30 +00:00 committed by Tom Christie
parent 2544762512
commit 590c77e03d
2 changed files with 106 additions and 7 deletions

View File

@ -3,7 +3,7 @@ import typing
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import BaseRoute, Route
from starlette.routing import BaseRoute, Mount, Route
try:
import yaml
@ -48,10 +48,22 @@ class BaseSchemaGenerator:
endpoints_info: list = []
for route in routes:
if not isinstance(route, Route) or not route.include_in_schema:
if isinstance(route, Mount):
routes = route.routes or []
sub_endpoints = [
EndpointInfo(
path="".join((route.path, sub_endpoint.path)),
http_method=sub_endpoint.http_method,
func=sub_endpoint.func,
)
for sub_endpoint in self.get_endpoints(routes)
]
endpoints_info.extend(sub_endpoints)
elif not isinstance(route, Route) or not route.include_in_schema:
continue
if inspect.isfunction(route.endpoint) or inspect.ismethod(route.endpoint):
elif inspect.isfunction(route.endpoint) or inspect.ismethod(route.endpoint):
for method in route.methods or ["GET"]:
if method == "HEAD":
continue
@ -66,6 +78,7 @@ class BaseSchemaGenerator:
endpoints_info.append(
EndpointInfo(route.path, method.lower(), func)
)
return endpoints_info
def parse_docstring(self, func_or_method: typing.Callable) -> dict:
@ -73,7 +86,22 @@ class BaseSchemaGenerator:
Given a function, parse the docstring as YAML and return a dictionary of info.
"""
docstring = func_or_method.__doc__
return yaml.safe_load(docstring) if docstring else {}
if not docstring:
return {}
# We support having regular docstrings before the schema
# definition. Here we return just the schema part from
# the docstring.
docstring = docstring.split("---")[-1]
parsed = yaml.safe_load(docstring)
if not isinstance(parsed, dict):
# A regular docstring (not yaml formatted) can return
# a simple string here, which wouldn't follow the schema.
return {}
return parsed
def OpenAPIResponse(self, request: Request) -> Response:
routes = request.app.routes
@ -91,9 +119,15 @@ class SchemaGenerator(BaseSchemaGenerator):
endpoints_info = self.get_endpoints(routes)
for endpoint in endpoints_info:
parsed = self.parse_docstring(endpoint.func)
if not parsed:
continue
if endpoint.path not in schema["paths"]:
schema["paths"][endpoint.path] = {}
schema["paths"][endpoint.path][endpoint.http_method] = self.parse_docstring(
endpoint.func
)
schema["paths"][endpoint.path][endpoint.http_method] = parsed
return schema

View File

@ -10,6 +10,10 @@ schemas = SchemaGenerator(
app = Starlette()
subapp = Starlette()
app.mount("/subapp", subapp)
@app.websocket_route("/ws")
def ws(session):
"""ws"""
@ -63,6 +67,43 @@ class OrganisationsEndpoint(HTTPEndpoint):
pass # pragma: no cover
@app.route("/regular-docstring-and-schema")
def regular_docstring_and_schema(request):
"""
This a regular docstring example (not included in schema)
---
responses:
200:
description: This is included in the schema.
"""
pass # pragma: no cover
@app.route("/regular-docstring")
def regular_docstring(request):
"""
This a regular docstring example (not included in schema)
"""
pass # pragma: no cover
@app.route("/no-docstring")
def no_docstring(request):
pass # pragma: no cover
@subapp.route("/subapp-endpoint")
def subapp_endpoint(request):
"""
responses:
200:
description: This endpoint is part of a subapp.
"""
pass # pragma: no cover
@app.route("/schema", methods=["GET"], include_in_schema=False)
def schema(request):
return schemas.OpenAPIResponse(request=request)
@ -92,6 +133,20 @@ def test_schema_generation():
}
},
},
"/regular-docstring-and-schema": {
"get": {
"responses": {
200: {"description": "This is included in the schema."}
}
}
},
"/subapp/subapp-endpoint": {
"get": {
"responses": {
200: {"description": "This endpoint is part of a subapp."}
}
}
},
"/users": {
"get": {
"responses": {
@ -136,6 +191,16 @@ paths:
description: An organisation.
examples:
name: Foo Corp.
/regular-docstring-and-schema:
get:
responses:
200:
description: This is included in the schema.
/subapp/subapp-endpoint:
get:
responses:
200:
description: This endpoint is part of a subapp.
/users:
get:
responses: