Skip to content

Commit e1960eb

Browse files
committed
NRL-746 add extension to model
1 parent 7f36bd5 commit e1960eb

File tree

6 files changed

+395
-46
lines changed

6 files changed

+395
-46
lines changed

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

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -424,42 +424,60 @@ class RequestHeaderCorrelationId(BaseModel):
424424
__root__: Annotated[str, Field(example="11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA")]
425425

426426

427-
class DocumentReferenceContent(BaseModel):
427+
class CodeableConcept(BaseModel):
428428
id: Annotated[
429429
str | None,
430430
Field(
431431
description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
432432
regex="[A-Za-z0-9\\-\\.]{1,64}",
433433
),
434434
] = None
435-
attachment: Annotated[
436-
Attachment,
435+
coding: list[Coding] | None = None
436+
text: Annotated[
437+
str | None,
437438
Field(
438-
description="The document or URL of the document along with critical metadata to prove content has integrity."
439+
description="A human language representation of the concept as seen/selected/uttered by the user who entered the data and/or which represents the intended meaning of the user.",
440+
regex="[ \\r\\n\\t\\S]+",
439441
),
440-
]
441-
format: Annotated[
442-
Coding | None,
442+
] = None
443+
444+
445+
class Extension(BaseModel):
446+
valueCodeableConcept: Annotated[
447+
CodeableConcept,
443448
Field(
444-
description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType."
449+
description="Details about the extension.",
445450
),
446-
] = None
451+
]
452+
url: Annotated[
453+
str, Field(description="The reference link for the details.", regex="\\S*")
454+
]
447455

448456

449-
class CodeableConcept(BaseModel):
457+
class DocumentReferenceContent(BaseModel):
450458
id: Annotated[
451459
str | None,
452460
Field(
453461
description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.",
454462
regex="[A-Za-z0-9\\-\\.]{1,64}",
455463
),
456464
] = None
457-
coding: list[Coding] | None = None
458-
text: Annotated[
459-
str | None,
465+
attachment: Annotated[
466+
Attachment,
460467
Field(
461-
description="A human language representation of the concept as seen/selected/uttered by the user who entered the data and/or which represents the intended meaning of the user.",
462-
regex="[ \\r\\n\\t\\S]+",
468+
description="The document or URL of the document along with critical metadata to prove content has integrity."
469+
),
470+
]
471+
format: Annotated[
472+
Coding | None,
473+
Field(
474+
description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType."
475+
),
476+
] = None
477+
extension: Annotated[
478+
list[Extension] | None,
479+
Field(
480+
description="Additional code system information for the document content."
463481
),
464482
] = None
465483

layer/nrlf/core/tests/test_validators.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,195 @@ def test_validate_category_coding_invalid_system():
599599
}
600600

601601

