Skip to content

Commit 87c5b12

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

File tree

3 files changed

+286
-6
lines changed

3 files changed

+286
-6
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: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -519,12 +519,83 @@ 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+
model_name = utils.pascal_case(name)
538+
539+
if isinstance(resolved_model_or_enum, EnumProperty):
540+
schemas.enums[model_name] = resolved_model_or_enum
541+
542+
elif isinstance(resolved_model_or_enum, ModelProperty):
543+
schemas.models[model_name] = resolved_model_or_enum
544+
545+
return schemas
546+
else:
547+
return PropertyError(data=data, detail="Failed to resolve local reference schemas.")
548+
549+
550+
def _resolve_model_or_enum_reference(
551+
name: str, data: oai.Reference, schemas: Schemas, references_by_name: Dict[str, oai.Reference]
552+
) -> Union[EnumProperty, ModelProperty, None]:
553+
target_model = _reference_model_name(data)
554+
target_name = _reference_name(data)
555+
556+
if target_model == name or target_name == name:
557+
return None # Avoid infinite loop
558+
559+
if target_name in references_by_name:
560+
return _resolve_model_or_enum_reference(
561+
target_name, references_by_name[target_name], schemas, references_by_name
562+
)
563+
564+
if target_model in schemas.enums:
565+
return schemas.enums[target_model]
566+
elif target_model in schemas.models:
567+
return schemas.models[target_model]
568+
569+
return None
570+
571+
572+
def _resolve_remote_reference_schema(
573+
name: str, data: oai.Reference, schemas: Schemas, references_by_name: Dict[str, oai.Reference]
574+
) -> Union[Schemas, PropertyError]:
575+
return PropertyError(data=data, detail="Remote reference schemas are not supported.")
576+
577+
578+
def _is_local_reference(reference: oai.Reference) -> bool:
579+
return reference.ref.startswith("#", 0)
580+
581+
582+
def _reference_model_name(reference: oai.Reference) -> str:
583+
return utils.pascal_case(_reference_name(reference))
584+
585+
586+
def _reference_name(reference: oai.Reference) -> str:
587+
parts = reference.ref.split("/")
588+
return parts[-1]
589+
590+
522591
def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) -> Schemas:
523592
""" Get a list of Schemas from an OpenAPI dict """
524593
schemas = Schemas()
525594
to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Schema]]] = components.items()
526595
processing = True
527596
errors: List[PropertyError] = []
597+
references_by_name: Dict[str, oai.Reference] = dict()
598+
references_to_process: List[Tuple[str, oai.Reference]] = list()
528599

529600
# References could have forward References so keep going as long as we are making progress
530601
while processing:
@@ -534,16 +605,26 @@ def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) ->
534605
# Only accumulate errors from the last round, since we might fix some along the way
535606
for name, data in to_process:
536607
if isinstance(data, oai.Reference):
537-
schemas.errors.append(PropertyError(data=data, detail="Reference schemas are not supported."))
608+
references_by_name[name] = data
609+
references_to_process.append((name, data))
538610
continue
611+
539612
schemas_or_err = update_schemas_with_data(name, data, schemas)
613+
540614
if isinstance(schemas_or_err, PropertyError):
541615
next_round.append((name, data))
542616
errors.append(schemas_or_err)
543617
else:
544618
schemas = schemas_or_err
545-
processing = True # We made some progress this round, do another after it's done
619+
processing = True # We made some progress this round, do another after it's donea
620+
546621
to_process = next_round
547-
schemas.errors.extend(errors)
548622

623+
for name, reference in references_to_process:
624+
schemas_or_err = resolve_reference_and_update_schemas(name, reference, schemas, references_by_name)
625+
626+
if isinstance(schemas_or_err, PropertyError):
627+
errors.append(schemas_or_err)
628+
629+
schemas.errors.extend(errors)
549630
return schemas

tests/test_parser/test_properties/test_init.py

