Skip to content

Commit de29192

Browse files
committed
parser / properties / resolve local $ref
1 parent af09640 commit de29192

File tree

3 files changed

+253
-5
lines changed

3 files changed

+253
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Additions
1111

12+
- Add support for properties local reference ($ref) resolution
1213
- New `--meta` command line option for specifying what type of metadata should be generated:
1314
- `poetry` is the default value, same behavior you're used to in previous versions
1415
- `setup` will generate a pyproject.toml with no Poetry information, and instead create a `setup.py` with the

openapi_python_client/parser/properties/__init__.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,12 +519,76 @@ def update_schemas_with_data(name: str, data: oai.Schema, schemas: Schemas) -> U
519519
return schemas
520520

521521

522+
def resolve_reference_and_update_schemas(
523+
name: str, data: oai.Reference, schemas: Schemas, references_by_name: Dict[str, oai.Reference]
524+
) -> Union[Schemas, PropertyError]:
525+
if _is_local_reference(data):
526+
return _resolve_local_reference_schema(name, data, schemas, references_by_name)
527+
else:
528+
return _resolve_remote_reference_schema(name, data, schemas, references_by_name)
529+
530+
531+
def _resolve_local_reference_schema(
532+
name: str, data: oai.Reference, schemas: Schemas, references_by_name: Dict[str, oai.Reference]
533+
) -> Union[Schemas, PropertyError]:
534+
resolved_model_or_enum = _resolve_model_or_enum_reference(name, data, schemas, references_by_name)
535+
536+
if resolved_model_or_enum:
537+
if isinstance(resolved_model_or_enum, EnumProperty):
538+
schemas.enums[name] = resolved_model_or_enum
539+
540+
elif isinstance(resolved_model_or_enum, ModelProperty):
541+
schemas.models[name] = resolved_model_or_enum
542+
543+
return schemas
544+
else:
545+
return PropertyError(data=data, detail="Failed to resolve local reference schemas.")
546+
547+
548+
def _resolve_model_or_enum_reference(
549+
name: str, data: oai.Reference, schemas: Schemas, references_by_name: Dict[str, oai.Reference]
550+
) -> Union[EnumProperty, ModelProperty, None]:
551+
target_model = _reference_model_name(data)
552+
553+
if target_model == name:
554+
return None # Avoid infinite loop
555+
556+
if target_model in references_by_name:
557+
return _resolve_model_or_enum_reference(
558+
target_model, references_by_name[target_model], schemas, references_by_name
559+
)
560+
561+
if target_model in schemas.enums:
562+
return schemas.enums[target_model]
563+
elif target_model in schemas.models:
564+
return schemas.models[target_model]
565+
566+
return None
567+
568+
569+
def _resolve_remote_reference_schema(
570+
name: str, data: oai.Reference, schemas: Schemas, references_by_name: Dict[str, oai.Reference]
571+
) -> Union[Schemas, PropertyError]:
572+
return PropertyError(data=data, detail="Remote reference schemas are not supported.")
573+
574+
575+
def _is_local_reference(reference: oai.Reference) -> bool:
576+
return reference.ref.startswith("#", 0)
577+
578+
579+
def _reference_model_name(reference: oai.Reference) -> str:
580+
parts = reference.ref.split("/")
581+
return utils.pascal_case(parts[-1])
582+
583+
522584
def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) -> Schemas:
523585
""" Get a list of Schemas from an OpenAPI dict """
524586
schemas = Schemas()
525587
to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Schema]]] = components.items()
526588
processing = True
527589
errors: List[PropertyError] = []
590+
references_by_name: Dict[str, oai.Reference] = dict()
591+
references_to_process: List[Tuple[str, oai.Reference]] = list()
528592

529593
# References could have forward References so keep going as long as we are making progress
530594
while processing:
@@ -534,16 +598,27 @@ def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) ->
534598
# Only accumulate errors from the last round, since we might fix some along the way
535599
for name, data in to_process:
536600
if isinstance(data, oai.Reference):
537-
schemas.errors.append(PropertyError(data=data, detail="Reference schemas are not supported."))
601+
references_by_name[name] = data
602+
references_to_process.append((name, data))
538603
continue
604+
539605
schemas_or_err = update_schemas_with_data(name, data, schemas)
606+
540607
if isinstance(schemas_or_err, PropertyError):
541608
next_round.append((name, data))
542609
errors.append(schemas_or_err)
543610
else:
544611
schemas = schemas_or_err
545-
processing = True # We made some progress this round, do another after it's done
612+
processing = True # We made some progress this round, do another after it's donea
613+
546614
to_process = next_round
615+
616+
for name, reference in references_to_process:
617+
schemas_or_err = resolve_reference_and_update_schemas(name, reference, schemas, references_by_name)
618+
619+
if isinstance(schemas_or_err, PropertyError):
620+
errors.append(schemas_or_err)
621+
547622
schemas.errors.extend(errors)
548623

549624
return schemas

tests/test_parser/test_properties/test_init.py

