Skip to content

Commit 7748c5e

Browse files
authored
Issue LIF-Initiative#35: Fix MDR bug with required entities not being included in '… (LIF-Initiative#37)
…required' array for schema export
1 parent 2d6f655 commit 7748c5e

File tree

4 files changed

+121
-41
lines changed

4 files changed

+121
-41
lines changed

components/lif/mdr_services/schema_generation_service.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
async def find_children(
3939
tree,
4040
parent,
41-
parent_properties,
41+
parent_schema,
4242
df_entity,
4343
session,
4444
include_attr_md,
@@ -49,6 +49,7 @@ async def find_children(
4949
full_export,
5050
):
5151
if parent in tree:
52+
parent_properties = parent_schema["properties"]
5253
for x in tree[parent]:
5354
if data_model.Type in ["OrgLIF", "PartnerLIF"]:
5455
child_association_query = select(EntityAssociation).where(
@@ -89,11 +90,12 @@ async def find_children(
8990
entity_data.UseConsiderations if entity_data.UseConsiderations else ""
9091
) # Using empty string instead of null to make it easier to diff w/ P1 lif.json schema
9192
required_elements = []
92-
if include_entity_md:
93-
for key, value in entity_data.__dict__.items():
93+
for key, value in entity_data.__dict__.items():
94+
if include_entity_md:
9495
parent_properties[entity_name][key] = value
95-
if key == "Required" and value == "Yes":
96-
required_elements.append(entity_name)
96+
if key == "Required" and value == "Yes" and entity_name not in parent_schema["required"]:
97+
parent_schema["required"].append(entity_name)
98+
if include_entity_md:
9799
if full_export:
98100
parent_properties[entity_name]["EntityAssociationId"] = child_association.Id
99101
parent_properties[entity_name]["EntityAssociationParentEntityId"] = (
@@ -252,7 +254,7 @@ async def find_children(
252254
await find_children(
253255
tree,
254256
x,
255-
parent_properties[entity_name]["properties"],
257+
parent_properties[entity_name],
256258
df_entity=df_entity,
257259
session=session,
258260
include_attr_md=include_attr_md,
@@ -323,13 +325,7 @@ async def find_ancestors(session, child_id, data_model_type, data_model_id, incl
323325

324326

325327
async def add_ref(
326-
parent_ancestors,
327-
child_ancestors,
328-
df_entity,
329-
parent_entity_name,
330-
child_entity_name,
331-
openapi_spec,
332-
key
328+
parent_ancestors, child_ancestors, df_entity, parent_entity_name, child_entity_name, openapi_spec, key
333329
):
334330
"""
335331
Inline the schema for a referenced child entity under the parent's OpenAPI schema entry.
@@ -366,17 +362,19 @@ async def add_ref(
366362
ref_data = deepcopy(referenced_schema)
367363
properties = ref_data.get("properties")
368364
if isinstance(properties, dict):
369-
# In LIF, "Reference" is intended to be instantiated as only the required fields of the referenced entity.
365+
# In LIF, "Reference" is intended to be instantiated as only the required fields of the referenced entity.
370366
# These can be used to look up the full entity.
371367
required_fields = ref_data.get("required", [])
372368
if isinstance(required_fields, list) and required_fields:
373369
required_fields_set = set(required_fields)
374370
ref_data["properties"] = {
375-
prop_name: prop_value for prop_name, prop_value in properties.items() if prop_name in required_fields_set
371+
prop_name: prop_value
372+
for prop_name, prop_value in properties.items()
373+
if prop_name in required_fields_set
376374
}
377375
else:
378376
ref_data["properties"] = {}
379-
ref_data["type"] = "object" # A reference should always be to a single object, not an array of objects.
377+
ref_data["type"] = "object" # A reference should always be to a single object, not an array of objects.
380378
logger.info(f"ref_data : {ref_data}")
381379

382380
if len(parent_ancestors) == 0:
@@ -714,10 +712,11 @@ async def generate_openapi_schema(
714712
]["ValueSet"]["Values"] = valueset_values_full
715713

716714
openapi_spec["components"]["schemas"][parent_entity.Name]["required"] = required_elements
715+
717716
await find_children(
718717
tree,
719718
parent,
720-
openapi_spec["components"]["schemas"][parent_entity.Name]["properties"],
719+
openapi_spec["components"]["schemas"][parent_entity.Name],
721720
session=session,
722721
df_entity=df_entity,
723722
include_attr_md=include_attr_md,
@@ -818,7 +817,7 @@ async def generate_openapi_schema(
818817
parent_entity_name=parent_entity_name,
819818
child_entity_name=child_entity_name,
820819
openapi_spec=openapi_spec,
821-
key=key
820+
key=key,
822821
)
823822

824823
if "Common" in openapi_spec["components"]["schemas"]:

test/bases/lif/mdr_restapi/data_model_example_datasource_full_openapi_schema.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"type": "object",
1313
"required": [
1414
"person",
15-
"id"
15+
"id",
16+
"employment"
1617
],
1718
"use_recommendations": "",
1819
"Description": null,
@@ -70,7 +71,7 @@
7071
},
7172
"employment": {
7273
"type": "object",
73-
"required": [],
74+
"required": ["preferences"],
7475
"use_recommendations": "",
7576
"Description": null,
7677
"ActivationDate": "2025-10-08T22:12:00+00:00",
@@ -109,7 +110,6 @@
109110
"preferences": {
110111
"type": "object",
111112
"required": [
112-
"preferences"
113113
],
114114
"use_recommendations": "",
115115
"Description": null,

test/components/lif/mdr_client/test_core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def _assert_openapi_data_model_results_from_file(data_model: dict):
252252
assert data_model["openapi"] == "3.0.0"
253253
assert data_model["components"]["schemas"] != {}
254254
assert "Person" in data_model["components"]["schemas"]
255-
assert "Organization" in data_model["components"]["schemas"]
255+
assert "OrganizationCode" in data_model["components"]["schemas"]
256256

257257

258258
def _assert_openapi_data_model_results_from_mdr(data_model: dict):

test/components/lif/mdr_services/test_schema_generation_service.py

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,30 @@ def first(self):
4444
return self._obj
4545

4646

47+
class _Attr:
48+
def __init__(self, id, name, required):
49+
self.Id = id
50+
self.Name = name
51+
self.Required = required
52+
self.DataType = "string"
53+
self.Array = "No"
54+
self.ValueSetId = None
55+
56+
def dict(self):
57+
return {
58+
"Description": None,
59+
"UseConsiderations": None,
60+
"Example": None,
61+
"Format": None,
62+
"Id": self.Id,
63+
"Name": self.Name,
64+
"Required": self.Required,
65+
"DataType": self.DataType,
66+
"Array": self.Array,
67+
"ValueSetId": self.ValueSetId,
68+
}
69+
70+
4771
@pytest.fixture
4872
def fake_session():
4973
s = MagicMock()
@@ -170,7 +194,9 @@ async def test_add_ref_nested_parent_path():
170194
key="ChildRef",
171195
)
172196

173-
parent_props = openapi_spec["components"]["schemas"]["Root"]["properties"]["Intermediate"]["properties"]["Parent"]["properties"]
197+
parent_props = openapi_spec["components"]["schemas"]["Root"]["properties"]["Intermediate"]["properties"]["Parent"][
198+
"properties"
199+
]
174200
child_ref_inline = parent_props["ChildRef"]
175201
assert child_ref_inline["type"] == "object"
176202
assert set(child_ref_inline["properties"].keys()) == {"a", "b"}
@@ -344,24 +370,6 @@ def get_entity_by_id_side_effect(session=None, id=None, **_):
344370

345371
monkeypatch.setattr(svc, "get_entity_by_id", AsyncMock(side_effect=get_entity_by_id_side_effect))
346372

347-
class _Attr:
348-
def __init__(self, id, name, required):
349-
self.Id = id
350-
self.Name = name
351-
self.Required = required
352-
self.DataType = "string"
353-
self.Array = "No"
354-
self.ValueSetId = None
355-
356-
def dict(self):
357-
return {
358-
"Description": None,
359-
"UseConsiderations": None,
360-
"Example": None,
361-
"Format": None,
362-
"DataType": self.DataType,
363-
}
364-
365373
def get_attributes_with_association_metadata_for_entity_side_effect(entity_id, **_):
366374
if entity_id == 101: # CompetencyFramework
367375
return [_Attr(1, "uri", "Yes"), _Attr(2, "name", "Yes"), _Attr(3, "description", "No")]
@@ -405,6 +413,79 @@ def get_attributes_with_association_metadata_for_entity_side_effect(entity_id, *
405413
assert assoc_schema["required"] == ["competencyFrameworkId", "competencyFrameworkType"]
406414

407415

416+
async def test_generate_openapi_schema_has_sub_entity_required_entity(fake_session, monkeypatch):
417+
"""
418+
Given:
419+
- BaseLIF CompetencyFramework entity with child entity Association in EntityAssociations
420+
- The Association Entity has Required="Yes" in EntityAssociations
421+
Result:
422+
- The generated schema for CompetencyFramework includes Association with its required fields listed
423+
"""
424+
dm = types.SimpleNamespace(
425+
Id=1,
426+
Name="BaseLIF",
427+
DataModelVersion="3.0",
428+
Type="BaseLIF",
429+
BaseDataModelId=None,
430+
ContributorOrganization="LIF",
431+
)
432+
monkeypatch.setattr(svc, "get_datamodel_by_id", AsyncMock(return_value=dm))
433+
434+
def get_entity_by_id_side_effect(session=None, id=None, **_):
435+
if id == 101:
436+
return types.SimpleNamespace(
437+
Id=101, Name="CompetencyFramework", Array="No", UseConsiderations=None, Deleted=False
438+
)
439+
elif id == 202:
440+
return types.SimpleNamespace(
441+
Id=202, Name="Association", Array="Yes", UseConsiderations=None, Deleted=False, Required="Yes"
442+
)
443+
else:
444+
raise ValueError(f"Unexpected entity_id {id}")
445+
446+
monkeypatch.setattr(svc, "get_entity_by_id", AsyncMock(side_effect=get_entity_by_id_side_effect))
447+
448+
def get_attributes_with_association_metadata_for_entity_side_effect(entity_id, **_):
449+
if entity_id == 101: # CompetencyFramework
450+
return [_Attr(1, "uri", "Yes"), _Attr(2, "name", "Yes"), _Attr(3, "description", "No")]
451+
elif entity_id == 202: # Association
452+
return [_Attr(1, "uri", "Yes"), _Attr(2, "name", "Yes"), _Attr(3, "description", "No")]
453+
else:
454+
raise ValueError(f"Unexpected entity_id {entity_id}")
455+
456+
monkeypatch.setattr(
457+
svc,
458+
"get_attributes_with_association_metadata_for_entity",
459+
AsyncMock(side_effect=get_attributes_with_association_metadata_for_entity_side_effect),
460+
)
461+
462+
RowIN = namedtuple("RowIN", ["Id", "Name"])
463+
fake_session.execute.side_effect = [
464+
# get embedded entity associations
465+
_FetchallResult([(101, 202)]),
466+
# get entities in DM not in union of associations
467+
_FetchallResult([]),
468+
# build df_entity (include both entities)
469+
_FetchallResult([RowIN(101, "CompetencyFramework"), RowIN(202, "Association")]),
470+
# find_children: get detailed association rows via .scalars().all()
471+
_ScalarListResult([types.SimpleNamespace(Relationship=None)]),
472+
# enums for attributes (none)
473+
_FetchallResult([]),
474+
_FetchallResult([]),
475+
# inter-entity "Reference" links
476+
_FetchallResult([]),
477+
]
478+
479+
out = await svc.generate_openapi_schema(
480+
fake_session, data_model_id=1, include_attr_md=False, include_entity_md=False
481+
)
482+
cf_schema = out["components"]["schemas"]["CompetencyFramework"]
483+
assert cf_schema["type"] == "object"
484+
assert "Association" in cf_schema["required"]
485+
assert "uri" in cf_schema["required"]
486+
assert "name" in cf_schema["required"]
487+
488+
408489
async def test_generate_openapi_schema_full_export(fake_session, monkeypatch):
409490
"""
410491
Given:

0 commit comments

Comments
 (0)