Skip to content

Commit a647702

Browse files
committed
NRL-1285 update parent model with validator for empty fields
1 parent 95f515e commit a647702

File tree

3 files changed

+131
-1
lines changed

3 files changed

+131
-1
lines changed

api/producer/createDocumentReference/tests/test_create_document_reference.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,50 @@ def test_create_document_reference_invalid_body():
384384
}
385385

386386

387+
def test_create_document_reference_empty_fields_in_body():
388+
doc_ref = load_document_reference("Y05868-736253002-Valid")
389+
doc_ref.author = []
390+
doc_ref.context = {"practiceSetting": {}, "sourcePatientInfo": None}
391+
doc_ref.category = [{"coding": [{"system": "", "code": None}]}]
392+
doc_ref.text = ""
393+
394+
event = create_test_api_gateway_event(
395+
headers=create_headers(),
396+
body=doc_ref.model_dump_json(exclude_none=True),
397+
)
398+
result = handler(event, create_mock_context())
399+
body = result.pop("body")
400+
401+
assert result == {
402+
"statusCode": "400",
403+
"headers": default_response_headers(),
404+
"isBase64Encoded": False,
405+
}
406+
407+
parsed_body = json.loads(body)
408+
409+
assert parsed_body == {
410+
"resourceType": "OperationOutcome",
411+
"issue": [
412+
{
413+
"severity": "error",
414+
"code": "invalid",
415+
"details": {
416+
"coding": [
417+
{
418+
"code": "MESSAGE_NOT_WELL_FORMED",
419+
"display": "Message not well formed",
420+
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
421+
}
422+
],
423+
},
424+
"diagnostics": "Request body could not be parsed (root: Value error, The following fields are empty: text, author, context.sourcePatientInfo, category[0].coding[0].system, category[0].coding[0].code)",
425+
"expression": ["root"],
426+
}
427+
],
428+
}
429+
430+
387431
def test_create_document_reference_invalid_resource():
388432
doc_ref = load_document_reference("Y05868-736253002-Valid")
389433
doc_ref.custodian = None

layer/nrlf/core/parent_model.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Annotated, List, Optional
22

3-
from pydantic import BaseModel, ConfigDict, Field
3+
from pydantic import BaseModel, ConfigDict, Field, model_validator
44

55

66
class ParentCoding(BaseModel):
@@ -81,6 +81,60 @@ class ParentExtension(BaseModel):
8181

8282

8383
class Parent(BaseModel):
84+
@model_validator(mode="before")
85+
@classmethod
86+
def validate_empty_fields(cls, values):
87+
"""
88+
Iteratively check every field in the model for emptiness.
89+
If a field is empty, add it to the error list with its full location.
90+
"""
91+
stack = [(None, values)]
92+
empty_fields = []
93+
94+
while stack:
95+
path, current_value = stack.pop()
96+
97+
if isinstance(current_value, dict):
98+
for key, value in current_value.items():
99+
full_path = f"{path}.{key}" if path else key
100+
if (
101+
value is None
102+
or value == ""
103+
or (isinstance(value, list) and not value)
104+
):
105+
empty_fields.append(full_path)
106+
else:
107+
stack.append((full_path, value))
108+
109+
elif isinstance(current_value, list):
110+
for index, item in enumerate(current_value):
111+
full_path = f"{path}[{index}]" if path else f"[{index}]"
112+
if (
113+
item is None
114+
or item == ""
115+
or (isinstance(item, dict) and not item)
116+
):
117+
empty_fields.append(full_path)
118+
else:
119+
stack.append((full_path, item))
120+
121+
elif isinstance(current_value, BaseModel):
122+
nested_values = current_value.model_dump(exclude_none=True)
123+
for nested_field, nested_value in nested_values.items():
124+
full_path = f"{path}.{nested_field}" if path else nested_field
125+
stack.append((full_path, nested_value))
126+
127+
else:
128+
if current_value is None or current_value == "":
129+
empty_fields.append(path)
130+
131+
if empty_fields:
132+
raise ValueError(
133+
f"The following fields are empty: {', '.join(empty_fields)}"
134+
)
135+
136+
return values
137+
84138
model_config = ConfigDict(regex_engine="python-re", extra="forbid")
85139
extension: Annotated[
86140
Optional[List[ParentExtension]],

tests/features/producer/createDocumentReference-failure.feature

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,3 +1014,35 @@ Feature: Producer - createDocumentReference - Failure Scenarios
10141014
]
10151015
}
10161016
"""
1017+
1018+
Scenario: Reject DocumentReference with empty non-mandatory field (author)
1019+
Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API
1020+
And the organisation 'TSTCUS' is authorised to access pointer types:
1021+
| system | value |
1022+
| http://snomed.info/sct | 736253002 |
1023+
When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'author' is:
1024+
"""
1025+
"author": []
1026+
"""
1027+
Then the response status code is 400
1028+
And the response is an OperationOutcome with 1 issue
1029+
And the OperationOutcome contains the issue:
1030+
"""
1031+
{
1032+
"severity": "error",
1033+
"code": "invalid",
1034+
"details": {
1035+
"coding": [
1036+
{
1037+
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
1038+
"code": "MESSAGE_NOT_WELL_FORMED",
1039+
"display": "Message not well formed"
1040+
}
1041+
]
1042+
},
1043+
"diagnostics": "Request body could not be parsed (author: Field is an empty list)",
1044+
"expression": [
1045+
"author"
1046+
]
1047+
}
1048+
"""

0 commit comments

Comments
 (0)