Skip to content

Commit 3417569

Browse files
committed
Object caster
1 parent efaa5ac commit 3417569

File tree

18 files changed

+507
-95
lines changed

18 files changed

+507
-95
lines changed

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

+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 IntegerCaster
6+
from openapi_core.casting.schemas.casters import NumberCaster
7+
from openapi_core.casting.schemas.casters import ObjectCaster
8+
from openapi_core.casting.schemas.casters import PrimitiveCaster
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", PrimitiveCaster),
33+
]
34+
)
35+
oas31_casters_dict = oas30_casters_dict.copy()
36+
oas31_casters_dict.update(
37+
{
38+
"null": PrimitiveCaster,
39+
}
40+
)
41+
42+
oas30_types_caster = TypesCaster(
43+
oas30_casters_dict,
44+
PrimitiveCaster,
45+
)
46+
oas31_types_caster = TypesCaster(
47+
oas31_casters_dict,
48+
PrimitiveCaster,
49+
multi=PrimitiveCaster,
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+
)

Diff for: openapi_core/casting/schemas/casters.py

+203-32
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,238 @@
11
from typing import TYPE_CHECKING
22
from typing import Any
33
from typing import Callable
4+
from typing import Generic
5+
from typing import Iterable
46
from typing import List
7+
from typing import Mapping
8+
from typing import Optional
9+
from typing import Type
10+
from typing import TypeVar
11+
from typing import Union
512

613
from jsonschema_path import SchemaPath
714

815
from openapi_core.casting.schemas.datatypes import CasterCallable
916
from openapi_core.casting.schemas.exceptions import CastError
17+
from openapi_core.schema.schemas import get_properties
18+
from openapi_core.util import forcebool
19+
from openapi_core.validation.schemas.validators import SchemaValidator
1020

11-
if TYPE_CHECKING:
12-
from openapi_core.casting.schemas.factories import SchemaCastersFactory
1321

14-
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:
20-
if value is None:
21-
return value
34+
return value
2235

23-
return self.cast(value)
2436

25-
def cast(self, value: Any) -> Any:
26-
raise NotImplementedError
37+
PrimitiveType = TypeVar("PrimitiveType")
2738

2839

29-
class CallableSchemaCaster(BaseSchemaCaster):
30-
def __init__(self, schema: SchemaPath, caster_callable: CasterCallable):
31-
super().__init__(schema)
32-
self.caster_callable = caster_callable
40+
class PrimitiveTypeCaster(Generic[PrimitiveType], PrimitiveCaster):
41+
primitive_type: Type[PrimitiveType] = NotImplemented
42+
43+
def __call__(self, value: Union[str, bytes]) -> Any:
44+
self.validate(value)
45+
46+
return self.primitive_type(value) # type: ignore [call-arg]
47+
48+
def validate(self, value: Any) -> None:
49+
# FIXME: don't cast data from media type deserializer
50+
# See https://github.com/python-openapi/openapi-core/issues/706
51+
# if not isinstance(value, (str, bytes)):
52+
# raise ValueError("should cast only from string or bytes")
53+
pass
54+
55+
56+
class IntegerCaster(PrimitiveTypeCaster[int]):
57+
primitive_type = int
58+
59+
60+
class NumberCaster(PrimitiveTypeCaster[float]):
61+
primitive_type = float
62+
63+
64+
class BooleanCaster(PrimitiveTypeCaster[bool]):
65+
primitive_type = bool
66+
67+
def __call__(self, value: Union[str, bytes]) -> Any:
68+
self.validate(value)
69+
70+
return self.primitive_type(forcebool(value))
71+
72+
def validate(self, value: Any) -> None:
73+
super().validate(value)
74+
75+
# FIXME: don't cast data from media type deserializer
76+
# See https://github.com/python-openapi/openapi-core/issues/706
77+
if isinstance(value, bool):
78+
return
79+
80+
if value.lower() not in ["false", "true"]:
81+
raise ValueError("not a boolean format")
82+
83+
84+
class ArrayCaster(PrimitiveCaster):
85+
@property
86+
def items_caster(self) -> "SchemaCaster":
87+
# sometimes we don't have any schema i.e. free-form objects
88+
items_schema = self.schema.get("items", SchemaPath.from_dict({}))
89+
return self.schema_caster.evolve(items_schema)
90+
91+
def __call__(self, value: Any) -> List[Any]:
92+
# str and bytes are not arrays according to the OpenAPI spec
93+
if isinstance(value, (str, bytes)) or not isinstance(value, Iterable):
94+
raise CastError(value, self.schema["type"])
3395

34-
def cast(self, value: Any) -> Any:
3596
try:
36-
return self.caster_callable(value)
97+
return list(map(self.items_caster.cast, value))
3798
except (ValueError, TypeError):
3899
raise CastError(value, self.schema["type"])
39100

40101

