Skip to content

Commit

Permalink
Add support for custom Swagger/ReDoc UI files URLs
Browse files Browse the repository at this point in the history
* Add support for custom Swagger UI files URL sources

* refactor: expose cdn opts

- added tests

* Fix test assertion

* Apply corrections before merge

- Do not assume that URLs are for CDNs
- Remove the connection between OpenAPIHandler and class with files URLs

* Restore contribution for PR #423

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update __init__.py

---------

Co-authored-by: benefactarch <[email protected]>
  • Loading branch information
RobertoPrevato and joshua-auchincloss authored Dec 15, 2023
1 parent 4c74ee5 commit 1a4ca42
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 56 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.2] - 2023-12-15 :christmas_tree:

- Upgrades default SwaggerUI files to version 5, by @sinisaos
- Fixes #427, handling WebSocket errors according to ASGI specification, by @Klavionik
- Adds support for custom files URLs for ReDoc and Swagger UI, by @joshua-auchincloss

## [2.0.1] - 2023-12-09 :mount_fuji:

- Fixes #441 causing the `refresh_token` endpoint for OpenID Connect
integrations to not work when authentication is required by default.
- Fixes #427, handling WebSocket errors according to ASGI specification.
- Fixes #443, raising a detailed exception when more than one application is
sharing the same instance of `Router`
- Fixes #438 and #436, restoring support for `uvicorn` used programmatically
Expand Down
2 changes: 1 addition & 1 deletion blacksheep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
used types to reduce the verbosity of the imports statements.
"""
__author__ = "Roberto Prevato <[email protected]>"
__version__ = "2.0.1"
__version__ = "2.0.2"

from .contents import Content as Content
from .contents import FormContent as FormContent
Expand Down
61 changes: 55 additions & 6 deletions blacksheep/server/openapi/ui.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Callable
from typing import Callable, Optional

from blacksheep.messages import Request, Response
from blacksheep.server.files.static import get_response_for_static_content
from blacksheep.server.resources import get_resource_file_content
from blacksheep.utils.time import utcnow

SWAGGER_UI_JS_URL = (
"https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"
)
SWAGGER_UI_CSS_URL = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css"
SWAGGER_UI_FONT = None

REDOC_UI_JS_URL = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
REDOC_UI_CSS_URL = None
REDOC_UI_FONT_URL = (
"https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
)


@dataclass
class UIFilesOptions:
js_url: str
css_url: Optional[str] = None
fonts_url: Optional[str] = None


@dataclass
class UIOptions:
Expand All @@ -15,9 +34,17 @@ class UIOptions:


class UIProvider(ABC):
def __init__(self, ui_path: str) -> None:
ui_files: UIFilesOptions
ui_path: str

def __init__(
self,
ui_path: str,
ui_files: Optional[UIFilesOptions] = None,
) -> None:
super().__init__()
self.ui_path = ui_path
self.ui_files = ui_files if ui_files else self.default_ui_files

@abstractmethod
def build_ui(self, options: UIOptions) -> None:
Expand All @@ -31,10 +58,18 @@ def get_ui_handler(self) -> Callable[[Request], Response]:
Returns a request handler for the route that serves a UI.
"""

@property
def default_ui_files(self) -> UIFilesOptions:
...


class SwaggerUIProvider(UIProvider):
def __init__(self, ui_path: str = "/docs") -> None:
super().__init__(ui_path)
def __init__(
self,
ui_path: str = "/docs",
ui_files_options: Optional[UIFilesOptions] = None,
) -> None:
super().__init__(ui_path, ui_files_options)

self._ui_html: bytes = b""

Expand All @@ -46,6 +81,8 @@ def get_openapi_ui_html(self, options: UIOptions) -> str:
get_resource_file_content("swagger-ui.html")
.replace("##SPEC_URL##", options.spec_url)
.replace("##PAGE_TITLE##", options.page_title)
.replace("##JS_URL##", self.ui_files.js_url)
.replace("##CSS_URL##", self.ui_files.css_url or "")
)

def build_ui(self, options: UIOptions) -> None:
Expand All @@ -61,10 +98,16 @@ def get_open_api_ui(request: Request) -> Response:

return get_open_api_ui