602+
def test_validate_content_extension_too_many_extensions():
603+
validator = DocumentReferenceValidator()
604+
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
605+
606+
document_ref_data["content"][0]["extension"].append(
607+
{
608+
"url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability",
609+
"valueCodeableConcept": {
610+
"coding": [
611+
{
612+
"system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability",
613+
"code": "static",
614+
"display": "static",
615+
}
616+
]
617+
},
618+
}
619+
)
620+
621+
result = validator.validate(document_ref_data)
622+
623+
assert result.is_valid is False
624+
assert result.resource.id == "Y05868-99999-99999-999999"
625+
assert len(result.issues) == 1
626+
assert result.issues[0].dict(exclude_none=True) == {
627+
"severity": "error",
628+
"code": "invalid",
629+
"details": {
630+
"coding": [
631+
{
632+
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
633+
"code": "INVALID_RESOURCE",
634+
"display": "Invalid validation of resource",
635+
}
636+
]
637+
},
638+
"diagnostics": "Invalid content extension length: 2 Extension must only contain a single value",
639+
"expression": ["content[0].extension"],
640+
}
641+
642+
643+
def test_validate_content_extension_invalid_code():
644+
validator = DocumentReferenceValidator()
645+
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
646+
647+
document_ref_data["content"][0]["extension"][0] = {
648+
"url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability",
649+
"valueCodeableConcept": {
650+
"coding": [
651+
{
652+
"system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability",
653+
"code": "invalid",
654+
"display": "invalid",
655+
}
656+
]
657+
},
658+
}
659+
660+
result = validator.validate(document_ref_data)
661+
662+
assert result.is_valid is False
663+
assert result.resource.id == "Y05868-99999-99999-999999"
664+
assert len(result.issues) == 1
665+
assert result.issues[0].dict(exclude_none=True) == {
666+
"severity": "error",
667+
"code": "value",
668+
"details": {
669+
"coding": [
670+
{
671+
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
672+
"code": "INVALID_RESOURCE",
673+
"display": "Invalid validation of resource",
674+
}
675+
]
676+
},
677+
"diagnostics": "Invalid content extension code: invalid Extension code must be 'static' or 'dynamic'",
678+
"expression": ["content[0].extension[0].valueCodeableConcept.coding[0].code"],
679+
}
680+
681+
682+
def test_validate_content_extension_invalid_system():
683+
validator = DocumentReferenceValidator()
684+
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
685+
686+
document_ref_data["content"][0]["extension"][0] = {
687+
"url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability",
688+
"valueCodeableConcept": {
689+
"coding": [
690+
{
691+
"system": "invalid",
692+
"code": "static",
693+
"display": "static",
694+
}
695+
]
696+
},
697+
}
698+
699+
result = validator.validate(document_ref_data)
700+
701+
assert result.is_valid is False
702+
assert result.resource.id == "Y05868-99999-99999-999999"
703+
assert len(result.issues) == 1
704+
assert result.issues[0].dict(exclude_none=True) == {
705+
"severity": "error",
706+
"code": "value",
707+
"details": {
708+
"coding": [
709+
{
710+
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
711+
"code": "INVALID_RESOURCE",
712+
"display": "Invalid validation of resource",
713+
}
714+
]
715+
},
716+
"diagnostics": "Invalid content extension system: invalid Extension system must be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability'",
717+
"expression": ["content[0].extension[0].valueCodeableConcept.coding[0].system"],
718+
}
719+
720+
721+
def test_validate_content_extension_invalid_url():
722+
validator = DocumentReferenceValidator()
723+
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
724+
725+
document_ref_data["content"][0]["extension"][0] = {
726+
"url": "invalid",
727+
"valueCodeableConcept": {
728+
"coding": [
729+
{
730+
"system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability",
731+
"code": "static",
732+
"display": "static",
733+
}
734+
]
735+
},
736+
}
737+
738+
result = validator.validate(document_ref_data)
739+
740+
assert result.is_valid is False
741+
assert result.resource.id == "Y05868-99999-99999-999999"
742+
assert len(result.issues) == 1
743+
assert result.issues[0].dict(exclude_none=True) == {
744+
"severity": "error",
745+
"code": "value",
746+
"details": {
747+
"coding": [
748+
{
749+
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
750+
"code": "INVALID_RESOURCE",
751+
"display": "Invalid validation of resource",
752+
}
753+
]
754+
},
755+
"diagnostics": "Invalid content extension url: invalid Extension url must be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability'",
756+
"expression": ["content[0].extension[0].url"],
757+
}
758+
759+
760+
def test_validate_content_extension_missing_coding():
761+
validator = DocumentReferenceValidator()
762+
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
763+
764+
document_ref_data["content"][0]["extension"][0] = {
765+
"url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability",
766+
"valueCodeableConcept": {"coding": []},
767+
}
768+
769+
result = validator.validate(document_ref_data)
770+
771+
assert result.is_valid is False
772+
assert result.resource.id == "Y05868-99999-99999-999999"
773+
assert len(result.issues) == 1
774+
assert result.issues[0].dict(exclude_none=True) == {
775+
"severity": "error",
776+
"code": "required",
777+
"details": {
778+
"coding": [
779+
{
780+
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
781+
"code": "INVALID_RESOURCE",
782+
"display": "Invalid validation of resource",
783+
}
784+
]
785+
},
786+
"diagnostics": "Missing content[0].extension[0].valueCodeableConcept.coding, extension must have at least one coding.",
787+
"expression": ["content[0].extension.valueCodeableConcept.coding"],
788+
}
789+
790+
602791
def test_validate_identifiers_invalid_systems():
603792
validator = DocumentReferenceValidator()
604793
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")

