Skip to content

Commit 18bccc0

Browse files
committed
Falcon integration
1 parent 09f87a9 commit 18bccc0

File tree

9 files changed

+385
-42
lines changed

9 files changed

+385
-42
lines changed

Diff for: openapi_core/contrib/falcon/handlers.py

+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+
}

Diff for: openapi_core/contrib/falcon/middlewares.py

+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+
)

Diff for: openapi_core/contrib/falcon/requests.py

+27-18
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,45 @@
11
"""OpenAPI core contrib falcon responses module"""
2-
import json
2+
from json import dumps
33

4-
from openapi_core.validation.request.datatypes import OpenAPIRequest, RequestParameters
54
from werkzeug.datastructures import ImmutableMultiDict
65

6+
from openapi_core.validation.request.datatypes import (
7+
OpenAPIRequest, RequestParameters,
8+
)
9+
710

811
class FalconOpenAPIRequestFactory:
12+
913
@classmethod
10-
def create(cls, req, route_params):
14+
def create(cls, request):
1115
"""
1216
Create OpenAPIRequest from falcon Request and route params.
1317
"""
14-
method = req.method.lower()
18+
method = request.method.lower()
1519

16-
# Convert keys to lowercase as that's what the OpenAPIRequest expects.
17-
headers = {key.lower(): value for key, value in req.headers.items()}
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]
1831

32+
query = ImmutableMultiDict(request.params.items())
1933
parameters = RequestParameters(
20-
path=route_params,
21-
query=ImmutableMultiDict(req.params.items()),
22-
header=headers,
23-
cookie=req.cookies,
34+
query=query,
35+
header=request.headers,
36+
cookie=request.cookies,
37+
path=path,
2438
)
2539
return OpenAPIRequest(
26-
host_url=None,
27-
path=req.path,
28-
path_pattern=req.uri_template or req.path,
40+
full_url_pattern=request.url,
2941
method=method,
3042
parameters=parameters,
31-
# Support falcon-jsonify.
32-
body=json.dumps(req.json)
33-
if getattr(req, "json", None)
34-
else req.bounded_stream.read(),
35-
mimetype=req.content_type.partition(";")[0] if req.content_type else "",
43+
body=body,
44+
mimetype=mimetype,
3645
)

Diff for: openapi_core/contrib/falcon/responses.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,17 @@
44

55
class FalconOpenAPIResponseFactory(object):
66
@classmethod
7-
def create(cls, resp):
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+
816
return OpenAPIResponse(
9-
data=resp.body,
10-
status_code=resp.status[:3],
11-
mimetype=resp.content_type.partition(";")[0] if resp.content_type else '',
17+
data=response.body,
18+
status_code=status_code,
19+
mimetype=mimetype,
1220
)

Diff for: openapi_core/contrib/falcon/views.py

Whitespace-only changes.

Diff for: tests/integration/contrib/falcon/conftest.py

+11-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
from falcon import Request, Response
1+
from falcon import Request, Response, RequestOptions, ResponseOptions
22
from falcon.routing import DefaultRouter
33
from falcon.testing import create_environ
44
import pytest
5-
from six import BytesIO
65

76

87
@pytest.fixture
@@ -18,20 +17,22 @@ def create_env(method, path, server_name):
1817
@pytest.fixture
1918
def router():
2019
router = DefaultRouter()
21-
router.add_route('/browse/<int:id>/', None)
20+
router.add_route("/browse/{id:int}/", lambda x: x)
2221
return router
2322

2423

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

29-
def create_request(method, path, subdomain=None, query_string=None):
28+
def create_request(
29+
method, path, subdomain=None, query_string=None,
30+
content_type='application/json'):
3031
environ = environ_factory(method, path, server_name)
31-
options = None
32+
options = RequestOptions()
3233
# return create_req(options=options, **environ)
3334
req = Request(environ, options)
34-
req.uri_template = router.find(path, req)
35+
resource, method_map, params, req.uri_template = router.find(path, req)
3536
return req
3637
return create_request
3738

@@ -40,10 +41,8 @@ def create_request(method, path, subdomain=None, query_string=None):
4041
def response_factory(environ_factory):
4142
def create_response(
4243
data, status_code=200, content_type='application/json'):
43-
options = {
44-
'content_type': content_type,
45-
'data': data,
46-
'status': status_code,
47-
}
48-
return Response(options)
44+
options = ResponseOptions()
45+
resp = Response(options)
46+
resp._media = data
47+
return resp
4948
return create_response

Diff for: tests/integration/contrib/falcon/data/v3.0/falcon_factory.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ info:
55
servers:
66
- url: 'http://localhost'
77
paths:
8-
'/browse/{id}/':
8+
'/browse/{id}':
99
parameters:
1010
- name: id
1111
in: path

0 commit comments

Comments
 (0)