Skip to content

Commit bf58542

Browse files
NRL-1554 Allow arbitrary extensions in content, change diagnostic messages
1 parent abcf5dc commit bf58542

File tree

9 files changed

+176
-106
lines changed

9 files changed

+176
-106
lines changed

api/consumer/swagger.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -813,9 +813,9 @@ components:
813813
oneOf:
814814
- $ref: "#/components/schemas/ContentStabilityExtension"
815815
- $ref: "#/components/schemas/RetrievalMechanismExtension"
816-
description: Additional extension for content stability.
816+
- $ref: "#/components/schemas/Extension"
817+
description: Additional extensions which include Content Stability and Retrieval Mechanism.
817818
minItems: 1
818-
maxItems: 2
819819
required:
820820
- attachment
821821
- format

api/producer/swagger.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,9 +1418,9 @@ components:
14181418
oneOf:
14191419
- $ref: "#/components/schemas/ContentStabilityExtension"
14201420
- $ref: "#/components/schemas/RetrievalMechanismExtension"
1421-
description: Additional extension for content stability.
1421+
- $ref: "#/components/schemas/Extension"
1422+
description: Additional extensions which include Content Stability and Retrieval Mechanism.
14221423
minItems: 1
1423-
maxItems: 2
14241424
required:
14251425
- attachment
14261426
- format

layer/nrlf/consumer/fhir/r4/model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: swagger.yaml
3-
# timestamp: 2025-08-07T11:40:24+00:00
3+
# timestamp: 2025-08-14T08:25:07+00:00
44

55
from __future__ import annotations
66

@@ -652,8 +652,8 @@ class DocumentReferenceContent(Parent):
652652
),
653653
]
654654
extension: Annotated[
655-
List[Union[ContentStabilityExtension, RetrievalMechanismExtension]],
656-
Field(max_length=2, min_length=1),
655+
List[Union[ContentStabilityExtension, RetrievalMechanismExtension, Extension]],
656+
Field(min_length=1),
657657
]
658658

659659

layer/nrlf/core/errors.py

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,41 @@
33
from pydantic import ValidationError
44
from pydantic_core import ErrorDetails
55

6-
from nrlf.core.constants import (
7-
CONTENT_FORMAT_CODE_URL,
8-
CONTENT_RETRIEVAL_SYSTEM_URL,
9-
CONTENT_STABILITY_SYSTEM_URL,
10-
)
116
from nrlf.core.response import Response
127
from nrlf.core.types import CodeableConcept
138
from nrlf.producer.fhir.r4 import model as producer_model
149
from nrlf.producer.fhir.r4.model import OperationOutcome, OperationOutcomeIssue
1510

1611

1712
def format_error_location(loc: List) -> str:
18-
# List of extension class names to exclude from error paths
19-
exclude_classes = {"ContentStabilityExtension", "RetrievalMechanismExtension"}
20-
filtered_loc = [each for each in loc if each not in exclude_classes]
21-
2213
formatted_loc = ""
23-
for each in filtered_loc:
14+
for each in loc:
2415
if isinstance(each, int):
2516
formatted_loc = f"{formatted_loc}[{each}]"
2617
else:
2718
formatted_loc = f"{formatted_loc}.{each}" if formatted_loc else str(each)
2819
return formatted_loc
2920

3021

31-
def append_value_set_url(loc_string: str) -> str:
32-
if loc_string.endswith(("url", "system")):
33-
return ""
34-
35-
if "content" in loc_string:
36-
if "extension" in loc_string:
37-
return f". See ValueSets: {CONTENT_STABILITY_SYSTEM_URL} & {CONTENT_RETRIEVAL_SYSTEM_URL}"
38-
if "format" in loc_string:
39-
return f". See ValueSet: {CONTENT_FORMAT_CODE_URL}"
40-
41-
return ""
42-
43-
44-
def diag_for_error(error: ErrorDetails) -> str:
22+
def diag_for_error(error: ErrorDetails, value_set: str, root_location: tuple) -> str:
4523
loc_string = format_error_location(error["loc"])
24+
if root_location:
25+
loc_string = format_error_location(root_location) + "." + loc_string
26+
4627
msg = f"{loc_string or 'DocumentReference'}: {error['msg']}"
47-
msg += append_value_set_url(loc_string)
28+
msg += f", see: {value_set}" if value_set else ""
4829
return msg
4930

