Closed LonelyVikingMichael closed 2 years ago
As far as I have experienced, the only thing standing between us supporting Swagger in Starlite is that Starlite (correctly) marks the schema as 3.1. SwaggerUI explicitly blocks this because they want to fully support the JSON schema in 3.1 first before allowing this, but if you have a 3.1 YAML schema, it already works perfectly fine with the JS bundle you can get right now.
I've implemented a controller to emulate the FastAPI behavior like this:
# -*- coding: utf-8 -*-
import orjson as json
from typing import Optional, Any
from starlite.datastructures import State
from starlite import Controller, Request, MediaType, get
class SwaggerController(Controller):
"""
This is an internal controller that exists to provide the /docs route
that is standard in FastAPI. Starlite only provides ReDoc by default, so
we patch it in like this as a convenience feature.
"""
path = "/docs"
tags = ["docs"]
def get_swagger_ui_html(
self,
openapi_url: str,
title: str,
swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui-bundle.js",
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui.css",
swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
oauth2_redirect_url: Optional[str] = None,
init_oauth: Optional[dict[str, Any]] = None,
swagger_ui_parameters: Optional[dict[str, Any]] = None,
) -> str:
current_swagger_ui_parameters = {
"dom_id": "'#swagger-ui'",
"layout": "'BaseLayout'",
"deepLinking": "true",
"showExtensions": "true",
"showCommonExtensions": "true",
}
if swagger_ui_parameters:
current_swagger_ui_parameters.update(swagger_ui_parameters)
html = f"""
<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
<link rel="shortcut icon" href="{swagger_favicon_url}">
<title>{title}</title>
</head>
<body>
<div id="swagger-ui"></div>
<script src="{swagger_js_url}"></script>
<script>
const ui = SwaggerUIBundle({{
url: '{openapi_url}',
"""
for key, value in current_swagger_ui_parameters.items():
html += f"{key}: {value},\n"
if oauth2_redirect_url:
html += (
f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
)
html += """
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
})"""
if init_oauth:
html += f"""
ui.initOAuth({json.dumps(init_oauth)})
"""
html += """
</script>
</body>
</html>
"""
return html
@get(media_type=MediaType.HTML, include_in_schema=False)
def docs(self, request: Request, state: State) -> str:
scheme = request.url.scheme if state.config.environment == "local" else "https" # app-specific, you would have to change this to a different detection mechanism
if request.url.port:
url = f"{scheme}://{request.url.hostname}:{request.url.port}/schema/openapi.yaml"
else:
url = f"{scheme}://{request.url.hostname}/schema/openapi.yaml"
return self.get_swagger_ui_html(
url,
"API Docs",
swagger_js_url="/static/swagger-ui-bundle.js",
)
The above controller vendors a patched SwaggerUI bundle at /static
that allows 3.1 to pass without an exception being thrown, and the rest is basically straight-up FastAPI Swagger handling.
swagger-ui-bundle.js.zip
This could be made to work right now by implementing the controller like above and monkey-patching the openapi version in the schema that is served to the bundle on the frontend to be 3.0 instead of 3.1 specifically for the Swagger route. This would allow us to not have to vendor a patched JS bundle and we could freely use the CDN just like FastAPI does.
@LonelyVikingMichael can you test what @timwedde posted above? It would certainly save a huge amount of work if its possible to patch like this in a satisfactory way.
Just looked into this a bit more so here's a working version that does not require vendoring a patched SwaggerUI bundle:
# -*- coding: utf-8 -*-
import copy
import orjson as json
from typing import Optional, Any
from starlite.datastructures import State
from starlite.enums import OpenAPIMediaType
from starlite import Controller, Request, MediaType, get
from openapi_schema_pydantic.v3.v3_1_0.open_api import OpenAPI
class SwaggerController(Controller):
path = "/docs"
tags = ["docs"]
def get_swagger_ui_html(
self,
openapi_url: str,
title: str,
swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui-bundle.js",
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui.css",
swagger_favicon_url: str = "https://starlite-api.github.io/starlite/images/starlite-favicon.ico",
oauth2_redirect_url: Optional[str] = None,
init_oauth: Optional[dict[str, Any]] = None,
swagger_ui_parameters: Optional[dict[str, Any]] = None,
) -> str:
current_swagger_ui_parameters = {
"dom_id": "'#swagger-ui'",
"layout": "'BaseLayout'",
"deepLinking": "true",
"showExtensions": "true",
"showCommonExtensions": "true",
}
if swagger_ui_parameters:
current_swagger_ui_parameters.update(swagger_ui_parameters)
html = f"""
<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
<link rel="shortcut icon" href="{swagger_favicon_url}">
<title>{title}</title>
</head>
<body>
<div id="swagger-ui"></div>
<script src="{swagger_js_url}"></script>
<script>
const ui = SwaggerUIBundle({{
url: '{openapi_url}',
"""
for key, value in current_swagger_ui_parameters.items():
html += f"{key}: {value},\n"
if oauth2_redirect_url:
html += (
f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
)
html += """
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
})"""
if init_oauth:
html += f"""
ui.initOAuth({json.dumps(init_oauth)})
"""
html += """
</script>
</body>
</html>
"""
return html
@get(
path="/openapi.yaml",
media_type=OpenAPIMediaType.OPENAPI_YAML,
include_in_schema=False,
)
def retrieve_schema_yaml(self, request: Request) -> OpenAPI:
schema = copy.deepcopy(request.app.openapi_schema)
schema.openapi = "3.0.3"
return schema
@get(media_type=MediaType.HTML, include_in_schema=False)
def docs(self, request: Request, state: State) -> str:
if request.url.port:
url = f"{request.url.scheme}://{request.url.hostname}:{request.url.port}/docs/openapi.yaml"
else:
url = f"{request.url.scheme}://{request.url.hostname}/docs/openapi.yaml"
return self.get_swagger_ui_html(url, request.app.openapi_schema.info.title)
You can see that I simply "trick" Swagger into believing this is an OpenAPI 3.0 spec by overriding the version specifier. With this trick, SwaggerUI works as expected, as far as I have been able to validate, and you can use the regular CDN bundles without any tweaks.
Just looked into this a bit more so here's a working version that does not require vendoring a patched SwaggerUI bundle:
# -*- coding: utf-8 -*- import copy import orjson as json from typing import Optional, Any from starlite.datastructures import State from starlite.enums import OpenAPIMediaType from starlite import Controller, Request, MediaType, get from openapi_schema_pydantic.v3.v3_1_0.open_api import OpenAPI class SwaggerController(Controller): path = "/docs" tags = ["docs"] def get_swagger_ui_html( self, openapi_url: str, title: str, swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui-bundle.js", swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui.css", swagger_favicon_url: str = "https://starlite-api.github.io/starlite/images/starlite-favicon.ico", oauth2_redirect_url: Optional[str] = None, init_oauth: Optional[dict[str, Any]] = None, swagger_ui_parameters: Optional[dict[str, Any]] = None, ) -> str: current_swagger_ui_parameters = { "dom_id": "'#swagger-ui'", "layout": "'BaseLayout'", "deepLinking": "true", "showExtensions": "true", "showCommonExtensions": "true", } if swagger_ui_parameters: current_swagger_ui_parameters.update(swagger_ui_parameters) html = f""" <!DOCTYPE html> <html> <head> <link type="text/css" rel="stylesheet" href="{swagger_css_url}"> <link rel="shortcut icon" href="{swagger_favicon_url}"> <title>{title}</title> </head> <body> <div id="swagger-ui"></div> <script src="{swagger_js_url}"></script> <script> const ui = SwaggerUIBundle({{ url: '{openapi_url}', """ for key, value in current_swagger_ui_parameters.items(): html += f"{key}: {value},\n" if oauth2_redirect_url: html += ( f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}'," ) html += """ presets: [ SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset ], })""" if init_oauth: html += f""" ui.initOAuth({json.dumps(init_oauth)}) """ html += """ </script> </body> </html> """ return html @get( path="/openapi.yaml", media_type=OpenAPIMediaType.OPENAPI_YAML, include_in_schema=False, ) def retrieve_schema_yaml(self, request: Request) -> OpenAPI: schema = copy.deepcopy(request.app.openapi_schema) schema.openapi = "3.0.3" return schema @get(media_type=MediaType.HTML, include_in_schema=False) def docs(self, request: Request, state: State) -> str: if request.url.port: url = f"{request.url.scheme}://{request.url.hostname}:{request.url.port}/docs/openapi.yaml" else: url = f"{request.url.scheme}://{request.url.hostname}/docs/openapi.yaml" return self.get_swagger_ui_html(url, request.app.openapi_schema.info.title)
You can see that I simply "trick" Swagger into believing this is an OpenAPI 3.0 spec by overriding the version specifier. With this trick, SwaggerUI works as expected, as far as I have been able to validate, and you can use the regular CDN bundles without any tweaks.
Do you want to add a PR extending starlite.openapi.OpenAPIControllers
? I'd suggest to simply add a swagger_ui
method to it, keeping it as simple as possible (no extra code we don't support for now). We can the. Test this branch using the example project.
Can do, I might get to it this evening already.
Cheers @timwedde - im testing your PR
I verified this works @timwedde -- please see https://github.com/starlite-api/starlite/pull/303
Swagger-UI support has been released in v1.7.1, and the docs have been accordingly updated.
This would enable support for interactive Swagger documentation.
The downside is that this will become redundant once Swagger UI adopts OpenAPI 3.1.0.