Skip to content

Commit

Permalink
Extract Swagger UI functionality into middleware
Browse files Browse the repository at this point in the history
Co-authored-by: Wojciech Paciorek <[email protected]>
  • Loading branch information
RobbeSneyders and arkkors committed Mar 31, 2022
1 parent c87f27f commit bbeb817
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 146 deletions.
4 changes: 2 additions & 2 deletions connexion/apis/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def add_swagger_ui(self):
"""


class AbstractAPI(AbstractSwaggerUIAPI):
class AbstractAPI(AbstractSpecAPI):
"""
Defines an abstract interface for a Swagger API
"""
Expand Down Expand Up @@ -471,4 +471,4 @@ def _serialize_data(cls, data, mimetype):
pass

def json_loads(self, data):
return self.jsonifier.loads(data)
return self.jsonifier.loads(data)
138 changes: 1 addition & 137 deletions connexion/apis/flask_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""

import logging
import pathlib
import warnings
from typing import Any

Expand All @@ -18,7 +17,7 @@
from connexion.jsonifier import Jsonifier
from connexion.lifecycle import ConnexionRequest, ConnexionResponse
from connexion.security import FlaskSecurityHandlerFactory
from connexion.utils import is_json_mimetype, yamldumper
from connexion.utils import is_json_mimetype

logger = logging.getLogger('connexion.apis.flask_api')

Expand All @@ -40,72 +39,6 @@ def _set_blueprint(self):
self.blueprint = flask.Blueprint(endpoint, __name__, url_prefix=self.base_path,
template_folder=str(self.options.openapi_console_ui_from_dir))

def add_openapi_json(self):
"""
Adds spec json to {base_path}/swagger.json
or {base_path}/openapi.json (for oas3)
"""
logger.debug('Adding spec json: %s/%s', self.base_path,
self.options.openapi_spec_path)
endpoint_name = f"{self.blueprint.name}_openapi_json"

self.blueprint.add_url_rule(self.options.openapi_spec_path,
endpoint_name,
self._handlers.get_json_spec)

def add_openapi_yaml(self):
"""
Adds spec yaml to {base_path}/swagger.yaml
or {base_path}/openapi.yaml (for oas3)
"""
if not self.options.openapi_spec_path.endswith("json"):
return

openapi_spec_path_yaml = \
self.options.openapi_spec_path[:-len("json")] + "yaml"
logger.debug('Adding spec yaml: %s/%s', self.base_path,
openapi_spec_path_yaml)
endpoint_name = f"{self.blueprint.name}_openapi_yaml"
self.blueprint.add_url_rule(
openapi_spec_path_yaml,
endpoint_name,
self._handlers.get_yaml_spec
)

def add_swagger_ui(self):
"""
Adds swagger ui to {base_path}/ui/
"""
console_ui_path = self.options.openapi_console_ui_path.strip('/')
logger.debug('Adding swagger-ui: %s/%s/',
self.base_path,
console_ui_path)

if self.options.openapi_console_ui_config is not None:
config_endpoint_name = f"{self.blueprint.name}_swagger_ui_config"
config_file_url = '/{console_ui_path}/swagger-ui-config.json'.format(
console_ui_path=console_ui_path)

self.blueprint.add_url_rule(config_file_url,
config_endpoint_name,
lambda: flask.jsonify(self.options.openapi_console_ui_config))

static_endpoint_name = f"{self.blueprint.name}_swagger_ui_static"
static_files_url = '/{console_ui_path}/<path:filename>'.format(
console_ui_path=console_ui_path)

self.blueprint.add_url_rule(static_files_url,
static_endpoint_name,
self._handlers.console_ui_static_files)

index_endpoint_name = f"{self.blueprint.name}_swagger_ui_index"
console_ui_url = '/{console_ui_path}/'.format(
console_ui_path=console_ui_path)

self.blueprint.add_url_rule(console_ui_url,
index_endpoint_name,
self._handlers.console_ui_home)

def add_auth_on_not_found(self, security, security_definitions):
"""
Adds a 404 error handler to authenticate and only expose the 404 status if the security validation pass.
Expand All @@ -127,13 +60,6 @@ def _add_operation_internal(self, method, path, operation):
function = operation.function
self.blueprint.add_url_rule(flask_path, endpoint_name, function, methods=[method])

@property
def _handlers(self):
# type: () -> InternalHandlers
if not hasattr(self, '_internal_handlers'):
self._internal_handlers = InternalHandlers(self.base_path, self.options, self.specification)
return self._internal_handlers