@property
def default_ui_files(self) -> UIFilesOptions:
return UIFilesOptions(SWAGGER_UI_JS_URL, SWAGGER_UI_CSS_URL, SWAGGER_UI_FONT)


class ReDocUIProvider(UIProvider):
def __init__(self, ui_path: str = "/redocs") -> None:
super().__init__(ui_path)
def __init__(
self, ui_path: str = "/redocs", ui_files: Optional[UIFilesOptions] = None
) -> None:
super().__init__(ui_path, ui_files)

self._ui_html: bytes = b""

Expand All @@ -76,6 +119,8 @@ def get_openapi_ui_html(self, options: UIOptions) -> str:
get_resource_file_content("redoc-ui.html")
.replace("##SPEC_URL##", options.spec_url)
.replace("##PAGE_TITLE##", options.page_title)
.replace("##JS_URL##", self.ui_files.js_url)
.replace("##FONT_URL##", self.ui_files.fonts_url or "")
)

def build_ui(self, options: UIOptions) -> None:
Expand All @@ -90,3 +135,7 @@ def get_open_api_ui(request: Request) -> Response:
)

return get_open_api_ui

@property
def default_ui_files(self) -> UIFilesOptions:
return UIFilesOptions(REDOC_UI_JS_URL, REDOC_UI_CSS_URL, REDOC_UI_FONT_URL)
10 changes: 5 additions & 5 deletions blacksheep/server/res/redoc-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<html>
<head>
<title>##PAGE_TITLE##</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/favicon.png"/>
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.png" />
<link href="##FONT_URL##" rel="stylesheet" />
<style>
body {
margin: 0;
Expand All @@ -15,6 +15,6 @@
</head>
<body>
<redoc spec-url="##SPEC_URL##"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
<script src="##JS_URL##"></script>
</body>
</html>
40 changes: 21 additions & 19 deletions blacksheep/server/res/swagger-ui.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<head>
<title>##PAGE_TITLE##</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.png" />
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
</head>
<body>
<link type="text/css" rel="stylesheet" href="##CSS_URL##" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script src="##JS_URL##"></script>
<script>
const ui = SwaggerUIBundle({
url: '##SPEC_URL##',
oauth2RedirectUrl: window.location.origin + '/docs/oauth2-redirect',
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout",
deepLinking: true,
showExtensions: true,
showCommonExtensions: true
})
const ui = SwaggerUIBundle({
url: "##SPEC_URL##",
oauth2RedirectUrl: window.location.origin + "/docs/oauth2-redirect",
dom_id: "#swagger-ui",
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout",
deepLinking: true,
showExtensions: true,
showCommonExtensions: true
});
</script>
</body>
</body>
</html>
20 changes: 20 additions & 0 deletions itests/app_4.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
from datetime import datetime

import uvicorn
from openapidocs.v3 import Info

from blacksheep import JSONContent, Response
from blacksheep.server import Application
from blacksheep.server.bindings import FromJSON
from blacksheep.server.compression import use_gzip_compression
from blacksheep.server.openapi.ui import ReDocUIProvider, UIFilesOptions
from blacksheep.server.openapi.v3 import OpenAPIHandler
from blacksheep.server.responses import json
from blacksheep.server.websocket import WebSocket
from blacksheep.settings.json import default_json_dumps, json_settings

from .utils import get_test_files_url

SINGLE_PID = None


Expand Down Expand Up @@ -150,6 +155,21 @@ async def echo_json(websocket: WebSocket):
await websocket.send_json(msg)


docs = OpenAPIHandler(info=Info(title="Cats API", version="0.0.1"))
docs.ui_providers[0].ui_files = UIFilesOptions(
js_url=get_test_files_url("swag-js"),
css_url=get_test_files_url("swag-css"),
)
docs.ui_providers.append(
ReDocUIProvider(
ui_files=UIFilesOptions(
js_url=get_test_files_url("redoc-js"),
fonts_url=get_test_files_url("redoc-fonts"),
)
)
)
docs.bind_app(app_4)

if __name__ == "__main__":
configure_json_settings()
uvicorn.run(app_4, host="127.0.0.1", port=44557, log_level="debug")
Loading

0 comments on commit 1a4ca42

Please sign in to comment.