Skip to content

Commit 3d4c9f1

Browse files
committed
Unmarshallers format validators refactor
1 parent 9975bac commit 3d4c9f1

File tree

11 files changed

+906
-809
lines changed

11 files changed

+906
-809
lines changed

Diff for: openapi_core/unmarshalling/schemas/__init__.py

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,53 @@
1-
from openapi_schema_validator import OAS30Validator
1+
from functools import partial
2+
3+
from isodate.isodatetime import parse_datetime
4+
from openapi_schema_validator import OAS30ReadValidator
5+
from openapi_schema_validator import OAS30WriteValidator
26
from openapi_schema_validator import OAS31Validator
37

48
from openapi_core.unmarshalling.schemas.enums import ValidationContext
59
from openapi_core.unmarshalling.schemas.factories import (
610
SchemaUnmarshallersFactory,
711
)
12+
from openapi_core.unmarshalling.schemas.util import format_byte
13+
from openapi_core.unmarshalling.schemas.util import format_date
14+
from openapi_core.unmarshalling.schemas.util import format_uuid
815

916
__all__ = [
17+
"oas30_format_unmarshallers",
18+
"oas31_format_unmarshallers",
1019
"oas30_request_schema_unmarshallers_factory",
1120
"oas30_response_schema_unmarshallers_factory",
1221
"oas31_request_schema_unmarshallers_factory",
1322
"oas31_response_schema_unmarshallers_factory",
1423
"oas31_schema_unmarshallers_factory",
1524
]
1625

26+
oas30_format_unmarshallers = {
27+
# string compatible
28+
"date": format_date,
29+
"date-time": parse_datetime,
30+
"binary": bytes,
31+
"uuid": format_uuid,
32+
"byte": format_byte,
33+
}
34+
oas31_format_unmarshallers = oas30_format_unmarshallers
35+
1736
oas30_request_schema_unmarshallers_factory = SchemaUnmarshallersFactory(
18-
OAS30Validator,
37+
OAS30WriteValidator,
38+
format_unmarshallers=oas30_format_unmarshallers,
1939
context=ValidationContext.REQUEST,
2040
)
2141

2242
oas30_response_schema_unmarshallers_factory = SchemaUnmarshallersFactory(
23-
OAS30Validator,
43+
OAS30ReadValidator,
44+
format_unmarshallers=oas30_format_unmarshallers,
2445
context=ValidationContext.RESPONSE,
2546
)
2647

2748
oas31_schema_unmarshallers_factory = SchemaUnmarshallersFactory(
2849
OAS31Validator,
50+
format_unmarshallers=oas31_format_unmarshallers,
2951
)
3052

3153
# alias to v31 version (request/response are the same bcs no context needed)

Diff for: openapi_core/unmarshalling/schemas/datatypes.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
from typing import Any
2+
from typing import Callable
13
from typing import Dict
24
from typing import Optional
35

46
from openapi_core.unmarshalling.schemas.formatters import Formatter
57

8+
FormatValidator = Callable[[Any], bool]
9+
FormatUnmarshaller = Callable[[Any], Any]
10+
611
CustomFormattersDict = Dict[str, Formatter]
712
FormattersDict = Dict[Optional[str], Formatter]
13+
UnmarshallersDict = Dict[str, Callable[[Any], Any]]

Diff for: openapi_core/unmarshalling/schemas/exceptions.py

+14-13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class UnmarshallerError(UnmarshalError):
1919

2020
@dataclass
2121
class InvalidSchemaValue(ValidateError):
22+
"""Value not valid for schema"""
23+
2224
value: str
2325
type: str
2426
schema_errors: Iterable[Exception] = field(default_factory=list)
@@ -30,28 +32,27 @@ def __str__(self) -> str:
3032

3133

3234
@dataclass
33-
class InvalidSchemaFormatValue(UnmarshallerError):
34-
"""Value failed to format with formatter"""
35+
class FormatterNotFoundError(UnmarshallerError):
36+
"""Formatter not found to unmarshal"""
37+
38+
type_format: str
39+
40+
def __str__(self) -> str:
41+
return f"Formatter not found for {self.type_format} format"
42+
43+
44+
class FormatUnmarshalError(UnmarshallerError):
45+
"""Unable to unmarshal value for format"""
3546

3647
value: str
3748
type: str
3849
original_exception: Exception
3950

4051
def __str__(self) -> str:
4152
return (
42-
"Failed to format value {value} to format {type}: {exception}"
53+
"Unable to unmarshal value {value} for format {type}: {exception}"
4354
).format(
4455
value=self.value,
4556
type=self.type,
4657
exception=self.original_exception,
4758
)
48-
49-
50-
@dataclass
51-
class FormatterNotFoundError(UnmarshallerError):
52-
"""Formatter not found to unmarshal"""
53-
54-
type_format: str
55-
56-
def __str__(self) -> str:
57-
return f"Formatter not found for {self.type_format} format"