Lines changed: 175 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,13 +1020,43 @@ def test_build_schemas(mocker):
10201020
assert result.errors == [error]
10211021

10221022

1023-
def test_build_parse_error_on_reference():
1023+
def test_build_parse_error_on_unknown_local_reference():
10241024
from openapi_python_client.parser.openapi import build_schemas
10251025

1026-
ref_schema = oai.Reference.construct()
1026+
ref_schema = oai.Reference.construct(ref="#/foobar")
10271027
in_data = {"1": ref_schema}
10281028
result = build_schemas(components=in_data)
1029-
assert result.errors[0] == PropertyError(data=ref_schema, detail="Reference schemas are not supported.")
1029+
assert result.errors[0] == PropertyError(data=ref_schema, detail="Failed to resolve local reference schemas.")
1030+
1031+
1032+
def test_build_parse_success_on_known_local_reference(mocker):
1033+
from openapi_python_client.parser.openapi import build_schemas
1034+
1035+
build_model_property = mocker.patch(f"{MODULE_NAME}.build_model_property")
1036+
schemas = mocker.MagicMock()
1037+
build_enum_property = mocker.patch(f"{MODULE_NAME}.build_enum_property", return_value=(mocker.MagicMock(), schemas))
1038+
in_data = {"1": oai.Reference.construct(ref="#/foobar"), "foobar": mocker.MagicMock(enum=["val1", "val2", "val3"])}
1039+
1040+
result = build_schemas(components=in_data)
1041+
1042+
assert len(result.errors) == 0
1043+
assert result.enums["1"] == result.enums["foobar"]
1044+
1045+
1046+
def test_build_parse_error_on_remote_reference():
1047+
from openapi_python_client.parser.openapi import build_schemas
1048+
1049+
ref_schemas = [
1050+
oai.Reference.construct(ref="http://foobar/../foobar.yaml#/foobar"),
1051+
oai.Reference.construct(ref="https://foobar/foobar.yaml#/foobar"),
1052+
oai.Reference.construct(ref="../foobar.yaml#/foobar"),
1053+
oai.Reference.construct(ref="foobar.yaml#/foobar"),
1054+
oai.Reference.construct(ref="//foobar#/foobar"),
1055+
]
1056+
for ref_schema in ref_schemas:
1057+
in_data = {"1": ref_schema}
1058+
result = build_schemas(components=in_data)
1059+
assert result.errors[0] == PropertyError(data=ref_schema, detail="Remote reference schemas are not supported.")
10301060

10311061

10321062
def test_build_enums(mocker):
@@ -1191,3 +1221,145 @@ def test_build_enum_property_bad_default():
11911221

