Skip to content

Commit

Permalink
Add an 'offline' option to embed the spec in the rendered page (#14)
Browse files Browse the repository at this point in the history
Add 'embed' mode for OpenAPI spec

When `embed` mode is enabled, an OpenAPI spec will be embed into HTML
page as a JSON blob. Why it can be useful? Well, the use case is simple
as the following:

 * Sphinx produces HTML files ready to be served by a web server, or
   ready to be opened directly in browser.

 * Hence, it would be nice to preserve compatibility with the use case
   where someone can open produced HTML files in browser and get
   everything working (no need for web server).
  • Loading branch information
etene authored and ikalnytskyi committed Jul 28, 2018
1 parent 6b1865d commit cda6a47
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 21 deletions.
8 changes: 8 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ do is to:
'name': 'Batcomputer API',
'page': 'api',
'spec': 'specs/batcomputer.yml',
'embed': True,
},
{
'name': 'Example API',
Expand Down Expand Up @@ -74,6 +75,13 @@ do is to:
A path to an OpenAPI spec to be rendered. Can be either an HTTP(s)
link to external source, or filesystem path relative to conf directory.

``embed`` (default: ``False``)
If ``True``, the ``spec`` will be embedded into the rendered HTML page.
Useful for cases when a browsable API ready to be used without any web
server is needed.
The ``spec`` must be an ``UTF-8`` encoded JSON on YAML OpenAPI spec;
embedding an external ``spec`` is currently not supported.

``opts``
An optional dictionary with some of ReDoc settings that might be
useful. Here they are
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'jinja2 >= 2.4',
'sphinx >= 1.5',
'six >= 1.5',
'PyYAML >= 3.12',
],
classifiers=[
'Topic :: Documentation',
Expand Down
16 changes: 15 additions & 1 deletion sphinxcontrib/redoc.j2
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</style>
</head>
<body>
<redoc spec-url="{{ pathto(spec, 1) }}"
<redoc
{{ 'lazy-rendering' if opts['lazy-rendering'] }}
{{ 'suppress-warnings' if opts['suppress-warnings'] }}
{{ 'hide-hostname' if opts['hide-hostname'] }}
Expand All @@ -21,6 +21,20 @@
{{ 'untrusted-spec' if opts['untrusted-spec'] }}
{{ 'expand-responses="%s"' % ','.join(opts['expand-responses']) if opts['expand-responses'] }}>
</redoc>

<script src="{{ pathto('_static/redoc.js', 1) }}"></script>
{% if embed %}
<script type="application/json" id="spec">
{{ spec }}
</script>
{% endif %}
<script>
{% if embed %}
var spec = JSON.parse(document.getElementById("spec").innerHTML);
{% else %}
var spec = "{{ spec }}";
{% endif %}
Redoc.init(spec);
</script>
</body>
</html>
24 changes: 21 additions & 3 deletions sphinxcontrib/redoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

import io
import os
import json
import yaml

import jinja2
import pkg_resources
Expand All @@ -30,10 +32,26 @@ def render(app):
# relies on them.
ctx.setdefault('opts', {})

# In embed mode, we are going to embed the whole OpenAPI spec into
# produced HTML. The rationale is very simple: we want to produce
# browsable HTMLs ready to be used without any web server.
if ctx.get('embed') is True:
# Parse & dump the spec to have it as properly formatted json
specfile = os.path.join(app.confdir, ctx['spec'])
with io.open(specfile, encoding='utf-8') as specfp:
try:
spec_contents = yaml.load(specfp)
except ValueError as ver:
raise ValueError('Cannot parse spec %r: %s'
% (ctx['spec'], ver))

ctx['spec'] = json.dumps(spec_contents)

# The 'spec' may contain either HTTP(s) link or filesystem path. In
# case of later we need to copy the spec into output directory, as
# otherwise it won't be available when the result is deployed.
if not ctx['spec'].startswith(('http', 'https')):
elif not ctx['spec'].startswith(('http', 'https')):

specpath = os.path.join(app.builder.outdir, '_specs')
specname = os.path.basename(ctx['spec'])

Expand All @@ -46,8 +64,8 @@ def render(app):
os.path.join(app.confdir, ctx['spec']),
os.path.join(specpath, specname))

# The link inside rendered document must refer to a new location,
# the place where it has being copied to.
# The link inside the rendered document must refer to a new
# location, the place where it has been copied to.
ctx['spec'] = os.path.join('_specs', specname)

# Propagate information about page rendering to Sphinx. There's
Expand Down
48 changes: 31 additions & 17 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import textwrap

import yaml
import json
import py
import pytest
import pkg_resources
Expand All @@ -21,7 +22,14 @@ def run_sphinx(tmpdir):
spec = py.path.local(here).join('..', 'docs', '_specs', 'github.yml')
spec.copy(src.mkdir('_specs').join('github.yml'))

def run(conf):
def run(redoc_options, conf=None):
defaultconf = {'name': 'Github API (v3)',
'page': 'api/github/index',
'spec': '_specs/github.yml',
}
if conf:
defaultconf.update(conf)
defaultconf['opts'] = redoc_options
confpy = jinja2.Template(textwrap.dedent('''
import os
Expand All @@ -31,21 +39,9 @@ def run(conf):
extensions = ['sphinxcontrib.redoc']
source_suffix = '.rst'
master_doc = 'index'
redoc = [
{
'name': 'Github API (v3)',
'page': 'api/github/index',
'spec': '_specs/github.yml',
{% if opts is not none %}
'opts': {
{% for key, value in opts.items() %}
'{{ key }}': {{ value }},
{% endfor %}
},
{% endif %}
},
]
''')).render(opts=conf)
redoc = {{ redoc }}
''')).render(redoc=[defaultconf])

src.join('conf.py').write_text(confpy, encoding='utf-8')
src.join('index.rst').ensure()

Expand Down Expand Up @@ -147,7 +143,25 @@ def test_redocjs_page_is_generated(run_sphinx, tmpdir, options, attributes):
html = tmpdir.join('out').join('api', 'github', 'index.html').read()
soup = bs4.BeautifulSoup(html, 'html.parser')

# spec url is passed directly as the first arg to the redoc init
del attributes["spec-url"]

assert soup.title.string == 'Github API (v3)'
assert soup.redoc.attrs == attributes
assert soup.script.attrs['src'] == os.path.join(
'..', '..', '_static', 'redoc.js')


def test_embedded_spec(run_sphinx, tmpdir):
run_sphinx({}, conf={'embed': True})
html = tmpdir.join('out').join('api', 'github', 'index.html').read()
specfile = tmpdir.join('src', '_specs', 'github.yml')
soup = bs4.BeautifulSoup(html, 'html.parser')

with open(str(specfile)) as fp:
original_spec = yaml.load(fp)

embedded_spec = soup.find(id='spec').get_text()
# ensure the embedded spec is present and corresponds to the original
assert embedded_spec
assert json.loads(embedded_spec) == original_spec
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ deps =
pytest
flake8
beautifulsoup4
pyyaml
commands =
{envpython} setup.py check --strict
{envpython} -m flake8 {posargs:.}
Expand Down

0 comments on commit cda6a47

Please sign in to comment.