Skip to content

Falcon integration #215

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

Merged
merged 6 commits into from
Mar 11, 2020
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
42 changes: 42 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,48 @@ You can use DjangoOpenAPIResponse as a Django response factory:
validator = ResponseValidator(spec)
result = validator.validate(openapi_request, openapi_response)

Falcon
******

This section describes integration with `Falcon <https://falconframework.org>`__ web framework.

Middleware
==========

Falcon API can be integrated by `FalconOpenAPIMiddleware` middleware.

.. code-block:: python

from openapi_core.contrib.falcon.middlewares import FalconOpenAPIMiddleware

openapi_middleware = FalconOpenAPIMiddleware.from_spec(spec)
api = falcon.API(middleware=[openapi_middleware])

Low level
=========

For Falcon you can use FalconOpenAPIRequest a Falcon request factory:

.. code-block:: python

from openapi_core.validation.request.validators import RequestValidator
from openapi_core.contrib.falcon import FalconOpenAPIRequest

openapi_request = FalconOpenAPIRequest(falcon_request)
validator = RequestValidator(spec)
result = validator.validate(openapi_request)

You can use FalconOpenAPIResponse as a Falcon response factory:

.. code-block:: python

from openapi_core.validation.response.validators import ResponseValidator
from openapi_core.contrib.falcon import FalconOpenAPIResponse

openapi_response = FalconOpenAPIResponse(falcon_response)
validator = ResponseValidator(spec)
result = validator.validate(openapi_request, openapi_response)

Flask
*****

Expand Down
5 changes: 5 additions & 0 deletions openapi_core/contrib/falcon/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from openapi_core.contrib.falcon.requests import FalconOpenAPIRequestFactory
from openapi_core.contrib.falcon.responses import FalconOpenAPIResponseFactory


__all__ = ["FalconOpenAPIRequestFactory", "FalconOpenAPIResponseFactory"]
52 changes: 52 additions & 0 deletions openapi_core/contrib/falcon/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""OpenAPI core contrib falcon handlers module"""
from json import dumps

from falcon.constants import MEDIA_JSON
from falcon.status_codes import (
HTTP_400, HTTP_404, HTTP_405, HTTP_415,
)
from openapi_core.schema.media_types.exceptions import InvalidContentType
from openapi_core.templating.paths.exceptions import (
ServerNotFound, OperationNotFound, PathNotFound,
)


class FalconOpenAPIErrorsHandler(object):

OPENAPI_ERROR_STATUS = {
ServerNotFound: 400,
OperationNotFound: 405,
PathNotFound: 404,
InvalidContentType: 415,
}

FALCON_STATUS_CODES = {
400: HTTP_400,
404: HTTP_404,
405: HTTP_405,
415: HTTP_415,
}

@classmethod
def handle(cls, req, resp, errors):
data_errors = [
cls.format_openapi_error(err)
for err in errors
]
data = {
'errors': data_errors,
}
data_error_max = max(data_errors, key=lambda x: x['status'])
resp.content_type = MEDIA_JSON
resp.status = cls.FALCON_STATUS_CODES.get(
data_error_max['status'], HTTP_400)
resp.body = dumps(data)
resp.complete = True

@classmethod
def format_openapi_error(cls, error):
return {
'title': str(error),
'status': cls.OPENAPI_ERROR_STATUS.get(error.__class__, 400),
'class': str(type(error)),
}
73 changes: 73 additions & 0 deletions openapi_core/contrib/falcon/middlewares.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""OpenAPI core contrib falcon middlewares module"""

from openapi_core.contrib.falcon.handlers import FalconOpenAPIErrorsHandler
from openapi_core.contrib.falcon.requests import FalconOpenAPIRequestFactory
from openapi_core.contrib.falcon.responses import FalconOpenAPIResponseFactory
from openapi_core.validation.processors import OpenAPIProcessor
from openapi_core.validation.request.validators import RequestValidator
from openapi_core.validation.response.validators import ResponseValidator


class FalconOpenAPIMiddleware(OpenAPIProcessor):

def __init__(
self,
request_validator,
response_validator,
request_factory,
response_factory,
openapi_errors_handler,
):
super(FalconOpenAPIMiddleware, self).__init__(
request_validator, response_validator)
self.request_factory = request_factory
self.response_factory = response_factory
self.openapi_errors_handler = openapi_errors_handler

def process_request(self, req, resp):
openapi_req = self._get_openapi_request(req)
req_result = super(FalconOpenAPIMiddleware, self).process_request(
openapi_req)
if req_result.errors:
return self._handle_request_errors(req, resp, req_result)
req.openapi = req_result

def process_response(self, req, resp, resource, req_succeeded):
openapi_req = self._get_openapi_request(req)
openapi_resp = self._get_openapi_response(resp)
resp_result = super(FalconOpenAPIMiddleware, self).process_response(
openapi_req, openapi_resp)
if resp_result.errors:
return self._handle_response_errors(req, resp, resp_result)