11921222
assert schemas == schemas
11931223
assert err == PropertyError(detail="B is an invalid default for enum Existing", data=data)
1224+
1225+
1226+
def test__is_local_reference():
1227+
from openapi_python_client.parser.properties import _is_local_reference
1228+
1229+
data_set = [
1230+
("//foobar#foobar", False),
1231+
("foobar#/foobar", False),
1232+
("foobar.json", False),
1233+
("foobar.yaml", False),
1234+
("../foo/bar.json#/foobar", False),
1235+
("#/foobar", True),
1236+
("#/foo/bar", True),
1237+
]
1238+
1239+
for data, expected_result in data_set:
1240+
ref = oai.Reference.construct(ref=data)
1241+
assert _is_local_reference(ref) == expected_result
1242+
1243+
1244+
def test__reference_model_name():
1245+
from openapi_python_client.parser.properties import _reference_model_name
1246+
1247+
data_set = [
1248+
("#/foobar", "Foobar"),
1249+
("#/foo/bar", "Bar"),
1250+
]
1251+
1252+
for data, expected_result in data_set:
1253+
ref = oai.Reference.construct(ref=data)
1254+
assert _reference_model_name(ref) == expected_result
1255+
1256+
1257+
def test__resolve_model_or_enum_reference(mocker):
1258+
from openapi_python_client.parser.properties import _resolve_model_or_enum_reference
1259+
from openapi_python_client.parser.properties.schemas import Schemas
1260+
1261+
references_by_name = {
1262+
"FooBarReferenceLoop": oai.Reference.construct(ref="#/foobar"),
1263+
"FooBarDeeperReferenceLoop": oai.Reference.construct(ref="#/FooBarReferenceLoop"),
1264+
"BarFooReferenceLoop": oai.Reference.construct(ref="#/barfoo"),
1265+
"BarFooDeeperReferenceLoop": oai.Reference.construct(ref="#/BarFooReferenceLoop"),
1266+
"InfiniteReferenceLoop": oai.Reference.construct(ref="#/InfiniteReferenceLoop"),
1267+
"UnknownReference": oai.Reference.construct(ref="#/unknown"),
1268+
}
1269+
schemas = Schemas(enums={"Foobar": 1}, models={"Barfoo": 2})
1270+
1271+
res_1 = _resolve_model_or_enum_reference(
1272+
"FooBarReferenceLoop", references_by_name["FooBarReferenceLoop"], schemas, references_by_name
1273+
)
1274+
res_2 = _resolve_model_or_enum_reference(
1275+
"FooBarDeeperReferenceLoop", references_by_name["FooBarDeeperReferenceLoop"], schemas, references_by_name
1276+
)
1277+
res_3 = _resolve_model_or_enum_reference(
1278+
"BarFooReferenceLoop", references_by_name["BarFooReferenceLoop"], schemas, references_by_name
1279+
)
1280+
res_4 = _resolve_model_or_enum_reference(
1281+
"BarFooDeeperReferenceLoop", references_by_name["BarFooDeeperReferenceLoop"], schemas, references_by_name
1282+
)
1283+
res_5 = _resolve_model_or_enum_reference(
1284+
"InfiniteReferenceLoop", references_by_name["InfiniteReferenceLoop"], schemas, references_by_name
1285+
)
1286+
res_6 = _resolve_model_or_enum_reference(
1287+
"UnknownReference", references_by_name["UnknownReference"], schemas, references_by_name
1288+
)
1289+
1290+
assert res_1 == schemas.enums["Foobar"]
1291+
assert res_2 == schemas.enums["Foobar"]
1292+
assert res_3 == schemas.models["Barfoo"]
1293+
assert res_4 == schemas.models["Barfoo"]
1294+
assert res_5 == None
1295+
assert res_6 == None
1296+
1297+
1298+
def test__resolve_local_reference_schema(mocker):
1299+
from openapi_python_client.parser.properties import _resolve_local_reference_schema
1300+
from openapi_python_client.parser.properties.enum_property import EnumProperty
1301+
from openapi_python_client.parser.properties.model_property import ModelProperty
1302+
from openapi_python_client.parser.properties.schemas import Schemas
1303+
1304+
references_by_name = {
1305+
"FooBarReferenceLoop": oai.Reference.construct(ref="#/foobar"),
1306+
"FooBarDeeperReferenceLoop": oai.Reference.construct(ref="#/FooBarReferenceLoop"),
1307+
"BarFooReferenceLoop": oai.Reference.construct(ref="#/barfoo"),
1308+
"BarFooDeeperReferenceLoop": oai.Reference.construct(ref="#/BarFooReferenceLoop"),
1309+
"InfiniteReferenceLoop": oai.Reference.construct(ref="#/InfiniteReferenceLoop"),
1310+
"UnknownReference": oai.Reference.construct(ref="#/unknown"),
1311+
}
1312+
schemas = Schemas(
1313+
enums={
1314+
"Foobar": EnumProperty(
1315+
name="Foobar",
1316+
required=False,
1317+
nullable=True,
1318+
default="foobar",
1319+
values=["foobar"],
1320+
value_type="str",
1321+
reference="",
1322+
)
1323+
},
1324+
models={
1325+
"Barfoo": ModelProperty(
1326+
name="Barfoo",
1327+
required=False,
1328+
nullable=True,
1329+
default="barfoo",
1330+
reference="",
1331+
required_properties=[],
1332+
optional_properties=[],
1333+
description="",
1334+
relative_imports=[],
1335+
additional_properties=[],
1336+
)
1337+
},
1338+
)
1339+
1340+
res_1 = _resolve_local_reference_schema(
1341+
"FooBarReferenceLoop", references_by_name["FooBarReferenceLoop"], schemas, references_by_name
1342+
)
1343+
res_2 = _resolve_local_reference_schema(
1344+
"FooBarDeeperReferenceLoop", references_by_name["FooBarDeeperReferenceLoop"], schemas, references_by_name
1345+
)
1346+
res_3 = _resolve_local_reference_schema(
1347+
"BarFooReferenceLoop", references_by_name["BarFooReferenceLoop"], schemas, references_by_name
1348+
)
1349+
res_4 = _resolve_local_reference_schema(
1350+
"BarFooDeeperReferenceLoop", references_by_name["BarFooDeeperReferenceLoop"], schemas, references_by_name
1351+
)
1352+
res_5 = _resolve_local_reference_schema(
1353+
"InfiniteReferenceLoop", references_by_name["InfiniteReferenceLoop"], schemas, references_by_name
1354+
)
1355+
res_6 = _resolve_local_reference_schema(
1356+
"UnknownReference", references_by_name["UnknownReference"], schemas, references_by_name
1357+
)
1358+
1359+
assert res_1 == res_2 == res_3 == res_4 == schemas
1360+
assert schemas.enums["FooBarReferenceLoop"] == schemas.enums["Foobar"]
1361+
assert schemas.enums["FooBarDeeperReferenceLoop"] == schemas.enums["Foobar"]
1362+
assert schemas.models["BarFooReferenceLoop"] == schemas.models["Barfoo"]
1363+
assert schemas.models["BarFooDeeperReferenceLoop"] == schemas.models["Barfoo"]
1364+
assert isinstance(res_5, PropertyError)
1365+
assert isinstance(res_6, PropertyError)

0 commit comments

Comments
 (0)