Skip to content

Commit

Permalink
Merge pull request sphinx-contrib#71 from sphinx-contrib/arch/renderers
Browse files Browse the repository at this point in the history
Add renderers concept
  • Loading branch information
ikalnytskyi authored Dec 29, 2019
2 parents f4d8e20 + 4df907f commit da54913
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 117 deletions.
44 changes: 38 additions & 6 deletions sphinxcontrib/openapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@
:license: BSD, see LICENSE for details.
"""


from pkg_resources import get_distribution, DistributionNotFound

from sphinxcontrib.openapi import directive
from sphinxcontrib.openapi import renderers, directive

try:
__version__ = get_distribution(__name__).version
Expand All @@ -21,8 +19,42 @@
__version__ = None


_BUILTIN_RENDERERS = {
"httpdomain:old": renderers.HttpdomainOldRenderer,
}
_DEFAULT_RENDERER_NAME = "httpdomain:old"


def _register_rendering_directives(app, conf):
"""Register rendering directives based on effective configuration."""

renderers_map = dict(_BUILTIN_RENDERERS, **conf.openapi_renderers)

for renderer_name, renderer_cls in renderers_map.items():
app.add_directive(
"openapi:%s" % renderer_name,
directive.create_directive_from_renderer(renderer_cls),
)

if conf.openapi_default_renderer not in renderers_map:
raise ValueError(
"invalid 'openapi_default_renderer' value: "
"no such renderer: '%s'" % conf.openapi_default_renderer
)

app.add_directive(
"openapi",
directive.create_directive_from_renderer(
renderers_map[conf.openapi_default_renderer]
),
)


def setup(app):
app.setup_extension('sphinxcontrib.httpdomain')
app.add_directive('openapi', directive.OpenApi)
app.add_config_value("openapi_default_renderer", _DEFAULT_RENDERER_NAME, "html")
app.add_config_value("openapi_renderers", {}, "html")

app.setup_extension("sphinxcontrib.httpdomain")
app.connect("config-inited", _register_rendering_directives)

return {'version': __version__, 'parallel_read_safe': True}
return {"version": __version__, "parallel_read_safe": True}
13 changes: 6 additions & 7 deletions sphinxcontrib/openapi/__main__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import argparse
import logging

from sphinxcontrib.openapi import directive
from sphinxcontrib.openapi import directive, renderers


def main():
Expand Down Expand Up @@ -58,13 +58,12 @@ def main():
openapi_options['examples'] = True
if options.group:
openapi_options['group'] = True
openapihttpdomain, spec = \
directive.get_openapihttpdomain(
openapi_options,
options.input,
options.encoding)

for line in openapihttpdomain(spec, **openapi_options):
openapi_options.setdefault('uri', 'file://%s' % options.input)
spec = directive._get_spec(options.input, options.encoding)
renderer = renderers.HttpdomainOldRenderer(None, openapi_options)

for line in renderer.render_restructuredtext_markup(spec):
options.output.write(line+'\n')
logging.debug(line)

Expand Down
109 changes: 35 additions & 74 deletions sphinxcontrib/openapi/directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,10 @@
import collections
import functools

from docutils import nodes
from docutils.parsers.rst import Directive, directives
from docutils.statemachine import ViewList
from docutils.parsers.rst import directives
from sphinx.util.docutils import SphinxDirective
import yaml

from sphinx.util.nodes import nested_parse_with_titles

from sphinxcontrib.openapi import openapi20
from sphinxcontrib.openapi import openapi30


# Dictionaries do not guarantee to preserve the keys order so when we load
# JSON or YAML - we may loose the order. In most cases it's not important
Expand All @@ -45,69 +39,36 @@ def _get_spec(abspath, encoding):
return yaml.load(stream, _YamlOrderedLoader)


def get_openapihttpdomain(options, abspath, encoding):
spec = _get_spec(abspath, encoding)

# URI parameter is crucial for resolving relative references. So
# we need to set this option properly as it's used later down the
# stack.
options.setdefault('uri', 'file://%s' % abspath)

# We support both OpenAPI 2.0 (f.k.a. Swagger) and OpenAPI 3.0.0, so
# determine which version we are parsing here.
spec_version = spec.get('openapi', spec.get('swagger', '2.0'))
if spec_version.startswith('2.'):
openapihttpdomain = openapi20.openapihttpdomain
elif spec_version.startswith('3.'):
openapihttpdomain = openapi30.openapihttpdomain
else:
raise ValueError('Unsupported OpenAPI version (%s)' % spec_version)
return openapihttpdomain, spec


class OpenApi(Directive):

required_arguments = 1 # path to openapi spec
final_argument_whitespace = True # path may contain whitespaces
option_spec = {
'encoding': directives.encoding, # useful for non-ascii cases :)
'paths': lambda s: s.split(), # endpoints to be rendered
'include': lambda s: s.split(), # endpoints to be included (regexp)
'exclude': lambda s: s.split(), # endpoints to be excluded (regexp)
'request': directives.flag, # print the request body structure
'examples': directives.flag, # render examples when passed
'group': directives.flag, # group paths by tag when passed
'format': str, # "rst" (default) or "markdown"
}

def run(self):
env = self.state.document.settings.env
relpath, abspath = env.relfn2path(directives.path(self.arguments[0]))

# Add OpenAPI spec as a dependency to the current document. That means
# the document will be rebuilt if the spec is changed.
env.note_dependency(relpath)

# Read the spec using encoding passed to the directive or fallback to
# the one specified in Sphinx's config.
encoding = self.options.get('encoding', env.config.source_encoding)

# Open the specification file
openapihttpdomain, spec = \
get_openapihttpdomain(self.options, abspath, encoding)

# reStructuredText DOM manipulation is pretty tricky task. It requires
# passing dozen arguments which is not easy without well-documented
# internals. So the idea here is to represent OpenAPI spec as
# reStructuredText in-memory text and parse it in order to produce a
# real DOM.
viewlist = ViewList()
for line in openapihttpdomain(spec, **self.options):
viewlist.append(line, '<openapi>')

# Parse reStructuredText contained in `viewlist` and return produced
# DOM nodes.
node = nodes.section()
node.document = self.state.document
nested_parse_with_titles(self.state, viewlist, node)
return node.children
def create_directive_from_renderer(renderer_cls):
"""Create rendering directive from a renderer class."""

class _RenderingDirective(SphinxDirective):
required_arguments = 1 # path to openapi spec
final_argument_whitespace = True # path may contain whitespaces
option_spec = dict(
{
'encoding': directives.encoding, # useful for non-ascii cases :)
},
**renderer_cls.option_spec
)

def run(self):
relpath, abspath = self.env.relfn2path(directives.path(self.arguments[0]))

# URI parameter is crucial for resolving relative references. So we
# need to set this option properly as it's used later down the
# stack.
self.options.setdefault('uri', 'file://%s' % abspath)

# Add a given OpenAPI spec as a dependency of the referring
# reStructuredText document, so the document is rebuilt each time
# the spec is changed.
self.env.note_dependency(relpath)

# Read the spec using encoding passed to the directive or fallback to
# the one specified in Sphinx's config.
encoding = self.options.get('encoding', self.config.source_encoding)
spec = _get_spec(abspath, encoding)
return renderer_cls(self.state, self.options).render(spec)

return _RenderingDirective
10 changes: 10 additions & 0 deletions sphinxcontrib/openapi/renderers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Here lies OpenAPI renderers."""

