Skip to content

Commit 3a43cf7

Browse files
committed
Object caster
1 parent efaa5ac commit 3a43cf7

File tree

11 files changed

+329
-73
lines changed

11 files changed

+329
-73
lines changed
+62-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,65 @@
1+
from collections import OrderedDict
2+
3+
from openapi_core.casting.schemas.casters import ArrayCaster
4+
from openapi_core.casting.schemas.casters import BooleanCaster
5+
from openapi_core.casting.schemas.casters import DummyCaster
6+
from openapi_core.casting.schemas.casters import IntegerCaster
7+
from openapi_core.casting.schemas.casters import NumberCaster
8+
from openapi_core.casting.schemas.casters import ObjectCaster
9+
from openapi_core.casting.schemas.casters import TypesCaster
110
from openapi_core.casting.schemas.factories import SchemaCastersFactory
11+
from openapi_core.validation.schemas import (
12+
oas30_read_schema_validators_factory,
13+
)
14+
from openapi_core.validation.schemas import (
15+
oas30_write_schema_validators_factory,
16+
)
17+
from openapi_core.validation.schemas import oas31_schema_validators_factory
18+
19+
__all__ = [
20+
"oas30_write_schema_casters_factory",
21+
"oas30_read_schema_casters_factory",
22+
"oas31_schema_casters_factory",
23+
]
24+
25+
oas30_casters_dict = OrderedDict(
26+
[
27+
("object", ObjectCaster),
28+
("array", ArrayCaster),
29+
("boolean", BooleanCaster),
30+
("integer", IntegerCaster),
31+
("number", NumberCaster),
32+
("string", DummyCaster),
33+
]
34+
)
35+
oas31_casters_dict = oas30_casters_dict.copy()
36+
oas31_casters_dict.update(
37+
{
38+
"null": DummyCaster,
39+
}
40+
)
41+
42+
oas30_types_caster = TypesCaster(
43+
oas30_casters_dict,
44+
DummyCaster,
45+
)
46+
oas31_types_caster = TypesCaster(
47+
oas31_casters_dict,
48+
DummyCaster,
49+
multi=DummyCaster,
50+
)
51+
52+
oas30_write_schema_casters_factory = SchemaCastersFactory(
53+
oas30_write_schema_validators_factory,
54+
oas30_types_caster,
55+
)
256

3-
__all__ = ["schema_casters_factory"]
57+
oas30_read_schema_casters_factory = SchemaCastersFactory(
58+
oas30_read_schema_validators_factory,
59+
oas30_types_caster,
60+
)
461

5-
schema_casters_factory = SchemaCastersFactory()
62+
oas31_schema_casters_factory = SchemaCastersFactory(
63+
oas31_schema_validators_factory,
64+
oas31_types_caster,
65+
)
+193-21
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,239 @@
11
from typing import TYPE_CHECKING
22
from typing import Any
33
from typing import Callable
4+
from typing import Iterable
45
from typing import List
6+
from typing import Mapping
7+
from typing import Optional
8+
from typing import Type
9+
from typing import Union
510

611
from jsonschema_path import SchemaPath
712

813
from openapi_core.casting.schemas.datatypes import CasterCallable
914
from openapi_core.casting.schemas.exceptions import CastError
15+
from openapi_core.schema.schemas import get_properties
16+
from openapi_core.validation.schemas.validators import SchemaValidator
1017

1118
if TYPE_CHECKING:
1219
from openapi_core.casting.schemas.factories import SchemaCastersFactory
1320

1421

15-
class BaseSchemaCaster:
16-
def __init__(self, schema: SchemaPath):
22+
class PrimitiveCaster:
23+
def __init__(
24+
self,
25+
schema: SchemaPath,
26+
schema_validator: SchemaValidator,
27+
schema_caster: "SchemaCaster",
28+
):
1729
self.schema = schema
30+
self.schema_validator = schema_validator
31+
self.schema_caster = schema_caster
1832

1933
def __call__(self, value: Any) -> Any:
2034
if value is None:
2135
return value
2236

2337
return self.cast(value)
2438

39+
def evolve(self, schema: SchemaPath) -> "SchemaCaster":
40+
cls = self.__class__
41+
42+
return cls(
43+
schema,
44+
self.schema_validator.evolve(schema),
45+
self.schema_caster.evolve(schema),
46+
)
47+
2548
def cast(self, value: Any) -> Any:
2649
raise NotImplementedError
2750

2851

29-
class CallableSchemaCaster(BaseSchemaCaster):
30-
def __init__(self, schema: SchemaPath, caster_callable: CasterCallable):
31-
super().__init__(schema)
32-
self.caster_callable = caster_callable
52+
class DummyCaster(PrimitiveCaster):
53+
def cast(self, value: Any) -> Any:
54+
return value
55+
56+
57+
class PrimitiveTypeCaster(PrimitiveCaster):
58+
primitive_type: CasterCallable = NotImplemented
3359