Diff for: openapi_core/unmarshalling/schemas/factories.py

+135-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import sys
22
import warnings
3+
from functools import partial
34
from typing import Any
5+
from typing import Callable
46
from typing import Dict
57
from typing import Iterable
68
from typing import Optional
@@ -11,12 +13,15 @@
1113
from functools import cached_property
1214
else:
1315
from backports.cached_property import cached_property
16+
from jsonschema._format import FormatChecker
1417
from jsonschema.protocols import Validator
1518
from openapi_schema_validator import OAS30Validator
1619

1720
from openapi_core.spec import Spec
1821
from openapi_core.unmarshalling.schemas.datatypes import CustomFormattersDict
19-
from openapi_core.unmarshalling.schemas.datatypes import FormattersDict
22+
from openapi_core.unmarshalling.schemas.datatypes import FormatUnmarshaller
23+
from openapi_core.unmarshalling.schemas.datatypes import FormatValidator
24+
from openapi_core.unmarshalling.schemas.datatypes import UnmarshallersDict
2025
from openapi_core.unmarshalling.schemas.enums import ValidationContext
2126
from openapi_core.unmarshalling.schemas.exceptions import (
2227
FormatterNotFoundError,
@@ -47,38 +52,95 @@
4752

4853

4954
class SchemaValidatorsFactory:
50-
CONTEXTS = {
51-
ValidationContext.REQUEST: "write",
52-
ValidationContext.RESPONSE: "read",
53-
}
54-
5555
def __init__(
5656
self,
5757
schema_validator_class: Type[Validator],
58+
base_format_checker: Optional[FormatChecker] = None,
59+
formatters: Optional[CustomFormattersDict] = None,
60+
format_unmarshallers: Optional[UnmarshallersDict] = None,
5861
custom_formatters: Optional[CustomFormattersDict] = None,
59-
context: Optional[ValidationContext] = None,
6062
):
6163
self.schema_validator_class = schema_validator_class
64+
if base_format_checker is None:
65+
base_format_checker = self.schema_validator_class.FORMAT_CHECKER
66+
self.base_format_checker = base_format_checker
67+
if formatters is None:
68+
formatters = {}
69+
self.formatters = formatters
70+
if format_unmarshallers is None:
71+
format_unmarshallers = {}
72+
self.format_unmarshallers = format_unmarshallers
6273
if custom_formatters is None:
6374
custom_formatters = {}
6475
self.custom_formatters = custom_formatters
65-
self.context = context
6676

67-
def create(self, schema: Spec) -> Validator:
68-
resolver = schema.accessor.resolver # type: ignore
69-
custom_format_checks = {
77+
@cached_property
78+
def format_checker(self) -> FormatChecker:
79+
format_checks: Dict[str, Callable[[Any], bool]] = {
7080
name: formatter.validate
71-
for name, formatter in self.custom_formatters.items()
72-
}
73-
format_checker = build_format_checker(**custom_format_checks)
74-
kwargs = {
75-
"resolver": resolver,
76-
"format_checker": format_checker,
81+
for formatters_list in [self.formatters, self.custom_formatters]
82+
for name, formatter in formatters_list.items()
7783
}
78-
if self.context is not None:
79-
kwargs[self.CONTEXTS[self.context]] = True
84+
format_checks.update(
85+
{
86+
name: self._create_checker(name)
87+
for name, _ in self.format_unmarshallers.items()
88+
}
89+
)
90+
return build_format_checker(self.base_format_checker, **format_checks)
91+
92+
def _create_checker(self, name: str) -> FormatValidator:
93+
if name in self.base_format_checker.checkers:
94+
return partial(self.base_format_checker.check, format=name)
95+
96+
return lambda x: True
97+
98+
def get_checker(self, name: str) -> FormatValidator:
99+
if name in self.format_checker.checkers:
100+
return partial(self.format_checker.check, format=name)
101+
102+
return lambda x: True
103+
104+
def create(self, schema: Spec) -> Validator:
105+
resolver = schema.accessor.resolver # type: ignore
80106
with schema.open() as schema_dict:
81-
return self.schema_validator_class(schema_dict, **kwargs)
107+
return self.schema_validator_class(
108+
schema_dict,
109+
resolver=resolver,
110+
format_checker=self.format_checker,
111+
)
112+
113+
114+
class SchemaFormatUnmarshallersFactory:
115+
def __init__(
116+
self,
117+
validators_factory: SchemaValidatorsFactory,
118+
formatters: Optional[CustomFormattersDict] = None,
119+
format_unmarshallers: Optional[UnmarshallersDict] = None,
120+
custom_formatters: Optional[CustomFormattersDict] = None,
121+
):
122+
self.validators_factory = validators_factory
123+
if formatters is None:
124+
formatters = {}
125+
self.formatters = formatters
126+
if format_unmarshallers is None:
127+
format_unmarshallers = {}
128+
self.format_unmarshallers = format_unmarshallers
129+
if custom_formatters is None:
130+
custom_formatters = {}
131+
self.custom_formatters = custom_formatters
132+
133+
def create(self, schema_format: str) -> Optional[FormatUnmarshaller]:
134+
if schema_format in self.custom_formatters:
135+
formatter = self.custom_formatters[schema_format]
136+
return formatter.format
137+
if schema_format in self.formatters:
138+
formatter = self.formatters[schema_format]
139+
return formatter.format
140+
if schema_format in self.format_unmarshallers:
141+
return self.format_unmarshallers[schema_format]
142+
143+
return None
82144

83145

84146
class SchemaUnmarshallersFactory:
@@ -102,23 +164,59 @@ class SchemaUnmarshallersFactory:
102164
def __init__(
103165
self,
104166
schema_validator_class: Type[Validator],
167+
base_format_checker: Optional[FormatChecker] = None,
168+
formatters: Optional[CustomFormattersDict] = None,
169+
format_unmarshallers: Optional[UnmarshallersDict] = None,
105170
custom_formatters: Optional[CustomFormattersDict] = None,
106171
context: Optional[ValidationContext] = None,
107172
):
108173
self.schema_validator_class = schema_validator_class
174+
self.base_format_checker = base_format_checker
109175
if custom_formatters is None:
110176
custom_formatters = {}
177+
else:
178+
warnings.warn(
179+
"custom_formatters is deprecated. "
180+
"Register new checks to FormatChecker to validate custom formats "
181+
"and add format_unmarshallers to unmarshal custom formats.",
182+
DeprecationWarning,
183+
)
184+
self.formatters = formatters
185+
if format_unmarshallers is None:
186+
format_unmarshallers = {}
187+
self.format_unmarshallers = format_unmarshallers
111188
self.custom_formatters = custom_formatters
112189
self.context = context
113190

114191
@cached_property
115192
def validators_factory(self) -> SchemaValidatorsFactory:
116193
return SchemaValidatorsFactory(
117194
self.schema_validator_class,
195+
self.base_format_checker,
196+
self.formatters,
197+
self.format_unmarshallers,
198+
self.custom_formatters,
199+
)
200+
201+
@cached_property
202+
def format_unmarshallers_factory(self) -> SchemaFormatUnmarshallersFactory:
203+
return SchemaFormatUnmarshallersFactory(
204+
self.validators_factory,
205+
self.formatters,
206+
self.format_unmarshallers,
118207
self.custom_formatters,
119-
self.context,
120208
)
121209

210+
def get_format_validator(
211+
self, validator: Validator, schema_format: str
212+
) -> Optional[FormatValidator]:
213+
if schema_format in validator.format_checker.checkers:
214+
return partial(
215+
validator.format_checker.check, format=schema_format
216+
)
217+
218+
return None
219+
122220
def create(
123221
self, schema: Spec, type_override: Optional[str] = None
124222
) -> BaseSchemaUnmarshaller:
@@ -132,7 +230,19 @@ def create(
132230
validator = self.validators_factory.create(schema)
133231

134232
schema_format = schema.getkey("format")
135-
formatter = self.custom_formatters.get(schema_format)
233+
234+
# FIXME: don;t raise exception on unknown format
235+
if (
236+
schema_format
237+
and schema_format
238+
not in self.validators_factory.format_checker.checkers
239+
):
240+
raise FormatterNotFoundError(schema_format)
241+
242+
format_unmarshaller = self.format_unmarshallers_factory.create(
243+
schema_format
244+
)
245+
format_validator = self.get_format_validator(validator, schema_format)
136246

137247
schema_type = type_override or schema.getkey("type", "any")
138248
if isinstance(schema_type, Iterable) and not isinstance(
@@ -141,7 +251,7 @@ def create(
141251
return MultiTypeUnmarshaller(
142252
schema,
143253
validator,
144-
formatter,
254+
format_unmarshaller,
145255
self.validators_factory,
146256
self,
147257
context=self.context,
@@ -151,7 +261,7 @@ def create(
151261
return complex_klass(
152262
schema,
153263
validator,
154-
formatter,
264+
format_unmarshaller,
155265
self.validators_factory,
156266
self,
157267
context=self.context,
@@ -161,7 +271,7 @@ def create(
161271
return klass(
162272
schema,
163273
validator,
164-
formatter,
274+
format_unmarshaller,
165275
self.validators_factory,
166276
self,
167277
)

0 commit comments

Comments
 (0)