from . import abc
from ._httpdomain_old import HttpdomainOldRenderer


__all__ = [
"abc",
"HttpdomainOldRenderer",
]
52 changes: 52 additions & 0 deletions sphinxcontrib/openapi/renderers/_httpdomain_old.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Here lies still breathing and only renderer implementation."""

from docutils.parsers.rst import directives

from . import abc
from .. import openapi20, openapi30, utils


class HttpdomainOldRenderer(abc.RestructuredTextRenderer):

option_spec = {
# A list of endpoints to be rendered. Endpoints must be whitespace
# delimited.
"paths": lambda s: s.split(),
# Regular expression patterns to includes/excludes endpoints to/from
# rendering. Similar to paths, the patterns must be whitespace
# delimited.
"include": lambda s: s.split(),
"exclude": lambda s: s.split(),
# Render the request body structure when passed.
"request": directives.flag,
# Render request/response examples when passed.
"examples": directives.flag, # render examples when passed
# Group endpoints by tags when passed. By default, no grouping is
# applied and endpoints are rendered in the order they met in spec.
"group": directives.flag,
# Markup format to render OpenAPI descriptions.
"format": str,
}

def __init__(self, state, options):
self._state = state
self._options = options

def render_restructuredtext_markup(self, spec):
# OpenAPI spec may contain JSON references, common properties, etc.
# Trying to render the spec "As Is" will require to put multiple if-s
# around the code. In order to simplify rendering flow, let's make it
# have only one (expected) schema, i.e. normalize it.
utils.normalize_spec(spec, **self._options)

# We support both OpenAPI 2.0 (f.k.a. Swagger) and OpenAPI 3.0.0, so
# determine which version we are parsing here.
spec_version = spec.get("openapi", spec.get("swagger", "2.0"))
if spec_version.startswith("2."):
openapihttpdomain = openapi20.openapihttpdomain
elif spec_version.startswith("3."):
openapihttpdomain = openapi30.openapihttpdomain
else:
raise ValueError("Unsupported OpenAPI version (%s)" % spec_version)

yield from openapihttpdomain(spec, **self._options)
46 changes: 46 additions & 0 deletions sphinxcontrib/openapi/renderers/abc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Abstract Base Classes (ABCs) for OpenAPI renderers."""

import abc

from docutils import nodes
from docutils.statemachine import ViewList
from sphinx.util.nodes import nested_parse_with_titles


class Renderer(metaclass=abc.ABCMeta):
"""Base class for OpenAPI renderers."""

def __init__(self, state, options):
self._state = state
self._options = options

@property
@abc.abstractmethod
def option_spec(self):
"""Renderer options and their converting functions."""

@abc.abstractmethod
def render(self, spec):
"""Render a given OpenAPI spec."""


class RestructuredTextRenderer(Renderer):
"""Base class for reStructuredText OpenAPI renderers.
Docutils DOM manipulation is quite a tricky task that requires passing
dozen arguments around. Because of that a lot of Sphinx extensions instead
of constructing DOM nodes directly produce and parse reStructuredText.
This Sphinx extension is not an exception, and that's why this class
exists. It's a convenient extension of :class:`Renderer` that converts
produced markup text into docutils DOM elements.
"""

def render(self, spec):
viewlist = ViewList()
for line in self.render_restructuredtext_markup(spec):
viewlist.append(line, "<openapi>")

node = nodes.section()
node.document = self._state.document
nested_parse_with_titles(self._state, viewlist, node)
return node.children
Loading

0 comments on commit da54913

Please sign in to comment.