Skip to content

Commit dcb7161

Browse files
committed
Path pattern finder
1 parent 817ff5c commit dcb7161

File tree

20 files changed

+753
-127
lines changed

20 files changed

+753
-127
lines changed

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

+6-2
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

Diff for: openapi_core/schema/servers/models.py

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

Diff for: openapi_core/templating/datatypes.py

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

Diff for: openapi_core/templating/paths/exceptions.py

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

Diff for: openapi_core/templating/paths/finders.py

+73-15
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,85 @@
11
"""OpenAPI core templating paths finders module"""
2-
from openapi_core.templating.paths.util import get_operation_pattern
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+
)
310

411

512
class PathFinder(object):
613

7-
def __init__(self, spec):
14+
def __init__(self, spec, base_url=None):
815
self.spec = spec
16+
self.base_url = base_url
917

1018
def find(self, request):
11-
operation_pattern = self._get_operation_pattern(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)
1234

13-
path = self.spec[operation_pattern]
14-
path_variables = {}
15-
operation = self.spec.get_operation(operation_pattern, request.method)
16-
servers = path.servers or operation.servers or self.spec.servers
17-
server = servers[0]
18-
server_variables = {}
35+
try:
36+
return next(servers_iter)
37+
except StopIteration:
38+
raise ServerNotFound(request.full_url_pattern)
1939

20-
return path, operation, server, path_variables, server_variables
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)
2152

22-
def _get_operation_pattern(self, request):
23-
server = self.spec.get_server(request.full_url_pattern)
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)
2459

25-
return get_operation_pattern(
26-
server.default_url, request.full_url_pattern
27-
)
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+
)

Diff for: openapi_core/templating/paths/util.py

-24
This file was deleted.

Diff for: openapi_core/templating/util.py

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

Diff for: openapi_core/validation/request/validators.py

+4-6
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,
@@ -30,7 +28,7 @@ def validate(self, request):
3028
try:
3129
path, operation, _, _, _ = self._find_path(request)
3230
# don't process if operation errors
33-
except (InvalidServer, InvalidPath, InvalidOperation) as exc:
31+
except PathError as exc:
3432
return RequestValidationResult([exc, ], None, None, None)
3533

3634
try:
@@ -53,7 +51,7 @@ def validate(self, request):
5351
def _validate_parameters(self, request):
5452
try:
5553
path, operation, _, _, _ = self._find_path(request)
56-
except (InvalidServer, InvalidPath, InvalidOperation) as exc:
54+
except PathError as exc:
5755
return RequestValidationResult([exc, ], None, None)
5856

5957
params, params_errors = self._get_parameters(
@@ -67,7 +65,7 @@ def _validate_parameters(self, request):
6765
def _validate_body(self, request):
6866
try:
6967
_, operation, _, _, _ = self._find_path(request)
70-
except (InvalidServer, InvalidOperation) as exc:
68+
except PathError as exc:
7169
return RequestValidationResult([exc, ], None, None)
7270

7371
body, body_errors = self._get_body(request, operation)

Diff for: openapi_core/validation/response/validators.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
"""OpenAPI core validation response validators module"""
22
from openapi_core.casting.schemas.exceptions import CastError
33
from openapi_core.deserializing.exceptions import DeserializeError
4-
from openapi_core.schema.operations.exceptions import InvalidOperation
54
from openapi_core.schema.media_types.exceptions import InvalidContentType
6-
from openapi_core.schema.paths.exceptions import InvalidPath
75
from openapi_core.schema.responses.exceptions import (
86
InvalidResponse, MissingResponseContent,
97
)
10-
from openapi_core.schema.servers.exceptions import InvalidServer
8+
from openapi_core.templating.paths.exceptions import PathError
119
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
1210
from openapi_core.unmarshalling.schemas.exceptions import (
1311
UnmarshalError, ValidateError,
@@ -22,7 +20,7 @@ def validate(self, request, response):
2220
try:
2321
_, operation, _, _, _ = self._find_path(request)
2422
# don't process if operation errors
25-
except (InvalidServer, InvalidPath, InvalidOperation) as exc:
23+
except PathError as exc:
2624
return ResponseValidationResult([exc, ], None, None)
2725

2826
try:
@@ -47,7 +45,7 @@ def _validate_data(self, request, response):
4745
try:
4846
_, operation, _, _, _ = self._find_path(request)
4947
# don't process if operation errors
50-
except (InvalidServer, InvalidPath, InvalidOperation) as exc:
48+
except PathError as exc:
5149
return ResponseValidationResult([exc, ], None, None)
5250

5351
try:

Diff for: openapi_core/validation/validators.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ class BaseValidator(object):
55

66
def __init__(
77
self, spec,
8+
base_url=None,
89
custom_formatters=None, custom_media_type_deserializers=None,
910
):
1011
self.spec = spec
12+
self.base_url = base_url
1113
self.custom_formatters = custom_formatters
1214
self.custom_media_type_deserializers = custom_media_type_deserializers
1315

1416
def _find_path(self, request):
1517
from openapi_core.templating.paths.finders import PathFinder
16-
finder = PathFinder(self.spec)
18+
finder = PathFinder(self.spec, base_url=self.base_url)
1719
return finder.find(request)
1820

1921
def _deserialise_media_type(self, media_type, value):

Diff for: requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ lazy-object-proxy
44
strict_rfc3339
55
isodate
66
attrs
7+
parse==1.14.0
8+
more-itertools>=5.0.0

Diff for: requirements_2.7.txt

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ backports.functools-partialmethod
66
enum34
77
strict_rfc3339
88
attrs
9+
more-itertools==5.0.0

Diff for: setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ install_requires =
3030
isodate
3131
attrs
3232
werkzeug
33+
parse
34+
more-itertools
3335
backports.functools-lru-cache; python_version<"3.0"
3436
backports.functools-partialmethod; python_version<"3.0"
3537
tests_require =

0 commit comments

Comments
 (0)