Skip to content

Commit 3eb92eb

Browse files
committed
Auto-detect request and response validator proxies
1 parent 9f1eabe commit 3eb92eb

File tree

13 files changed

+363
-109
lines changed

13 files changed

+363
-109
lines changed

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/request/__init__.py

+34-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,10 +18,15 @@
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",
1725
"openapi_request_body_validator",
1826
"openapi_request_parameters_validator",
1927
"openapi_request_security_validator",
2028
"openapi_request_validator",
29+
"openapi_request_validator_proxy",
2130
]
2231

2332
openapi_v30_request_body_validator = RequestBodyValidator(
@@ -33,8 +42,29 @@
3342
schema_unmarshallers_factory=oas30_request_schema_unmarshallers_factory,
3443
)
3544

45+
openapi_v31_request_body_validator = RequestBodyValidator(
46+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
47+
)
48+
openapi_v31_request_parameters_validator = RequestParametersValidator(
49+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
50+
)
51+
openapi_v31_request_security_validator = RequestSecurityValidator(
52+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
53+
)
54+
openapi_v31_request_validator = RequestValidator(
55+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
56+
)
57+
3658
# 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
59+
openapi_request_body_validator = openapi_v31_request_body_validator
60+
openapi_request_parameters_validator = openapi_v31_request_parameters_validator
61+
openapi_request_security_validator = openapi_v31_request_security_validator
62+
openapi_request_validator = openapi_v31_request_validator
63+
64+
# detect version spec
65+
openapi_request_validator_proxy = DetectRequestValidatorProxy(
66+
{
67+
("openapi", "3.0"): openapi_v30_request_validator,
68+
("openapi", "3.1"): openapi_v31_request_validator,
69+
},
70+
)
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 RequestValidator
15+
16+
17+
class DetectRequestValidatorProxy:
18+
def __init__(self, choices: Mapping[Tuple[str, str], RequestValidator]):
19+
self.choices = choices
20+
21+
def detect(self, spec: Spec) -> RequestValidator:
22+
for (key, value), validator in self.choices.items():
23+
if key in spec and spec[key].startswith(value):
24+
return validator
25+
raise ValidatorDetectError("Spec schema version not detected")
26+
27+
def validate(
28+
self,
29+
spec: Spec,
30+
request: Request,
31+
base_url: Optional[str] = None,
32+
) -> RequestValidationResult:
33+
validator = self.detect(spec)
34+
return validator.validate(spec, request, base_url=base_url)
35+
36+
def is_valid(
37+
self,
38+
spec: Spec,
39+
request: Request,
40+
base_url: Optional[str] = None,
41+
) -> bool:
42+
validator = self.detect(spec)
43+
error = next(
44+
validator.iter_errors(spec, request, base_url=base_url), None
45+
)
46+
return error is None
47+
48+
def iter_errors(
49+
self,
50+
spec: Spec,
51+
request: Request,
52+
base_url: Optional[str] = None,
53+
) -> Iterator[OpenAPIError]:
54+
validator = self.detect(spec)
55+
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[OpenAPIError]:
75+
result = self.validate(spec, request, base_url=base_url)
76+
yield from result.errors
77+
6778
def validate(
6879
self,
6980
spec: Spec,

openapi_core/validation/response/__init__.py

+31-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
from openapi_core.unmarshalling.schemas import (
33
oas30_response_schema_unmarshallers_factory,
44
)
5+
from openapi_core.unmarshalling.schemas import (
6+
oas31_schema_unmarshallers_factory,
7+
)
8+
from openapi_core.validation.response.proxies import (
9+
DetectResponseValidatorProxy,
10+
)
511
from openapi_core.validation.response.validators import ResponseDataValidator
612
from openapi_core.validation.response.validators import (
713
ResponseHeadersValidator,
@@ -12,9 +18,13 @@
1218
"openapi_v30_response_data_validator",
1319
"openapi_v30_response_headers_validator",
1420
"openapi_v30_response_validator",
21+
"openapi_v31_response_data_validator",
22+
"openapi_v31_response_headers_validator",
23+
"openapi_v31_response_validator",
1524
"openapi_response_data_validator",
1625
"openapi_response_headers_validator",
1726
"openapi_response_validator",
27+
"openapi_response_validator_proxy",
1828
]
1929

2030
openapi_v30_response_data_validator = ResponseDataValidator(
@@ -27,7 +37,25 @@
2737
schema_unmarshallers_factory=oas30_response_schema_unmarshallers_factory,
2838
)
2939

40+
openapi_v31_response_data_validator = ResponseDataValidator(
41+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
42+
)
43+
openapi_v31_response_headers_validator = ResponseHeadersValidator(
44+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
45+
)
46+
openapi_v31_response_validator = ResponseValidator(
47+
schema_unmarshallers_factory=oas31_schema_unmarshallers_factory,
48+
)
49+
3050
# alias to the latest v3 version
31-
openapi_response_data_validator = openapi_v30_response_data_validator
32-
openapi_response_headers_validator = openapi_v30_response_headers_validator
33-
openapi_response_validator = openapi_v30_response_validator
51+
openapi_response_data_validator = openapi_v31_response_data_validator
52+
openapi_response_headers_validator = openapi_v31_response_headers_validator
53+
openapi_response_validator = openapi_v31_response_validator
54+
55+
# detect version spec
56+
openapi_response_validator_proxy = DetectResponseValidatorProxy(
57+
{
58+
("openapi", "3.0"): openapi_v30_response_validator,
59+
("openapi", "3.1"): openapi_v31_response_validator,
60+
},
61+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.protocols import Request
13+
from openapi_core.validation.response.datatypes import ResponseValidationResult
14+
from openapi_core.validation.response.protocols import Response
15+
from openapi_core.validation.response.validators import ResponseValidator
16+
17+
18+
class DetectResponseValidatorProxy:
19+
def __init__(self, choices: Mapping[Tuple[str, str], ResponseValidator]):
20+
self.choices = choices
21+
22+
def detect(self, spec: Spec) -> ResponseValidator:
23+
for (key, value), validator in self.choices.items():
24+
if key in spec and spec[key].startswith(value):
25+
return validator
26+
raise ValidatorDetectError("Spec schema version not detected")
27+
28+
def validate(
29+
self,
30+
spec: Spec,
31+
request: Request,
32+
response: Response,
33+
base_url: Optional[str] = None,
34+
) -> ResponseValidationResult:
35+
validator = self.detect(spec)
36+
return validator.validate(spec, request, response, base_url=base_url)
37+
38+
def is_valid(
39+
self,
40+
spec: Spec,
41+
request: Request,
42+
response: Response,
43+
base_url: Optional[str] = None,
44+
) -> bool:
45+
validator = self.detect(spec)
46+
error = next(
47+
validator.iter_errors(spec, request, response, base_url=base_url),
48+
None,
49+
)
50+
return error is None
51+
52+
def iter_errors(
53+
self,
54+
spec: Spec,
55+
request: Request,
56+
response: Response,
57+
base_url: Optional[str] = None,
58+
) -> Iterator[OpenAPIError]:
59+
validator = self.detect(spec)
60+
yield from validator.iter_errors(
61+
spec, request, response, base_url=base_url
62+
)

openapi_core/validation/response/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 List
67
from typing import Optional
78

@@ -30,6 +31,16 @@
3031

3132

3233
class BaseResponseValidator(BaseValidator):
34+
def iter_errors(
35+
self,
36+
spec: Spec,
37+
request: Request,
38+
response: Response,
39+
base_url: Optional[str] = None,
40+
) -> Iterator[OpenAPIError]:
41+
result = self.validate(spec, request, response, base_url=base_url)
42+
yield from result.errors
43+
3344
def validate(
3445
self,
3546
spec: Spec,

openapi_core/validation/shortcuts.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
from typing import Optional
33

44
from openapi_core.spec import Spec
5-
from openapi_core.validation.request import openapi_request_validator
5+
from openapi_core.validation.request import openapi_request_validator_proxy
66
from openapi_core.validation.request.datatypes import RequestValidationResult
77
from openapi_core.validation.request.protocols import Request
88
from openapi_core.validation.request.validators import RequestValidator
9-
from openapi_core.validation.response import openapi_response_validator
9+
from openapi_core.validation.response import openapi_response_validator_proxy
1010
from openapi_core.validation.response.datatypes import ResponseValidationResult
1111
from openapi_core.validation.response.protocols import Response
1212
from openapi_core.validation.response.validators import ResponseValidator
@@ -16,7 +16,7 @@ def validate_request(
1616
spec: Spec,
1717
request: Request,
1818
base_url: Optional[str] = None,
19-
validator: RequestValidator = openapi_request_validator,
19+
validator: RequestValidator = openapi_request_validator_proxy,
2020
) -> RequestValidationResult:
2121
result = validator.validate(spec, request, base_url=base_url)
2222
result.raise_for_errors()
@@ -28,7 +28,7 @@ def validate_response(
2828
request: Request,
2929
response: Response,
3030
base_url: Optional[str] = None,
31-
validator: ResponseValidator = openapi_response_validator,
31+
validator: ResponseValidator = openapi_response_validator_proxy,
3232
) -> ResponseValidationResult:
3333
result = validator.validate(spec, request, response, base_url=base_url)
3434
result.raise_for_errors()

0 commit comments

Comments
 (0)