41-
class DummyCaster(BaseSchemaCaster):
42-
def cast(self, value: Any) -> Any:
102+
class ObjectCaster(PrimitiveCaster):
103+
def __call__(self, value: Any) -> Any:
104+
return self._cast_proparties(value)
105+
106+
def evolve(self, schema: SchemaPath) -> "ObjectCaster":
107+
cls = self.__class__
108+
109+
return cls(
110+
schema,
111+
self.schema_validator.evolve(schema),
112+
self.schema_caster.evolve(schema),
113+
)
114+
115+
def _cast_proparties(self, value: Any, schema_only: bool = False) -> Any:
116+
if not isinstance(value, dict):
117+
raise CastError(value, self.schema["type"])
118+
119+
all_of_schemas = self.schema_validator.iter_all_of_schemas(value)
120+
for all_of_schema in all_of_schemas:
121+
all_of_properties = self.evolve(all_of_schema)._cast_proparties(
122+
value, schema_only=True
123+
)
124+
value.update(all_of_properties)
125+
126+
for prop_name, prop_schema in get_properties(self.schema).items():
127+
try:
128+
prop_value = value[prop_name]
129+
except KeyError:
130+
continue
131+
value[prop_name] = self.schema_caster.evolve(prop_schema).cast(
132+
prop_value
133+
)
134+
135+
if schema_only:
136+
return value
137+
138+
additional_properties = self.schema.getkey(
139+
"additionalProperties", True
140+
)
141+
if additional_properties is not False:
142+
# free-form object
143+
if additional_properties is True:
144+
additional_prop_schema = SchemaPath.from_dict(
145+
{"nullable": True}
146+
)
147+
# defined schema
148+
else:
149+
additional_prop_schema = self.schema / "additionalProperties"
150+
additional_prop_caster = self.schema_caster.evolve(
151+
additional_prop_schema
152+
)
153+
for prop_name, prop_value in value.items():
154+
if prop_name in value:
155+
continue
156+
value[prop_name] = additional_prop_caster.cast(prop_value)
157+
43158
return value
44159

45160

46-
class ComplexCaster(BaseSchemaCaster):
161+
class TypesCaster:
162+
casters: Mapping[str, Type[PrimitiveCaster]] = {}
163+
multi: Optional[Type[PrimitiveCaster]] = None
164+
47165
def __init__(
48-
self, schema: SchemaPath, casters_factory: "SchemaCastersFactory"
166+
self,
167+
casters: Mapping[str, Type[PrimitiveCaster]],
168+
default: Type[PrimitiveCaster],
169+
multi: Optional[Type[PrimitiveCaster]] = None,
49170
):
50-
super().__init__(schema)
51-
self.casters_factory = casters_factory
171+
self.casters = casters
172+
self.default = default
173+
self.multi = multi
174+
175+
def get_caster(
176+
self,
177+
schema_type: Optional[Union[Iterable[str], str]],
178+
) -> Type["PrimitiveCaster"]:
179+
if schema_type is None:
180+
return self.default
181+
if isinstance(schema_type, Iterable) and not isinstance(
182+
schema_type, str
183+
):
184+
if self.multi is None:
185+
raise TypeError("caster does not accept multiple types")
186+
return self.multi
187+
188+
return self.casters[schema_type]
189+
190+
191+
class SchemaCaster:
192+
def __init__(
193+
self,
194+
schema: SchemaPath,
195+
schema_validator: SchemaValidator,
196+
types_caster: TypesCaster,
197+
):
198+
self.schema = schema
199+
self.schema_validator = schema_validator
52200

201+
self.types_caster = types_caster
53202

54-
class ArrayCaster(ComplexCaster):
55-
@property
56-
def items_caster(self) -> BaseSchemaCaster:
57-
return self.casters_factory.create(self.schema / "items")
203+
def cast(self, value: Any) -> Any:
204+
# skip casting for nullable in OpenAPI 3.0
205+
if value is None and self.schema.getkey("nullable", False):
206+
return value
58207

59-
def cast(self, value: Any) -> List[Any]:
60-
# str and bytes are not arrays according to the OpenAPI spec
61-
if isinstance(value, (str, bytes)):
62-
raise CastError(value, self.schema["type"])
208+
schema_type = self.schema.getkey("type")
209+
210+
type_caster = self.get_type_caster(schema_type)
211+
212+
if value is None:
213+
return value
63214

64215
try:
65-
return list(map(self.items_caster, value))
216+
return type_caster(value)
66217
except (ValueError, TypeError):
67-
raise CastError(value, self.schema["type"])
218+
raise CastError(value, schema_type)
219+
220+
def get_type_caster(
221+
self,
222+
schema_type: Optional[Union[Iterable[str], str]],
223+
) -> PrimitiveCaster:
224+
caster_cls = self.types_caster.get_caster(schema_type)
225+
return caster_cls(
226+
self.schema,
227+
self.schema_validator,
228+
self,
229+
)
230+
231+
def evolve(self, schema: SchemaPath) -> "SchemaCaster":
232+
cls = self.__class__
233+
234+
return cls(
235+
schema,
236+
self.schema_validator.evolve(schema),
237+
self.types_caster,
238+
)

0 commit comments

Comments
 (0)