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
Lines changed: 62 additions & 2 deletions
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+
)
Lines changed: 193 additions & 21 deletions
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+
)
Lines changed: 26 additions & 29 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy