Skip to content

Commit 2670d11

Browse files
committed
Add allOf support for responses definitions
1 parent 88ca206 commit 2670d11

File tree

4 files changed

+137
-50
lines changed

4 files changed

+137
-50
lines changed

openapi_python_client/parser/openapi.py

+52-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterable
14
from copy import deepcopy
25
from dataclasses import dataclass, field
36
from enum import Enum
@@ -217,11 +220,43 @@ class Model:
217220
"""
218221

219222
reference: Reference
223+
references: List[oai.Reference]
220224
required_properties: List[Property]
221225
optional_properties: List[Property]
222226
description: str
223227
relative_imports: Set[str]
224228

229+
def resolve_references(self, schemas) -> Union[List[Property], ParseError]:
230+
required_set = set()
231+
props = {}
232+
while self.references:
233+
reference = self.references.pop()
234+
prop = schemas[Reference.from_ref(reference.ref).class_name]
235+
props.update(prop.properties or {})
236+
for sub_prop in prop.allOf or []:
237+
if isinstance(sub_prop, oai.Reference):
238+
self.references += [sub_prop]
239+
else:
240+
props.update(sub_prop.properties)
241+
if isinstance(prop.required, Iterable):
242+
for sub_prop_name in prop.required:
243+
required_set.add(sub_prop_name)
244+
245+
for key, value in (props or {}).items():
246+
required = key in required_set
247+
p = property_from_data(name=key, required=required, data=value)
248+
if isinstance(p, ParseError):
249+
return p
250+
if required:
251+
self.required_properties.append(p)
252+
# Remove the optional version
253+
self.optional_properties = [op for op in self.optional_properties if op.name != p.name]
254+
elif not any(ep for ep in (self.optional_properties + self.required_properties) if ep.name == p.name):
255+
self.optional_properties.append(p)
256+
self.relative_imports.update(p.get_imports(prefix=".."))
257+
258+
return self.required_properties + self.optional_properties
259+
225260
@staticmethod
226261
def from_data(*, data: oai.Schema, name: str) -> Union["Model", ParseError]:
227262
"""A single Model from its OAI data
@@ -235,11 +270,24 @@ def from_data(*, data: oai.Schema, name: str) -> Union["Model", ParseError]:
235270
required_properties: List[Property] = []
236271
optional_properties: List[Property] = []
237272
relative_imports: Set[str] = set()
273+
references: List[Reference] = []
238274

239275
ref = Reference.from_ref(data.title or name)
276+
all_props = data.properties or {}
240277

241-
for key, value in (data.properties or {}).items():
278+
if not isinstance(data, oai.Reference) and data.allOf:
279+
for sub_prop in data.allOf:
280+
if isinstance(sub_prop, oai.Reference):
281+
references += [sub_prop]
282+
else:
283+
all_props.update(sub_prop.properties)
284+
required_set.update(sub_prop.required or [])
285+
286+
for key, value in all_props.items():
242287
required = key in required_set
288+
if not isinstance(value, oai.Reference) and value.allOf:
289+
# resolved later
290+
continue
243291
p = property_from_data(name=key, required=required, data=value)
244292
if isinstance(p, ParseError):
245293
return p
@@ -251,6 +299,7 @@ def from_data(*, data: oai.Schema, name: str) -> Union["Model", ParseError]:
251299

252300
model = Model(
253301
reference=ref,
302+
references=references,
254303
required_properties=required_properties,
255304
optional_properties=optional_properties,
256305
relative_imports=relative_imports,
@@ -289,6 +338,8 @@ def build(*, schemas: Dict[str, Union[oai.Reference, oai.Schema]]) -> "Schemas":
289338
result.errors.append(s)
290339
else:
291340
result.models[s.reference.class_name] = s
341+
for model in result.models.values():
342+
model.resolve_references(schemas)
292343
return result
293344

294345

openapi_python_client/parser/properties.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,9 @@ def _property_from_data(
520520
nullable=data.nullable,
521521
)
522522
if not data.type:
523-
return PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined.")
523+
return PropertyError(
524+
data=data, detail="Schemas must either have one of enum, anyOf, oneOf, allOf, or type defined."
525+
)
524526
if data.type == "string":
525527
return _string_based_property(name=name, required=required, data=data)
526528
elif data.type == "number":

tests/test_openapi_parser/test_openapi.py

+71-12
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,23 @@ class TestModel:
7171
def test_from_data(self, mocker):
7272
from openapi_python_client.parser.properties import Property
7373

74+
required_property = mocker.MagicMock(autospec=Property)
75+
required_property.allOf = None
76+
required_imports = mocker.MagicMock()
77+
required_property.get_imports.return_value = {required_imports}
78+
optional_property = mocker.MagicMock(autospec=Property)
79+
optional_property.allOf = None
80+
optional_imports = mocker.MagicMock()
81+
optional_property.get_imports.return_value = {optional_imports}
7482
in_data = oai.Schema.construct(
7583
title=mocker.MagicMock(),
7684
description=mocker.MagicMock(),
7785
required=["RequiredEnum"],
7886
properties={
79-
"RequiredEnum": mocker.MagicMock(),
80-
"OptionalDateTime": mocker.MagicMock(),
87+
"RequiredEnum": required_property,
88+
"OptionalDateTime": optional_property,
8189
},
8290
)
83-
required_property = mocker.MagicMock(autospec=Property)
84-
required_imports = mocker.MagicMock()
85-
required_property.get_imports.return_value = {required_imports}
86-
optional_property = mocker.MagicMock(autospec=Property)
87-
optional_imports = mocker.MagicMock()
88-
optional_property.get_imports.return_value = {optional_imports}
8991
property_from_data = mocker.patch(
9092
f"{MODULE_NAME}.property_from_data",
9193
side_effect=[required_property, optional_property],
@@ -107,6 +109,7 @@ def test_from_data(self, mocker):
107109
optional_property.get_imports.assert_called_once_with(prefix="..")
108110
assert result == Model(
109111
reference=from_ref(),
112+
references=[],
110113
required_properties=[required_property],
111114
optional_properties=[optional_property],
112115
relative_imports={
@@ -117,14 +120,17 @@ def test_from_data(self, mocker):
117120
)
118121

119122
def test_from_data_property_parse_error(self, mocker):
123+
from openapi_python_client.parser.properties import Property
124+
125+
required_property = mocker.MagicMock(autospec=Property)
126+
required_property.allOf = None
127+
optional_property = mocker.MagicMock(autospec=Property)
128+
optional_property.allOf = None
120129
in_data = oai.Schema.construct(
121130
title=mocker.MagicMock(),
122131
description=mocker.MagicMock(),
123132
required=["RequiredEnum"],
124-
properties={
125-
"RequiredEnum": mocker.MagicMock(),
126-
"OptionalDateTime": mocker.MagicMock(),
127-
},
133+
properties={"RequiredEnum": required_property, "OptionalDateTime": optional_property},
128134
)
129135
parse_error = ParseError(data=mocker.MagicMock())
130136
property_from_data = mocker.patch(
@@ -144,6 +150,57 @@ def test_from_data_property_parse_error(self, mocker):
144150

145151
assert result == parse_error
146152

153+
def test_resolve_references(self, mocker):
154+
155+
schemas = {
156+
"RefA": oai.Schema.construct(
157+
title=mocker.MagicMock(),
158+
description=mocker.MagicMock(),
159+
required=["String"],
160+
properties={
161+
"String": oai.Schema.construct(type="string"),
162+
"Enum": oai.Schema.construct(type="string", enum=["aValue"]),
163+
"DateTime": oai.Schema.construct(type="string", format="date-time"),
164+
},
165+
),
166+
"RefB": oai.Schema.construct(
167+
title=mocker.MagicMock(),
168+
description=mocker.MagicMock(),
169+
required=["DateTime"],
170+
properties={
171+
"Int": oai.Schema.construct(type="integer"),
172+
"DateTime": oai.Schema.construct(type="string", format="date-time"),
173+
"Float": oai.Schema.construct(type="number", format="float")
174+
},
175+
),
176+
}
177+
178+
model_schema = oai.Schema.construct(
179+
allOf=[
180+
oai.Reference.construct(ref="#/components/schemas/RefA"),
181+
oai.Reference.construct(ref="#/components/schemas/RefB"),
182+
oai.Schema.construct(
183+
title=mocker.MagicMock(),
184+
description=mocker.MagicMock(),
185+
required=["Float"],
186+
properties={
187+
"String": oai.Schema.construct(type="string"),
188+
"Float": oai.Schema.construct(type="number", format="float"),
189+
},
190+
),
191+
]
192+
)
193+
194+
from openapi_python_client.parser.openapi import Model
195+
196+
model = Model.from_data(data=model_schema, name="Model")
197+
model.resolve_references(schemas)
198+
print(f"{model=}")
199+
assert sorted(p.name for p in model.required_properties) == ["DateTime", "Float", "String"]
200+
assert all(p.required for p in model.required_properties)
201+
assert sorted(p.name for p in model.optional_properties) == ["Enum", "Int"]
202+
assert all(not p.required for p in model.optional_properties)
203+
147204

148205
class TestSchemas:
149206
def test_build(self, mocker):
@@ -159,6 +216,8 @@ def test_build(self, mocker):
159216
result = Schemas.build(schemas=in_data)
160217

161218
from_data.assert_has_calls([mocker.call(data=value, name=name) for (name, value) in in_data.items()])
219+
schema_1.resolve_references.assert_called_once_with(in_data)
220+
schema_2.resolve_references.assert_called_once_with(in_data)
162221
assert result == Schemas(
163222
models={
164223
schema_1.reference.class_name: schema_1,

tests/test_openapi_parser/test_properties.py

+11-36
Original file line numberDiff line numberDiff line change
@@ -614,9 +614,7 @@ def test_property_from_data_enum(self, mocker):
614614
data.title = mocker.MagicMock()
615615

616616
property_from_data(
617-
name=name,
618-
required=required,
619-
data=data,
617+
name=name, required=required, data=data,
620618
)
621619
EnumProperty.assert_called_once_with(
622620
name=name,
@@ -688,19 +686,14 @@ def test_property_from_data_simple_types(self, mocker, openapi_type, python_type
688686
data.nullable = mocker.MagicMock()
689687

690688
property_from_data(
691-
name=name,
692-
required=required,
693-
data=data,
689+
name=name, required=required, data=data,
694690
)
695691
clazz.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable)
696692

697693
def test_property_from_data_array(self, mocker):
698694
name = mocker.MagicMock()
699695
required = mocker.MagicMock()
700-
data = oai.Schema(
701-
type="array",
702-
items={"type": "number", "default": "0.0"},
703-
)
696+
data = oai.Schema(type="array", items={"type": "number", "default": "0.0"},)
704697
ListProperty = mocker.patch(f"{MODULE_NAME}.ListProperty")
705698
FloatProperty = mocker.patch(f"{MODULE_NAME}.FloatProperty")
706699
mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name)
@@ -729,10 +722,7 @@ def test_property_from_data_array_no_items(self, mocker):
729722
def test_property_from_data_array_invalid_items(self, mocker):
730723
name = mocker.MagicMock()
731724
required = mocker.MagicMock()
732-
data = oai.Schema(
733-
type="array",
734-
items={},
735-
)
725+
data = oai.Schema(type="array", items={},)
736726
mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name)
737727

738728
from openapi_python_client.parser.properties import property_from_data
@@ -744,12 +734,7 @@ def test_property_from_data_array_invalid_items(self, mocker):
744734
def test_property_from_data_union(self, mocker):
745735
name = mocker.MagicMock()
746736
required = mocker.MagicMock()
747-
data = oai.Schema(
748-
anyOf=[{"type": "number", "default": "0.0"}],
749-
oneOf=[
750-
{"type": "integer", "default": "0"},
751-
],
752-
)
737+
data = oai.Schema(anyOf=[{"type": "number", "default": "0.0"}], oneOf=[{"type": "integer", "default": "0"},],)
753738
UnionProperty = mocker.patch(f"{MODULE_NAME}.UnionProperty")
754739
FloatProperty = mocker.patch(f"{MODULE_NAME}.FloatProperty")
755740
IntProperty = mocker.patch(f"{MODULE_NAME}.IntProperty")
@@ -800,7 +785,7 @@ def test_property_from_data_no_valid_props_in_data(self):
800785

801786
data = oai.Schema()
802787
assert property_from_data(name="blah", required=True, data=data) == PropertyError(
803-
data=data, detail="Schemas must either have one of enum, anyOf, or type defined."
788+
data=data, detail="Schemas must either have one of enum, anyOf, oneOf, allOf, or type defined."
804789
)
805790

806791
def test_property_from_data_validation_error(self, mocker):
@@ -837,9 +822,7 @@ def test__string_based_property_no_format(self, mocker):
837822
data.pattern = mocker.MagicMock()
838823

839824
_string_based_property(
840-
name=name,
841-
required=required,
842-
data=data,
825+
name=name, required=required, data=data,
843826
)
844827
StringProperty.assert_called_once_with(
845828
name=name, required=required, pattern=data.pattern, default=data.default, nullable=data.nullable
@@ -863,9 +846,7 @@ def test__string_based_property_datetime_format(self, mocker):
863846
data.default = mocker.MagicMock()
864847

865848
_string_based_property(
866-
name=name,
867-
required=required,
868-
data=data,
849+
name=name, required=required, data=data,
869850
)
870851
DateTimeProperty.assert_called_once_with(
871852
name=name, required=required, default=data.default, nullable=data.nullable
@@ -888,9 +869,7 @@ def test__string_based_property_date_format(self, mocker):
888869
data.default = mocker.MagicMock()
889870

890871
_string_based_property(
891-
name=name,
892-
required=required,
893-
data=data,
872+
name=name, required=required, data=data,
894873
)
895874
DateProperty.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable)
896875

@@ -911,9 +890,7 @@ def test__string_based_property_binary_format(self, mocker):
911890
data.default = mocker.MagicMock()
912891

913892
_string_based_property(
914-
name=name,
915-
required=required,
916-
data=data,
893+
name=name, required=required, data=data,
917894
)
918895
FileProperty.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable)
919896

@@ -939,9 +916,7 @@ def test__string_based_property_unsupported_format(self, mocker):
939916
data.pattern = mocker.MagicMock()
940917

941918
_string_based_property(
942-
name=name,
943-
required=required,
944-
data=data,
919+
name=name, required=required, data=data,
945920
)
946921
StringProperty.assert_called_once_with(
947922
name=name, required=required, pattern=data.pattern, default=data.default, nullable=data.nullable

0 commit comments

Comments
 (0)