Skip to content

Commit 9ce9fcb

Browse files
authored
Merge pull request #419 from p1c2u/feature/auto-detect-request-and-response-validator-proxies
OpenAPI 3.1 support + Auto-detect proxies and request / response validator protocols
2 parents 9f1eabe + 0404d48 commit 9ce9fcb

30 files changed

+724
-141
lines changed

README.rst

+16-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ About
1919
#####
2020

2121
Openapi-core is a Python library that adds client-side and server-side support
22-
for the `OpenAPI Specification v3 <https://git.1-hub.cnOAI/OpenAPI-Specification>`__.
22+
for the `OpenAPI v3.0 <https://git.1-hub.cnOAI/OpenAPI-Specification/blob/master/versions/3.0.3.md>`__
23+
and `OpenAPI v3.1 <https://git.1-hub.cnOAI/OpenAPI-Specification/blob/main/versions/3.1.0.md>`__ specification.
2324

2425
Key features
2526
************
@@ -57,7 +58,7 @@ Alternatively you can download the code and install from the repository:
5758
Usage
5859
#####
5960

60-
Firstly create your specification object:
61+
Firstly create your specification object. By default, OpenAPI spec version is detected:
6162

6263
.. code-block:: python
6364
@@ -132,6 +133,19 @@ and unmarshal response data from validation result
132133
133134
Response object should implement OpenAPI Response protocol (See `Integrations <https://openapi-core.readthedocs.io/en/latest/integrations.html>`__).
134135

136+
In order to explicitly validate a:
137+
138+
* OpenAPI 3.0 spec, import ``openapi_v30_request_validator`` or ``openapi_v30_response_validator``
139+
* OpenAPI 3.1 spec, import ``openapi_v31_request_validator`` or ``openapi_v31_response_validator``
140+
141+
.. code:: python
142+
143+
from openapi_core.validation.response import openapi_v31_response_validator
144+
145+
result = openapi_v31_response_validator.validate(spec, request, response)
146+
147+
You can also explicitly import ``openapi_v3_request_validator`` or ``openapi_v3_response_validator`` which is a shortcut to the latest v3 release.
148+
135149
Related projects
136150
################
137151
* `bottle-openapi-3 <https://git.1-hub.cncope-systems/bottle-openapi-3>`__

docs/index.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ Welcome to openapi-core's documentation!
77
========================================
88

99
Openapi-core is a Python library that adds client-side and server-side support
10-
for the `OpenAPI Specification v3 <https://git.1-hub.cnOAI/OpenAPI-Specification>`__.
10+
for the `OpenAPI v3.0 <https://git.1-hub.cnOAI/OpenAPI-Specification/blob/master/versions/3.0.3.md>`__
11+
and `OpenAPI v3.1 <https://git.1-hub.cnOAI/OpenAPI-Specification/blob/main/versions/3.1.0.md>`__ specification.
1112

1213
Key features
1314
------------

docs/usage.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Usage
22
=====
33

4-
Firstly create your specification: object
4+
Firstly create your specification object. By default, OpenAPI spec version is detected:
55

66
.. code-block:: python
77
@@ -46,7 +46,7 @@ and unmarshal request data from validation result
4646
# get security data
4747
validated_security = result.security
4848
49-
Request object should be instance of OpenAPIRequest class (See :doc:`integrations`).
49+
Request object should implement OpenAPI Request protocol (See :doc:`integrations`).
5050

5151
Response
5252
--------
@@ -75,7 +75,7 @@ and unmarshal response data from validation result
7575
# get data
7676
validated_data = result.data
7777
78-
Response object should be instance of OpenAPIResponse class (See :doc:`integrations`).
78+
Response object should implement OpenAPI Response protocol (See :doc:`integrations`).
7979

8080
Security
8181
--------

openapi_core/__init__.py

+15-17
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
"""OpenAPI core module"""
22
from openapi_core.spec import Spec
3-
from openapi_core.validation.request.validators import RequestBodyValidator
4-
from openapi_core.validation.request.validators import (
5-
RequestParametersValidator,
3+
from openapi_core.validation.request import openapi_request_body_validator
4+
from openapi_core.validation.request import (
5+
openapi_request_parameters_validator,
66
)
7-
from openapi_core.validation.request.validators import RequestSecurityValidator
8-
from openapi_core.validation.request.validators import RequestValidator
9-
from openapi_core.validation.response.validators import ResponseDataValidator
10-
from openapi_core.validation.response.validators import (
11-
ResponseHeadersValidator,
12-
)
13-
from openapi_core.validation.response.validators import ResponseValidator
7+
from openapi_core.validation.request import openapi_request_security_validator
8+
from openapi_core.validation.request import openapi_request_validator
9+
from openapi_core.validation.response import openapi_response_data_validator
10+
from openapi_core.validation.response import openapi_response_headers_validator
11+
from openapi_core.validation.response import openapi_response_validator
1412
from openapi_core.validation.shortcuts import validate_request
1513
from openapi_core.validation.shortcuts import validate_response
1614

@@ -24,11 +22,11 @@
2422
"Spec",
2523
"validate_request",
2624
"validate_response",
27-
"RequestValidator",
28-
"ResponseValidator",
29-
"RequestBodyValidator",
30-
"RequestParametersValidator",
31-
"RequestSecurityValidator",
32-
"ResponseDataValidator",
33-
"ResponseHeadersValidator",
25+
"openapi_request_body_validator",
26+
"openapi_request_parameters_validator",
27+
"openapi_request_security_validator",
28+
"openapi_request_validator",
29+
"openapi_response_data_validator",
30+
"openapi_response_headers_validator",
31+
"openapi_response_validator",
3432
]

openapi_core/contrib/flask/decorators.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
from openapi_core.validation.processors import OpenAPIProcessor
1717
from openapi_core.validation.request import openapi_request_validator
1818
from openapi_core.validation.request.datatypes import RequestValidationResult
19-
from openapi_core.validation.request.validators import RequestValidator
19+
from openapi_core.validation.request.protocols import RequestValidator
2020
from openapi_core.validation.response import openapi_response_validator
2121
from openapi_core.validation.response.datatypes import ResponseValidationResult
22-
from openapi_core.validation.response.validators import ResponseValidator
22+
from openapi_core.validation.response.protocols import ResponseValidator
2323

2424

2525
class FlaskOpenAPIViewDecorator(OpenAPIProcessor):

openapi_core/unmarshalling/schemas/__init__.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from openapi_schema_validator import OAS30Validator
2+
from openapi_schema_validator import OAS31Validator
23

34
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
45
from openapi_core.unmarshalling.schemas.factories import (
@@ -8,6 +9,9 @@
89
__all__ = [
910
"oas30_request_schema_unmarshallers_factory",
1011
"oas30_response_schema_unmarshallers_factory",
12+
"oas31_request_schema_unmarshallers_factory",
13+
"oas31_response_schema_unmarshallers_factory",
14+
"oas31_schema_unmarshallers_factory",
1115
]
1216

1317
oas30_request_schema_unmarshallers_factory = SchemaUnmarshallersFactory(
@@ -19,3 +23,13 @@
1923
OAS30Validator,
2024
context=UnmarshalContext.RESPONSE,
2125
)
26+
27+
oas31_schema_unmarshallers_factory = SchemaUnmarshallersFactory(
28+
OAS31Validator,
29+
)
30+
31+
# alias to v31 version (request/response are the same bcs no context needed)
32+
oas31_request_schema_unmarshallers_factory = oas31_schema_unmarshallers_factory
33+
oas31_response_schema_unmarshallers_factory = (
34+
oas31_schema_unmarshallers_factory
35+
)

openapi_core/validation/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
from openapi_core.exceptions import OpenAPIError
55

66

7+
class ValidatorDetectError(OpenAPIError):
8+
pass
9+
10+
711
class ValidationError(OpenAPIError):
812
pass
913

openapi_core/validation/processors.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from openapi_core.spec import Spec
33
from openapi_core.validation.request.datatypes import RequestValidationResult
44
from openapi_core.validation.request.protocols import Request
5-
from openapi_core.validation.request.validators import RequestValidator
5+
from openapi_core.validation.request.protocols import RequestValidator
66
from openapi_core.validation.response.datatypes import ResponseValidationResult
77
from openapi_core.validation.response.protocols import Response
8-
from openapi_core.validation.response.validators import ResponseValidator
8+
from openapi_core.validation.response.protocols import ResponseValidator
99

1010

1111
class OpenAPIProcessor:

openapi_core/validation/request/__init__.py

+57-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
from openapi_core.unmarshalling.schemas import (
33
oas30_request_schema_unmarshallers_factory,
44
)
5+
from openapi_core.unmarshalling.schemas import (
6+
oas31_schema_unmarshallers_factory,
7+
)
8+
from openapi_core.validation.request.proxies import DetectRequestValidatorProxy
59
from openapi_core.validation.request.validators import RequestBodyValidator
610
from openapi_core.validation.request.validators import (
711
RequestParametersValidator,
@@ -14,6 +18,14 @@
1418
"openapi_v30_request_parameters_validator",
1519
"openapi_v30_request_security_validator",
1620
"openapi_v30_request_validator",
21+
"openapi_v31_request_body_validator",
22+
"openapi_v31_request_parameters_validator",
23+
"openapi_v31_request_security_validator",
24+
"openapi_v31_request_validator",
25+
"openapi_v3_request_body_validator",
26+
"openapi_v3_request_parameters_validator",
27+
"openapi_v3_request_security_validator",
28+
"openapi_v3_request_validator",
1729
"openapi_request_body_validator",
1830
"openapi_request_parameters_validator",
1931
"openapi_request_security_validator",
@@ -33,8 +45,49 @@
3345
schema_unmarshallers_factory=oas30_request_schema_unmarshallers_factory,
3446
)
3547

48+
openapi_v31_request_body_validator = RequestBodyValidator(
49+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
50+
)
51+
openapi_v31_request_parameters_validator = RequestParametersValidator(
52+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
53+
)
54+
openapi_v31_request_security_validator = RequestSecurityValidator(
55+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
56+
)
57+
openapi_v31_request_validator = RequestValidator(
58+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
59+
)
60+
3661
# alias to the latest v3 version
37-
openapi_request_body_validator = openapi_v30_request_body_validator
38-
openapi_request_parameters_validator = openapi_v30_request_parameters_validator
39-
openapi_request_security_validator = openapi_v30_request_security_validator
40-
openapi_request_validator = openapi_v30_request_validator
62+
openapi_v3_request_body_validator = openapi_v31_request_body_validator
63+
openapi_v3_request_parameters_validator = (
64+
openapi_v31_request_parameters_validator
65+
)
66+
openapi_v3_request_security_validator = openapi_v31_request_security_validator
67+
openapi_v3_request_validator = openapi_v31_request_validator
68+
69+
# detect version spec
70+
openapi_request_body_validator = DetectRequestValidatorProxy(
71+
{
72+
("openapi", "3.0"): openapi_v30_request_body_validator,
73+
("openapi", "3.1"): openapi_v31_request_body_validator,
74+
},
75+
)
76+
openapi_request_parameters_validator = DetectRequestValidatorProxy(
77+
{
78+
("openapi", "3.0"): openapi_v30_request_parameters_validator,
79+
("openapi", "3.1"): openapi_v31_request_parameters_validator,
80+
},
81+
)
82+
openapi_request_security_validator = DetectRequestValidatorProxy(
83+
{
84+
("openapi", "3.0"): openapi_v30_request_security_validator,
85+
("openapi", "3.1"): openapi_v31_request_security_validator,
86+
},
87+
)
88+
openapi_request_validator = DetectRequestValidatorProxy(
89+
{
90+
("openapi", "3.0"): openapi_v30_request_validator,
91+
("openapi", "3.1"): openapi_v31_request_validator,
92+
},
93+
)

openapi_core/validation/request/protocols.py

+13
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from typing_extensions import Protocol
1414
from typing_extensions import runtime_checkable
1515

16+
from openapi_core.spec import Spec
1617
from openapi_core.validation.request.datatypes import RequestParameters
18+
from openapi_core.validation.request.datatypes import RequestValidationResult
1719

1820

1921
@runtime_checkable
@@ -85,3 +87,14 @@ class SupportsPathPattern(Protocol):
8587
@property
8688
def path_pattern(self) -> str:
8789
...
90+
91+
92+
@runtime_checkable
93+
class RequestValidator(Protocol):
94+
def validate(
95+
self,
96+
spec: Spec,
97+
request: Request,
98+
base_url: Optional[str] = None,
99+
) -> RequestValidationResult:
100+
...
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""OpenAPI spec validator validation proxies module."""
2+
from typing import Any
3+
from typing import Hashable
4+
from typing import Iterator
5+
from typing import Mapping
6+
from typing import Optional
7+
from typing import Tuple
8+
9+
from openapi_core.exceptions import OpenAPIError
10+
from openapi_core.spec import Spec
11+
from openapi_core.validation.exceptions import ValidatorDetectError
12+
from openapi_core.validation.request.datatypes import RequestValidationResult
13+
from openapi_core.validation.request.protocols import Request
14+
from openapi_core.validation.request.validators import BaseRequestValidator
15+
16+
17+
class DetectRequestValidatorProxy:
18+
def __init__(
19+
self, choices: Mapping[Tuple[str, str], BaseRequestValidator]
20+
):
21+
self.choices = choices
22+
23+
def detect(self, spec: Spec) -> BaseRequestValidator:
24+
for (key, value), validator in self.choices.items():
25+
if key in spec and spec[key].startswith(value):
26+
return validator
27+
raise ValidatorDetectError("Spec schema version not detected")
28+
29+
def validate(
30+
self,
31+
spec: Spec,
32+
request: Request,
33+
base_url: Optional[str] = None,
34+
) -> RequestValidationResult:
35+
validator = self.detect(spec)
36+
return validator.validate(spec, request, base_url=base_url)
37+
38+
def is_valid(
39+
self,
40+
spec: Spec,
41+
request: Request,
42+
base_url: Optional[str] = None,
43+
) -> bool:
44+
validator = self.detect(spec)
45+
error = next(
46+
validator.iter_errors(spec, request, base_url=base_url), None
47+
)
48+
return error is None
49+
50+
def iter_errors(
51+
self,
52+
spec: Spec,
53+
request: Request,
54+
base_url: Optional[str] = None,
55+
) -> Iterator[Exception]:
56+
validator = self.detect(spec)
57+
yield from validator.iter_errors(spec, request, base_url=base_url)

openapi_core/validation/request/validators.py

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import warnings
33
from typing import Any
44
from typing import Dict
5+
from typing import Iterator
56
from typing import Optional
67

78
from openapi_core.casting.schemas import schema_casters_factory
@@ -20,6 +21,7 @@
2021
from openapi_core.deserializing.parameters.factories import (
2122
ParameterDeserializersFactory,
2223
)
24+
from openapi_core.exceptions import OpenAPIError
2325
from openapi_core.security import security_provider_factory
2426
from openapi_core.security.exceptions import SecurityError
2527
from openapi_core.security.factories import SecurityProviderFactory
@@ -64,6 +66,15 @@ def __init__(
6466
)
6567
self.security_provider_factory = security_provider_factory
6668

69+
def iter_errors(
70+
self,
71+
spec: Spec,
72+
request: Request,
73+
base_url: Optional[str] = None,
74+
) -> Iterator[Exception]:
75+
result = self.validate(spec, request, base_url=base_url)
76+
yield from result.errors
77+
6778
def validate(
6879
self,
6980
spec: Spec,

0 commit comments

Comments
 (0)