Skip to content

Commit eef515f

Browse files
authored
Merge pull request #215 from p1c2u/feature/falcon-inteagration
Falcon integration
2 parents 7789cda + 9baea54 commit eef515f

File tree

13 files changed

+576
-0
lines changed

13 files changed

+576
-0
lines changed

README.rst

+42
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,48 @@ You can use DjangoOpenAPIResponse as a Django response factory:
206206
validator = ResponseValidator(spec)
207207
result = validator.validate(openapi_request, openapi_response)
208208
209+
Falcon
210+
******
211+
212+
This section describes integration with `Falcon <https://falconframework.org>`__ web framework.
213+
214+
Middleware
215+
==========
216+
217+
Falcon API can be integrated by `FalconOpenAPIMiddleware` middleware.
218+
219+
.. code-block:: python
220+
221+
from openapi_core.contrib.falcon.middlewares import FalconOpenAPIMiddleware
222+
223+
openapi_middleware = FalconOpenAPIMiddleware.from_spec(spec)
224+
api = falcon.API(middleware=[openapi_middleware])
225+
226+
Low level
227+
=========
228+
229+
For Falcon you can use FalconOpenAPIRequest a Falcon request factory:
230+
231+
.. code-block:: python
232+
233+
from openapi_core.validation.request.validators import RequestValidator
234+
from openapi_core.contrib.falcon import FalconOpenAPIRequest
235+
236+
openapi_request = FalconOpenAPIRequest(falcon_request)
237+
validator = RequestValidator(spec)
238+
result = validator.validate(openapi_request)
239+
240+
You can use FalconOpenAPIResponse as a Falcon response factory:
241+
242+
.. code-block:: python
243+
244+
from openapi_core.validation.response.validators import ResponseValidator
245+
from openapi_core.contrib.falcon import FalconOpenAPIResponse
246+
247+
openapi_response = FalconOpenAPIResponse(falcon_response)
248+
validator = ResponseValidator(spec)
249+
result = validator.validate(openapi_request, openapi_response)
250+
209251
Flask
210252
*****
211253

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from openapi_core.contrib.falcon.requests import FalconOpenAPIRequestFactory
2+
from openapi_core.contrib.falcon.responses import FalconOpenAPIResponseFactory
3+
4+
5+
__all__ = ["FalconOpenAPIRequestFactory", "FalconOpenAPIResponseFactory"]
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""OpenAPI core contrib falcon handlers module"""
2+
from json import dumps
3+
4+
from falcon.constants import MEDIA_JSON
5+
from falcon.status_codes import (
6+
HTTP_400, HTTP_404, HTTP_405, HTTP_415,
7+
)
8+
from openapi_core.schema.media_types.exceptions import InvalidContentType
9+
from openapi_core.templating.paths.exceptions import (
10+
ServerNotFound, OperationNotFound, PathNotFound,
11+
)
12+
13+
14+
class FalconOpenAPIErrorsHandler(object):
15+
16+
OPENAPI_ERROR_STATUS = {
17+
ServerNotFound: 400,
18+
OperationNotFound: 405,
19+
PathNotFound: 404,
20+
InvalidContentType: 415,
21+
}
22+
23+
FALCON_STATUS_CODES = {
24+
400: HTTP_400,
25+
404: HTTP_404,
26+
405: HTTP_405,
27+
415: HTTP_415,
28+
}
29+
30+
@classmethod
31+
def handle(cls, req, resp, errors):
32+
data_errors = [
33+
cls.format_openapi_error(err)
34+
for err in errors
35+
]
36+
data = {
37+
'errors': data_errors,
38+
}
39+
data_error_max = max(data_errors, key=lambda x: x['status'])
40+
resp.content_type = MEDIA_JSON
41+
resp.status = cls.FALCON_STATUS_CODES.get(
42+
data_error_max['status'], HTTP_400)
43+
resp.body = dumps(data)
44+
resp.complete = True
45+
46+
@classmethod
47+
def format_openapi_error(cls, error):
48+
return {
49+
'title': str(error),
50+
'status': cls.OPENAPI_ERROR_STATUS.get(error.__class__, 400),
51+
'class': str(type(error)),
52+
}
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""OpenAPI core contrib falcon middlewares module"""
2+
3+
from openapi_core.contrib.falcon.handlers import FalconOpenAPIErrorsHandler
4+
from openapi_core.contrib.falcon.requests import FalconOpenAPIRequestFactory
5+
from openapi_core.contrib.falcon.responses import FalconOpenAPIResponseFactory
6+
from openapi_core.validation.processors import OpenAPIProcessor
7+
from openapi_core.validation.request.validators import RequestValidator
8+
from openapi_core.validation.response.validators import ResponseValidator
9+
10+
11+
class FalconOpenAPIMiddleware(OpenAPIProcessor):
12+
13+
def __init__(
14+
self,
15+
request_validator,
16+
response_validator,
17+
request_factory,
18+
response_factory,
19+
openapi_errors_handler,
20+
):
21+
super(FalconOpenAPIMiddleware, self).__init__(
22+
request_validator, response_validator)
23+
self.request_factory = request_factory
24+
self.response_factory = response_factory
25+
self.openapi_errors_handler = openapi_errors_handler
26+
27+
def process_request(self, req, resp):
28+
openapi_req = self._get_openapi_request(req)
29+
req_result = super(FalconOpenAPIMiddleware, self).process_request(
30+
openapi_req)
31+
if req_result.errors:
32+
return self._handle_request_errors(req, resp, req_result)
33+
req.openapi = req_result
34+
35+
def process_response(self, req, resp, resource, req_succeeded):
36+
openapi_req = self._get_openapi_request(req)
37+
openapi_resp = self._get_openapi_response(resp)
38+
resp_result = super(FalconOpenAPIMiddleware, self).process_response(
39+
openapi_req, openapi_resp)
40+
if resp_result.errors:
41+
return self._handle_response_errors(req, resp, resp_result)
42+
43+
def _handle_request_errors(self, req, resp, request_result):
44+
return self.openapi_errors_handler.handle(
45+
req, resp, request_result.errors)
46+
47+
def _handle_response_errors(self, req, resp, response_result):
48+
return self.openapi_errors_handler.handle(
49+
req, resp, response_result.errors)
50+
51+
def _get_openapi_request(self, request):
52+
return self.request_factory.create(request)
53+
54+
def _get_openapi_response(self, response):
55+
return self.response_factory.create(response)
56+
57+
@classmethod
58+
def from_spec(
59+
cls,
60+
spec,
61+
request_factory=FalconOpenAPIRequestFactory,
62+
response_factory=FalconOpenAPIResponseFactory,
63+
openapi_errors_handler=FalconOpenAPIErrorsHandler,
64+
):
65+
request_validator = RequestValidator(spec)
66+
response_validator = ResponseValidator(spec)
67+
return cls(
68+
request_validator=request_validator,
69+
response_validator=response_validator,
70+
request_factory=request_factory,
71+
response_factory=response_factory,
72+
openapi_errors_handler=openapi_errors_handler,
73+
)
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""OpenAPI core contrib falcon responses module"""
2+
from json import dumps
3+
4+
from werkzeug.datastructures import ImmutableMultiDict
5+
6+
from openapi_core.validation.request.datatypes import (
7+
OpenAPIRequest, RequestParameters,
8+
)
9+
10+
11+
class FalconOpenAPIRequestFactory:
12+
13+
@classmethod
14+
def create(cls, request):
15+
"""
16+
Create OpenAPIRequest from falcon Request and route params.
17+
"""
18+
method = request.method.lower()
19+
20+
# gets deduced by path finder against spec
21+
path = {}
22+
23+
# Support falcon-jsonify.
24+
body = (
25+
dumps(request.json) if getattr(request, "json", None)
26+
else request.bounded_stream.read()
27+
)
28+
mimetype = request.options.default_media_type
29+
if request.content_type:
30+
mimetype = request.content_type.partition(";")[0]
31+
32+
query = ImmutableMultiDict(request.params.items())
33+
parameters = RequestParameters(
34+
query=query,
35+
header=request.headers,
36+
cookie=request.cookies,
37+
path=path,
38+
)
39+
return OpenAPIRequest(
40+
full_url_pattern=request.url,
41+
method=method,
42+
parameters=parameters,
43+
body=body,
44+
mimetype=mimetype,
45+
)
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""OpenAPI core contrib falcon responses module"""
2+
from openapi_core.validation.response.datatypes import OpenAPIResponse
3+
4+
5+
class FalconOpenAPIResponseFactory(object):
6+
@classmethod
7+
def create(cls, response):
8+
status_code = int(response.status[:3])
9+
10+
mimetype = ''
11+
if response.content_type:
12+
mimetype = response.content_type.partition(";")[0]
13+
else:
14+
mimetype = response.options.default_media_type
15+
16+
return OpenAPIResponse(
17+
data=response.body,
18+
status_code=status_code,
19+
mimetype=mimetype,
20+
)

openapi_core/contrib/falcon/views.py

Whitespace-only changes.

requirements_dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mock==2.0.0
22
pytest==3.5.0
33
pytest-flake8
44
pytest-cov==2.5.1
5+
falcon==2.0.0
56
flask
67
django==2.2.10; python_version>="3.0"
78
requests==2.22.0

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ tests_require =
3838
pytest
3939
pytest-flake8
4040
pytest-cov
41+
falcon
4142
flask
4243
webob
4344

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from falcon import Request, Response, RequestOptions, ResponseOptions
2+
from falcon.routing import DefaultRouter
3+
from falcon.status_codes import HTTP_200
4+
from falcon.testing import create_environ
5+
import pytest
6+
7+
8+
@pytest.fixture
9+
def environ_factory():
10+
def create_env(method, path, server_name):
11+
return create_environ(
12+
host=server_name,
13+
path=path,
14+
)
15+
return create_env
16+
17+
18+
@pytest.fixture
19+
def router():
20+
router = DefaultRouter()
21+
router.add_route("/browse/{id:int}/", lambda x: x)
22+
return router
23+
24+
25+
@pytest.fixture
26+
def request_factory(environ_factory, router):
27+
server_name = 'localhost'
28+
29+
def create_request(
30+
method, path, subdomain=None, query_string=None,
31+
content_type='application/json'):
32+
environ = environ_factory(method, path, server_name)
33+
options = RequestOptions()
34+
# return create_req(options=options, **environ)
35+
req = Request(environ, options)
36+
resource, method_map, params, req.uri_template = router.find(path, req)
37+
return req
38+
return create_request
39+
40+
41+
@pytest.fixture
42+
def response_factory(environ_factory):
43+
def create_response(
44+
data, status_code=200, content_type='application/json'):
45+
options = ResponseOptions()
46+
resp = Response(options)
47+
resp.body = data
48+
resp.content_type = content_type
49+
resp.status = HTTP_200
50+
return resp
51+
return create_response
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
openapi: "3.0.0"
2+
info:
3+
title: Basic OpenAPI specification used with test_falcon.TestFalconOpenAPIIValidation
4+
version: "0.1"
5+
servers:
6+
- url: 'http://localhost'
7+
paths:
8+
'/browse/{id}':
9+
parameters:
10+
- name: id
11+
in: path
12+
required: true
13+
description: the ID of the resource to retrieve
14+
schema:
15+
type: integer
16+
get:
17+
responses:
18+
200:
19+
description: Return the resource.
20+
content:
21+
application/json:
22+
schema:
23+
type: object
24+
required:
25+
- data
26+
properties:
27+
data:
28+
type: string
29+
default:
30+
description: Return errors.
31+
content:
32+
application/json:
33+
schema:
34+
type: object
35+
required:
36+
- errors
37+
properties:
38+
errors:
39+
type: array
40+
items:
41+
type: object
42+
properties:
43+
title:
44+
type: string
45+
code:
46+
type: string
47+
message:
48+
type: string

0 commit comments

Comments
 (0)