3460
def cast(self, value: Any) -> Any:
61+
if isinstance(value, self.primitive_type):
62+
return value
63+
64+
if not isinstance(value, (str, bytes)):
65+
raise CastError(value, self.schema["type"])
66+
3567
try:
36-
return self.caster_callable(value)
68+
return self.primitive_type(value)
3769
except (ValueError, TypeError):
3870
raise CastError(value, self.schema["type"])
3971

4072

41-
class DummyCaster(BaseSchemaCaster):
42-
def cast(self, value: Any) -> Any:
43-
return value
73+
class IntegerCaster(PrimitiveTypeCaster):
74+
primitive_type = int
4475

4576

46-
class ComplexCaster(BaseSchemaCaster):
47-
def __init__(
48-
self, schema: SchemaPath, casters_factory: "SchemaCastersFactory"
49-
):
50-
super().__init__(schema)
51-
self.casters_factory = casters_factory
77+
class NumberCaster(PrimitiveTypeCaster):
78+
primitive_type = float
79+
80+
81+
class BooleanCaster(PrimitiveTypeCaster):
82+
primitive_type = bool
5283

5384

54-
class ArrayCaster(ComplexCaster):
85+
class ArrayCaster(PrimitiveCaster):
5586
@property
56-
def items_caster(self) -> BaseSchemaCaster:
57-
return self.casters_factory.create(self.schema / "items")
87+
def items_caster(self) -> "SchemaUnmarshaller":
88+
# sometimes we don't have any schema i.e. free-form objects
89+
items_schema = self.schema.get("items", SchemaPath.from_dict({}))
90+
return self.schema_caster.evolve(items_schema)
5891

5992
def cast(self, value: Any) -> List[Any]:
6093
# str and bytes are not arrays according to the OpenAPI spec
61-
if isinstance(value, (str, bytes)):
94+
if isinstance(value, (str, bytes)) or not isinstance(value, Iterable):
6295
raise CastError(value, self.schema["type"])
6396

6497
try:
65-
return list(map(self.items_caster, value))
98+
return list(map(self.items_caster.cast, value))
6699
except (ValueError, TypeError):
67100
raise CastError(value, self.schema["type"])
101+
102+
103+
class ObjectCaster(PrimitiveCaster):
104+
def cast(self, value: Any, schema_only: bool = False) -> Any:
105+
if not isinstance(value, dict):
106+
raise CastError(value, self.schema["type"])
107+
108+
one_of_schema = self.schema_validator.get_one_of_schema(value)
109+
if one_of_schema is not None:
110+
one_of_properties = self.evolve(one_of_schema).cast(
111+
value, schema_only=True
112+
)
113+
value.update(one_of_properties)
114+
115+
any_of_schemas = self.schema_validator.iter_any_of_schemas(value)
116+
for any_of_schema in any_of_schemas:
117+
any_of_properties = self.evolve(any_of_schema).cast(
118+
value, schema_only=True
119+
)
120+
value.update(any_of_properties)
121+
122+
all_of_schemas = self.schema_validator.iter_all_of_schemas(value)
123+
for all_of_schema in all_of_schemas:
124+
all_of_properties = self.evolve(all_of_schema).cast(
125+
value, schema_only=True
126+
)
127+
value.update(all_of_properties)
128+
129+
for prop_name, prop_schema in get_properties(self.schema).items():
130+
try:
131+
prop_value = value[prop_name]
132+
except KeyError:
133+
if "default" not in prop_schema:
134+
continue
135+
prop_value = prop_schema["default"]
136+
137+
value[prop_name] = self.schema_caster.evolve(prop_schema).cast(
138+
prop_value
139+
)
140+
141+
if schema_only:
142+
return value
143+
144+
additional_properties = self.schema.getkey(
145+
"additionalProperties", True
146+
)
147+
if additional_properties is not False:
148+
# free-form object
149+
if additional_properties is True:
150+
additional_prop_schema = SchemaPath.from_dict(
151+
{"nullable": True}
152+
)
153+
# defined schema
154+
else:
155+
additional_prop_schema = self.schema / "additionalProperties"
156+
additional_prop_caster = self.schema_caster.evolve(
157+
additional_prop_schema
158+
)
159+
for prop_name, prop_value in value.items():
160+
if prop_name in value:
161+
continue
162+
value[prop_name] = additional_prop_caster.cast(prop_value)
163+
164+
return value
165+
166+
167+
class TypesCaster:
168+
casters: Mapping[str, Type[PrimitiveCaster]] = {}
169+
multi: Optional[Type[PrimitiveCaster]] = None
170+
171+
def __init__(
172+
self,
173+
casters: Mapping[str, Type[PrimitiveCaster]],
174+
default: Type[PrimitiveCaster],
175+
multi: Optional[Type[PrimitiveCaster]] = None,
176+
):
177+
self.casters = casters
178+
self.default = default
179+
self.multi = multi
180+
181+
def get_types(self) -> List[str]:
182+
return list(self.casters.keys())
183+
184+
def get_caster(
185+
self,
186+
schema_type: Optional[Union[Iterable[str], str]],
187+
) -> Type["PrimitiveCaster"]:
188+
if schema_type is None:
189+
return self.default
190+
if isinstance(schema_type, Iterable) and not isinstance(
191+
schema_type, str
192+
):
193+
if self.multi is None:
194+
raise TypeError("caster does not accept multiple types")
195+
return self.multi
196+
197+
return self.casters[schema_type]
198+
199+
200+
class SchemaCaster:
201+
def __init__(
202+
self,
203+
schema: SchemaPath,
204+
schema_validator: SchemaValidator,
205+
types_caster: TypesCaster,
206+
):
207+
self.schema = schema
208+
self.schema_validator = schema_validator
209+
210+
self.types_caster = types_caster
211+
212+
def cast(self, value: Any) -> Any:
213+
# skip casting for nullable in OpenAPI 3.0
214+
if value is None and self.schema.getkey("nullable", False):
215+
return value
216+
217+
schema_type = self.schema.getkey("type")
218+
type_caster = self.get_type_caster(schema_type)
219+
return type_caster(value)
220+
221+
def get_type_caster(
222+
self,
223+
schema_type: Optional[Union[Iterable[str], str]],
224+
) -> PrimitiveCaster:
225+
caster_cls = self.types_caster.get_caster(schema_type)
226+
return caster_cls(
227+
self.schema,
228+
self.schema_validator,
229+
self,
230+
)
231+
232+
def evolve(self, schema: SchemaPath) -> "SchemaCaster":
233+
cls = self.__class__
234+
235+
return cls(
236+
schema,
237+
self.schema_validator.evolve(schema),
238+
self.types_caster,
239+
)
+26-29
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,35 @@
11
from typing import Dict
2+
from typing import Optional
23