def _handle_request_errors(self, req, resp, request_result):
return self.openapi_errors_handler.handle(
req, resp, request_result.errors)

def _handle_response_errors(self, req, resp, response_result):
return self.openapi_errors_handler.handle(
req, resp, response_result.errors)

def _get_openapi_request(self, request):
return self.request_factory.create(request)

def _get_openapi_response(self, response):
return self.response_factory.create(response)

@classmethod
def from_spec(
cls,
spec,
request_factory=FalconOpenAPIRequestFactory,
response_factory=FalconOpenAPIResponseFactory,
openapi_errors_handler=FalconOpenAPIErrorsHandler,
):
request_validator = RequestValidator(spec)
response_validator = ResponseValidator(spec)
return cls(
request_validator=request_validator,
response_validator=response_validator,
request_factory=request_factory,
response_factory=response_factory,
openapi_errors_handler=openapi_errors_handler,
)
45 changes: 45 additions & 0 deletions openapi_core/contrib/falcon/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""OpenAPI core contrib falcon responses module"""
from json import dumps

from werkzeug.datastructures import ImmutableMultiDict

from openapi_core.validation.request.datatypes import (
OpenAPIRequest, RequestParameters,
)


class FalconOpenAPIRequestFactory:

@classmethod
def create(cls, request):
"""
Create OpenAPIRequest from falcon Request and route params.
"""
method = request.method.lower()

# gets deduced by path finder against spec
path = {}

# Support falcon-jsonify.
body = (
dumps(request.json) if getattr(request, "json", None)
else request.bounded_stream.read()
)
mimetype = request.options.default_media_type
if request.content_type:
mimetype = request.content_type.partition(";")[0]

query = ImmutableMultiDict(request.params.items())
parameters = RequestParameters(
query=query,
header=request.headers,
cookie=request.cookies,
path=path,
)
return OpenAPIRequest(
full_url_pattern=request.url,
method=method,
parameters=parameters,
body=body,
mimetype=mimetype,
)
20 changes: 20 additions & 0 deletions openapi_core/contrib/falcon/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""OpenAPI core contrib falcon responses module"""
from openapi_core.validation.response.datatypes import OpenAPIResponse


class FalconOpenAPIResponseFactory(object):
@classmethod
def create(cls, response):
status_code = int(response.status[:3])

mimetype = ''
if response.content_type:
mimetype = response.content_type.partition(";")[0]
else:
mimetype = response.options.default_media_type

return OpenAPIResponse(
data=response.body,
status_code=status_code,
mimetype=mimetype,
)
Empty file.
1 change: 1 addition & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mock==2.0.0
pytest==3.5.0
pytest-flake8
pytest-cov==2.5.1
falcon==2.0.0
flask
django==2.2.10; python_version>="3.0"
requests==2.22.0
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ tests_require =
pytest
pytest-flake8
pytest-cov
falcon
flask
webob

Expand Down
51 changes: 51 additions & 0 deletions tests/integration/contrib/falcon/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from falcon import Request, Response, RequestOptions, ResponseOptions
from falcon.routing import DefaultRouter
from falcon.status_codes import HTTP_200
from falcon.testing import create_environ
import pytest


@pytest.fixture
def environ_factory():
def create_env(method, path, server_name):
return create_environ(
host=server_name,
path=path,
)
return create_env


@pytest.fixture
def router():
router = DefaultRouter()
router.add_route("/browse/{id:int}/", lambda x: x)
return router


@pytest.fixture
def request_factory(environ_factory, router):
server_name = 'localhost'

def create_request(
method, path, subdomain=None, query_string=None,
content_type='application/json'):
environ = environ_factory(method, path, server_name)
options = RequestOptions()
# return create_req(options=options, **environ)
req = Request(environ, options)
resource, method_map, params, req.uri_template = router.find(path, req)
return req
return create_request


@pytest.fixture
def response_factory(environ_factory):
def create_response(
data, status_code=200, content_type='application/json'):
options = ResponseOptions()
resp = Response(options)
resp.body = data
resp.content_type = content_type
resp.status = HTTP_200
return resp
return create_response
48 changes: 48 additions & 0 deletions tests/integration/contrib/falcon/data/v3.0/falcon_factory.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
openapi: "3.0.0"
info:
title: Basic OpenAPI specification used with test_falcon.TestFalconOpenAPIIValidation
version: "0.1"
servers:
- url: 'http://localhost'
paths:
'/browse/{id}':
parameters:
- name: id
in: path
required: true
description: the ID of the resource to retrieve
schema:
type: integer
get:
responses:
200:
description: Return the resource.
content:
application/json:
schema:
type: object
required:
- data
properties:
data:
type: string
default:
description: Return errors.
content:
application/json:
schema:
type: object
required:
- errors
properties:
errors:
type: array
items:
type: object
properties:
title:
type: string
code:
type: string
message:
type: string
Loading