5031

51-
def expression_for_error(error: ErrorDetails) -> Optional[str]:
52-
return format_error_location(error["loc"]) or "DocumentReference"
32+
def expression_for_error(error: ErrorDetails, root_location: tuple) -> Optional[str]:
33+
loc_string = format_error_location(error["loc"]) or "DocumentReference"
34+
if root_location and error["loc"]:
35+
loc_string = (
36+
format_error_location(root_location)
37+
+ "."
38+
+ format_error_location(error["loc"])
39+
)
40+
return loc_string
5341

5442

5543
class OperationOutcomeError(Exception):
@@ -99,15 +87,20 @@ def __init__(self, issues: List[OperationOutcomeIssue]):
9987

10088
@classmethod
10189
def from_validation_error(
102-
cls, exc: ValidationError, details: CodeableConcept, msg: str = ""
90+
cls,
91+
exc: ValidationError,
92+
details: CodeableConcept,
93+
msg: str = "",
94+
value_set: str = "",
95+
root_location: tuple = None,
10396
):
10497
issues = [
10598
producer_model.OperationOutcomeIssue(
10699
severity="error",
107100
code="invalid",
108101
details=details, # type: ignore
109-
diagnostics=f"{msg} ({diag_for_error(error)})",
110-
expression=[expression_for_error(error)], # type: ignore
102+
diagnostics=f"{msg} ({diag_for_error(error, value_set, root_location)})",
103+
expression=[expression_for_error(error, root_location)], # type: ignore
111104
)
112105
for error in exc.errors()
113106
]

layer/nrlf/core/tests/test_pydantic_errors.py

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -85,47 +85,11 @@ def test_validate_content_missing_format():
8585
}
8686
]
8787
},
88-
"diagnostics": "Failed to parse DocumentReference resource (content[0].format: Field required. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode)",
88+
"diagnostics": "Failed to parse DocumentReference resource (content[0].format: Field required)",
8989
"expression": ["content[0].format"],
9090
}
9191

9292

93-
def test_validate_content_multiple_content_stability_extensions():
94-
validator = DocumentReferenceValidator()
95-
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
96-
97-
# Add a second duplicate contentStability extension
98-
document_ref_data["content"][0]["extension"].append(
99-
document_ref_data["content"][0]["extension"][0]
100-
)
101-
102-
# Add a third duplicate contentStability extension
103-
document_ref_data["content"][0]["extension"].append(
104-
document_ref_data["content"][0]["extension"][0]
105-
)
106-
107-
with pytest.raises(ParseError) as error:
108-
validator.validate(document_ref_data)
109-
110-
exc = error.value
111-
assert len(exc.issues) == 1
112-
assert exc.issues[0].model_dump(exclude_none=True) == {
113-
"severity": "error",
114-
"code": "invalid",
115-
"details": {
116-
"coding": [
117-
{
118-
"system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode",
119-
"code": "BAD_REQUEST",
120-
"display": "Bad request",
121-
}
122-
]
123-
},
124-
"diagnostics": "Failed to parse DocumentReference resource (content[0].extension: List should have at most 2 items after validation, not 3. See ValueSets: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability & https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism)",
125-
"expression": ["content[0].extension"],
126-
}
127-
128-
12993
def test_validate_content_invalid_content_stability_code():
13094
validator = DocumentReferenceValidator()
13195
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
@@ -150,7 +114,7 @@ def test_validate_content_invalid_content_stability_code():
150114
}
151115
]
152116
},
153-
"diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic'. See ValueSets: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability & https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism)",
117+
"diagnostics": "Invalid content stability extension (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)",
154118
"expression": ["content[0].extension[0].valueCodeableConcept.coding[0].code"],
155119
}
156120

