Skip to content

Commit 2f91ea3

Browse files
authored
Merge pull request #202 from p1c2u/feature/path-patterns-finder
Path patterns finder
2 parents 1894489 + dcb7161 commit 2f91ea3

File tree

22 files changed

+842
-231
lines changed

22 files changed

+842
-231
lines changed

openapi_core/contrib/flask/handlers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
from flask.json import dumps
44

55
from openapi_core.schema.media_types.exceptions import InvalidContentType
6-
from openapi_core.schema.servers.exceptions import InvalidServer
6+
from openapi_core.templating.paths.exceptions import (
7+
ServerNotFound, OperationNotFound, PathNotFound,
8+
)
79

810

911
class FlaskOpenAPIErrorsHandler(object):
1012

1113
OPENAPI_ERROR_STATUS = {
12-
InvalidServer: 500,
14+
ServerNotFound: 400,
15+
OperationNotFound: 405,
16+
PathNotFound: 404,
1317
InvalidContentType: 415,
1418
}
1519

openapi_core/schema/servers/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""OpenAPI core servers models module"""
22
from six import iteritems
3+
from six.moves.urllib.parse import urljoin
34

45

56
class Server(object):
@@ -25,6 +26,15 @@ def get_url(self, **variables):
2526
variables = self.default_variables
2627
return self.url.format(**variables)
2728

29+
@staticmethod
30+
def is_absolute(url):
31+
return url.startswith('//') or '://' in url
32+
33+
def get_absolute_url(self, base_url=None):
34+
if base_url is not None and not self.is_absolute(self.url):
35+
return urljoin(base_url, self.url)
36+
return self.url
37+
2838

2939
class ServerVariable(object):
3040

openapi_core/templating/__init__.py

Whitespace-only changes.

openapi_core/templating/datatypes.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import attr
2+
3+
4+
@attr.s
5+
class TemplateResult(object):
6+
pattern = attr.ib(default=None)
7+
variables = attr.ib(default=None)
8+
9+
@property
10+
def resolved(self):
11+
if not self.variables:
12+
return self.pattern
13+
return self.pattern.format(**self.variables)

openapi_core/templating/paths/__init__.py

Whitespace-only changes.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import attr
2+
3+
from openapi_core.exceptions import OpenAPIError
4+
5+
6+
class PathError(OpenAPIError):
7+
"""Path error"""
8+
9+
10+
@attr.s(hash=True)
11+
class PathNotFound(PathError):
12+
"""Find path error"""
13+
url = attr.ib()
14+
15+
def __str__(self):
16+
return "Path not found for {0}".format(self.url)
17+
18+
19+
@attr.s(hash=True)
20+
class OperationNotFound(PathError):
21+
"""Find path operation error"""
22+
url = attr.ib()
23+
method = attr.ib()
24+
25+
def __str__(self):
26+
return "Operation {0} not found for {1}".format(
27+
self.method, self.url)
28+
29+
30+
@attr.s(hash=True)
31+
class ServerNotFound(PathError):
32+
"""Find server error"""
33+
url = attr.ib()
34+
35+
def __str__(self):
36+
return "Server not found for {0}".format(self.url)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""OpenAPI core templating paths finders module"""
2+
from more_itertools import peekable
3+
from six import iteritems
4+
5+
from openapi_core.templating.datatypes import TemplateResult
6+
from openapi_core.templating.util import parse, search
7+
from openapi_core.templating.paths.exceptions import (
8+
PathNotFound, OperationNotFound, ServerNotFound,
9+
)
10+
11+
12+
class PathFinder(object):
13+
14+
def __init__(self, spec, base_url=None):
15+
self.spec = spec
16+
self.base_url = base_url
17+
18+
def find(self, request):
19+
paths_iter = self._get_paths_iter(request.full_url_pattern)
20+
paths_iter_peek = peekable(paths_iter)
21+
22+
if not paths_iter_peek:
23+
raise PathNotFound(request.full_url_pattern)
24+
25+
operations_iter = self._get_operations_iter(
26+
request.method, paths_iter_peek)
27+
operations_iter_peek = peekable(operations_iter)
28+
29+
if not operations_iter_peek:
30+
raise OperationNotFound(request.full_url_pattern, request.method)
31+
32+
servers_iter = self._get_servers_iter(
33+
request.full_url_pattern, operations_iter_peek)
34+
35+
try:
36+
return next(servers_iter)
37+
except StopIteration:
38+
raise ServerNotFound(request.full_url_pattern)
39+
40+
def _get_paths_iter(self, full_url_pattern):
41+
for path_pattern, path in iteritems(self.spec.paths):
42+
# simple path
43+
if full_url_pattern.endswith(path_pattern):
44+
path_result = TemplateResult(path_pattern, {})
45+
yield (path, path_result)
46+
# template path
47+
else:
48+
result = search(path_pattern, full_url_pattern)
49+
if result:
50+
path_result = TemplateResult(path_pattern, result.named)
51+
yield (path, path_result)
52+
53+
def _get_operations_iter(self, request_method, paths_iter):
54+
for path, path_result in paths_iter:
55+
if request_method not in path.operations:
56+
continue
57+
operation = path.operations[request_method]
58+
yield (path, operation, path_result)
59+
60+
def _get_servers_iter(self, full_url_pattern, ooperations_iter):
61+
for path, operation, path_result in ooperations_iter:
62+
servers = path.servers or operation.servers or self.spec.servers
63+
for server in servers:
64+
server_url_pattern = full_url_pattern.rsplit(
65+
path_result.resolved, 1)[0]
66+
server_url = server.get_absolute_url(self.base_url)
67+
if server_url.endswith('/'):
68+
server_url = server_url[:-1]
69+
# simple path
70+
if server_url_pattern.startswith(server_url):
71+
server_result = TemplateResult(server.url, {})
72+
yield (
73+
path, operation, server,
74+
path_result, server_result,
75+
)
76+
# template path
77+
else:
78+
result = parse(server.url, server_url_pattern)
79+
if result:
80+
server_result = TemplateResult(
81+
server.url, result.named)
82+
yield (
83+
path, operation, server,
84+
path_result, server_result,
85+
)