@classmethod
def get_response(cls, response, mimetype=None, request=None):
"""Gets ConnexionResponse instance for the operation handler
Expand Down Expand Up @@ -267,65 +193,3 @@ def _get_context():


context = LocalProxy(_get_context)


class InternalHandlers:
"""
Flask handlers for internally registered endpoints.
"""

def __init__(self, base_path, options, specification):
self.base_path = base_path
self.options = options
self.specification = specification

def console_ui_home(self):
"""
Home page of the OpenAPI Console UI.
:return:
"""
openapi_json_route_name = "{blueprint}.{prefix}_openapi_json"
escaped = flask_utils.flaskify_endpoint(self.base_path)
openapi_json_route_name = openapi_json_route_name.format(
blueprint=escaped,
prefix=escaped
)
template_variables = {
'openapi_spec_url': flask.url_for(openapi_json_route_name),
**self.options.openapi_console_ui_index_template_variables,
}
if self.options.openapi_console_ui_config is not None:
template_variables['configUrl'] = 'swagger-ui-config.json'

# Use `render_template_string` instead of `render_template` to circumvent the flask
# template lookup mechanism and explicitly render the template of the current blueprint.
# https://github.com/zalando/connexion/issues/1289#issuecomment-884105076
template_dir = pathlib.Path(self.options.openapi_console_ui_from_dir)
index_path = template_dir / 'index.j2'
return flask.render_template_string(index_path.read_text(), **template_variables)

def console_ui_static_files(self, filename):
"""
Servers the static files for the OpenAPI Console UI.
:param filename: Requested file contents.
:return:
"""
# convert PosixPath to str
static_dir = str(self.options.openapi_console_ui_from_dir)
return flask.send_from_directory(static_dir, filename)

def get_json_spec(self):
return flask.jsonify(self._spec_for_prefix())

def get_yaml_spec(self):
return yamldumper(self._spec_for_prefix()), 200, {"Content-Type": "text/yaml"}

def _spec_for_prefix(self):
"""
Modify base_path in the spec based on incoming url
This fixes problems with reverse proxies changing the path.
"""
base_path = flask.url_for(flask.request.endpoint).rsplit("/", 1)[0]
return self.specification.with_base_path(base_path).raw
18 changes: 17 additions & 1 deletion connexion/apps/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def __init__(self, import_name, api_cls, port=None, specification_dir='',
self.server = server
self.server_args = dict() if server_args is None else server_args
self.app = self.create_app()
self._apply_middleware()
self.middleware = self._apply_middleware()

# we get our application root path to avoid duplicating logic
self.root_path = self.get_root_path()
Expand Down Expand Up @@ -153,6 +153,22 @@ def add_api(self, specification, base_path=None, arguments=None,

api_options = self.options.extend(options)

self.middleware.add_api(
specification,
base_path=base_path,
arguments=arguments,
resolver=resolver,
resolver_error_handler=resolver_error_handler,
validate_responses=validate_responses,
strict_validation=strict_validation,
auth_all_paths=auth_all_paths,
debug=self.debug,
validator_map=validator_map,
pythonic_params=pythonic_params,
pass_context_arg_name=pass_context_arg_name,
options=api_options.as_dict()
)

api = self.api_cls(specification,
base_path=base_path,
arguments=arguments,
Expand Down
7 changes: 4 additions & 3 deletions connexion/apps/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ def __init__(self, import_name, server='flask', extra_files=None, **kwargs):
See :class:`~connexion.AbstractApp` for additional parameters.
"""
self.extra_files = extra_files or []
self.middleware = None

super().__init__(import_name, FlaskApi, server=server, **kwargs)

Expand All @@ -45,10 +44,12 @@ def create_app(self):
def _apply_middleware(self):
middlewares = [*ConnexionMiddleware.default_middlewares,
a2wsgi.WSGIMiddleware]
self.middleware = ConnexionMiddleware(self.app.wsgi_app, middlewares=middlewares)
middleware = ConnexionMiddleware(self.app.wsgi_app, middlewares=middlewares)

# Wrap with ASGI to WSGI middleware for usage with development server and test client
self.app.wsgi_app = a2wsgi.ASGIMiddleware(self.middleware)
self.app.wsgi_app = a2wsgi.ASGIMiddleware(middleware)

return middleware

def get_root_path(self):
return pathlib.Path(self.app.root_path)
Expand Down
1 change: 1 addition & 0 deletions connexion/middleware/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .main import ConnexionMiddleware # NOQA
from .swagger_ui import SwaggerUIMiddleware # NOQA
10 changes: 10 additions & 0 deletions connexion/middleware/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import abc
import pathlib
import typing as t


class AppMiddleware(abc.ABC):

@abc.abstractmethod
def add_api(self, specification: t.Union[pathlib.Path, str, dict], **kwargs) -> None:
pass
12 changes: 10 additions & 2 deletions connexion/middleware/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import pathlib
import typing as t

from starlette.exceptions import ExceptionMiddleware
from starlette.types import ASGIApp, Receive, Scope, Send

from connexion.middleware.base import AppMiddleware
from connexion.middleware.swagger_ui import SwaggerUIMiddleware


class ConnexionMiddleware:

default_middlewares = [
ExceptionMiddleware,
SwaggerUIMiddleware,
]

def __init__(
Expand All @@ -25,8 +31,6 @@ def __init__(
middlewares = self.default_middlewares
self.app, self.apps = self._apply_middlewares(app, middlewares)

self._routing_middleware = None

@staticmethod
def _apply_middlewares(app: ASGIApp, middlewares: t.List[t.Type[ASGIApp]]) \
-> t.Tuple[ASGIApp, t.Iterable[ASGIApp]]:
Expand All @@ -49,13 +53,17 @@ def add_api(
specification: t.Union[pathlib.Path, str, dict],
base_path: t.Optional[str] = None,
arguments: t.Optional[dict] = None,
**kwargs
) -> None:
"""Add an API to the underlying routing middleware based on a OpenAPI spec.
:param specification: OpenAPI spec as dict or path to file.
:param base_path: Base path where to add this API.
:param arguments: Jinja arguments to replace in the spec.
"""
for app in self.apps:
if isinstance(app, AppMiddleware):
app.add_api(specification, base_path=base_path, arguments=arguments, **kwargs)

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await self.app(scope, receive, send)
Loading

0 comments on commit bbeb817

Please sign in to comment.