Skip to content

Commit

Permalink
Add support for inline swagger pages (#54)
Browse files Browse the repository at this point in the history
Closes #53
  • Loading branch information
kasium authored Jul 3, 2023
1 parent 856795d commit 11580f0
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 3.2.0

* Support for inline swagger pages

## 3.1.0

* Support Sphinx 7
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ Just run `pip install swagger-plugin-for-sphinx`

## Usage

### Enable the plugin

First, add the plugin to the extensions list:
```python
extensions = ["swagger_plugin_for_sphinx"]
```

### Global configuration

Then add the main configuration for swagger:
```python
swagger_present_uri = ""
Expand All @@ -28,12 +32,15 @@ swagger_css_uri = ""
These correspond to the modules explained [here](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/installation.md).
By default, the latest release is used from [here](https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest).


### Standalone page
As a last step, define the swagger configuration as follows:
```python
swagger = [
{
"name": "Service API",
"page": "openapi",
"id": "my-page",
"options": {
"url": "openapi.yaml",
},
Expand All @@ -44,12 +51,21 @@ Each item on the list will generate a new swagger HTML page.
The `name` is the HTML page name and `page` defines the file name without an extension. This needs to be included in the TOC.
The `options` are then used for the `SwaggerUIBundle` as defined [here](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md).
Please don't specify the `dom_id` since it's hardcoded in the HTML page.
If the specification is provided as a file, don't forget to copy it (e.g., by putting it into the `html_static_path`).
To silence the warning `toctree contains reference to nonexisting document`, just put a dummy file with the same name as `page` into the source folder.

In the sphinx build, an HTML page is created and put into the `_static` directory of the build.
## Inline swagger page
To include a swagger page into a sphinx page use the directive ``inline-swagger``:

If the specification is provided as a file, don't forget to copy it (e.g., by putting it into the `html_static_path`).
```rst
.. inline-swagger::
:id: my-page
```

To silence the warning `toctree contains reference to nonexisting document`, just put a dummy file with the same name as `page` into the source folder.
The ``id`` links to an existing configuration in ``conf.py`` as shows above.
In this case, the configuration ``page`` will be ignored.
Behind the scenes, a swagger HTML page is generated and then inserted using the ``.. raw::``
directive.

## Build and Publish

Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "swagger-plugin-for-sphinx"
version = "3.1.0"
version = "3.2.0"
description = "Sphinx plugin which renders a OpenAPI specification with Swagger"
authors = [{ name = "Kai Mueller", email = "kai.mueller01@sap.com"}]
readme = "README.md"
Expand All @@ -31,6 +31,7 @@ requires-python = ">=3.8,<4"
dependencies = [
"sphinx>=6.0,<8",
"jinja2~=3.0",
"docutils",
]

[project.license]
Expand All @@ -53,6 +54,7 @@ dev = [
"isort==5.12.0",
"flake8==6.0.0",
"mypy==1.3.0",
"types-docutils==0.20.0.1"
]

[tool.setuptools.packages.find]
Expand Down
48 changes: 48 additions & 0 deletions swagger_plugin_for_sphinx/_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,57 @@
from typing import Any, Iterator

import jinja2
from docutils.parsers.rst import directives
from docutils.parsers.rst.directives.misc import Raw
from sphinx.application import Sphinx
from sphinx.errors import SphinxError

_HERE = Path(__file__).parent.resolve()


class InlineSwaggerDirective(Raw): # type:ignore[misc]
"""Directive for inline swagger pages."""

required_arguments = 0
option_spec = {"id": directives.unchanged_required}
has_content = False

def run(self) -> Any:
app: Sphinx = self.state.document.settings.env.app
template_path = _HERE / "swagger.j2"

# find the right configuration for the page
for index, context in enumerate(app.config.swagger):
if context.get("id") == self.options["id"]:
break
else:
raise SphinxError(
f"Cannot find any swagger configuration with id '{self.options['id']}'"
)

# remove the configuration, so that we have no double generation
app.config.swagger.pop(index)

with template_path.open(encoding="utf-8") as handle:
template = jinja2.Template(handle.read())

context.setdefault("options", {})
context["css_uri"] = app.config.swagger_css_uri
context["bundle_uri"] = app.config.swagger_bundle_uri
context["present_uri"] = app.config.swagger_present_uri

static_folder = Path(app.builder.outdir) / "_static"
static_folder.mkdir(exist_ok=True)
content = template.render(context)

html_file = static_folder / (context["id"] + ".html")
html_file.write_text(content)

self.arguments = ["html"]
self.options["file"] = str(html_file)
return super().run()


def render(app: Sphinx) -> Iterator[tuple[Any, ...]]:
"""Render the swagger HTML pages."""
for context in app.config.swagger:
Expand Down Expand Up @@ -71,6 +117,8 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.connect("html-collect-pages", render)
app.connect("build-finished", assets)

app.add_directive("inline-swagger", InlineSwaggerDirective)

return {
"version": version("swagger_plugin_for_sphinx"),
"parallel_read_safe": True,
Expand Down
28 changes: 28 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,34 @@ def test(sphinx_runner: SphinxRunner, tmp_path: Path) -> None:
assert expected == html


def test_inline(sphinx_runner: SphinxRunner, tmp_path: Path) -> None:
docs = tmp_path / "docs"
api_file = docs / "api.rst"
api_file.write_text(
"API\n===\n\n.. inline-swagger::\n :id: myid\n", encoding="utf-8"
)

sphinx_runner(
swagger=[
{"name": "API1", "page": "openapi", "options": {"url": "openapi.yaml"}},
{"name": "API2", "id": "myid", "options": {"url": "openapi.yaml"}},
]
)

build = tmp_path / "build"
static = build / "_static"

assert (static / "swagger-ui.css").is_file()
assert (static / "swagger-ui-bundle.js").is_file()
assert (static / "swagger-ui-standalone-preset.js").is_file()

with open(build / "api.html", encoding="utf-8") as file:
html = file.read()

assert '<script src="_static/sphinx_highlight.js">' in html
assert "window.ui = SwaggerUIBundle(config);" in html


@pytest.mark.parametrize(
"present_uri,bundle_uri,css_uri,expected_present_uri,expected_bundle_uri,expected_css_uri",
[
Expand Down

0 comments on commit 11580f0

Please sign in to comment.