Lines changed: 201 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,171 @@ 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_name():
1245+
from openapi_python_client.parser.properties import _reference_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_name(ref) == expected_result
1255+
1256+
1257+
def test__reference_model_name():
1258+
from openapi_python_client.parser.properties import _reference_model_name
1259+
1260+
data_set = [
1261+
("#/foobar", "Foobar"),
1262+
("#/foo/bar", "Bar"),
1263+
]
1264+
1265+
for data, expected_result in data_set:
1266+
ref = oai.Reference.construct(ref=data)
1267+
assert _reference_model_name(ref) == expected_result
1268+
1269+
1270+
def test__resolve_model_or_enum_reference(mocker):
1271+
from openapi_python_client.parser.properties import _resolve_model_or_enum_reference
1272+
from openapi_python_client.parser.properties.schemas import Schemas
1273+
1274+
references_by_name = {
1275+
"FooBarReferenceLoop": oai.Reference.construct(ref="#/foobar"),
1276+
"FooBarDeeperReferenceLoop": oai.Reference.construct(ref="#/FooBarReferenceLoop"),
1277+
"BarFooReferenceLoop": oai.Reference.construct(ref="#/barfoo"),
1278+
"BarFooDeeperReferenceLoop": oai.Reference.construct(ref="#/BarFooReferenceLoop"),
1279+
"InfiniteReferenceLoop": oai.Reference.construct(ref="#/InfiniteReferenceLoop"),
1280+
"UnknownReference": oai.Reference.construct(ref="#/unknown"),
1281+
}
1282+
schemas = Schemas(enums={"Foobar": 1}, models={"Barfoo": 2})
1283+
1284+
res_1 = _resolve_model_or_enum_reference(
1285+
"FooBarReferenceLoop", references_by_name["FooBarReferenceLoop"], schemas, references_by_name
1286+
)
1287+
res_2 = _resolve_model_or_enum_reference(
1288+
"FooBarDeeperReferenceLoop", references_by_name["FooBarDeeperReferenceLoop"], schemas, references_by_name
1289+
)
1290+
res_3 = _resolve_model_or_enum_reference(
1291+
"BarFooReferenceLoop", references_by_name["BarFooReferenceLoop"], schemas, references_by_name
1292+
)
1293+
res_4 = _resolve_model_or_enum_reference(
1294+
"BarFooDeeperReferenceLoop", references_by_name["BarFooDeeperReferenceLoop"], schemas, references_by_name
1295+
)
1296+
res_5 = _resolve_model_or_enum_reference(
1297+
"InfiniteReferenceLoop", references_by_name["InfiniteReferenceLoop"], schemas, references_by_name
1298+
)
1299+
res_6 = _resolve_model_or_enum_reference(
1300+
"UnknownReference", references_by_name["UnknownReference"], schemas, references_by_name
1301+
)
1302+
1303+
assert res_1 == schemas.enums["Foobar"]
1304+
assert res_2 == schemas.enums["Foobar"]
1305+
assert res_3 == schemas.models["Barfoo"]
1306+
assert res_4 == schemas.models["Barfoo"]
1307+
assert res_5 == None
1308+
assert res_6 == None
1309+
1310+
1311+
def test__resolve_local_reference_schema(mocker):
1312+
from openapi_python_client.parser.properties import _resolve_local_reference_schema
1313+
from openapi_python_client.parser.properties.enum_property import EnumProperty
1314+
from openapi_python_client.parser.properties.model_property import ModelProperty
1315+
from openapi_python_client.parser.properties.schemas import Schemas
1316+
1317+
references_by_name = {
1318+
"FooBarReferenceLoop": oai.Reference.construct(ref="#/foobar"),
1319+
"FooBarDeeperReferenceLoop": oai.Reference.construct(ref="#/FooBarReferenceLoop"),
1320+
"fooBarLowerCaseReferenceLoop": oai.Reference.construct(ref="#/foobar"),
1321+
"fooBarLowerCaseDeeperReferenceLoop": oai.Reference.construct(ref="#/fooBarLowerCaseReferenceLoop"),
1322+
"BarFooReferenceLoop": oai.Reference.construct(ref="#/barfoo"),
1323+
"BarFooDeeperReferenceLoop": oai.Reference.construct(ref="#/BarFooReferenceLoop"),
1324+
"InfiniteReferenceLoop": oai.Reference.construct(ref="#/InfiniteReferenceLoop"),
1325+
"UnknownReference": oai.Reference.construct(ref="#/unknown"),
1326+
}
1327+
schemas = Schemas(
1328+
enums={
1329+
"Foobar": EnumProperty(
1330+
name="Foobar",
1331+
required=False,
1332+
nullable=True,
1333+
default="foobar",
1334+
values=["foobar"],
1335+
value_type="str",
1336+
reference="",
1337+
)
1338+
},
1339+
models={
1340+
"Barfoo": ModelProperty(
1341+
name="Barfoo",
1342+
required=False,
1343+
nullable=True,
1344+
default="barfoo",
1345+
reference="",
1346+
required_properties=[],
1347+
optional_properties=[],
1348+
description="",
1349+
relative_imports=[],
1350+
additional_properties=[],
1351+
)
1352+
},
1353+
)
1354+
1355+
res_1 = _resolve_local_reference_schema(
1356+
"FooBarReferenceLoop", references_by_name["FooBarReferenceLoop"], schemas, references_by_name
1357+
)
1358+
res_2 = _resolve_local_reference_schema(
1359+
"FooBarDeeperReferenceLoop", references_by_name["FooBarDeeperReferenceLoop"], schemas, references_by_name
1360+
)
1361+
res_3 = _resolve_local_reference_schema(
1362+
"BarFooReferenceLoop", references_by_name["BarFooReferenceLoop"], schemas, references_by_name
1363+
)
1364+
res_4 = _resolve_local_reference_schema(
1365+
"BarFooDeeperReferenceLoop", references_by_name["BarFooDeeperReferenceLoop"], schemas, references_by_name
1366+
)
1367+
res_5 = _resolve_local_reference_schema(
1368+
"fooBarLowerCaseReferenceLoop", references_by_name["fooBarLowerCaseReferenceLoop"], schemas, references_by_name
1369+
)
1370+
res_6 = _resolve_local_reference_schema(
1371+
"fooBarLowerCaseDeeperReferenceLoop",
1372+
references_by_name["fooBarLowerCaseDeeperReferenceLoop"],
1373+
schemas,
1374+
references_by_name,
1375+
)
1376+
res_7 = _resolve_local_reference_schema(
1377+
"InfiniteReferenceLoop", references_by_name["InfiniteReferenceLoop"], schemas, references_by_name
1378+
)
1379+
res_8 = _resolve_local_reference_schema(
1380+
"UnknownReference", references_by_name["UnknownReference"], schemas, references_by_name
1381+
)
1382+
1383+
assert res_1 == res_2 == res_3 == res_4 == res_5 == res_6 == schemas
1384+
assert schemas.enums["FooBarReferenceLoop"] == schemas.enums["Foobar"]
1385+
assert schemas.enums["FooBarDeeperReferenceLoop"] == schemas.enums["Foobar"]
1386+
assert schemas.models["BarFooReferenceLoop"] == schemas.models["Barfoo"]
1387+
assert schemas.models["BarFooDeeperReferenceLoop"] == schemas.models["Barfoo"]
1388+
assert schemas.enums["FooBarLowerCaseReferenceLoop"] == schemas.enums["Foobar"]
1389+
assert schemas.enums["FooBarLowerCaseDeeperReferenceLoop"] == schemas.enums["Foobar"]
1390+
assert isinstance(res_7, PropertyError)
1391+
assert isinstance(res_8, PropertyError)

0 commit comments

Comments
 (0)