@@ -179,7 +143,7 @@ def test_validate_content_invalid_content_stability_display():
179143
}
180144
]
181145
},
182-
"diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic'. See ValueSets: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability & https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism)",
146+
"diagnostics": "Invalid content stability extension (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)",
183147
"expression": [
184148
"content[0].extension[0].valueCodeableConcept.coding[0].display"
185149
],
@@ -210,7 +174,7 @@ def test_validate_content_invalid_content_stability_system():
210174
}
211175
]
212176
},
213-
"diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].system: Input should be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')",
177+
"diagnostics": "Invalid content stability extension (content[0].extension[0].valueCodeableConcept.coding[0].system: Input should be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)",
214178
"expression": ["content[0].extension[0].valueCodeableConcept.coding[0].system"],
215179
}
216180

@@ -238,7 +202,7 @@ def test_validate_content_invalid_content_stability_url():
238202
}
239203
]
240204
},
241-
"diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability')",
205+
"diagnostics": "Invalid content stability extension (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)",
242206
"expression": ["content[0].extension[0].url"],
243207
}
244208

layer/nrlf/core/tests/test_validators.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1402,7 +1402,7 @@ def test_validate_content_extension_missing_content_stability():
14021402
}
14031403
]
14041404
},
1405-
"diagnostics": "Invalid content extension: Extension must have one content stability extension see value set ('https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')",
1405+
"diagnostics": "Invalid content extension: Extension must have one content stability extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability')",
14061406
"expression": ["content[0].extension"],
14071407
}
14081408

@@ -1559,3 +1559,74 @@ def test_validate_nrl_format_code_display_mismatch(
15591559
"diagnostics": f"Invalid display for format code '{format_code}'. Expected '{expected_display}'",
15601560
"expression": ["content[0].format.display"],
15611561
}
1562+
1563+
1564+
def test_validate_content_multiple_content_stability_extensions():
1565+
validator = DocumentReferenceValidator()
1566+
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
1567+
1568+
# Add a second duplicate contentStability extension
1569+
document_ref_data["content"][0]["extension"].append(
1570+
document_ref_data["content"][0]["extension"][0]
1571+
)
1572+
1573+
result = validator.validate(document_ref_data)
1574+
1575+
assert result.is_valid is False
1576+
assert len(result.issues) == 1
1577+
assert result.issues[0].model_dump(exclude_none=True) == {
1578+
"severity": "error",
1579+
"code": "business-rule",
1580+
"details": {
1581+
"coding": [
1582+
{
1583+
"system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode",
1584+
"code": "UNPROCESSABLE_ENTITY",
1585+
"display": "Unprocessable Entity",
1586+
}
1587+
]
1588+
},
1589+
"diagnostics": "Invalid content extension: Extension must have one content stability extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability')",
1590+
"expression": ["content[0].extension"],
1591+
}
1592+
1593+
1594+
def test_validate_content_multiple_content_retrieval_extensions():
1595+
validator = DocumentReferenceValidator()
1596+
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
1597+
1598+
# Add 2 content retrieval extensions
1599+
content_retrieval_extension = {
1600+
"url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism",
1601+
"valueCodeableConcept": {
1602+
"coding": [
1603+
{
1604+
"system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism",
1605+
"code": "Direct",
1606+
"display": "Direct",
1607+
}
1608+
]
1609+
},
1610+
}
1611+
document_ref_data["content"][0]["extension"].append(content_retrieval_extension)
1612+
document_ref_data["content"][0]["extension"].append(content_retrieval_extension)
1613+
1614+
result = validator.validate(document_ref_data)
1615+
1616+
assert result.is_valid is False
1617+
assert len(result.issues) == 1
1618+
assert result.issues[0].model_dump(exclude_none=True) == {
1619+
"severity": "error",
1620+
"code": "business-rule",
1621+
"details": {
1622+
"coding": [
1623+
{
1624+
"system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode",
1625+
"code": "UNPROCESSABLE_ENTITY",
1626+
"display": "Unprocessable Entity",
1627+
}
1628+
]
1629+
},
1630+
"diagnostics": "Invalid content retrieval extension: Extension must have one content retrieval extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism')",
1631+
"expression": ["content[0].extension"],
1632+
}

0 commit comments

Comments
 (0)