Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

swagger_ui: add directives that provide API Explorer and spec information #79

Merged
merged 4 commits into from
Mar 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ Alphabetically-ordered List of the people who contributed to Cornice Swagger:
- Gabriela Surita <gsurita@mozilla.com>
- Jason Haury
- Josip Delic
- Marcin Lulek
- Simone Marzola <marzolasimone@gmail.com>
59 changes: 59 additions & 0 deletions cornice_swagger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from cornice_swagger.swagger import CorniceSwagger
from pyramid.security import NO_PERMISSION_REQUIRED

__author__ = """Josip Delic"""
__email__ = 'delicj@delijati.net'
Expand Down Expand Up @@ -28,3 +29,61 @@ def includeme(config):
config.add_view_predicate('tags', CorniceSwaggerPredicate)
config.add_view_predicate('operation_id', CorniceSwaggerPredicate)
config.add_view_predicate('api_security', CorniceSwaggerPredicate)
config.add_directive(
'cornice_enable_openapi_view', cornice_enable_openapi_view)
config.add_directive(
'cornice_enable_openapi_explorer', cornice_enable_openapi_explorer)


def cornice_enable_openapi_view(
config,
api_path='/api-explorer/swagger.json',
permission=NO_PERMISSION_REQUIRED,
route_factory=None):
"""
:param config:
Pyramid configurator object
:param api_path:
where to expose swagger JSON definition view
:param permission:
pyramid permission for those views
:param route_factory:
factory for context object for those routes

This registers and configures the view that serves api definitions
"""

config.add_route('cornice_swagger.open_api_path', api_path,
factory=route_factory)
config.add_view('cornice_swagger.views.open_api_json_view',
renderer='json', permission=permission,
route_name='cornice_swagger.open_api_path')


def cornice_enable_openapi_explorer(
config,
api_explorer_path='/api-explorer',
permission=NO_PERMISSION_REQUIRED,
route_factory=None,
**kwargs):
"""
:param config:
Pyramid configurator object
:param api_explorer_path:
where to expose Swagger UI interface view
:param permission:
pyramid permission for those views
:param route_factory:
factory for context object for those routes
:param kwargs:
kwargs that will be passed to CorniceSwagger's `generate()`

This registers and configures the view that serves api explorer
"""

config.registry.settings['cornice_swagger.spec_kwargs'] = kwargs
config.add_route('cornice_swagger.api_explorer_path', api_explorer_path,
factory=route_factory)
config.add_view('cornice_swagger.views.swagger_ui_template_view',
permission=permission,
route_name='cornice_swagger.api_explorer_path')
73 changes: 73 additions & 0 deletions cornice_swagger/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Ship the font here maybe instead of leaking to google?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old method will still work.

We would need to ship statics and update them in the package - the fonts link exists also in the official Swagger UI package, its not something I've made - I did point to swagger UI JS files on CDN to avoid shipping big statics.

<link rel="stylesheet" type="text/css" href="${ui_css_url}" >
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}

body {
margin:0;
background: #fafafa;
}
</style>
</head>

<body>

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>

<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
</symbol>

<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
</symbol>

<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
</symbol>

<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
</symbol>


<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
</symbol>

<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
</symbol>

</defs>
</svg>

<div id="swagger-ui"></div>

<script src="${ui_js_bundle_url}"> </script>
<script src="${ui_js_standalone_url}"> </script>
${swagger_ui_script}
</body>

</html>
21 changes: 21 additions & 0 deletions cornice_swagger/templates/index_script_template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script>
window.onload = function() {

// Build a system
const ui = SwaggerUIBundle({
url: "${swagger_spec_url}",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})

window.ui = ui
}
</script>
75 changes: 75 additions & 0 deletions cornice_swagger/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import importlib
import pkg_resources
from string import Template

import cornice
import cornice_swagger
from pyramid.response import Response

# hardcode for now since that will work for vast majority of users
# maybe later add minified resources for behind firewall support?
ui_css_url = 'https://cdnjs.cloudflare.com/ajax/libs/' \
'swagger-ui/3.12.9/swagger-ui.css'
ui_js_bundle_url = 'https://cdnjs.cloudflare.com/ajax/libs/' \
'swagger-ui/3.12.9/swagger-ui-bundle.js'
ui_js_standalone_url = 'https://cdnjs.cloudflare.com/ajax/libs/' \
'swagger-ui/3.12.9/swagger-ui-standalone-preset.js'


def swagger_ui_template_view(request):
"""
Serves Swagger UI page, default Swagger UI config is used but you can
override the callable that generates the `<script>` tag by setting
`cornice_swagger.swagger_ui_script_generator` in pyramid config, it defaults
to 'cornice_swagger.views:swagger_ui_script_template'

:param request:
:return:
"""
script_generator = request.registry.settings.get(
'cornice_swagger.swagger_ui_script_generator',
'cornice_swagger.views:swagger_ui_script_template')
package, callable = script_generator.split(':')
imported_package = importlib.import_module(package)
script_callable = getattr(imported_package, callable)
template = pkg_resources.resource_string(
'cornice_swagger', 'templates/index.html').decode('utf8')

html = Template(template).safe_substitute(
ui_css_url=ui_css_url,
ui_js_bundle_url=ui_js_bundle_url,
ui_js_standalone_url=ui_js_standalone_url,
swagger_ui_script=script_callable(request),
)
return Response(html)