layer/nrlf/core/validators.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ def validate(self, data: Dict[str, Any] | DocumentReference):
116116
self._validate_relates_to(resource)
117117
self._validate_ssp_asid(resource)
118118
self._validate_category(resource)
119+
if resource.content[0].extension:
120+
self._validate_content_extension(resource)
119121

120122
except StopValidationError:
121123
logger.log(LogReference.VALIDATOR003)
@@ -388,3 +390,90 @@ def _validate_category(self, model: DocumentReference):
388390
diagnostics=f"category code '{coding.code}' must have a display value of '{CATEGORIES.get(coding.code)}'",
389391
field=f"category[0].coding[{0}].display",
390392
)
393+
394+
def _validate_content_extension(self, model: DocumentReference):
395+
"""
396+
Validate the content.extension field contains an appropriate coding.
397+
"""
398+
logger.log(LogReference.VALIDATOR001, step="content extension")
399+
400+
logger.debug("Validating extension")
401+
for i, content in enumerate(model.content):
402+
if len(content.extension) > 1:
403+
logger.log(
404+
LogReference.VALIDATOR001,
405+
step=f"content[{i}].extension",
406+
reason="extension_too_long",
407+
)
408+
self.result.add_error(
409+
issue_code="invalid",
410+
error_code="INVALID_RESOURCE",
411+
diagnostics=f"Invalid content extension length: {len(content.extension)} Extension must only contain a single value",
412+
field=f"content[{i}].extension",
413+
)
414+
return
415+
416+
if len(content.extension[0].valueCodeableConcept.coding) < 1:
417+
logger.log(
418+
LogReference.VALIDATOR001,
419+
step=f"content[{i}].extension.valueCodeableConcept.coding",
420+
reason="extension_coding_does_not_exist",
421+
)
422+
self.result.add_error(
423+
issue_code="required",
424+
error_code="INVALID_RESOURCE",
425+
diagnostics=f"Missing content[{i}].extension[0].valueCodeableConcept.coding, extension must have at least one coding.",
426+
field=f"content[{i}].extension.valueCodeableConcept.coding",
427+
)
428+
return
429+
430+
if (
431+
content.extension[0].valueCodeableConcept.coding[0].system
432+
!= "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability"
433+
):
434+
logger.log(
435+
LogReference.VALIDATOR001,
436+
step=f"content[{i}].extension.valueCodeableConcept.coding[0].system",
437+
reason="extension_coding_system_invalid",
438+
)
439+
self.result.add_error(
440+
issue_code="value",
441+
error_code="INVALID_RESOURCE",
442+
diagnostics=f"Invalid content extension system: {content.extension[0].valueCodeableConcept.coding[0].system} Extension system must be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability'",
443+
field=f"content[0].extension[0].valueCodeableConcept.coding[0].system",
444+
)
445+
return
446+
447+
if content.extension[0].valueCodeableConcept.coding[0].code not in [
448+
"static",
449+
"dynamic",
450+
]:
451+
logger.log(
452+
LogReference.VALIDATOR001,
453+
step=f"content[{i}].extension.valueCodeableConcept.coding[0].code",
454+
reason="extension_coding_code_invalid",
455+
)
456+
self.result.add_error(
457+
issue_code="value",
458+
error_code="INVALID_RESOURCE",
459+
diagnostics=f"Invalid content extension code: {content.extension[0].valueCodeableConcept.coding[0].code} Extension code must be 'static' or 'dynamic'",
460+
field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].code",
461+
)
462+
return
463+
464+
if (
465+
content.extension[0].url
466+
!= "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability"
467+
):
468+
logger.log(
469+
LogReference.VALIDATOR001,
470+
step=f"content[{i}].extension.url",
471+
reason="extension_url_invalid",
472+
)
473+
self.result.add_error(
474+
issue_code="value",
475+
error_code="INVALID_RESOURCE",
476+
diagnostics=f"Invalid content extension url: {content.extension[0].url} Extension url must be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability'",
477+
field=f"content[{i}].extension[0].url",
478+
)
479+
return

0 commit comments

Comments
 (0)