34
from jsonschema_path import SchemaPath
45

5-
from openapi_core.casting.schemas.casters import ArrayCaster
6-
from openapi_core.casting.schemas.casters import BaseSchemaCaster
7-
from openapi_core.casting.schemas.casters import CallableSchemaCaster
8-
from openapi_core.casting.schemas.casters import DummyCaster
6+
from openapi_core.casting.schemas.casters import SchemaCaster
7+
from openapi_core.casting.schemas.casters import TypesCaster
98
from openapi_core.casting.schemas.datatypes import CasterCallable
109
from openapi_core.util import forcebool
10+
from openapi_core.validation.schemas.datatypes import FormatValidatorsDict
11+
from openapi_core.validation.schemas.factories import SchemaValidatorsFactory
1112

1213

1314
class SchemaCastersFactory:
14-
DUMMY_CASTERS = [
15-
"string",
16-
"object",
17-
"any",
18-
]
19-
PRIMITIVE_CASTERS: Dict[str, CasterCallable] = {
20-
"integer": int,
21-
"number": float,
22-
"boolean": forcebool,
23-
}
24-
COMPLEX_CASTERS = {
25-
"array": ArrayCaster,
26-
}
27-
28-
def create(self, schema: SchemaPath) -> BaseSchemaCaster:
29-
schema_type = schema.getkey("type", "any")
30-
31-
if schema_type in self.DUMMY_CASTERS:
32-
return DummyCaster(schema)
33-
34-
if schema_type in self.PRIMITIVE_CASTERS:
35-
caster_callable = self.PRIMITIVE_CASTERS[schema_type]
36-
return CallableSchemaCaster(schema, caster_callable)
37-
38-
return ArrayCaster(schema, self)
15+
def __init__(
16+
self,
17+
schema_validators_factory: SchemaValidatorsFactory,
18+
types_caster: TypesCaster,
19+
):
20+
self.schema_validators_factory = schema_validators_factory
21+
self.types_caster = types_caster
22+
23+
def create(
24+
self,
25+
schema: SchemaPath,
26+
format_validators: Optional[FormatValidatorsDict] = None,
27+
extra_format_validators: Optional[FormatValidatorsDict] = None,
28+
) -> SchemaCaster:
29+
schema_validator = self.schema_validators_factory.create(
30+
schema,
31+
format_validators=format_validators,
32+
extra_format_validators=extra_format_validators,
33+
)
34+
35+
return SchemaCaster(schema, schema_validator, self.types_caster)

0 commit comments

Comments
 (0)