def open_api_json_view(request):
"""
:param request:
:return:

Generates JSON representation of Swagger spec
"""
doc = cornice_swagger.CorniceSwagger(cornice.service.get_services())
kwargs = request.registry.settings['cornice_swagger.spec_kwargs']
my_spec = doc.generate(**kwargs)
return my_spec
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be an example on how to replace this with a custom view.

For example, in Kinto we inherit the CorniceSwagger class and instantiate our own stuff the the view :)

https://github.com/Kinto/kinto/blob/8db533939f47d33c44deb66e0d1285258d99d68b/kinto/core/views/openapi.py#L22-L32

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you want to replace this with a custom view - then you don't use the directive, because it basicly just adds 2 views so you don't have to override them. If someone wants to override they'd have to redo all the work directive does basicly.



def swagger_ui_script_template(request, **kwargs):
"""
:param request:
:return:

Generates the <script> code that bootstraps Swagger UI, it will be injected
into index template
"""
swagger_spec_url = request.route_url('cornice_swagger.open_api_path')
template = pkg_resources.resource_string(
'cornice_swagger',
'templates/index_script_template.html'
).decode('utf8')
return Template(template).safe_substitute(
swagger_spec_url=swagger_spec_url,
)
16 changes: 16 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ Basic Generator
:members:
:member-order: bysource

cornice_enable_openapi_view directive
=====================================

.. py:module:: cornice_swagger

.. autofunction:: cornice_swagger.cornice_enable_openapi_view


cornice_enable_openapi_explorer directive
=========================================

.. py:module:: cornice_swagger

.. autofunction:: cornice_swagger.cornice_enable_openapi_explorer


Generator Internals
===================

Expand Down
20 changes: 20 additions & 0 deletions docs/source/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ You can than create your OpenAPI/Swagger JSON using::
my_spec = my_generator('MyAPI', '1.0.0')


Alternatively you can use a directive to set up OpenAPI/Swagger JSON and
serve API explorer on your application::


config = Configurator()
config.include('cornice')
config.include('cornice_swagger')
config.cornice_enable_openapi_view(
api_path='/api-explorer/swagger.json'
)
config.cornice_enable_openapi_explorer(
api_explorer_path='/api-explorer',
title='MyAPI',
description="OpenAPI documentation",
version='1.0.0')

Then you will be able to access Swagger UI API explorer on url:

http://localhost:8000/api-explorer (in the example above)

Using a scaffold
================

Expand Down
30 changes: 30 additions & 0 deletions docs/source/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,23 @@ your Pyramid config. For that you may use:
config = Configurator()
config.include('cornice')
config.include('cornice_swagger')
config.cornice_enable_openapi_view()
config.cornice_enable_openapi_explorer(
title='MyAPI',
description="OpenAPI documentation",
version='1.0.0')


If you don't know what this is about or need more information, please check the
`Pyramid documentation <http://docs.pylonsproject.org/projects/pyramid>`_

By default API explorer will be served under `/api-explorer` path in your
application. You can easily configure the paths, required permissions and
Pyramid route factory.

Additional ``kwargs`` passed to this directive will be passed to
``CorniceSwagger.generate`` method.


Extracting path parameters
==========================
Expand Down Expand Up @@ -448,3 +460,21 @@ the following swagger summary:
}
}
}

Custom Swagger UI <script> bootstrap
====================================

By default standard Swagger UI (https://swagger.io/swagger-ui/)
config is used, but you can customize the generated script tag
by providing your own callable path in config.

The default one is:

``cornice_swagger.swagger_ui_script_generator = cornice_swagger.views:swagger_ui_script_template``

It points to the following callable that accepts a request object:

.. literalinclude:: ../../cornice_swagger/views.py
:pyobject: swagger_ui_script_template
:language: python

28 changes: 13 additions & 15 deletions examples/minimalist.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import colander
from cornice import Service
from cornice.service import get_services
from cornice.validators import colander_body_validator
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from cornice_swagger import CorniceSwagger


_VALUES = {}
Expand Down Expand Up @@ -53,24 +51,21 @@ def set_value(request):
return _VALUES.get(key)


# Create a service to serve our OpenAPI spec
swagger = Service(name='OpenAPI',
path='/__api__',
description="OpenAPI documentation")


@swagger.get()
def openAPI_spec(request):
doc = CorniceSwagger(get_services())
my_spec = doc.generate('MyAPI', '1.0.0')
return my_spec
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this to another example?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the minimalist example should show the easiest way to archieve that?
How to generate the docs is mentioned above so I'm not sure if it makes sense to duplicate the example just show that.



# Setup and run our app
def setup():
config = Configurator()
config.include('cornice')
config.include('cornice_swagger')
# Create views to serve our OpenAPI spec
config.cornice_enable_openapi_view(
api_path='/__api__'
)
# Create views to serve OpenAPI spec UI explorer
config.cornice_enable_openapi_explorer(
api_explorer_path='/api-explorer',
title='MyAPI',
description="OpenAPI documentation",
version='1.0.0')
config.scan()
app = config.make_wsgi_app()
return app
Expand All @@ -79,4 +74,7 @@ def setup():
if __name__ == '__main__':
app = setup()
server = make_server('127.0.0.1', 8000, app)
print('Visit me on http://127.0.0.1:8000')
print('''You can see the API explorer here:
http://127.0.0.1:8000/api-explorer''')
server.serve_forever()
Loading