openapi_core/templating/util.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from parse import Parser
2+
3+
4+
def search(path_pattern, full_url_pattern):
5+
p = Parser(path_pattern)
6+
p._expression = p._expression + '$'
7+
return p.search(full_url_pattern)
8+
9+
10+
def parse(server_url, server_url_pattern):
11+
p = Parser(server_url)
12+
p._expression = '^' + p._expression
13+
return p.parse(server_url_pattern)

openapi_core/validation/request/validators.py

Lines changed: 11 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
from openapi_core.casting.schemas.exceptions import CastError
66
from openapi_core.deserializing.exceptions import DeserializeError
77
from openapi_core.schema.media_types.exceptions import InvalidContentType
8-
from openapi_core.schema.operations.exceptions import InvalidOperation
98
from openapi_core.schema.parameters.exceptions import (
109
MissingRequiredParameter, MissingParameter,
1110
)
12-
from openapi_core.schema.paths.exceptions import InvalidPath
1311
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
14-
from openapi_core.schema.servers.exceptions import InvalidServer
1512
from openapi_core.security.exceptions import SecurityError
13+
from openapi_core.templating.paths.exceptions import PathError
1614
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
1715
from openapi_core.unmarshalling.schemas.exceptions import (
1816
UnmarshalError, ValidateError,
@@ -21,25 +19,16 @@
2119
from openapi_core.validation.request.datatypes import (
2220
RequestParameters, RequestValidationResult,
2321
)
24-
from openapi_core.validation.util import get_operation_pattern
22+
from openapi_core.validation.validators import BaseValidator
2523

2624

27-
class RequestValidator(object):
28-
29-
def __init__(
30-
self, spec,
31-
custom_formatters=None, custom_media_type_deserializers=None,
32-
):
33-
self.spec = spec
34-
self.custom_formatters = custom_formatters
35-
self.custom_media_type_deserializers = custom_media_type_deserializers
25+
class RequestValidator(BaseValidator):
3626

3727
def validate(self, request):
3828
try:
39-
path = self._get_path(request)
40-
operation = self._get_operation(request)
29+
path, operation, _, _, _ = self._find_path(request)
4130
# don't process if operation errors
42-
except (InvalidServer, InvalidPath, InvalidOperation) as exc:
31+
except PathError as exc:
4332
return RequestValidationResult([exc, ], None, None, None)
4433

4534
try:
@@ -61,9 +50,8 @@ def validate(self, request):
6150

6251
def _validate_parameters(self, request):
6352
try:
64-
path = self._get_path(request)
65-
operation = self._get_operation(request)
66-
except (InvalidServer, InvalidPath, InvalidOperation) as exc:
53+
path, operation, _, _, _ = self._find_path(request)
54+
except PathError as exc:
6755
return RequestValidationResult([exc, ], None, None)
6856

6957
params, params_errors = self._get_parameters(
@@ -76,30 +64,13 @@ def _validate_parameters(self, request):
7664

7765
def _validate_body(self, request):
7866
try:
79-
operation = self._get_operation(request)
80-
except (InvalidServer, InvalidOperation) as exc:
67+
_, operation, _, _, _ = self._find_path(request)
68+
except PathError as exc:
8169
return RequestValidationResult([exc, ], None, None)
8270

8371
body, body_errors = self._get_body(request, operation)
8472
return RequestValidationResult(body_errors, body, None, None)
8573

86-
def _get_operation_pattern(self, request):
87-
server = self.spec.get_server(request.full_url_pattern)
88-
89-
return get_operation_pattern(
90-
server.default_url, request.full_url_pattern
91-
)
92-
93-
def _get_path(self, request):
94-
operation_pattern = self._get_operation_pattern(request)
95-
96-
return self.spec[operation_pattern]
97-
98-
def _get_operation(self, request):
99-
operation_pattern = self._get_operation_pattern(request)
100-
101-
return self.spec.get_operation(operation_pattern, request.method)
102-
10374
def _get_security(self, request, operation):
10475
security = operation.security or self.spec.security
10576
if not security:
@@ -222,15 +193,6 @@ def _get_body_value(self, request_body, request):
222193
raise MissingRequestBody(request)
223194
return request.body
224195

225-
def _deserialise_media_type(self, media_type, value):
226-
from openapi_core.deserializing.media_types.factories import (
227-
MediaTypeDeserializersFactory,
228-
)
229-
deserializers_factory = MediaTypeDeserializersFactory(
230-
self.custom_media_type_deserializers)
231-
deserializer = deserializers_factory.create(media_type)
232-
return deserializer(value)
233-
234196
def _deserialise_parameter(self, param, value):
235197
from openapi_core.deserializing.parameters.factories import (
236198
ParameterDeserializersFactory,
@@ -239,27 +201,7 @@ def _deserialise_parameter(self, param, value):
239201
deserializer = deserializers_factory.create(param)
240202
return deserializer(value)
241203

242-
def _cast(self, param_or_media_type, value):
243-
# return param_or_media_type.cast(value)
244-
if not param_or_media_type.schema:
245-
return value
246-
247-
from openapi_core.casting.schemas.factories import SchemaCastersFactory
248-
casters_factory = SchemaCastersFactory()
249-
caster = casters_factory.create(param_or_media_type.schema)
250-
return caster(value)
251-
252204
def _unmarshal(self, param_or_media_type, value):
253-
if not param_or_media_type.schema:
254-
return value
255-
256-
from openapi_core.unmarshalling.schemas.factories import (
257-
SchemaUnmarshallersFactory,
258-
)
259-
unmarshallers_factory = SchemaUnmarshallersFactory(
260-
self.spec._resolver, self.custom_formatters,
261-
context=UnmarshalContext.REQUEST,
205+
return super(RequestValidator, self)._unmarshal(
206+
param_or_media_type, value, context=UnmarshalContext.REQUEST,
262207
)
263-
unmarshaller = unmarshallers_factory.create(
264-
param_or_media_type.schema)
265-
return unmarshaller(value)

0 commit comments

Comments
 (0)