From 64cf5f4891d4833660528fc4263f55e7c42771d0 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 27 Nov 2024 09:23:43 +0000 Subject: [PATCH 01/40] NRL-518 Validate display as per the definition --- layer/nrlf/core/tests/test_validators.py | 42 ++++++++++++++++++- layer/nrlf/core/validators.py | 40 ++++++++++-------- .../RQI-736253002-Valid.json | 2 +- .../Y05868-736253002-Valid.json | 2 +- 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index fd02deb20..00da67405 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1029,7 +1029,7 @@ def test_validate_content_extension_invalid_display(): } ] }, - "diagnostics": "Invalid content extension display: invalid Extension display must be the same as code either 'static' or 'dynamic'", + "diagnostics": "Invalid content extension display: invalid Extension display must be 'Static' or 'Dynamic'", "expression": [ "content[0].extension[0].valueCodeableConcept.coding[0].display" ], @@ -1522,3 +1522,43 @@ def test_validate_ssp_content_with_multiple_asids(): "diagnostics": "Multiple ASID identifiers provided. Only a single valid ASID identifier can be provided in the context.related.", "expression": ["context.related"], } + + def test_validate_content_extension_invalid_code_and_display_mismatch(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["extension"][0] = { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Dynamic", + } + ] + }, + } + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + assert result.resource.id == "Y05868-99999-99999-999999" + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Invalid content extension display: Dynamic Extension display must be the same as code either 'Static' or 'Dynamic'", + "expression": [ + "content[0].extension[0].valueCodeableConcept.coding[0].display" + ], + } diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index bdb01b78b..c40eeae7e 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -508,50 +508,54 @@ def _validate_content_extension(self, model: DocumentReference): return if ( - content.extension[0].valueCodeableConcept.coding[0].system + content.extension[0].url + != "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ): + self.result.add_error( + issue_code="value", + error_code="INVALID_RESOURCE", + diagnostics=f"Invalid content extension url: {content.extension[0].url} Extension url must be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability'", + field=f"content[{i}].extension[0].url", + ) + return + + coding = content.extension[0].valueCodeableConcept.coding[0] + if ( + coding.system != "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" ): self.result.add_error( issue_code="value", error_code="INVALID_RESOURCE", - 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'", + diagnostics=f"Invalid content extension system: {coding.system} Extension system must be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability'", field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].system", ) return - if content.extension[0].valueCodeableConcept.coding[0].code not in [ - "static", - "dynamic", - ]: + if coding.code not in ["static", "dynamic"]: self.result.add_error( issue_code="value", error_code="INVALID_RESOURCE", - diagnostics=f"Invalid content extension code: {content.extension[0].valueCodeableConcept.coding[0].code} Extension code must be 'static' or 'dynamic'", + diagnostics=f"Invalid content extension code: {coding.code} Extension code must be 'static' or 'dynamic'", field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].code", ) return - if ( - content.extension[0].valueCodeableConcept.coding[0].code - != content.extension[0].valueCodeableConcept.coding[0].display.lower() - ): + if coding.display not in ["Static", "Dynamic"]: self.result.add_error( issue_code="value", error_code="INVALID_RESOURCE", - diagnostics=f"Invalid content extension display: {content.extension[0].valueCodeableConcept.coding[0].display} Extension display must be the same as code either 'static' or 'dynamic'", + diagnostics=f"Invalid content extension display: {coding.display} Extension display must be 'Static' or 'Dynamic'", field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].display", ) return - if ( - content.extension[0].url - != "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - ): + if coding.code != coding.display.lower(): self.result.add_error( issue_code="value", error_code="INVALID_RESOURCE", - diagnostics=f"Invalid content extension url: {content.extension[0].url} Extension url must be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability'", - field=f"content[{i}].extension[0].url", + diagnostics=f"Invalid content extension display: {coding.display} Extension display must be the same as code either 'Static' or 'Dynamic'", + field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].display", ) return diff --git a/tests/data/DocumentReference/RQI-736253002-Valid.json b/tests/data/DocumentReference/RQI-736253002-Valid.json index db7503fbf..429d088a4 100644 --- a/tests/data/DocumentReference/RQI-736253002-Valid.json +++ b/tests/data/DocumentReference/RQI-736253002-Valid.json @@ -79,7 +79,7 @@ { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", "code": "static", - "display": "static" + "display": "Static" } ] } diff --git a/tests/data/DocumentReference/Y05868-736253002-Valid.json b/tests/data/DocumentReference/Y05868-736253002-Valid.json index b0a18b1d5..d83b5a3f6 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid.json @@ -79,7 +79,7 @@ { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", "code": "static", - "display": "static" + "display": "Static" } ] } From dcde0a9ff829426dd5294fabf198d96575c5751a Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Mon, 2 Dec 2024 16:47:49 +0000 Subject: [PATCH 02/40] NRL-518 Fix pydantic error reporting --- layer/nrlf/core/errors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/layer/nrlf/core/errors.py b/layer/nrlf/core/errors.py index f5bdc3a47..6e7b21686 100644 --- a/layer/nrlf/core/errors.py +++ b/layer/nrlf/core/errors.py @@ -11,14 +11,14 @@ def diag_for_error(error: ErrorDetails) -> str: if error["loc"]: - loc_string = ".".join(each for each in error["loc"]) + loc_string = ".".join(str(each) for each in error["loc"]) return f"{loc_string}: {error['msg']}" else: return f"root: {error['msg']}" def expression_for_error(error: ErrorDetails) -> Optional[str]: - return str(".".join(each for each in error["loc"]) if error["loc"] else "root") + return str(".".join(str(each) for each in error["loc"]) if error["loc"] else "root") class OperationOutcomeError(Exception): From a50f1a5eddda86b6fad385b9218a3e1fa0907a66 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Mon, 2 Dec 2024 16:48:21 +0000 Subject: [PATCH 03/40] NRL-518 Update models with mandatory fields --- api/consumer/swagger.yaml | 6 +++++- api/producer/swagger.yaml | 6 +++++- layer/nrlf/consumer/fhir/r4/model.py | 16 ++++++++-------- layer/nrlf/producer/fhir/r4/model.py | 16 ++++++++-------- layer/nrlf/producer/fhir/r4/strict_model.py | 15 +++++++-------- 5 files changed, 33 insertions(+), 26 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 47c1a072c..7caec31fd 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1074,6 +1074,7 @@ components: description: Additional content defined by implementations. required: - attachment + - format DocumentReferenceRelatesTo: type: object properties: @@ -1100,7 +1101,7 @@ components: description: Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces. contentType: type: string - pattern: "[^\\s]+(\\s[^\\s]+)*" + pattern: "^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$" description: Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate. language: type: string @@ -1130,6 +1131,9 @@ components: type: string pattern: ([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)? description: The date that the attachment was first created. + required: + - contentType + - url CodeableConcept: type: object properties: diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index bdd0127b8..6b1fa4457 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1639,6 +1639,7 @@ components: description: Additional content defined by implementations. required: - attachment + - format DocumentReferenceRelatesTo: type: object properties: @@ -1665,7 +1666,7 @@ components: description: Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces. contentType: type: string - pattern: "[^\\s]+(\\s[^\\s]+)*" + pattern: "^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$" description: Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate. language: type: string @@ -1695,6 +1696,9 @@ components: type: string pattern: ([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)? description: The date that the attachment was first created. + required: + - contentType + - url CodeableConcept: type: object properties: diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 4665533b5..a44aab138 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-20T09:43:58+00:00 +# timestamp: 2024-11-26T15:05:35+00:00 from __future__ import annotations @@ -133,12 +133,12 @@ class Attachment(BaseModel): ), ] = None contentType: Annotated[ - Optional[str], + str, Field( description="Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate.", - pattern="[^\\s]+(\\s[^\\s]+)*", + pattern="^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$", ), - ] = None + ] language: Annotated[ Optional[str], Field( @@ -154,9 +154,9 @@ class Attachment(BaseModel): ), ] = None url: Annotated[ - Optional[str], + str, Field(description="A location where the data can be accessed.", pattern="\\S*"), - ] = None + ] size: Annotated[ Optional[int], Field( @@ -833,11 +833,11 @@ class DocumentReferenceContent(BaseModel): ), ] format: Annotated[ - Optional[Coding], + Coding, Field( description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." ), - ] = None + ] extension: Optional[List[Extension]] = None diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index d96b7ce73..fe399cff7 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-20T10:10:52+00:00 +# timestamp: 2024-11-26T15:05:33+00:00 from __future__ import annotations @@ -133,12 +133,12 @@ class Attachment(BaseModel): ), ] = None contentType: Annotated[ - Optional[str], + str, Field( description="Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate.", - pattern="[^\\s]+(\\s[^\\s]+)*", + pattern="^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$", ), - ] = None + ] language: Annotated[ Optional[str], Field( @@ -154,9 +154,9 @@ class Attachment(BaseModel): ), ] = None url: Annotated[ - Optional[str], + str, Field(description="A location where the data can be accessed.", pattern="\\S*"), - ] = None + ] size: Annotated[ Optional[int], Field( @@ -817,11 +817,11 @@ class DocumentReferenceContent(BaseModel): ), ] format: Annotated[ - Optional[Coding], + Coding, Field( description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." ), - ] = None + ] extension: Optional[List[Extension]] = None diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index e4edefc58..ca3ec452f 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-20T10:10:54+00:00 +# timestamp: 2024-11-26T15:05:34+00:00 from __future__ import annotations @@ -126,11 +126,11 @@ class Attachment(BaseModel): ), ] = None contentType: Annotated[ - Optional[StrictStr], + StrictStr, Field( description="Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate." ), - ] = None + ] language: Annotated[ Optional[StrictStr], Field( @@ -144,9 +144,8 @@ class Attachment(BaseModel): ), ] = None url: Annotated[ - Optional[StrictStr], - Field(description="A location where the data can be accessed."), - ] = None + StrictStr, Field(description="A location where the data can be accessed.") + ] size: Annotated[ Optional[StrictInt], Field( @@ -721,11 +720,11 @@ class DocumentReferenceContent(BaseModel): ), ] format: Annotated[ - Optional[Coding], + Coding, Field( description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." ), - ] = None + ] extension: Optional[List[Extension]] = None From d29aa5f1655e163073881f9814a89458d2375c23 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Mon, 2 Dec 2024 17:28:45 +0000 Subject: [PATCH 04/40] NRL-518 Add step for upserts, refactor create and upsert into one method --- tests/features/steps/2_request.py | 25 +++++++++++++++++++------ tests/utilities/api_clients.py | 8 ++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/features/steps/2_request.py b/tests/features/steps/2_request.py index a008aed17..4dfd974e4 100644 --- a/tests/features/steps/2_request.py +++ b/tests/features/steps/2_request.py @@ -94,17 +94,16 @@ def create_post_document_reference_step(context: Context, ods_code: str): context.add_cleanup(lambda: context.repository.delete_by_id(doc_ref_id)) -@when( - "producer 'TSTCUS' requests creation of a DocumentReference with default test values except '{section}' is" -) -def create_post_body_step(context: Context, section: str): +def _create_or_upsert_body_step( + context: Context, method: str, pointer_id: str = "TSTCUS-sample-id-00000" +): client = producer_client_from_context(context, "TSTCUS") if not context.text: raise ValueError("No document reference text snippet provided") - doc_ref = create_test_document_reference_with_defaults(section, context.text) - context.response = client.create_text(doc_ref) + doc_ref = create_test_document_reference_with_defaults("content", context.text) + context.response = getattr(client, method)(doc_ref) if context.response.status_code == 201: doc_ref_id = context.response.headers["Location"].split("/")[-1] @@ -114,6 +113,20 @@ def create_post_body_step(context: Context, section: str): context.add_cleanup(lambda: context.repository.delete_by_id(doc_ref_id)) +@when( + "producer 'TSTCUS' requests creation of a DocumentReference with default test values except '{section}' is" +) +def create_post_body_step(context: Context, section: str): + _create_or_upsert_body_step(context, "create_text") + + +@when( + "producer 'TSTCUS' requests upsert of a DocumentReference with pointerId '{pointer_id}' and default test values except '{section}' is" +) +def upsert_post_body_step(context: Context, section: str, pointer_id: str): + _create_or_upsert_body_step(context, "upsert_text", pointer_id) + + @when("producer '{ods_code}' upserts a DocumentReference with values") def create_put_document_reference_step(context: Context, ods_code: str): client = producer_client_from_context(context, ods_code) diff --git a/tests/utilities/api_clients.py b/tests/utilities/api_clients.py index 1d06bf2a8..3b1b78e9f 100644 --- a/tests/utilities/api_clients.py +++ b/tests/utilities/api_clients.py @@ -213,6 +213,14 @@ def upsert(self, doc_ref): cert=self.config.client_cert, ) + def upsert_text(self, doc_ref): + return requests.put( + f"{self.api_url}/DocumentReference", + data=doc_ref, + headers=self.request_headers, + cert=self.config.client_cert, + ) + def update(self, doc_ref, doc_ref_id: str): return requests.put( f"{self.api_url}/DocumentReference/{doc_ref_id}", From d58fdca7bd4f81ff8a04aa85648a6377bc873915 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 09:01:49 +0000 Subject: [PATCH 05/40] NRL-518 Add default format to default test document reference --- tests/features/utils/data.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/features/utils/data.py b/tests/features/utils/data.py index 11288945a..68ee5fccb 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -28,14 +28,25 @@ def create_test_document_reference(items: dict) -> DocumentReference: base_doc_ref = DocumentReference.model_construct( resourceType="DocumentReference", status=items.get("status", "current"), - content=[ - DocumentReferenceContent( - attachment=Attachment( - contentType=items.get("contentType", "application/json"), - url=items["url"], + content=items.get( + "content", + [ + DocumentReferenceContent( + attachment=Attachment( + contentType=items.get("contentType", "application/json"), + url=items["url"], + ), + format=Coding( + system=items.get( + "formatSystem", + "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + ), + code=items.get("formatCode", "urn:nhs-ic:unstructured"), + display=items.get("formatDisplay", "Unstructured document"), + ), ) - ) - ], + ], + ), context=DocumentReferenceContext( practiceSetting=CodeableConcept( coding=[ From 548435bdb1a822d9e2e832b74b30d8abcb0cde1a Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 09:06:20 +0000 Subject: [PATCH 06/40] NRL-518 Fix parameter should not be fix to content --- tests/features/steps/2_request.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/features/steps/2_request.py b/tests/features/steps/2_request.py index 4dfd974e4..1ca36903c 100644 --- a/tests/features/steps/2_request.py +++ b/tests/features/steps/2_request.py @@ -95,14 +95,17 @@ def create_post_document_reference_step(context: Context, ods_code: str): def _create_or_upsert_body_step( - context: Context, method: str, pointer_id: str = "TSTCUS-sample-id-00000" + context: Context, + method: str, + section: str, + pointer_id: str = "TSTCUS-sample-id-00000", ): client = producer_client_from_context(context, "TSTCUS") if not context.text: raise ValueError("No document reference text snippet provided") - doc_ref = create_test_document_reference_with_defaults("content", context.text) + doc_ref = create_test_document_reference_with_defaults(section, context.text) context.response = getattr(client, method)(doc_ref) if context.response.status_code == 201: @@ -117,14 +120,14 @@ def _create_or_upsert_body_step( "producer 'TSTCUS' requests creation of a DocumentReference with default test values except '{section}' is" ) def create_post_body_step(context: Context, section: str): - _create_or_upsert_body_step(context, "create_text") + _create_or_upsert_body_step(context, "create_text", section) @when( "producer 'TSTCUS' requests upsert of a DocumentReference with pointerId '{pointer_id}' and default test values except '{section}' is" ) def upsert_post_body_step(context: Context, section: str, pointer_id: str): - _create_or_upsert_body_step(context, "upsert_text", pointer_id) + _create_or_upsert_body_step(context, "upsert_text", section, pointer_id) @when("producer '{ods_code}' upserts a DocumentReference with values") From 84a5f103090a0afe74aeb1cbd1de9cbd8ec5418a Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 09:07:01 +0000 Subject: [PATCH 07/40] NRL-518 Add method to change default parameters for an update --- tests/features/steps/2_request.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/features/steps/2_request.py b/tests/features/steps/2_request.py index 1ca36903c..9caaab2e9 100644 --- a/tests/features/steps/2_request.py +++ b/tests/features/steps/2_request.py @@ -1,3 +1,5 @@ +import json + from behave import * # noqa from behave.runner import Context @@ -130,6 +132,28 @@ def upsert_post_body_step(context: Context, section: str, pointer_id: str): _create_or_upsert_body_step(context, "upsert_text", section, pointer_id) +@when( + "producer 'TSTCUS' requests update of a DocumentReference with pointerId '{pointer_id}' and only changing" +) +def update_post_body_step(context: Context, pointer_id: str): + """ + Updates an existing DocumentReference with new values for a specific section + """ + consumer_client = consumer_client_from_context(context, "TSTCUS") + context.response = consumer_client.read(pointer_id) + + if context.response.status_code != 200: + raise ValueError(f"Failed to read existing pointer: {context.response.text}") + + doc_ref = context.response.json() + custom_data = json.loads(context.text) + for key in custom_data: + doc_ref[key] = custom_data[key] + + producer_client = producer_client_from_context(context, "TSTCUS") + context.response = producer_client.update(doc_ref, pointer_id) + + @when("producer '{ods_code}' upserts a DocumentReference with values") def create_put_document_reference_step(context: Context, ods_code: str): client = producer_client_from_context(context, ods_code) From d3f26c6500d93d838893744f0904421b5cb55e18 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 09:07:39 +0000 Subject: [PATCH 08/40] NRL-518 Add validator method for content and add tests --- layer/nrlf/core/tests/test_validators.py | 93 ++++++++++ layer/nrlf/core/validators.py | 34 ++++ .../createDocumentReference-failure.feature | 114 +++++++++++++ .../updateDocumentReference-failure.feature | 159 ++++++++++++++++++ .../upsertDocumentReference-failure.feature | 115 +++++++++++++ 5 files changed, 515 insertions(+) diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 00da67405..20f08411f 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1562,3 +1562,96 @@ def test_validate_content_extension_invalid_code_and_display_mismatch(): "content[0].extension[0].valueCodeableConcept.coding[0].display" ], } + + def test_validate_content_missing_attachment(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0].pop("attachment") + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "required", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Missing attachment in content", + "expression": ["content[0].attachment"], + } + + def test_validate_content_missing_content_type(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["attachment"].pop("contentType") + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "required", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Missing contentType in content.attachment", + "expression": ["content[0].attachment.contentType"], + } + + def test_validate_content_invalid_content_type(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["attachment"][ + "contentType" + ] = "invalid/type" + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Invalid contentType: invalid/type. Must be 'application/pdf' or 'text/html'", + "expression": ["content[0].attachment.contentType"], + } + + def test_validate_content_valid_content_type(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["attachment"][ + "contentType" + ] = "application/pdf" + + result = validator.validate(document_ref_data) + + assert result.is_valid is True + assert result.issues == [] diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index c40eeae7e..7e910913b 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -141,6 +141,7 @@ def validate(self, data: Dict[str, Any] | DocumentReference): self._validate_category(resource) self._validate_author(resource) self._validate_type_category_mapping(resource) + self._validate_content(resource) if resource.content[0].extension: self._validate_content_extension(resource) @@ -603,3 +604,36 @@ def _validate_author(self, model: DocumentReference): field=f"author[0].identifier.value", ) return + + def _validate_content(self, model: DocumentReference): + """ + Validate that the contentType is present and is either 'application/pdf' or 'text/html'. + """ + logger.log(LogReference.VALIDATOR001, step="content") + + for i, content in enumerate(model.content): + if not content.attachment: + self.result.add_error( + issue_code="required", + error_code="INVALID_RESOURCE", + diagnostics="Missing attachment in content", + field=f"content[{i}].attachment", + ) + continue + + if not content.attachment.contentType: + self.result.add_error( + issue_code="required", + error_code="INVALID_RESOURCE", + diagnostics="Missing contentType in content.attachment", + field=f"content[{i}].attachment.contentType", + ) + continue + + if content.attachment.contentType not in ["application/pdf", "text/html"]: + self.result.add_error( + issue_code="value", + error_code="INVALID_RESOURCE", + diagnostics=f"Invalid contentType: {content.attachment.contentType}. Must be 'application/pdf' or 'text/html'", + field=f"content[{i}].attachment.contentType", + ) diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index c1d09f483..cf78d96bd 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -655,3 +655,117 @@ Feature: Producer - createDocumentReference - Failure Scenarios | type-system | type-code | category-code | type-display | correct-display | | https://nicip.nhs.uk | MAULR | 721981007 | "Nonsense display" | MRA Upper Limb Rt | | https://nicip.nhs.uk | MAXIB | 103693007 | "Nonsense display" | MRI Axilla Both | + + Scenario: Missing content + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed" + } + ] + }, + "diagnostics": "Request body could not be parsed (content: List should have at least 1 item after validation, not 0)", + "expression": [ + "content" + ] + } + """ + + Scenario: Missing contentType + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "", + "url": "https://spine-proxy.national.ncrs.nhs.uk/https%3A%2F%2Fp1.nhs.uk%2FMentalhealthCrisisPlanReport.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured document" + } + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed" + } + ] + }, + "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$')", + "expression": [ + "content.0.attachment.contentType" + ] + } + """ + + Scenario: Invalid contentType + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'ANGY1' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'ANGY1' creates a DocumentReference with values: + | property | value | + | subject | 9999999999 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | custodian | ANGY1 | + | author | HAR1 | + | url | https://example.org/my-doc.pdf | + | contentType | application/invalid | + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource" + } + ] + }, + "diagnostics": "Invalid contentType: application/invalid. Must be 'application/pdf' or 'text/html'", + "expression": [ + "content[0].attachment.contentType" + ] + } + """ diff --git a/tests/features/producer/updateDocumentReference-failure.feature b/tests/features/producer/updateDocumentReference-failure.feature index b909b46cf..bd0fbbe76 100644 --- a/tests/features/producer/updateDocumentReference-failure.feature +++ b/tests/features/producer/updateDocumentReference-failure.feature @@ -48,3 +48,162 @@ Feature: Producer - updateDocumentReference - Failure Scenarios ] } """ + + Scenario: Missing content + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + And a DocumentReference resource exists with values: + | property | value | + | id | TSTCUS-1114567890-updateDocTest | + | subject | 9999999999 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc.pdf | + | custodian | TSTCUS | + | author | TSTCUS | + When producer 'TSTCUS' requests update of a DocumentReference with pointerId 'TSTCUS-1114567890-updateDocTest' and only changing: + """ + { + "content": [] + } + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed" + } + ] + }, + "diagnostics": "Request body could not be parsed (content: List should have at least 1 item after validation, not 0)", + "expression": [ + "content" + ] + } + """ + + Scenario: Missing contentType + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + And a DocumentReference resource exists with values: + | property | value | + | id | TSTCUS-1114567891-updateDocTest | + | subject | 9999999999 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc.pdf | + | custodian | TSTCUS | + | author | TSTCUS | + When producer 'TSTCUS' requests update of a DocumentReference with pointerId 'TSTCUS-1114567891-updateDocTest' and only changing: + """ + { + "content": [ + { + "attachment": { + "contentType": "", + "url": "https://spine-proxy.national.ncrs.nhs.uk/https%3A%2F%2Fp1.nhs.uk%2FMentalhealthCrisisPlanReport.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured document" + } + } + ] + } + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed" + } + ] + }, + "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$')", + "expression": [ + "content.0.attachment.contentType" + ] + } + """ + + Scenario: Invalid contentType + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + And a DocumentReference resource exists with values: + | property | value | + | id | TSTCUS-1114567892-updateDocTest | + | subject | 9999999999 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc.pdf | + | custodian | TSTCUS | + | author | TSTCUS | + When producer 'TSTCUS' requests update of a DocumentReference with pointerId 'TSTCUS-1114567892-updateDocTest' and only changing: + """ + { + "content": [ + { + "attachment": { + "contentType": "application/invalid", + "url": "https://spine-proxy.national.ncrs.nhs.uk/https%3A%2F%2Fp1.nhs.uk%2FMentalhealthCrisisPlanReport.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured document" + } + } + ] + } + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource" + } + ] + }, + "diagnostics": "Invalid contentType: application/invalid. Must be 'application/pdf' or 'text/html'", + "expression": [ + "content[0].attachment.contentType" + ] + } + """ diff --git a/tests/features/producer/upsertDocumentReference-failure.feature b/tests/features/producer/upsertDocumentReference-failure.feature index 3fb5d6ebd..5876e7db6 100644 --- a/tests/features/producer/upsertDocumentReference-failure.feature +++ b/tests/features/producer/upsertDocumentReference-failure.feature @@ -236,3 +236,118 @@ Feature: Producer - upsertDocumentReference - Failure Scenarios | type-system | type-code | category-code | type-display | correct-display | | https://nicip.nhs.uk | MAULR | 721981007 | "Nonsense display" | MRA Upper Limb Rt | | https://nicip.nhs.uk | MAXIB | 103693007 | "Nonsense display" | MRI Axilla Both | + + Scenario: Missing content + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests upsert of a DocumentReference with pointerId 'TSTCUS-sample-id-00001' and default test values except 'content' is: + """ + "content": [] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed" + } + ] + }, + "diagnostics": "Request body could not be parsed (content: List should have at least 1 item after validation, not 0)", + "expression": [ + "content" + ] + } + """ + + Scenario: Missing contentType + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests upsert of a DocumentReference with pointerId 'TSTCUS-sample-id-00002' and default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "", + "url": "https://spine-proxy.national.ncrs.nhs.uk/https%3A%2F%2Fp1.nhs.uk%2FMentalhealthCrisisPlanReport.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured document" + } + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed" + } + ] + }, + "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$')", + "expression": [ + "content.0.attachment.contentType" + ] + } + """ + + Scenario: Invalid contentType + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'ANGY1' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'ANGY1' upserts a DocumentReference with values: + | property | value | + | id | TSTCUS-sample-id-00003 | + | subject | 9999999999 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | custodian | ANGY1 | + | author | HAR1 | + | url | https://example.org/my-doc.pdf | + | contentType | application/invalid | + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource" + } + ] + }, + "diagnostics": "Invalid contentType: application/invalid. Must be 'application/pdf' or 'text/html'", + "expression": [ + "content[0].attachment.contentType" + ] + } + """ From 02a9a7e91241014f58685095d6123edd1bc8c24f Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 09:11:09 +0000 Subject: [PATCH 09/40] NRL-518 Fix default contentType --- tests/features/utils/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/features/utils/data.py b/tests/features/utils/data.py index 68ee5fccb..6cf930114 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -33,7 +33,7 @@ def create_test_document_reference(items: dict) -> DocumentReference: [ DocumentReferenceContent( attachment=Attachment( - contentType=items.get("contentType", "application/json"), + contentType=items.get("contentType", "application/pdf"), url=items["url"], ), format=Coding( From de9065918f04d1179a9bd887fbb87a38d45305bf Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 09:24:30 +0000 Subject: [PATCH 10/40] NRL-518 Fix integration tests expected without format --- .../consumer/readDocumentReference-success.feature | 10 ++++++++++ .../producer/readDocumentReference-success.feature | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/tests/features/consumer/readDocumentReference-success.feature b/tests/features/consumer/readDocumentReference-success.feature index b1e1add80..5c0d83979 100644 --- a/tests/features/consumer/readDocumentReference-success.feature +++ b/tests/features/consumer/readDocumentReference-success.feature @@ -69,6 +69,11 @@ Feature: Consumer - readDocumentReference - Success Scenarios "attachment": { "contentType": "application/pdf", "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured document" } } ], @@ -155,6 +160,11 @@ Feature: Consumer - readDocumentReference - Success Scenarios "attachment": { "contentType": "application/pdf", "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured document" } } ], diff --git a/tests/features/producer/readDocumentReference-success.feature b/tests/features/producer/readDocumentReference-success.feature index bf0e340b1..04c4228ec 100644 --- a/tests/features/producer/readDocumentReference-success.feature +++ b/tests/features/producer/readDocumentReference-success.feature @@ -71,6 +71,11 @@ Feature: Producer - readDocumentReference - Success Scenarios "attachment": { "contentType": "application/pdf", "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured document" } } ], From 48ace9e5c3c79112270a8ea37cd50fa81af010bc Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 09:42:11 +0000 Subject: [PATCH 11/40] NRL-518 Add ContentStability value set --- .../fhir/NRLF-ContentStability-ValueSet.json | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 resources/fhir/NRLF-ContentStability-ValueSet.json diff --git a/resources/fhir/NRLF-ContentStability-ValueSet.json b/resources/fhir/NRLF-ContentStability-ValueSet.json new file mode 100644 index 000000000..26a2ca381 --- /dev/null +++ b/resources/fhir/NRLF-ContentStability-ValueSet.json @@ -0,0 +1,37 @@ +{ + "resourceType": "ValueSet", + "id": "NRLF-ContentStability", + "url": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "version": "1.0.0", + "name": "NRLF Content Stability", + "status": "draft", + "date": "2024-12-03T00:00:00+00:00", + "publisher": "NHS Digital", + "contact": { + "name": "NRL Team at NHS Digital", + "telecom": { + "system": "email", + "value": "nrls@nhs.net", + "use": "work" + } + }, + "description": "A code from the NRL Content Stability coding system to represent the stability of the content.", + "copyright": "Copyright 2024 NHS Digital.", + "compose": { + "include": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "concept": [ + { + "code": "static", + "display": "Static" + }, + { + "code": "dynamic", + "display": "Dynamic" + } + ] + } + ] + } +} From 7a6e4307e3c8b44f3748cb8ff55025b29ff88f4b Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 10:03:36 +0000 Subject: [PATCH 12/40] NRL-518 Fix smoke tests missing format --- tests/smoke/setup.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index a44ebe9bb..0c0a578c5 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -32,7 +32,16 @@ def build_document_reference( attachment=Attachment( contentType=content_type, url=content_url, - ) + ), + format=CodeableConcept( + coding=[ + Coding( + system="https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + code="urn:nhs-ic:unstructured", + display="Unstructured document", + ) + ] + ), ) ], type=CodeableConcept( From 3c6c1b86339ad48070eaaad93aad2bcbf177aa0a Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 10:37:58 +0000 Subject: [PATCH 13/40] NRL-518 Fix unused parameter --- tests/features/steps/2_request.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/features/steps/2_request.py b/tests/features/steps/2_request.py index 9caaab2e9..993fae1c7 100644 --- a/tests/features/steps/2_request.py +++ b/tests/features/steps/2_request.py @@ -107,7 +107,9 @@ def _create_or_upsert_body_step( if not context.text: raise ValueError("No document reference text snippet provided") - doc_ref = create_test_document_reference_with_defaults(section, context.text) + doc_ref = create_test_document_reference_with_defaults( + section, context.text, pointer_id + ) context.response = getattr(client, method)(doc_ref) if context.response.status_code == 201: From 4820d089d112064a854bcb44fe92e4bfaf8b6a59 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 10:58:17 +0000 Subject: [PATCH 14/40] NRL-518 Fix content_type for smoke test --- tests/smoke/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index 0c0a578c5..cb1423ec4 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -20,7 +20,7 @@ def build_document_reference( category: str = Categories.CARE_PLAN.coding_value(), type: str = PointerTypes.MENTAL_HEALTH_PLAN.coding_value(), author: str = "SMOKETEST", - content_type: str = "application/json", + content_type: str = "application/pdf", content_url: str = "https://testing.record-locator.national.nhs.uk/_smoke_test_pointer_content", replaces_id: str | None = None, ) -> DocumentReference: From 32e1363fb802b5baa790699ce96561eb9b556cfa Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 3 Dec 2024 12:20:22 +0000 Subject: [PATCH 15/40] NRL-518 Fix format for smoke tests --- tests/smoke/setup.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index cb1423ec4..49ddeb766 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -33,14 +33,10 @@ def build_document_reference( contentType=content_type, url=content_url, ), - format=CodeableConcept( - coding=[ - Coding( - system="https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", - code="urn:nhs-ic:unstructured", - display="Unstructured document", - ) - ] + format=Coding( + system="https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + code="urn:nhs-ic:unstructured", + display="Unstructured document", ), ) ], From 7b7f2bd985ffc1efad3061aa7b7c64726944e248 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 4 Dec 2024 15:07:36 +0000 Subject: [PATCH 16/40] NRL-518 Add documentation for content validation --- swagger/producer-static/narrative.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/swagger/producer-static/narrative.yaml b/swagger/producer-static/narrative.yaml index 4341faf86..91041187e 100644 --- a/swagger/producer-static/narrative.yaml +++ b/swagger/producer-static/narrative.yaml @@ -276,7 +276,12 @@ paths: ] ``` * `content` MUST have at least one entry. - * `content[].format[]` SHOULD indicate whether the data is structured or not, e.g. + * `content` MUST include an `attachment` entry. + * `content` MUST include a `format` entry. + * `content` MUST include the content stability extension (https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability). + * `content[].attachment` MUST include a `url` to the document. + * `content[].attachment` MUST include a `contentType` and be a valid MIME type, specifically `application/pdf` for documents or `text/html` for contact details. + * `content[].format[]` MUST indicate whether the data is structured or not, e.g. ``` "format": [ { From 6b8e9e63e152e44555c00dedde9bff7c9f920db7 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 4 Dec 2024 15:08:12 +0000 Subject: [PATCH 17/40] NRL-518 Add NRLFormatCode valueset --- resources/fhir/NRLF-FormatCode-ValueSet.json | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 resources/fhir/NRLF-FormatCode-ValueSet.json diff --git a/resources/fhir/NRLF-FormatCode-ValueSet.json b/resources/fhir/NRLF-FormatCode-ValueSet.json new file mode 100644 index 000000000..a0fe1cc77 --- /dev/null +++ b/resources/fhir/NRLF-FormatCode-ValueSet.json @@ -0,0 +1,37 @@ +{ + "resourceType": "ValueSet", + "id": "NRLF-FormatCode", + "url": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "version": "1.0.0", + "name": "NRLF Format Code", + "status": "draft", + "date": "2024-12-03T00:00:00+00:00", + "publisher": "NHS Digital", + "contact": { + "name": "NRL Team at NHS Digital", + "telecom": { + "system": "email", + "value": "nrls@nhs.net", + "use": "work" + } + }, + "description": "A ValueSet that identifies the format of the content of the target document or record of a National Record Locator pointer.", + "copyright": "Copyright © 2024 NHS Digital", + "compose": { + "include": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "concept": [ + { + "code": "urn:nhs-ic:record-contact", + "display": "Contact details (HTTP Unsecured)" + }, + { + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + } + ] + } + ] + } +} From 202b4f71fdfc61b7bde56e1ae5bfa9731d438b8d Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 4 Dec 2024 15:09:28 +0000 Subject: [PATCH 18/40] NRL-518 Update postman link in documentation --- swagger/consumer-static/narrative.yaml | 2 +- swagger/producer-static/narrative.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/swagger/consumer-static/narrative.yaml b/swagger/consumer-static/narrative.yaml index a349758ea..6b4522a33 100644 --- a/swagger/consumer-static/narrative.yaml +++ b/swagger/consumer-static/narrative.yaml @@ -190,7 +190,7 @@ info: Right click the icon and save link as... to save the Postman collection to your device - [![Right click and save link as...](https://run.pstmn.io/button.svg)](https://github.com/NHSDigital/NRLF/raw/main/postman_collection.json) + [![Right click and save link as...](https://run.pstmn.io/button.svg)](https://github.com/NHSDigital/NRLF/raw/develop/postman_collection.json) ### Integration testing diff --git a/swagger/producer-static/narrative.yaml b/swagger/producer-static/narrative.yaml index 91041187e..0a3af911f 100644 --- a/swagger/producer-static/narrative.yaml +++ b/swagger/producer-static/narrative.yaml @@ -179,7 +179,7 @@ info: Right click the icon and save link as... to save the Postman collection to your device - [![Right click and save link as...](https://run.pstmn.io/button.svg)](https://github.com/NHSDigital/NRLF/raw/main/postman_collection.json) + [![Right click and save link as...](https://run.pstmn.io/button.svg)](https://github.com/NHSDigital/NRLF/raw/develop/postman_collection.json) ### Integration testing From 5a46d45261b432456c840aaa6837f26f124cf997 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Thu, 5 Dec 2024 12:30:58 +0000 Subject: [PATCH 19/40] NRL-518 Remove tests no longer valid due to upstream pydantic validation --- layer/nrlf/core/tests/test_pydantic_errors.py | 86 +++++++++ layer/nrlf/core/tests/test_validators.py | 165 ++++-------------- 2 files changed, 122 insertions(+), 129 deletions(-) create mode 100644 layer/nrlf/core/tests/test_pydantic_errors.py diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py new file mode 100644 index 000000000..4ea579d3b --- /dev/null +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -0,0 +1,86 @@ +from unittest.mock import Mock +import pytest +from nrlf.core.errors import ParseError +from nrlf.core.validators import DocumentReferenceValidator +from nrlf.tests.data import load_document_reference_json + +def test_validate_content_missing_attachment(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0].pop("attachment") + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content.0.attachment: Field required)", + "expression": ["content.0.attachment"], + } + +def test_validate_content_missing_content_type(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["attachment"].pop("contentType") + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content.0.attachment.contentType: Field required)", + "expression": ["content.0.attachment.contentType"], + } + +def test_validate_content_invalid_content_type(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["attachment"]["contentType"] = "invalid/type" + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content.0.attachment.contentType: String should match pattern '^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$')", + "expression": ["content.0.attachment.contentType"], + } diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 20f08411f..d4a06d9b7 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1523,135 +1523,42 @@ def test_validate_ssp_content_with_multiple_asids(): "expression": ["context.related"], } - def test_validate_content_extension_invalid_code_and_display_mismatch(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["content"][0]["extension"][0] = { - "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", - "valueCodeableConcept": { - "coding": [ - { - "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", - "code": "static", - "display": "Dynamic", - } - ] - }, - } - - result = validator.validate(document_ref_data) - - assert result.is_valid is False - assert result.resource.id == "Y05868-99999-99999-999999" - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "value", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Invalid content extension display: Dynamic Extension display must be the same as code either 'Static' or 'Dynamic'", - "expression": [ - "content[0].extension[0].valueCodeableConcept.coding[0].display" - ], - } - - def test_validate_content_missing_attachment(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["content"][0].pop("attachment") - - result = validator.validate(document_ref_data) - - assert result.is_valid is False - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "required", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Missing attachment in content", - "expression": ["content[0].attachment"], - } - - def test_validate_content_missing_content_type(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["content"][0]["attachment"].pop("contentType") - - result = validator.validate(document_ref_data) - - assert result.is_valid is False - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "required", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Missing contentType in content.attachment", - "expression": ["content[0].attachment.contentType"], - } - - def test_validate_content_invalid_content_type(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["content"][0]["attachment"][ - "contentType" - ] = "invalid/type" - - result = validator.validate(document_ref_data) - - assert result.is_valid is False - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "value", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Invalid contentType: invalid/type. Must be 'application/pdf' or 'text/html'", - "expression": ["content[0].attachment.contentType"], - } - - def test_validate_content_valid_content_type(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") +def test_validate_content_extension_invalid_code_and_display_mismatch(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - document_ref_data["content"][0]["attachment"][ - "contentType" - ] = "application/pdf" + document_ref_data["content"][0]["extension"][0] = { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Dynamic", + } + ] + }, + } - result = validator.validate(document_ref_data) + result = validator.validate(document_ref_data) - assert result.is_valid is True - assert result.issues == [] + assert result.is_valid is False + assert result.resource.id == "Y05868-99999-99999-999999" + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Invalid content extension display: Dynamic Extension display must be the same as code either 'Static' or 'Dynamic'", + "expression": [ + "content[0].extension[0].valueCodeableConcept.coding[0].display" + ], + } From e6d10c6042ff53ebfab08455cc55752fedf8edcc Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Thu, 5 Dec 2024 16:27:32 +0000 Subject: [PATCH 20/40] NRL-518 Simplify test, fix lint warnings --- layer/nrlf/core/tests/test_pydantic_errors.py | 5 +++++ layer/nrlf/core/tests/test_validators.py | 15 +++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 4ea579d3b..dd8bfba54 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -1,9 +1,12 @@ from unittest.mock import Mock + import pytest + from nrlf.core.errors import ParseError from nrlf.core.validators import DocumentReferenceValidator from nrlf.tests.data import load_document_reference_json + def test_validate_content_missing_attachment(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") @@ -31,6 +34,7 @@ def test_validate_content_missing_attachment(): "expression": ["content.0.attachment"], } + def test_validate_content_missing_content_type(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") @@ -58,6 +62,7 @@ def test_validate_content_missing_content_type(): "expression": ["content.0.attachment.contentType"], } + def test_validate_content_invalid_content_type(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index d4a06d9b7..58af89bd6 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -960,18 +960,8 @@ def test_validate_content_extension_invalid_code(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - document_ref_data["content"][0]["extension"][0] = { - "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", - "valueCodeableConcept": { - "coding": [ - { - "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", - "code": "invalid", - "display": "invalid", - } - ] - }, - } + content_extension = document_ref_data["content"][0]["extension"][0] + content_extension["valueCodeableConcept"]["coding"][0]["code"] = "invalid" result = validator.validate(document_ref_data) @@ -1523,6 +1513,7 @@ def test_validate_ssp_content_with_multiple_asids(): "expression": ["context.related"], } + def test_validate_content_extension_invalid_code_and_display_mismatch(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") From 1d58d6522564d5e5543c335cf04210bba4fe0359 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Thu, 5 Dec 2024 17:05:49 +0000 Subject: [PATCH 21/40] NRL-518 Revert contentType regex to simple version --- api/consumer/swagger.yaml | 2 +- api/producer/swagger.yaml | 2 +- layer/nrlf/consumer/fhir/r4/model.py | 4 +-- layer/nrlf/core/tests/test_pydantic_errors.py | 28 ------------------- layer/nrlf/producer/fhir/r4/model.py | 4 +-- layer/nrlf/producer/fhir/r4/strict_model.py | 2 +- .../createDocumentReference-failure.feature | 2 +- .../updateDocumentReference-failure.feature | 2 +- .../upsertDocumentReference-failure.feature | 2 +- 9 files changed, 10 insertions(+), 38 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 7caec31fd..10632c719 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1101,7 +1101,7 @@ components: description: Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces. contentType: type: string - pattern: "^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$" + pattern: "[^\\s]+(\\s[^\\s]+)*" description: Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate. language: type: string diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 6b1fa4457..cb99f13b7 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1666,7 +1666,7 @@ components: description: Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces. contentType: type: string - pattern: "^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$" + pattern: "[^\\s]+(\\s[^\\s]+)*" description: Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate. language: type: string diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index a44aab138..b36fe952e 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-26T15:05:35+00:00 +# timestamp: 2024-12-05T16:54:49+00:00 from __future__ import annotations @@ -136,7 +136,7 @@ class Attachment(BaseModel): str, Field( description="Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate.", - pattern="^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$", + pattern="[^\\s]+(\\s[^\\s]+)*", ), ] language: Annotated[ diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index dd8bfba54..9b63550f8 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -61,31 +61,3 @@ def test_validate_content_missing_content_type(): "diagnostics": "Failed to parse DocumentReference resource (content.0.attachment.contentType: Field required)", "expression": ["content.0.attachment.contentType"], } - - -def test_validate_content_invalid_content_type(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["content"][0]["attachment"]["contentType"] = "invalid/type" - - with pytest.raises(ParseError) as error: - validator.validate(document_ref_data) - - exc = error.value - assert len(exc.issues) == 1 - assert exc.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "invalid", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Failed to parse DocumentReference resource (content.0.attachment.contentType: String should match pattern '^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$')", - "expression": ["content.0.attachment.contentType"], - } diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index fe399cff7..17f8653c4 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-26T15:05:33+00:00 +# timestamp: 2024-12-05T16:54:46+00:00 from __future__ import annotations @@ -136,7 +136,7 @@ class Attachment(BaseModel): str, Field( description="Identifies the type of the data in the attachment and allows a method to be chosen to interpret or render the data. Includes mime type parameters such as charset where appropriate.", - pattern="^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$", + pattern="[^\\s]+(\\s[^\\s]+)*", ), ] language: Annotated[ diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index ca3ec452f..db9a93d5d 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-11-26T15:05:34+00:00 +# timestamp: 2024-12-05T16:54:48+00:00 from __future__ import annotations diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index cf78d96bd..14d824c40 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -725,7 +725,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$')", + "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", "expression": [ "content.0.attachment.contentType" ] diff --git a/tests/features/producer/updateDocumentReference-failure.feature b/tests/features/producer/updateDocumentReference-failure.feature index bd0fbbe76..a19cd64d2 100644 --- a/tests/features/producer/updateDocumentReference-failure.feature +++ b/tests/features/producer/updateDocumentReference-failure.feature @@ -144,7 +144,7 @@ Feature: Producer - updateDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$')", + "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", "expression": [ "content.0.attachment.contentType" ] diff --git a/tests/features/producer/upsertDocumentReference-failure.feature b/tests/features/producer/upsertDocumentReference-failure.feature index 5876e7db6..6ca9c54d8 100644 --- a/tests/features/producer/upsertDocumentReference-failure.feature +++ b/tests/features/producer/upsertDocumentReference-failure.feature @@ -306,7 +306,7 @@ Feature: Producer - upsertDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '^(application|audio|image|message|model|multipart|text|video)/[a-zA-Z0-9!#$&^_+.-]+(;[a-zA-Z0-9!#$&^_+.-]+=[a-zA-Z0-9!#$&^_+.-]+)*$')", + "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", "expression": [ "content.0.attachment.contentType" ] From 90822a6da61609ac1f94dc18a21411bc73dd5952 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Thu, 5 Dec 2024 17:09:03 +0000 Subject: [PATCH 22/40] NRL-518 Fix lint issue --- layer/nrlf/core/tests/test_pydantic_errors.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 9b63550f8..249151a3f 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -1,5 +1,3 @@ -from unittest.mock import Mock - import pytest from nrlf.core.errors import ParseError From 07fd1e3d88984d48ef2ca64bec8d615ef150da11 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Thu, 5 Dec 2024 22:21:22 +0000 Subject: [PATCH 23/40] NRL-518 Remove checks, pydantic validates that, add invalid contentType test --- layer/nrlf/core/tests/test_validators.py | 27 ++++++++++++++++++++++++ layer/nrlf/core/validators.py | 18 ---------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 58af89bd6..34c32a915 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1553,3 +1553,30 @@ def test_validate_content_extension_invalid_code_and_display_mismatch(): "content[0].extension[0].valueCodeableConcept.coding[0].display" ], } + + +def test_validate_content_invalid_content_type(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["attachment"]["contentType"] = "invalid/type" + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Invalid contentType: invalid/type. Must be 'application/pdf' or 'text/html'", + "expression": ["content[0].attachment.contentType"], + } diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 7e910913b..6d92be8aa 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -612,24 +612,6 @@ def _validate_content(self, model: DocumentReference): logger.log(LogReference.VALIDATOR001, step="content") for i, content in enumerate(model.content): - if not content.attachment: - self.result.add_error( - issue_code="required", - error_code="INVALID_RESOURCE", - diagnostics="Missing attachment in content", - field=f"content[{i}].attachment", - ) - continue - - if not content.attachment.contentType: - self.result.add_error( - issue_code="required", - error_code="INVALID_RESOURCE", - diagnostics="Missing contentType in content.attachment", - field=f"content[{i}].attachment.contentType", - ) - continue - if content.attachment.contentType not in ["application/pdf", "text/html"]: self.result.add_error( issue_code="value", From 3d8f0bdbabe4d951988f82c628ba3fad8e06dafc Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Thu, 5 Dec 2024 22:22:28 +0000 Subject: [PATCH 24/40] NRL-518 Improve pydantic diagnostic message --- layer/nrlf/core/errors.py | 19 ++++++---- layer/nrlf/core/tests/test_pydantic_errors.py | 36 ++++++++++++++++--- .../createDocumentReference-failure.feature | 4 +-- .../updateDocumentReference-failure.feature | 4 +-- .../upsertDocumentReference-failure.feature | 4 +-- 5 files changed, 51 insertions(+), 16 deletions(-) diff --git a/layer/nrlf/core/errors.py b/layer/nrlf/core/errors.py index 6e7b21686..ccd17628f 100644 --- a/layer/nrlf/core/errors.py +++ b/layer/nrlf/core/errors.py @@ -9,16 +9,23 @@ from nrlf.producer.fhir.r4.model import OperationOutcome, OperationOutcomeIssue +def format_error_location(loc: List) -> str: + formatted_loc = "" + for each in loc: + if isinstance(each, int): + formatted_loc = f"{formatted_loc}[{each}]" + else: + formatted_loc = f"{formatted_loc}.{each}" if formatted_loc else str(each) + return formatted_loc + + def diag_for_error(error: ErrorDetails) -> str: - if error["loc"]: - loc_string = ".".join(str(each) for each in error["loc"]) - return f"{loc_string}: {error['msg']}" - else: - return f"root: {error['msg']}" + loc_string = format_error_location(error["loc"]) + return f"{loc_string}: {error['msg']}" if loc_string else f"root: {error['msg']}" def expression_for_error(error: ErrorDetails) -> Optional[str]: - return str(".".join(str(each) for each in error["loc"]) if error["loc"] else "root") + return format_error_location(error["loc"]) or "root" class OperationOutcomeError(Exception): diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 249151a3f..169fb272d 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -28,8 +28,8 @@ def test_validate_content_missing_attachment(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content.0.attachment: Field required)", - "expression": ["content.0.attachment"], + "diagnostics": "Failed to parse DocumentReference resource (content[0].attachment: Field required)", + "expression": ["content[0].attachment"], } @@ -56,6 +56,34 @@ def test_validate_content_missing_content_type(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content.0.attachment.contentType: Field required)", - "expression": ["content.0.attachment.contentType"], + "diagnostics": "Failed to parse DocumentReference resource (content[0].attachment.contentType: Field required)", + "expression": ["content[0].attachment.contentType"], + } + + +def test_validate_content_missing_format(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0].pop("format") + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content[0].format: Field required)", + "expression": ["content[0].format"], } diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 14d824c40..4bbbac805 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -725,9 +725,9 @@ Feature: Producer - createDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", + "diagnostics": "Request body could not be parsed (content[0].attachment.contentType: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", "expression": [ - "content.0.attachment.contentType" + "content[0].attachment.contentType" ] } """ diff --git a/tests/features/producer/updateDocumentReference-failure.feature b/tests/features/producer/updateDocumentReference-failure.feature index a19cd64d2..fc134f6cb 100644 --- a/tests/features/producer/updateDocumentReference-failure.feature +++ b/tests/features/producer/updateDocumentReference-failure.feature @@ -144,9 +144,9 @@ Feature: Producer - updateDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", + "diagnostics": "Request body could not be parsed (content[0].attachment.contentType: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", "expression": [ - "content.0.attachment.contentType" + "content[0].attachment.contentType" ] } """ diff --git a/tests/features/producer/upsertDocumentReference-failure.feature b/tests/features/producer/upsertDocumentReference-failure.feature index 6ca9c54d8..f1d2412c8 100644 --- a/tests/features/producer/upsertDocumentReference-failure.feature +++ b/tests/features/producer/upsertDocumentReference-failure.feature @@ -306,9 +306,9 @@ Feature: Producer - upsertDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Request body could not be parsed (content.0.attachment.contentType: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", + "diagnostics": "Request body could not be parsed (content[0].attachment.contentType: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", "expression": [ - "content.0.attachment.contentType" + "content[0].attachment.contentType" ] } """ From 75534a3c318b98800a43ca060f40d268d76ed411 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 6 Dec 2024 03:12:50 +0000 Subject: [PATCH 25/40] NRL-518 ContentStability validation mostly on pydantic --- api/consumer/swagger.yaml | 47 +++- api/producer/swagger.yaml | 47 +++- layer/nrlf/consumer/fhir/r4/model.py | 70 ++++-- layer/nrlf/core/errors.py | 2 +- layer/nrlf/core/tests/test_pydantic_errors.py | 214 +++++++++++++++++ layer/nrlf/core/tests/test_validators.py | 220 ------------------ layer/nrlf/core/validators.py | 60 ----- layer/nrlf/producer/fhir/r4/model.py | 70 ++++-- layer/nrlf/producer/fhir/r4/strict_model.py | 68 ++++-- ...-Valid-with-date-and-meta-lastupdated.json | 16 +- .../Y05868-736253002-Valid-with-date.json | 16 +- ...736253002-Valid-with-meta-lastupdated.json | 16 +- ...5868-736253002-Valid-with-ssp-content.json | 16 +- 13 files changed, 499 insertions(+), 363 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 10632c719..e71257dfe 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1070,11 +1070,14 @@ components: extension: type: array items: - $ref: "#/components/schemas/Extension" - description: Additional content defined by implementations. + $ref: "#/components/schemas/ContentStabilityExtension" + description: Additional extension for content stability. + minItems: 1 + maxItems: 1 required: - attachment - format + - extension DocumentReferenceRelatesTo: type: object properties: @@ -1191,6 +1194,46 @@ components: type: string pattern: \S* description: The reference details for the link. + ContentStabilityExtension: + type: object + properties: + url: + type: string + enum: + [ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + ] + valueCodeableConcept: + type: object + properties: + coding: + type: array + items: + type: object + properties: + system: + type: string + enum: + [ + "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + ] + code: + type: string + enum: ["static", "dynamic"] + display: + type: string + enum: ["Static", "Dynamic"] + required: + - system + - code + - display + minItems: 1 + maxItems: 1 + required: + - coding + required: + - url + - valueCodeableConcept Identifier: type: object properties: diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index cb99f13b7..bf1fc5067 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1635,11 +1635,14 @@ components: extension: type: array items: - $ref: "#/components/schemas/Extension" - description: Additional content defined by implementations. + $ref: "#/components/schemas/ContentStabilityExtension" + description: Additional extension for content stability. + minItems: 1 + maxItems: 1 required: - attachment - format + - extension DocumentReferenceRelatesTo: type: object properties: @@ -1757,6 +1760,46 @@ components: type: string pattern: \S* description: The reference details for the link. + ContentStabilityExtension: + type: object + properties: + url: + type: string + enum: + [ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + ] + valueCodeableConcept: + type: object + properties: + coding: + type: array + items: + type: object + properties: + system: + type: string + enum: + [ + "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + ] + code: + type: string + enum: ["static", "dynamic"] + display: + type: string + enum: ["Static", "Dynamic"] + required: + - system + - code + - display + minItems: 1 + maxItems: 1 + required: + - coding + required: + - url + - valueCodeableConcept Identifier: type: object properties: diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index b36fe952e..5a50e1f5c 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-05T16:54:49+00:00 +# timestamp: 2024-12-06T02:57:59+00:00 from __future__ import annotations @@ -230,6 +230,25 @@ class Coding(BaseModel): ] = None +class CodingItem(BaseModel): + system: Literal[ + "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" + ] + code: Literal["static", "dynamic"] + display: Literal["Static", "Dynamic"] + + +class ValueCodeableConcept(BaseModel): + coding: Annotated[List[CodingItem], Field(max_length=1, min_length=1)] + + +class ContentStabilityExtension(BaseModel): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ] + valueCodeableConcept: ValueCodeableConcept + + class Period(BaseModel): id: Annotated[ Optional[str], @@ -427,6 +446,31 @@ class RequestHeaderCorrelationId(RootModel[str]): root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] +class DocumentReferenceContent(BaseModel): + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + attachment: Annotated[ + Attachment, + Field( + description="The document or URL of the document along with critical metadata to prove content has integrity." + ), + ] + format: Annotated[ + Coding, + Field( + description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." + ), + ] + extension: Annotated[ + List[ContentStabilityExtension], Field(max_length=1, min_length=1) + ] + + class RequestHeader(BaseModel): odsCode: RequestHeaderOdsCode @@ -818,29 +862,6 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceContent(BaseModel): - id: Annotated[ - Optional[str], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", - pattern="[A-Za-z0-9\\-\\.]{1,64}", - ), - ] = None - attachment: Annotated[ - Attachment, - Field( - description="The document or URL of the document along with critical metadata to prove content has integrity." - ), - ] - format: Annotated[ - Coding, - Field( - description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." - ), - ] - extension: Optional[List[Extension]] = None - - class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[str], @@ -1032,7 +1053,6 @@ class Signature(BaseModel): Bundle.model_rebuild() BundleEntry.model_rebuild() DocumentReferenceContext.model_rebuild() -DocumentReferenceContent.model_rebuild() DocumentReferenceRelatesTo.model_rebuild() CodeableConcept.model_rebuild() Identifier.model_rebuild() diff --git a/layer/nrlf/core/errors.py b/layer/nrlf/core/errors.py index ccd17628f..9172c32e9 100644 --- a/layer/nrlf/core/errors.py +++ b/layer/nrlf/core/errors.py @@ -21,7 +21,7 @@ def format_error_location(loc: List) -> str: def diag_for_error(error: ErrorDetails) -> str: loc_string = format_error_location(error["loc"]) - return f"{loc_string}: {error['msg']}" if loc_string else f"root: {error['msg']}" + return f"{loc_string or 'root'}: {error['msg']}" def expression_for_error(error: ErrorDetails) -> Optional[str]: diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 169fb272d..6b40f4a9d 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -87,3 +87,217 @@ def test_validate_content_missing_format(): "diagnostics": "Failed to parse DocumentReference resource (content[0].format: Field required)", "expression": ["content[0].format"], } + + +def test_validate_content_multiple_content_stability_extensions(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Add a second duplicate contentStability extension + document_ref_data["content"][0]["extension"].append( + document_ref_data["content"][0]["extension"][0] + ) + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension: List should have at most 1 item after validation, not 2)", + "expression": ["content[0].extension"], + } + + +def test_validate_content_invalid_content_stability_code(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Set an invalid code for contentStability extension + content_extension = document_ref_data["content"][0]["extension"][0] + content_extension["valueCodeableConcept"]["coding"][0]["code"] = "invalid" + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic')", + "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].code"], + } + + +def test_validate_content_invalid_content_stability_display(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Set an invalid display for contentStability extension + content_extension = document_ref_data["content"][0]["extension"][0] + content_extension["valueCodeableConcept"]["coding"][0]["display"] = "invalid" + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic')", + "expression": [ + "content[0].extension[0].valueCodeableConcept.coding[0].display" + ], + } + + +def test_validate_content_invalid_content_stability_system(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Set an invalid system for contentStability extension + content_extension = document_ref_data["content"][0]["extension"][0] + content_extension["valueCodeableConcept"]["coding"][0]["system"] = "invalid" + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "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')", + "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].system"], + } + + +def test_validate_content_invalid_content_stability_url(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Set an invalid URL for contentStability extension + document_ref_data["content"][0]["extension"][0]["url"] = "invalid" + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability')", + "expression": ["content[0].extension[0].url"], + } + + +def test_validate_content_empty_content_stability_coding(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Set an empty coding list for contentStability extension + document_ref_data["content"][0]["extension"][0]["valueCodeableConcept"][ + "coding" + ] = [] + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding: List should have at least 1 item after validation, not 0)", + "expression": ["content[0].extension[0].valueCodeableConcept.coding"], + } + + +def test_validate_content_missing_content_stability_coding(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Remove the coding key from contentStability extension + del document_ref_data["content"][0]["extension"][0]["valueCodeableConcept"][ + "coding" + ] + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding: Field required)", + "expression": ["content[0].extension[0].valueCodeableConcept.coding"], + } diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 34c32a915..851b5011f 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -781,47 +781,6 @@ def test_validate_type_coding_display_mismatch(type_str: str, display: str): } -def test_validate_content_extension_too_many_extensions(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["content"][0]["extension"].append( - { - "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", - "valueCodeableConcept": { - "coding": [ - { - "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", - "code": "static", - "display": "static", - } - ] - }, - } - ) - - result = validator.validate(document_ref_data) - - assert result.is_valid is False - assert result.resource.id == "Y05868-99999-99999-999999" - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "invalid", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Invalid content extension length: 2 Extension must only contain a single value", - "expression": ["content[0].extension"], - } - - def test_validate_author_too_many_authors(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") @@ -956,185 +915,6 @@ def test_validate_author_value_too_long(): } -def test_validate_content_extension_invalid_code(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - content_extension = document_ref_data["content"][0]["extension"][0] - content_extension["valueCodeableConcept"]["coding"][0]["code"] = "invalid" - - result = validator.validate(document_ref_data) - - assert result.is_valid is False - assert result.resource.id == "Y05868-99999-99999-999999" - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "value", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Invalid content extension code: invalid Extension code must be 'static' or 'dynamic'", - "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].code"], - } - - -def test_validate_content_extension_invalid_display(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["content"][0]["extension"][0] = { - "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", - "valueCodeableConcept": { - "coding": [ - { - "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", - "code": "static", - "display": "invalid", - } - ] - }, - } - - result = validator.validate(document_ref_data) - - assert result.is_valid is False - assert result.resource.id == "Y05868-99999-99999-999999" - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "value", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Invalid content extension display: invalid Extension display must be 'Static' or 'Dynamic'", - "expression": [ - "content[0].extension[0].valueCodeableConcept.coding[0].display" - ], - } - - -def test_validate_content_extension_invalid_system(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["content"][0]["extension"][0] = { - "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", - "valueCodeableConcept": { - "coding": [ - { - "system": "invalid", - "code": "static", - "display": "static", - } - ] - }, - } - - result = validator.validate(document_ref_data) - - assert result.is_valid is False - assert result.resource.id == "Y05868-99999-99999-999999" - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "value", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Invalid content extension system: invalid Extension system must be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability'", - "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].system"], - } - - -def test_validate_content_extension_invalid_url(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["content"][0]["extension"][0] = { - "url": "invalid", - "valueCodeableConcept": { - "coding": [ - { - "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", - "code": "static", - "display": "static", - } - ] - }, - } - - result = validator.validate(document_ref_data) - - assert result.is_valid is False - assert result.resource.id == "Y05868-99999-99999-999999" - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "value", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Invalid content extension url: invalid Extension url must be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability'", - "expression": ["content[0].extension[0].url"], - } - - -def test_validate_content_extension_missing_coding(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["content"][0]["extension"][0] = { - "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", - "valueCodeableConcept": {"coding": []}, - } - - result = validator.validate(document_ref_data) - - assert result.is_valid is False - assert result.resource.id == "Y05868-99999-99999-999999" - assert len(result.issues) == 1 - assert result.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "required", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "INVALID_RESOURCE", - "display": "Invalid validation of resource", - } - ] - }, - "diagnostics": "Missing content[0].extension[0].valueCodeableConcept.coding, extension must have at least one coding.", - "expression": ["content[0].extension.valueCodeableConcept.coding"], - } - - def test_validate_identifiers_invalid_systems(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 6d92be8aa..552fc1465 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -490,67 +490,7 @@ def _validate_content_extension(self, model: DocumentReference): logger.debug("Validating extension") for i, content in enumerate(model.content): - if len(content.extension) > 1: - self.result.add_error( - issue_code="invalid", - error_code="INVALID_RESOURCE", - diagnostics=f"Invalid content extension length: {len(content.extension)} Extension must only contain a single value", - field=f"content[{i}].extension", - ) - return - - if len(content.extension[0].valueCodeableConcept.coding) < 1: - self.result.add_error( - issue_code="required", - error_code="INVALID_RESOURCE", - diagnostics=f"Missing content[{i}].extension[0].valueCodeableConcept.coding, extension must have at least one coding.", - field=f"content[{i}].extension.valueCodeableConcept.coding", - ) - return - - if ( - content.extension[0].url - != "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - ): - self.result.add_error( - issue_code="value", - error_code="INVALID_RESOURCE", - diagnostics=f"Invalid content extension url: {content.extension[0].url} Extension url must be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability'", - field=f"content[{i}].extension[0].url", - ) - return - coding = content.extension[0].valueCodeableConcept.coding[0] - if ( - coding.system - != "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" - ): - self.result.add_error( - issue_code="value", - error_code="INVALID_RESOURCE", - diagnostics=f"Invalid content extension system: {coding.system} Extension system must be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability'", - field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].system", - ) - return - - if coding.code not in ["static", "dynamic"]: - self.result.add_error( - issue_code="value", - error_code="INVALID_RESOURCE", - diagnostics=f"Invalid content extension code: {coding.code} Extension code must be 'static' or 'dynamic'", - field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].code", - ) - return - - if coding.display not in ["Static", "Dynamic"]: - self.result.add_error( - issue_code="value", - error_code="INVALID_RESOURCE", - diagnostics=f"Invalid content extension display: {coding.display} Extension display must be 'Static' or 'Dynamic'", - field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].display", - ) - return - if coding.code != coding.display.lower(): self.result.add_error( issue_code="value", diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 17f8653c4..98041bca8 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-05T16:54:46+00:00 +# timestamp: 2024-12-06T02:57:56+00:00 from __future__ import annotations @@ -230,6 +230,25 @@ class Coding(BaseModel): ] = None +class CodingItem(BaseModel): + system: Literal[ + "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" + ] + code: Literal["static", "dynamic"] + display: Literal["Static", "Dynamic"] + + +class ValueCodeableConcept(BaseModel): + coding: Annotated[List[CodingItem], Field(max_length=1, min_length=1)] + + +class ContentStabilityExtension(BaseModel): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ] + valueCodeableConcept: ValueCodeableConcept + + class Period(BaseModel): id: Annotated[ Optional[str], @@ -417,6 +436,31 @@ class RequestHeaderCorrelationId(RootModel[str]): root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] +class DocumentReferenceContent(BaseModel): + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + attachment: Annotated[ + Attachment, + Field( + description="The document or URL of the document along with critical metadata to prove content has integrity." + ), + ] + format: Annotated[ + Coding, + Field( + description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." + ), + ] + extension: Annotated[ + List[ContentStabilityExtension], Field(max_length=1, min_length=1) + ] + + class RequestHeader(BaseModel): odsCode: RequestHeaderOdsCode @@ -802,29 +846,6 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceContent(BaseModel): - id: Annotated[ - Optional[str], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", - pattern="[A-Za-z0-9\\-\\.]{1,64}", - ), - ] = None - attachment: Annotated[ - Attachment, - Field( - description="The document or URL of the document along with critical metadata to prove content has integrity." - ), - ] - format: Annotated[ - Coding, - Field( - description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." - ), - ] - extension: Optional[List[Extension]] = None - - class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[str], @@ -1016,7 +1037,6 @@ class Signature(BaseModel): Bundle.model_rebuild() BundleEntry.model_rebuild() DocumentReferenceContext.model_rebuild() -DocumentReferenceContent.model_rebuild() DocumentReferenceRelatesTo.model_rebuild() CodeableConcept.model_rebuild() Identifier.model_rebuild() diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index db9a93d5d..3ae1d3511 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-05T16:54:48+00:00 +# timestamp: 2024-12-06T02:57:58+00:00 from __future__ import annotations @@ -207,6 +207,25 @@ class Coding(BaseModel): ] = None +class CodingItem(BaseModel): + system: Literal[ + "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" + ] + code: Literal["static", "dynamic"] + display: Literal["Static", "Dynamic"] + + +class ValueCodeableConcept(BaseModel): + coding: Annotated[List[CodingItem], Field(max_length=1, min_length=1)] + + +class ContentStabilityExtension(BaseModel): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ] + valueCodeableConcept: ValueCodeableConcept + + class Period(BaseModel): id: Annotated[ Optional[StrictStr], @@ -365,6 +384,30 @@ class RequestHeaderCorrelationId(RootModel[StrictStr]): root: Annotated[StrictStr, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] +class DocumentReferenceContent(BaseModel): + id: Annotated[ + Optional[StrictStr], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + ), + ] = None + attachment: Annotated[ + Attachment, + Field( + description="The document or URL of the document along with critical metadata to prove content has integrity." + ), + ] + format: Annotated[ + Coding, + Field( + description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." + ), + ] + extension: Annotated[ + List[ContentStabilityExtension], Field(max_length=1, min_length=1) + ] + + class RequestHeader(BaseModel): odsCode: RequestHeaderOdsCode @@ -706,28 +749,6 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceContent(BaseModel): - id: Annotated[ - Optional[StrictStr], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - ), - ] = None - attachment: Annotated[ - Attachment, - Field( - description="The document or URL of the document along with critical metadata to prove content has integrity." - ), - ] - format: Annotated[ - Coding, - Field( - description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." - ), - ] - extension: Optional[List[Extension]] = None - - class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[StrictStr], @@ -895,7 +916,6 @@ class Signature(BaseModel): Bundle.model_rebuild() BundleEntry.model_rebuild() DocumentReferenceContext.model_rebuild() -DocumentReferenceContent.model_rebuild() DocumentReferenceRelatesTo.model_rebuild() CodeableConcept.model_rebuild() Identifier.model_rebuild() diff --git a/tests/data/DocumentReference/Y05868-736253002-Valid-with-date-and-meta-lastupdated.json b/tests/data/DocumentReference/Y05868-736253002-Valid-with-date-and-meta-lastupdated.json index 2bf5f63df..8bfae4aaf 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid-with-date-and-meta-lastupdated.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid-with-date-and-meta-lastupdated.json @@ -74,7 +74,21 @@ "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ], "context": { diff --git a/tests/data/DocumentReference/Y05868-736253002-Valid-with-date.json b/tests/data/DocumentReference/Y05868-736253002-Valid-with-date.json index 0aee9f067..72389bf26 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid-with-date.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid-with-date.json @@ -71,7 +71,21 @@ "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ], "context": { diff --git a/tests/data/DocumentReference/Y05868-736253002-Valid-with-meta-lastupdated.json b/tests/data/DocumentReference/Y05868-736253002-Valid-with-meta-lastupdated.json index ca7dfbcd5..5b9e6b994 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid-with-meta-lastupdated.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid-with-meta-lastupdated.json @@ -73,7 +73,21 @@ "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ], "context": { diff --git a/tests/data/DocumentReference/Y05868-736253002-Valid-with-ssp-content.json b/tests/data/DocumentReference/Y05868-736253002-Valid-with-ssp-content.json index c88029c05..dbb356058 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid-with-ssp-content.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid-with-ssp-content.json @@ -70,7 +70,21 @@ "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ], "context": { From 3452fa3e57082ed7992bc1c3b7fdde1473275f92 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 6 Dec 2024 03:27:23 +0000 Subject: [PATCH 26/40] NRL-518 Better naming for scenario --- tests/features/producer/createDocumentReference-failure.feature | 2 +- tests/features/producer/updateDocumentReference-failure.feature | 2 +- tests/features/producer/upsertDocumentReference-failure.feature | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 4bbbac805..fea9ac2a7 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -688,7 +688,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios } """ - Scenario: Missing contentType + Scenario: contentType empty string Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'TSTCUS' is authorised to access pointer types: | system | value | diff --git a/tests/features/producer/updateDocumentReference-failure.feature b/tests/features/producer/updateDocumentReference-failure.feature index fc134f6cb..0c911d6c8 100644 --- a/tests/features/producer/updateDocumentReference-failure.feature +++ b/tests/features/producer/updateDocumentReference-failure.feature @@ -94,7 +94,7 @@ Feature: Producer - updateDocumentReference - Failure Scenarios } """ - Scenario: Missing contentType + Scenario: contentType empty string Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'TSTCUS' is authorised to access pointer types: | system | value | diff --git a/tests/features/producer/upsertDocumentReference-failure.feature b/tests/features/producer/upsertDocumentReference-failure.feature index f1d2412c8..5b658d3eb 100644 --- a/tests/features/producer/upsertDocumentReference-failure.feature +++ b/tests/features/producer/upsertDocumentReference-failure.feature @@ -269,7 +269,7 @@ Feature: Producer - upsertDocumentReference - Failure Scenarios } """ - Scenario: Missing contentType + Scenario: contentType empty string Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'TSTCUS' is authorised to access pointer types: | system | value | From 9bc5d31bccfe74e312d7997c7fdfb61fe909d992 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 6 Dec 2024 03:55:59 +0000 Subject: [PATCH 27/40] NRL-518 Better format for single value enum in swagger --- api/consumer/swagger.yaml | 8 ++------ api/producer/swagger.yaml | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index e71257dfe..183fa77cf 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1200,9 +1200,7 @@ components: url: type: string enum: - [ - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", - ] + - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" valueCodeableConcept: type: object properties: @@ -1214,9 +1212,7 @@ components: system: type: string enum: - [ - "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", - ] + - "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" code: type: string enum: ["static", "dynamic"] diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index bf1fc5067..c185e1087 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1766,9 +1766,7 @@ components: url: type: string enum: - [ - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", - ] + - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" valueCodeableConcept: type: object properties: @@ -1780,9 +1778,7 @@ components: system: type: string enum: - [ - "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", - ] + - "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" code: type: string enum: ["static", "dynamic"] From 52f1d24836f04274c532d8db6155f1f256c4e38d Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 6 Dec 2024 03:56:26 +0000 Subject: [PATCH 28/40] NRL-518 Pydantic will ensure extension is always there --- layer/nrlf/core/validators.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 552fc1465..8083e323a 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -142,8 +142,7 @@ def validate(self, data: Dict[str, Any] | DocumentReference): self._validate_author(resource) self._validate_type_category_mapping(resource) self._validate_content(resource) - if resource.content[0].extension: - self._validate_content_extension(resource) + self._validate_content_extension(resource) except StopValidationError: logger.log(LogReference.VALIDATOR003) From f8663e782e7154f0eb98724531a1ddc63389386f Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 6 Dec 2024 04:13:58 +0000 Subject: [PATCH 29/40] NRL-518 Pydantic format validation, fix casing format display --- api/consumer/swagger.yaml | 26 +++++++++++++++- api/producer/swagger.yaml | 30 +++++++++++++++++-- layer/nrlf/consumer/fhir/r4/model.py | 19 ++++++++++-- layer/nrlf/producer/fhir/r4/model.py | 19 ++++++++++-- layer/nrlf/producer/fhir/r4/strict_model.py | 19 ++++++++++-- swagger/producer-static/components.yaml | 2 +- swagger/producer-static/narrative.yaml | 2 +- .../RQI-736253002-Valid.json | 2 +- ...-Valid-with-date-and-meta-lastupdated.json | 2 +- .../Y05868-736253002-Valid-with-date.json | 2 +- ...736253002-Valid-with-meta-lastupdated.json | 2 +- ...5868-736253002-Valid-with-ssp-content.json | 2 +- .../Y05868-736253002-Valid.json | 2 +- .../readDocumentReference-success.feature | 4 +-- .../createDocumentReference-failure.feature | 2 +- .../readDocumentReference-success.feature | 2 +- .../updateDocumentReference-failure.feature | 4 +-- .../upsertDocumentReference-failure.feature | 2 +- tests/features/utils/constants.py | 2 +- tests/features/utils/data.py | 2 +- tests/smoke/setup.py | 2 +- 21 files changed, 121 insertions(+), 28 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 183fa77cf..dbe549ea6 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1065,7 +1065,7 @@ components: $ref: "#/components/schemas/Attachment" description: The document or URL of the document along with critical metadata to prove content has integrity. format: - $ref: "#/components/schemas/Coding" + $ref: "#/components/schemas/NRLFormatCode" description: An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType. extension: type: array @@ -1230,6 +1230,30 @@ components: required: - url - valueCodeableConcept + NRLFormatCode: + type: object + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" + description: The system URL for the NRLF Format Code. + code: + type: string + enum: + - "urn:nhs-ic:record-contact" + - "urn:nhs-ic:unstructured" + description: The code representing the format of the document. + display: + type: string + enum: + - "Contact details (HTTP Unsecured)" + - "Unstructured Document" + description: The display text for the code. + required: + - system + - code + - display Identifier: type: object properties: diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index c185e1087..a00d5239e 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -334,7 +334,7 @@ paths: { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured" - "display": "Unstructured document" + "display": "Unstructured Document" } ] ``` @@ -1215,7 +1215,7 @@ components: format: system: https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode code: "urn:nhs-ic:unstructured" - display: Unstructured document + display: Unstructured Document context: event: - coding: @@ -1630,7 +1630,7 @@ components: $ref: "#/components/schemas/Attachment" description: The document or URL of the document along with critical metadata to prove content has integrity. format: - $ref: "#/components/schemas/Coding" + $ref: "#/components/schemas/NRLFormatCode" description: An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType. extension: type: array @@ -1796,6 +1796,30 @@ components: required: - url - valueCodeableConcept + NRLFormatCode: + type: object + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" + description: The system URL for the NRLF Format Code. + code: + type: string + enum: + - "urn:nhs-ic:record-contact" + - "urn:nhs-ic:unstructured" + description: The code representing the format of the document. + display: + type: string + enum: + - "Contact details (HTTP Unsecured)" + - "Unstructured Document" + description: The display text for the code. + required: + - system + - code + - display Identifier: type: object properties: diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 5a50e1f5c..161fabb1f 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-06T02:57:59+00:00 +# timestamp: 2024-12-06T04:10:37+00:00 from __future__ import annotations @@ -249,6 +249,21 @@ class ContentStabilityExtension(BaseModel): valueCodeableConcept: ValueCodeableConcept +class NRLFormatCode(BaseModel): + system: Annotated[ + Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], + Field(description="The system URL for the NRLF Format Code."), + ] + code: Annotated[ + Literal["urn:nhs-ic:record-contact", "urn:nhs-ic:unstructured"], + Field(description="The code representing the format of the document."), + ] + display: Annotated[ + Literal["Contact details (HTTP Unsecured)", "Unstructured Document"], + Field(description="The display text for the code."), + ] + + class Period(BaseModel): id: Annotated[ Optional[str], @@ -461,7 +476,7 @@ class DocumentReferenceContent(BaseModel): ), ] format: Annotated[ - Coding, + NRLFormatCode, Field( description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." ), diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 98041bca8..580836343 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-06T02:57:56+00:00 +# timestamp: 2024-12-06T04:10:35+00:00 from __future__ import annotations @@ -249,6 +249,21 @@ class ContentStabilityExtension(BaseModel): valueCodeableConcept: ValueCodeableConcept +class NRLFormatCode(BaseModel): + system: Annotated[ + Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], + Field(description="The system URL for the NRLF Format Code."), + ] + code: Annotated[ + Literal["urn:nhs-ic:record-contact", "urn:nhs-ic:unstructured"], + Field(description="The code representing the format of the document."), + ] + display: Annotated[ + Literal["Contact details (HTTP Unsecured)", "Unstructured Document"], + Field(description="The display text for the code."), + ] + + class Period(BaseModel): id: Annotated[ Optional[str], @@ -451,7 +466,7 @@ class DocumentReferenceContent(BaseModel): ), ] format: Annotated[ - Coding, + NRLFormatCode, Field( description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." ), diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index 3ae1d3511..6d1e371b7 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-06T02:57:58+00:00 +# timestamp: 2024-12-06T04:10:36+00:00 from __future__ import annotations @@ -226,6 +226,21 @@ class ContentStabilityExtension(BaseModel): valueCodeableConcept: ValueCodeableConcept +class NRLFormatCode(BaseModel): + system: Annotated[ + Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], + Field(description="The system URL for the NRLF Format Code."), + ] + code: Annotated[ + Literal["urn:nhs-ic:record-contact", "urn:nhs-ic:unstructured"], + Field(description="The code representing the format of the document."), + ] + display: Annotated[ + Literal["Contact details (HTTP Unsecured)", "Unstructured Document"], + Field(description="The display text for the code."), + ] + + class Period(BaseModel): id: Annotated[ Optional[StrictStr], @@ -398,7 +413,7 @@ class DocumentReferenceContent(BaseModel): ), ] format: Annotated[ - Coding, + NRLFormatCode, Field( description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." ), diff --git a/swagger/producer-static/components.yaml b/swagger/producer-static/components.yaml index 22a096326..8c984b796 100644 --- a/swagger/producer-static/components.yaml +++ b/swagger/producer-static/components.yaml @@ -122,7 +122,7 @@ components: format: system: https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode code: "urn:nhs-ic:unstructured" - display: Unstructured document + display: Unstructured Document context: practiceSetting: coding: diff --git a/swagger/producer-static/narrative.yaml b/swagger/producer-static/narrative.yaml index 0a3af911f..fd567ac08 100644 --- a/swagger/producer-static/narrative.yaml +++ b/swagger/producer-static/narrative.yaml @@ -287,7 +287,7 @@ paths: { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured" - "display": "Unstructured document" + "display": "Unstructured Document" } ] ``` diff --git a/tests/data/DocumentReference/RQI-736253002-Valid.json b/tests/data/DocumentReference/RQI-736253002-Valid.json index 429d088a4..873434086 100644 --- a/tests/data/DocumentReference/RQI-736253002-Valid.json +++ b/tests/data/DocumentReference/RQI-736253002-Valid.json @@ -69,7 +69,7 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" }, "extension": [ { diff --git a/tests/data/DocumentReference/Y05868-736253002-Valid-with-date-and-meta-lastupdated.json b/tests/data/DocumentReference/Y05868-736253002-Valid-with-date-and-meta-lastupdated.json index 8bfae4aaf..de51a0df6 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid-with-date-and-meta-lastupdated.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid-with-date-and-meta-lastupdated.json @@ -73,7 +73,7 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" }, "extension": [ { diff --git a/tests/data/DocumentReference/Y05868-736253002-Valid-with-date.json b/tests/data/DocumentReference/Y05868-736253002-Valid-with-date.json index 72389bf26..49e5484ca 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid-with-date.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid-with-date.json @@ -70,7 +70,7 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" }, "extension": [ { diff --git a/tests/data/DocumentReference/Y05868-736253002-Valid-with-meta-lastupdated.json b/tests/data/DocumentReference/Y05868-736253002-Valid-with-meta-lastupdated.json index 5b9e6b994..9e6a0e73b 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid-with-meta-lastupdated.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid-with-meta-lastupdated.json @@ -72,7 +72,7 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" }, "extension": [ { diff --git a/tests/data/DocumentReference/Y05868-736253002-Valid-with-ssp-content.json b/tests/data/DocumentReference/Y05868-736253002-Valid-with-ssp-content.json index dbb356058..a20f81d33 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid-with-ssp-content.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid-with-ssp-content.json @@ -69,7 +69,7 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" }, "extension": [ { diff --git a/tests/data/DocumentReference/Y05868-736253002-Valid.json b/tests/data/DocumentReference/Y05868-736253002-Valid.json index d83b5a3f6..6b79042e8 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid.json @@ -69,7 +69,7 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" }, "extension": [ { diff --git a/tests/features/consumer/readDocumentReference-success.feature b/tests/features/consumer/readDocumentReference-success.feature index 5c0d83979..356594b57 100644 --- a/tests/features/consumer/readDocumentReference-success.feature +++ b/tests/features/consumer/readDocumentReference-success.feature @@ -73,7 +73,7 @@ Feature: Consumer - readDocumentReference - Success Scenarios "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" } } ], @@ -164,7 +164,7 @@ Feature: Consumer - readDocumentReference - Success Scenarios "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" } } ], diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index fea9ac2a7..663006526 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -704,7 +704,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" } } ] diff --git a/tests/features/producer/readDocumentReference-success.feature b/tests/features/producer/readDocumentReference-success.feature index 04c4228ec..147c2bd81 100644 --- a/tests/features/producer/readDocumentReference-success.feature +++ b/tests/features/producer/readDocumentReference-success.feature @@ -75,7 +75,7 @@ Feature: Producer - readDocumentReference - Success Scenarios "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" } } ], diff --git a/tests/features/producer/updateDocumentReference-failure.feature b/tests/features/producer/updateDocumentReference-failure.feature index 0c911d6c8..a19633829 100644 --- a/tests/features/producer/updateDocumentReference-failure.feature +++ b/tests/features/producer/updateDocumentReference-failure.feature @@ -122,7 +122,7 @@ Feature: Producer - updateDocumentReference - Failure Scenarios "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" } } ] @@ -179,7 +179,7 @@ Feature: Producer - updateDocumentReference - Failure Scenarios "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" } } ] diff --git a/tests/features/producer/upsertDocumentReference-failure.feature b/tests/features/producer/upsertDocumentReference-failure.feature index 5b658d3eb..83f3d7de7 100644 --- a/tests/features/producer/upsertDocumentReference-failure.feature +++ b/tests/features/producer/upsertDocumentReference-failure.feature @@ -285,7 +285,7 @@ Feature: Producer - upsertDocumentReference - Failure Scenarios "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" } } ] diff --git a/tests/features/utils/constants.py b/tests/features/utils/constants.py index 48228a74d..f5e479df3 100644 --- a/tests/features/utils/constants.py +++ b/tests/features/utils/constants.py @@ -112,7 +112,7 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" + "display": "Unstructured Document" } } ] diff --git a/tests/features/utils/data.py b/tests/features/utils/data.py index 6cf930114..e3e5ec745 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -42,7 +42,7 @@ def create_test_document_reference(items: dict) -> DocumentReference: "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", ), code=items.get("formatCode", "urn:nhs-ic:unstructured"), - display=items.get("formatDisplay", "Unstructured document"), + display=items.get("formatDisplay", "Unstructured Document"), ), ) ], diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index 49ddeb766..7cdd73b5a 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -36,7 +36,7 @@ def build_document_reference( format=Coding( system="https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", code="urn:nhs-ic:unstructured", - display="Unstructured document", + display="Unstructured Document", ), ) ], From 5098eca959c19ab3c76a611a2a1afd5fc2c00aa8 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 6 Dec 2024 05:27:23 +0000 Subject: [PATCH 30/40] NRL-518 Fix integration tests --- .../readDocumentReference-success.feature | 32 +++++++++++++++++-- .../createDocumentReference-failure.feature | 16 +++++++++- .../readDocumentReference-success.feature | 16 +++++++++- .../updateDocumentReference-failure.feature | 32 +++++++++++++++++-- .../upsertDocumentReference-failure.feature | 16 +++++++++- tests/features/utils/constants.py | 16 +++++++++- tests/features/utils/data.py | 20 +++++++++++- 7 files changed, 139 insertions(+), 9 deletions(-) diff --git a/tests/features/consumer/readDocumentReference-success.feature b/tests/features/consumer/readDocumentReference-success.feature index 356594b57..974afa244 100644 --- a/tests/features/consumer/readDocumentReference-success.feature +++ b/tests/features/consumer/readDocumentReference-success.feature @@ -74,7 +74,21 @@ Feature: Consumer - readDocumentReference - Success Scenarios "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured Document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ], "context": { @@ -165,7 +179,21 @@ Feature: Consumer - readDocumentReference - Success Scenarios "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured Document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ], "context": { diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 663006526..33f472a76 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -705,7 +705,21 @@ Feature: Producer - createDocumentReference - Failure Scenarios "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured Document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ] """ diff --git a/tests/features/producer/readDocumentReference-success.feature b/tests/features/producer/readDocumentReference-success.feature index 147c2bd81..4cc883e20 100644 --- a/tests/features/producer/readDocumentReference-success.feature +++ b/tests/features/producer/readDocumentReference-success.feature @@ -76,7 +76,21 @@ Feature: Producer - readDocumentReference - Success Scenarios "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured Document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ], "context": { diff --git a/tests/features/producer/updateDocumentReference-failure.feature b/tests/features/producer/updateDocumentReference-failure.feature index a19633829..99754f509 100644 --- a/tests/features/producer/updateDocumentReference-failure.feature +++ b/tests/features/producer/updateDocumentReference-failure.feature @@ -123,7 +123,21 @@ Feature: Producer - updateDocumentReference - Failure Scenarios "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured Document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ] } @@ -180,7 +194,21 @@ Feature: Producer - updateDocumentReference - Failure Scenarios "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured Document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ] } diff --git a/tests/features/producer/upsertDocumentReference-failure.feature b/tests/features/producer/upsertDocumentReference-failure.feature index 83f3d7de7..a8de6ee35 100644 --- a/tests/features/producer/upsertDocumentReference-failure.feature +++ b/tests/features/producer/upsertDocumentReference-failure.feature @@ -286,7 +286,21 @@ Feature: Producer - upsertDocumentReference - Failure Scenarios "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured Document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ] """ diff --git a/tests/features/utils/constants.py b/tests/features/utils/constants.py index f5e479df3..b51d6d886 100644 --- a/tests/features/utils/constants.py +++ b/tests/features/utils/constants.py @@ -113,7 +113,21 @@ "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", "display": "Unstructured Document" - } + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ] """ diff --git a/tests/features/utils/data.py b/tests/features/utils/data.py index e3e5ec745..723c358a6 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -3,12 +3,16 @@ Attachment, CodeableConcept, Coding, + CodingItem, + ContentStabilityExtension, DocumentReference, DocumentReferenceContent, DocumentReferenceContext, DocumentReferenceRelatesTo, Identifier, + NRLFormatCode, Reference, + ValueCodeableConcept, ) from tests.features.utils.constants import ( DEFAULT_TEST_AUTHOR, @@ -36,7 +40,7 @@ def create_test_document_reference(items: dict) -> DocumentReference: contentType=items.get("contentType", "application/pdf"), url=items["url"], ), - format=Coding( + format=NRLFormatCode( system=items.get( "formatSystem", "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", @@ -44,6 +48,20 @@ def create_test_document_reference(items: dict) -> DocumentReference: code=items.get("formatCode", "urn:nhs-ic:unstructured"), display=items.get("formatDisplay", "Unstructured Document"), ), + extension=[ + ContentStabilityExtension( + url="https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + valueCodeableConcept=ValueCodeableConcept( + coding=[ + CodingItem( + system="https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + code="static", + display="Static", + ) + ] + ), + ) + ], ) ], ), From aca3ca4dfcb85ec0e2b588f7ce9e0eb76ff87937 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 6 Dec 2024 06:24:52 +0000 Subject: [PATCH 31/40] NRL-518 Add value set for pydantic diagnostic messages --- layer/nrlf/core/errors.py | 18 +++++++++++++++++- layer/nrlf/core/tests/test_pydantic_errors.py | 12 ++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/layer/nrlf/core/errors.py b/layer/nrlf/core/errors.py index 9172c32e9..44567c974 100644 --- a/layer/nrlf/core/errors.py +++ b/layer/nrlf/core/errors.py @@ -19,9 +19,25 @@ def format_error_location(loc: List) -> str: return formatted_loc +def append_value_set_url(loc_string: str) -> str: + if loc_string.endswith(("url", "system")): + return "" + + if "content" in loc_string: + if "extension" in loc_string: + return ". See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" + if "format" in loc_string: + return ". See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" + + return "" + + def diag_for_error(error: ErrorDetails) -> str: loc_string = format_error_location(error["loc"]) - return f"{loc_string or 'root'}: {error['msg']}" + print(f"Error location: {loc_string}") + msg = f"{loc_string or 'root'}: {error['msg']}" + msg += append_value_set_url(loc_string) + return msg def expression_for_error(error: ErrorDetails) -> Optional[str]: diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 6b40f4a9d..7ff69e6be 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -84,7 +84,7 @@ def test_validate_content_missing_format(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].format: Field required)", + "diagnostics": "Failed to parse DocumentReference resource (content[0].format: Field required. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode)", "expression": ["content[0].format"], } @@ -115,7 +115,7 @@ def test_validate_content_multiple_content_stability_extensions(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension: List should have at most 1 item after validation, not 2)", + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension: List should have at most 1 item after validation, not 2. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", "expression": ["content[0].extension"], } @@ -145,7 +145,7 @@ def test_validate_content_invalid_content_stability_code(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic')", + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic'. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].code"], } @@ -175,7 +175,7 @@ def test_validate_content_invalid_content_stability_display(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic')", + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic'. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", "expression": [ "content[0].extension[0].valueCodeableConcept.coding[0].display" ], @@ -267,7 +267,7 @@ def test_validate_content_empty_content_stability_coding(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding: List should have at least 1 item after validation, not 0)", + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding: List should have at least 1 item after validation, not 0. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", "expression": ["content[0].extension[0].valueCodeableConcept.coding"], } @@ -298,6 +298,6 @@ def test_validate_content_missing_content_stability_coding(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding: Field required)", + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding: Field required. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", "expression": ["content[0].extension[0].valueCodeableConcept.coding"], } From 7886799147b05304f65b3aa81a9c12412e3314e6 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 6 Dec 2024 06:30:58 +0000 Subject: [PATCH 32/40] NRL-518 Fix smoke tests --- tests/smoke/setup.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index 7cdd73b5a..a8f8bdb7d 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -33,11 +33,25 @@ def build_document_reference( contentType=content_type, url=content_url, ), - format=Coding( + format=NRLFormatCode( system="https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", code="urn:nhs-ic:unstructured", display="Unstructured Document", ), + extension=[ + ContentStabilityExtension( + url="https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + valueCodeableConcept=ValueCodeableConcept( + coding=[ + CodingItem( + system="https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + code="static", + display="Static", + ) + ] + ), + ) + ], ) ], type=CodeableConcept( From 3137b908ab01e0ba2f028cb1f258837812991cfe Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 6 Dec 2024 06:46:14 +0000 Subject: [PATCH 33/40] NRL-518 Fix linting issue --- layer/nrlf/core/errors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/layer/nrlf/core/errors.py b/layer/nrlf/core/errors.py index 44567c974..e4cc82e21 100644 --- a/layer/nrlf/core/errors.py +++ b/layer/nrlf/core/errors.py @@ -34,7 +34,6 @@ def append_value_set_url(loc_string: str) -> str: def diag_for_error(error: ErrorDetails) -> str: loc_string = format_error_location(error["loc"]) - print(f"Error location: {loc_string}") msg = f"{loc_string or 'root'}: {error['msg']}" msg += append_value_set_url(loc_string) return msg From c113df836bcee55c9ffee85cfab67eb392d05e01 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 11 Dec 2024 13:01:34 +0000 Subject: [PATCH 34/40] NRL-518 Add validation for format code display mismatch --- layer/nrlf/core/tests/test_validators.py | 70 ++++++++++++++++++++++++ layer/nrlf/core/validators.py | 17 ++++++ 2 files changed, 87 insertions(+) diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 851b5011f..0af045a13 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1360,3 +1360,73 @@ def test_validate_content_invalid_content_type(): "diagnostics": "Invalid contentType: invalid/type. Must be 'application/pdf' or 'text/html'", "expression": ["content[0].attachment.contentType"], } + + +@pytest.mark.parametrize( + "format_code, format_display", + [ + ("urn:nhs-ic:record-contact", "Contact details (HTTP Unsecured)"), + ("urn:nhs-ic:unstructured", "Unstructured Document"), + ], +) +def test_validate_nrl_format_code_valid_match(format_code, format_display): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["format"] = { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": format_code, + "display": format_display, + } + + result = validator.validate(document_ref_data) + + assert result.is_valid is True + + +@pytest.mark.parametrize( + "format_code, format_display, expected_display", + [ + ( + "urn:nhs-ic:unstructured", + "Contact details (HTTP Unsecured)", + "Unstructured Document", + ), + ( + "urn:nhs-ic:record-contact", + "Unstructured Document", + "Contact details (HTTP Unsecured)", + ), + ], +) +def test_validate_nrl_format_code_display_mismatch( + format_code, format_display, expected_display +): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["format"] = { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": format_code, + "display": format_display, + } + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource", + } + ] + }, + "diagnostics": f"Invalid display for format code '{format_code}'. Expected '{expected_display}'", + "expression": ["content[0].format.display"], + } diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 8083e323a..98770748c 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -550,6 +550,11 @@ def _validate_content(self, model: DocumentReference): """ logger.log(LogReference.VALIDATOR001, step="content") + format_code_display_map = { + "urn:nhs-ic:record-contact": "Contact details (HTTP Unsecured)", + "urn:nhs-ic:unstructured": "Unstructured Document", + } + for i, content in enumerate(model.content): if content.attachment.contentType not in ["application/pdf", "text/html"]: self.result.add_error( @@ -558,3 +563,15 @@ def _validate_content(self, model: DocumentReference): diagnostics=f"Invalid contentType: {content.attachment.contentType}. Must be 'application/pdf' or 'text/html'", field=f"content[{i}].attachment.contentType", ) + + # Validate NRLFormatCode + format_code = content.format.code + format_display = content.format.display + expected_display = format_code_display_map.get(format_code) + if expected_display and format_display != expected_display: + self.result.add_error( + issue_code="value", + error_code="INVALID_RESOURCE", + diagnostics=f"Invalid display for format code '{format_code}'. Expected '{expected_display}'", + field=f"content[{i}].format.display", + ) From e505b70218f44b25e3e3b146145863870aa248db Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 11 Dec 2024 13:26:09 +0000 Subject: [PATCH 35/40] NRL-518 Add feature tests for format code display mismatch --- .../createDocumentReference-failure.feature | 58 +++++++++++++++ .../updateDocumentReference-failure.feature | 71 +++++++++++++++++++ .../upsertDocumentReference-failure.feature | 58 +++++++++++++++ 3 files changed, 187 insertions(+) diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 33f472a76..9bf18f6d6 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -783,3 +783,61 @@ Feature: Producer - createDocumentReference - Failure Scenarios ] } """ + + Scenario: Mismatched format code and display + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:record-contact", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource" + } + ] + }, + "diagnostics": "Invalid display for format code 'urn:nhs-ic:record-contact'. Expected 'Contact details (HTTP Unsecured)'", + "expression": [ + "content[0].format.display" + ] + } + """ diff --git a/tests/features/producer/updateDocumentReference-failure.feature b/tests/features/producer/updateDocumentReference-failure.feature index 99754f509..20a8bed4f 100644 --- a/tests/features/producer/updateDocumentReference-failure.feature +++ b/tests/features/producer/updateDocumentReference-failure.feature @@ -235,3 +235,74 @@ Feature: Producer - updateDocumentReference - Failure Scenarios ] } """ + + Scenario: Mismatched format code and display + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + And a DocumentReference resource exists with values: + | property | value | + | id | TSTCUS-1114567893-updateDocTest | + | subject | 9999999999 | + | status | current | + | type | 736253002 | + | category | 734163000 | + | contentType | application/pdf | + | url | https://example.org/my-doc.pdf | + | custodian | TSTCUS | + | author | TSTCUS | + When producer 'TSTCUS' requests update of a DocumentReference with pointerId 'TSTCUS-1114567893-updateDocTest' and only changing: + """ + { + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:record-contact", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + } + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource" + } + ] + }, + "diagnostics": "Invalid display for format code 'urn:nhs-ic:record-contact'. Expected 'Contact details (HTTP Unsecured)'", + "expression": [ + "content[0].format.display" + ] + } + """ diff --git a/tests/features/producer/upsertDocumentReference-failure.feature b/tests/features/producer/upsertDocumentReference-failure.feature index a8de6ee35..c4fc4ea68 100644 --- a/tests/features/producer/upsertDocumentReference-failure.feature +++ b/tests/features/producer/upsertDocumentReference-failure.feature @@ -365,3 +365,61 @@ Feature: Producer - upsertDocumentReference - Failure Scenarios ] } """ + + Scenario: Mismatched format code and display + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests upsert of a DocumentReference with pointerId 'TSTCUS-testid-upsert-0001-0001' and default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:record-contact", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "value", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + "code": "INVALID_RESOURCE", + "display": "Invalid validation of resource" + } + ] + }, + "diagnostics": "Invalid display for format code 'urn:nhs-ic:record-contact'. Expected 'Contact details (HTTP Unsecured)'", + "expression": [ + "content[0].format.display" + ] + } + """ From 85efa9e06e6a41a67be7adb95c4cf75cd09b3a8d Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 11 Dec 2024 17:05:36 +0000 Subject: [PATCH 36/40] NRL-518 Better model inheritance --- api/consumer/swagger.yaml | 112 ++++++++++--------- api/producer/swagger.yaml | 113 +++++++++++--------- layer/nrlf/consumer/fhir/r4/model.py | 82 +++++++------- layer/nrlf/producer/fhir/r4/model.py | 82 +++++++------- layer/nrlf/producer/fhir/r4/strict_model.py | 80 +++++++------- tests/features/utils/data.py | 8 +- tests/smoke/setup.py | 7 +- 7 files changed, 261 insertions(+), 223 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index dbe549ea6..38173f417 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1195,65 +1195,77 @@ components: pattern: \S* description: The reference details for the link. ContentStabilityExtension: - type: object - properties: - url: - type: string - enum: - - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - valueCodeableConcept: - type: object + allOf: + - $ref: "#/components/schemas/Extension" + - type: object + properties: + url: + type: string + enum: + - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + valueCodeableConcept: + $ref: "#/components/schemas/ContentStabilityExtensionValueCodeableConcept" + required: + - url + - valueCodeableConcept + ContentStabilityExtensionValueCodeableConcept: + allOf: + - $ref: "#/components/schemas/CodeableConcept" + - type: object properties: coding: type: array items: - type: object - properties: - system: - type: string - enum: - - "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" - code: - type: string - enum: ["static", "dynamic"] - display: - type: string - enum: ["Static", "Dynamic"] - required: - - system - - code - - display + $ref: "#/components/schemas/ContentStabilityExtensionCoding" minItems: 1 maxItems: 1 required: - coding - required: - - url - - valueCodeableConcept + ContentStabilityExtensionCoding: + allOf: + - $ref: "#/components/schemas/Coding" + - type: object + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" + code: + type: string + enum: ["static", "dynamic"] + display: + type: string + enum: ["Static", "Dynamic"] + required: + - system + - code + - display NRLFormatCode: - type: object - properties: - system: - type: string - enum: - - "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" - description: The system URL for the NRLF Format Code. - code: - type: string - enum: - - "urn:nhs-ic:record-contact" - - "urn:nhs-ic:unstructured" - description: The code representing the format of the document. - display: - type: string - enum: - - "Contact details (HTTP Unsecured)" - - "Unstructured Document" - description: The display text for the code. - required: - - system - - code - - display + allOf: + - $ref: "#/components/schemas/Coding" + - type: object + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" + description: The system URL for the NRLF Format Code. + code: + type: string + enum: + - "urn:nhs-ic:record-contact" + - "urn:nhs-ic:unstructured" + description: The code representing the format of the document. + display: + type: string + enum: + - "Contact details (HTTP Unsecured)" + - "Unstructured Document" + description: The display text for the code. + required: + - system + - code + - display Identifier: type: object properties: diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index a00d5239e..8c014a3a7 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1723,7 +1723,6 @@ components: items: $ref: "#/components/schemas/Extension" description: Additional content defined by implementations. - Coding: type: object properties: @@ -1761,65 +1760,77 @@ components: pattern: \S* description: The reference details for the link. ContentStabilityExtension: - type: object - properties: - url: - type: string - enum: - - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - valueCodeableConcept: - type: object + allOf: + - $ref: "#/components/schemas/Extension" + - type: object + properties: + url: + type: string + enum: + - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + valueCodeableConcept: + $ref: "#/components/schemas/ContentStabilityExtensionValueCodeableConcept" + required: + - url + - valueCodeableConcept + ContentStabilityExtensionValueCodeableConcept: + allOf: + - $ref: "#/components/schemas/CodeableConcept" + - type: object properties: coding: type: array items: - type: object - properties: - system: - type: string - enum: - - "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" - code: - type: string - enum: ["static", "dynamic"] - display: - type: string - enum: ["Static", "Dynamic"] - required: - - system - - code - - display + $ref: "#/components/schemas/ContentStabilityExtensionCoding" minItems: 1 maxItems: 1 required: - coding - required: - - url - - valueCodeableConcept + ContentStabilityExtensionCoding: + allOf: + - $ref: "#/components/schemas/Coding" + - type: object + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" + code: + type: string + enum: ["static", "dynamic"] + display: + type: string + enum: ["Static", "Dynamic"] + required: + - system + - code + - display NRLFormatCode: - type: object - properties: - system: - type: string - enum: - - "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" - description: The system URL for the NRLF Format Code. - code: - type: string - enum: - - "urn:nhs-ic:record-contact" - - "urn:nhs-ic:unstructured" - description: The code representing the format of the document. - display: - type: string - enum: - - "Contact details (HTTP Unsecured)" - - "Unstructured Document" - description: The display text for the code. - required: - - system - - code - - display + allOf: + - $ref: "#/components/schemas/Coding" + - type: object + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" + description: The system URL for the NRLF Format Code. + code: + type: string + enum: + - "urn:nhs-ic:record-contact" + - "urn:nhs-ic:unstructured" + description: The code representing the format of the document. + display: + type: string + enum: + - "Contact details (HTTP Unsecured)" + - "Unstructured Document" + description: The display text for the code. + required: + - system + - code + - display Identifier: type: object properties: diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 161fabb1f..3dc3d9265 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-06T04:10:37+00:00 +# timestamp: 2024-12-11T16:30:20+00:00 from __future__ import annotations @@ -230,7 +230,7 @@ class Coding(BaseModel): ] = None -class CodingItem(BaseModel): +class ContentStabilityExtensionCoding(Coding): system: Literal[ "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" ] @@ -238,18 +238,7 @@ class CodingItem(BaseModel): display: Literal["Static", "Dynamic"] -class ValueCodeableConcept(BaseModel): - coding: Annotated[List[CodingItem], Field(max_length=1, min_length=1)] - - -class ContentStabilityExtension(BaseModel): - url: Literal[ - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - ] - valueCodeableConcept: ValueCodeableConcept - - -class NRLFormatCode(BaseModel): +class NRLFormatCode(Coding): system: Annotated[ Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], Field(description="The system URL for the NRLF Format Code."), @@ -461,31 +450,6 @@ class RequestHeaderCorrelationId(RootModel[str]): root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] -class DocumentReferenceContent(BaseModel): - id: Annotated[ - Optional[str], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", - pattern="[A-Za-z0-9\\-\\.]{1,64}", - ), - ] = None - attachment: Annotated[ - Attachment, - Field( - description="The document or URL of the document along with critical metadata to prove content has integrity." - ), - ] - format: Annotated[ - NRLFormatCode, - Field( - description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." - ), - ] - extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) - ] - - class RequestHeader(BaseModel): odsCode: RequestHeaderOdsCode @@ -877,6 +841,31 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None +class DocumentReferenceContent(BaseModel): + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + attachment: Annotated[ + Attachment, + Field( + description="The document or URL of the document along with critical metadata to prove content has integrity." + ), + ] + format: Annotated[ + NRLFormatCode, + Field( + description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." + ), + ] + extension: Annotated[ + List[ContentStabilityExtension], Field(max_length=1, min_length=1) + ] + + class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[str], @@ -1062,12 +1051,27 @@ class Signature(BaseModel): ] = None +class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[ContentStabilityExtensionCoding], Field(max_length=1, min_length=1) + ] + + +class ContentStabilityExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ] + valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept + + OperationOutcome.model_rebuild() OperationOutcomeIssue.model_rebuild() DocumentReference.model_rebuild() Bundle.model_rebuild() BundleEntry.model_rebuild() DocumentReferenceContext.model_rebuild() +DocumentReferenceContent.model_rebuild() DocumentReferenceRelatesTo.model_rebuild() CodeableConcept.model_rebuild() Identifier.model_rebuild() +ContentStabilityExtensionValueCodeableConcept.model_rebuild() diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 580836343..afd77dae2 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-06T04:10:35+00:00 +# timestamp: 2024-12-11T16:30:17+00:00 from __future__ import annotations @@ -230,7 +230,7 @@ class Coding(BaseModel): ] = None -class CodingItem(BaseModel): +class ContentStabilityExtensionCoding(Coding): system: Literal[ "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" ] @@ -238,18 +238,7 @@ class CodingItem(BaseModel): display: Literal["Static", "Dynamic"] -class ValueCodeableConcept(BaseModel): - coding: Annotated[List[CodingItem], Field(max_length=1, min_length=1)] - - -class ContentStabilityExtension(BaseModel): - url: Literal[ - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - ] - valueCodeableConcept: ValueCodeableConcept - - -class NRLFormatCode(BaseModel): +class NRLFormatCode(Coding): system: Annotated[ Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], Field(description="The system URL for the NRLF Format Code."), @@ -451,31 +440,6 @@ class RequestHeaderCorrelationId(RootModel[str]): root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] -class DocumentReferenceContent(BaseModel): - id: Annotated[ - Optional[str], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", - pattern="[A-Za-z0-9\\-\\.]{1,64}", - ), - ] = None - attachment: Annotated[ - Attachment, - Field( - description="The document or URL of the document along with critical metadata to prove content has integrity." - ), - ] - format: Annotated[ - NRLFormatCode, - Field( - description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." - ), - ] - extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) - ] - - class RequestHeader(BaseModel): odsCode: RequestHeaderOdsCode @@ -861,6 +825,31 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None +class DocumentReferenceContent(BaseModel): + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + attachment: Annotated[ + Attachment, + Field( + description="The document or URL of the document along with critical metadata to prove content has integrity." + ), + ] + format: Annotated[ + NRLFormatCode, + Field( + description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." + ), + ] + extension: Annotated[ + List[ContentStabilityExtension], Field(max_length=1, min_length=1) + ] + + class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[str], @@ -1046,12 +1035,27 @@ class Signature(BaseModel): ] = None +class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[ContentStabilityExtensionCoding], Field(max_length=1, min_length=1) + ] + + +class ContentStabilityExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ] + valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept + + OperationOutcome.model_rebuild() OperationOutcomeIssue.model_rebuild() DocumentReference.model_rebuild() Bundle.model_rebuild() BundleEntry.model_rebuild() DocumentReferenceContext.model_rebuild() +DocumentReferenceContent.model_rebuild() DocumentReferenceRelatesTo.model_rebuild() CodeableConcept.model_rebuild() Identifier.model_rebuild() +ContentStabilityExtensionValueCodeableConcept.model_rebuild() diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index 6d1e371b7..17074f3b5 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-06T04:10:36+00:00 +# timestamp: 2024-12-11T16:30:18+00:00 from __future__ import annotations @@ -207,7 +207,7 @@ class Coding(BaseModel): ] = None -class CodingItem(BaseModel): +class ContentStabilityExtensionCoding(Coding): system: Literal[ "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" ] @@ -215,18 +215,7 @@ class CodingItem(BaseModel): display: Literal["Static", "Dynamic"] -class ValueCodeableConcept(BaseModel): - coding: Annotated[List[CodingItem], Field(max_length=1, min_length=1)] - - -class ContentStabilityExtension(BaseModel): - url: Literal[ - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - ] - valueCodeableConcept: ValueCodeableConcept - - -class NRLFormatCode(BaseModel): +class NRLFormatCode(Coding): system: Annotated[ Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], Field(description="The system URL for the NRLF Format Code."), @@ -399,30 +388,6 @@ class RequestHeaderCorrelationId(RootModel[StrictStr]): root: Annotated[StrictStr, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] -class DocumentReferenceContent(BaseModel): - id: Annotated[ - Optional[StrictStr], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - ), - ] = None - attachment: Annotated[ - Attachment, - Field( - description="The document or URL of the document along with critical metadata to prove content has integrity." - ), - ] - format: Annotated[ - NRLFormatCode, - Field( - description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." - ), - ] - extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) - ] - - class RequestHeader(BaseModel): odsCode: RequestHeaderOdsCode @@ -764,6 +729,30 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None +class DocumentReferenceContent(BaseModel): + id: Annotated[ + Optional[StrictStr], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + ), + ] = None + attachment: Annotated[ + Attachment, + Field( + description="The document or URL of the document along with critical metadata to prove content has integrity." + ), + ] + format: Annotated[ + NRLFormatCode, + Field( + description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." + ), + ] + extension: Annotated[ + List[ContentStabilityExtension], Field(max_length=1, min_length=1) + ] + + class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[StrictStr], @@ -925,12 +914,27 @@ class Signature(BaseModel): ] = None +class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[ContentStabilityExtensionCoding], Field(max_length=1, min_length=1) + ] + + +class ContentStabilityExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ] + valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept + + OperationOutcome.model_rebuild() OperationOutcomeIssue.model_rebuild() DocumentReference.model_rebuild() Bundle.model_rebuild() BundleEntry.model_rebuild() DocumentReferenceContext.model_rebuild() +DocumentReferenceContent.model_rebuild() DocumentReferenceRelatesTo.model_rebuild() CodeableConcept.model_rebuild() Identifier.model_rebuild() +ContentStabilityExtensionValueCodeableConcept.model_rebuild() diff --git a/tests/features/utils/data.py b/tests/features/utils/data.py index 723c358a6..609a220e5 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -3,8 +3,9 @@ Attachment, CodeableConcept, Coding, - CodingItem, ContentStabilityExtension, + ContentStabilityExtensionCoding, + ContentStabilityExtensionValueCodeableConcept, DocumentReference, DocumentReferenceContent, DocumentReferenceContext, @@ -12,7 +13,6 @@ Identifier, NRLFormatCode, Reference, - ValueCodeableConcept, ) from tests.features.utils.constants import ( DEFAULT_TEST_AUTHOR, @@ -51,9 +51,9 @@ def create_test_document_reference(items: dict) -> DocumentReference: extension=[ ContentStabilityExtension( url="https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", - valueCodeableConcept=ValueCodeableConcept( + valueCodeableConcept=ContentStabilityExtensionValueCodeableConcept( coding=[ - CodingItem( + ContentStabilityExtensionCoding( system="https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", code="static", display="Static", diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index a8f8bdb7d..d50f649cc 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -3,6 +3,9 @@ Attachment, CodeableConcept, Coding, + ContentStabilityExtension, + ContentStabilityExtensionCoding, + ContentStabilityExtensionValueCodeableConcept, DocumentReference, DocumentReferenceContent, DocumentReferenceContext, @@ -41,9 +44,9 @@ def build_document_reference( extension=[ ContentStabilityExtension( url="https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", - valueCodeableConcept=ValueCodeableConcept( + valueCodeableConcept=ContentStabilityExtensionValueCodeableConcept( coding=[ - CodingItem( + ContentStabilityExtensionCoding( system="https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", code="static", display="Static", From 22523d985062fe08c24417506d13b10585d81b60 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 11 Dec 2024 17:30:11 +0000 Subject: [PATCH 37/40] NRL-518 Update narrative --- swagger/producer-static/narrative.yaml | 38 ++++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/swagger/producer-static/narrative.yaml b/swagger/producer-static/narrative.yaml index fd567ac08..8380e0879 100644 --- a/swagger/producer-static/narrative.yaml +++ b/swagger/producer-static/narrative.yaml @@ -276,18 +276,40 @@ paths: ] ``` * `content` MUST have at least one entry. - * `content` MUST include an `attachment` entry. - * `content` MUST include a `format` entry. - * `content` MUST include the content stability extension (https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability). + * `content[]` MUST include an `attachment` entry. + * `content[]` MUST include a `format` entry. (https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode) + * `content[]` MUST include the content stability extension (https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability). * `content[].attachment` MUST include a `url` to the document. * `content[].attachment` MUST include a `contentType` and be a valid MIME type, specifically `application/pdf` for documents or `text/html` for contact details. - * `content[].format[]` MUST indicate whether the data is structured or not, e.g. + * `content[].format` MUST indicate whether the data is structured or not + * Example of the content section: ``` - "format": [ + "content": [ { - "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", - "code": "urn:nhs-ic:unstructured" - "display": "Unstructured Document" + "attachment": { + "contentType": "application/pdf", + "url": "https://provider-ods-code.thirdparty.nhs.uk/path/to/document.pdf", + "creation": "2022-12-22T09:45:41+11:00" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured" + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] } ] ``` From d48ab5a928b66b05283b218f942b1461bf4376ed Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Thu, 12 Dec 2024 11:27:29 +0000 Subject: [PATCH 38/40] NRL-518 Move content fhir url strings to constants --- layer/nrlf/core/constants.py | 7 +++++++ layer/nrlf/core/errors.py | 5 +++-- tests/features/utils/data.py | 14 ++++++++++---- tests/smoke/setup.py | 15 +++++++++++---- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/layer/nrlf/core/constants.py b/layer/nrlf/core/constants.py index fa8e961ea..9fa73906d 100644 --- a/layer/nrlf/core/constants.py +++ b/layer/nrlf/core/constants.py @@ -185,3 +185,10 @@ def coding_value(self): SYSTEM_SHORT_IDS = {"http://snomed.info/sct": "SCT", "https://nicip.nhs.uk": "NICIP"} +CONTENT_STABILITY_EXTENSION_URL = ( + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" +) +CONTENT_STABILITY_SYSTEM_URL = ( + "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" +) +CONTENT_FORMAT_CODE_URL = "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" diff --git a/layer/nrlf/core/errors.py b/layer/nrlf/core/errors.py index e4cc82e21..615ef9a2a 100644 --- a/layer/nrlf/core/errors.py +++ b/layer/nrlf/core/errors.py @@ -3,6 +3,7 @@ from pydantic import ValidationError from pydantic_core import ErrorDetails +from nrlf.core.constants import CONTENT_FORMAT_CODE_URL, CONTENT_STABILITY_SYSTEM_URL from nrlf.core.response import Response from nrlf.core.types import CodeableConcept from nrlf.producer.fhir.r4 import model as producer_model @@ -25,9 +26,9 @@ def append_value_set_url(loc_string: str) -> str: if "content" in loc_string: if "extension" in loc_string: - return ". See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" + return f". See ValueSet: {CONTENT_STABILITY_SYSTEM_URL}" if "format" in loc_string: - return ". See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" + return f". See ValueSet: {CONTENT_FORMAT_CODE_URL}" return "" diff --git a/tests/features/utils/data.py b/tests/features/utils/data.py index 609a220e5..ed3a3e948 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -1,4 +1,10 @@ -from layer.nrlf.core.constants import CATEGORY_ATTRIBUTES, TYPE_ATTRIBUTES +from layer.nrlf.core.constants import ( + CATEGORY_ATTRIBUTES, + CONTENT_FORMAT_CODE_URL, + CONTENT_STABILITY_EXTENSION_URL, + CONTENT_STABILITY_SYSTEM_URL, + TYPE_ATTRIBUTES, +) from nrlf.producer.fhir.r4.model import ( Attachment, CodeableConcept, @@ -43,18 +49,18 @@ def create_test_document_reference(items: dict) -> DocumentReference: format=NRLFormatCode( system=items.get( "formatSystem", - "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + CONTENT_FORMAT_CODE_URL, ), code=items.get("formatCode", "urn:nhs-ic:unstructured"), display=items.get("formatDisplay", "Unstructured Document"), ), extension=[ ContentStabilityExtension( - url="https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + url=CONTENT_STABILITY_EXTENSION_URL, valueCodeableConcept=ContentStabilityExtensionValueCodeableConcept( coding=[ ContentStabilityExtensionCoding( - system="https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + system=CONTENT_STABILITY_SYSTEM_URL, code="static", display="Static", ) diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index d50f649cc..e40f2e823 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -1,4 +1,11 @@ -from nrlf.core.constants import TYPE_ATTRIBUTES, Categories, PointerTypes +from nrlf.core.constants import ( + CONTENT_FORMAT_CODE_URL, + CONTENT_STABILITY_EXTENSION_URL, + CONTENT_STABILITY_SYSTEM_URL, + TYPE_ATTRIBUTES, + Categories, + PointerTypes, +) from nrlf.producer.fhir.r4.model import ( Attachment, CodeableConcept, @@ -37,17 +44,17 @@ def build_document_reference( url=content_url, ), format=NRLFormatCode( - system="https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + system=CONTENT_FORMAT_CODE_URL, code="urn:nhs-ic:unstructured", display="Unstructured Document", ), extension=[ ContentStabilityExtension( - url="https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + url=CONTENT_STABILITY_EXTENSION_URL, valueCodeableConcept=ContentStabilityExtensionValueCodeableConcept( coding=[ ContentStabilityExtensionCoding( - system="https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + system=CONTENT_STABILITY_SYSTEM_URL, code="static", display="Static", ) From 64e6312c54e03d4c5f5fe82c8624adb5cb8def45 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Thu, 12 Dec 2024 15:30:40 +0000 Subject: [PATCH 39/40] NRL-518 Remove extension from CodeableConcept --- api/consumer/swagger.yaml | 5 - api/producer/swagger.yaml | 5 - layer/nrlf/consumer/fhir/r4/model.py | 194 ++++++++++---------- layer/nrlf/producer/fhir/r4/model.py | 194 ++++++++++---------- layer/nrlf/producer/fhir/r4/strict_model.py | 176 +++++++++--------- 5 files changed, 273 insertions(+), 301 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 38173f417..cb1474228 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1153,11 +1153,6 @@ components: type: string pattern: "[ \\r\\n\\t\\S]+" 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. - extension: - type: array - items: - $ref: "#/components/schemas/Extension" - description: Additional content defined by implementations. Coding: type: object properties: diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 8c014a3a7..a0f83a3c9 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1718,11 +1718,6 @@ components: type: string pattern: "[ \\r\\n\\t\\S]+" 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. - extension: - type: array - items: - $ref: "#/components/schemas/Extension" - description: Additional content defined by implementations. Coding: type: object properties: diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 3dc3d9265..d8e069ddc 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-11T16:30:20+00:00 +# timestamp: 2024-12-12T13:19:56+00:00 from __future__ import annotations @@ -450,6 +450,43 @@ class RequestHeaderCorrelationId(RootModel[str]): root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] +class CodeableConcept(BaseModel): + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + coding: Optional[List[Coding]] = None + text: Annotated[ + Optional[str], + Field( + 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.", + pattern="[ \\r\\n\\t\\S]+", + ), + ] = None + + +class Extension(BaseModel): + valueCodeableConcept: Annotated[ + Optional[CodeableConcept], + Field( + description="A name which details the functional use for this link – see [http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1](http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1)." + ), + ] = None + url: Annotated[ + Optional[str], + Field(description="The reference details for the link.", pattern="\\S*"), + ] = None + + +class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[ContentStabilityExtensionCoding], Field(max_length=1, min_length=1) + ] + + class RequestHeader(BaseModel): odsCode: RequestHeaderOdsCode @@ -474,6 +511,52 @@ class CountRequestParams(BaseModel): ] +class OperationOutcomeIssue(BaseModel): + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + severity: Annotated[ + str, + Field( + description="Indicates whether the issue indicates a variation from successful processing.", + pattern="[^\\s]+(\\s[^\\s]+)*", + ), + ] + code: Annotated[ + str, + Field( + description="Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element.", + pattern="[^\\s]+(\\s[^\\s]+)*", + ), + ] + details: Annotated[ + Optional[CodeableConcept], + Field( + description="Additional details about the error. This may be a text description of the error or a system code that identifies the error." + ), + ] = None + diagnostics: Annotated[ + Optional[str], + Field( + description="Additional diagnostic information about the issue.", + pattern="[ \\r\\n\\t\\S]+", + ), + ] = None + location: Optional[List[LocationItem]] = None + expression: Optional[List[ExpressionItem]] = None + + +class ContentStabilityExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ] + valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept + + class OperationOutcome(BaseModel): resourceType: Literal["OperationOutcome"] id: Annotated[ @@ -512,7 +595,7 @@ class OperationOutcome(BaseModel): issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)] -class OperationOutcomeIssue(BaseModel): +class DocumentReferenceContent(BaseModel): id: Annotated[ Optional[str], Field( @@ -520,35 +603,21 @@ class OperationOutcomeIssue(BaseModel): pattern="[A-Za-z0-9\\-\\.]{1,64}", ), ] = None - severity: Annotated[ - str, + attachment: Annotated[ + Attachment, Field( - description="Indicates whether the issue indicates a variation from successful processing.", - pattern="[^\\s]+(\\s[^\\s]+)*", + description="The document or URL of the document along with critical metadata to prove content has integrity." ), ] - code: Annotated[ - str, + format: Annotated[ + NRLFormatCode, Field( - description="Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element.", - pattern="[^\\s]+(\\s[^\\s]+)*", + description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." ), ] - details: Annotated[ - Optional[CodeableConcept], - Field( - description="Additional details about the error. This may be a text description of the error or a system code that identifies the error." - ), - ] = None - diagnostics: Annotated[ - Optional[str], - Field( - description="Additional diagnostic information about the issue.", - pattern="[ \\r\\n\\t\\S]+", - ), - ] = None - location: Optional[List[LocationItem]] = None - expression: Optional[List[ExpressionItem]] = None + extension: Annotated[ + List[ContentStabilityExtension], Field(max_length=1, min_length=1) + ] class DocumentReference(BaseModel): @@ -841,31 +910,6 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceContent(BaseModel): - id: Annotated[ - Optional[str], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", - pattern="[A-Za-z0-9\\-\\.]{1,64}", - ), - ] = None - attachment: Annotated[ - Attachment, - Field( - description="The document or URL of the document along with critical metadata to prove content has integrity." - ), - ] - format: Annotated[ - NRLFormatCode, - Field( - description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." - ), - ] - extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) - ] - - class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[str], @@ -886,38 +930,6 @@ class DocumentReferenceRelatesTo(BaseModel): ] -class CodeableConcept(BaseModel): - id: Annotated[ - Optional[str], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", - pattern="[A-Za-z0-9\\-\\.]{1,64}", - ), - ] = None - coding: Optional[List[Coding]] = None - text: Annotated[ - Optional[str], - Field( - 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.", - pattern="[ \\r\\n\\t\\S]+", - ), - ] = None - extension: Optional[List[Extension]] = None - - -class Extension(BaseModel): - valueCodeableConcept: Annotated[ - Optional[CodeableConcept], - Field( - description="A name which details the functional use for this link – see [http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1](http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1)." - ), - ] = None - url: Annotated[ - Optional[str], - Field(description="The reference details for the link.", pattern="\\S*"), - ] = None - - class Identifier(BaseModel): id: Annotated[ Optional[str], @@ -1051,27 +1063,9 @@ class Signature(BaseModel): ] = None -class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): - coding: Annotated[ - List[ContentStabilityExtensionCoding], Field(max_length=1, min_length=1) - ] - - -class ContentStabilityExtension(Extension): - url: Literal[ - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - ] - valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept - - -OperationOutcome.model_rebuild() -OperationOutcomeIssue.model_rebuild() DocumentReference.model_rebuild() Bundle.model_rebuild() BundleEntry.model_rebuild() DocumentReferenceContext.model_rebuild() -DocumentReferenceContent.model_rebuild() DocumentReferenceRelatesTo.model_rebuild() -CodeableConcept.model_rebuild() Identifier.model_rebuild() -ContentStabilityExtensionValueCodeableConcept.model_rebuild() diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index afd77dae2..538659e9d 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-11T16:30:17+00:00 +# timestamp: 2024-12-12T13:19:54+00:00 from __future__ import annotations @@ -440,6 +440,43 @@ class RequestHeaderCorrelationId(RootModel[str]): root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] +class CodeableConcept(BaseModel): + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + coding: Optional[List[Coding]] = None + text: Annotated[ + Optional[str], + Field( + 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.", + pattern="[ \\r\\n\\t\\S]+", + ), + ] = None + + +class Extension(BaseModel): + valueCodeableConcept: Annotated[ + Optional[CodeableConcept], + Field( + description="A name which details the functional use for this link – see [http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1](http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1)." + ), + ] = None + url: Annotated[ + Optional[str], + Field(description="The reference details for the link.", pattern="\\S*"), + ] = None + + +class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[ContentStabilityExtensionCoding], Field(max_length=1, min_length=1) + ] + + class RequestHeader(BaseModel): odsCode: RequestHeaderOdsCode @@ -455,6 +492,52 @@ class RequestParams(BaseModel): ] = None +class OperationOutcomeIssue(BaseModel): + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + severity: Annotated[ + str, + Field( + description="Indicates whether the issue indicates a variation from successful processing.", + pattern="[^\\s]+(\\s[^\\s]+)*", + ), + ] + code: Annotated[ + str, + Field( + description="Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element.", + pattern="[^\\s]+(\\s[^\\s]+)*", + ), + ] + details: Annotated[ + Optional[CodeableConcept], + Field( + description="Additional details about the error. This may be a text description of the error or a system code that identifies the error." + ), + ] = None + diagnostics: Annotated[ + Optional[str], + Field( + description="Additional diagnostic information about the issue.", + pattern="[ \\r\\n\\t\\S]+", + ), + ] = None + location: Optional[List[LocationItem]] = None + expression: Optional[List[ExpressionItem]] = None + + +class ContentStabilityExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ] + valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept + + class OperationOutcome(BaseModel): resourceType: Literal["OperationOutcome"] id: Annotated[ @@ -493,7 +576,7 @@ class OperationOutcome(BaseModel): issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)] -class OperationOutcomeIssue(BaseModel): +class DocumentReferenceContent(BaseModel): id: Annotated[ Optional[str], Field( @@ -501,35 +584,21 @@ class OperationOutcomeIssue(BaseModel): pattern="[A-Za-z0-9\\-\\.]{1,64}", ), ] = None - severity: Annotated[ - str, + attachment: Annotated[ + Attachment, Field( - description="Indicates whether the issue indicates a variation from successful processing.", - pattern="[^\\s]+(\\s[^\\s]+)*", + description="The document or URL of the document along with critical metadata to prove content has integrity." ), ] - code: Annotated[ - str, + format: Annotated[ + NRLFormatCode, Field( - description="Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element.", - pattern="[^\\s]+(\\s[^\\s]+)*", + description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." ), ] - details: Annotated[ - Optional[CodeableConcept], - Field( - description="Additional details about the error. This may be a text description of the error or a system code that identifies the error." - ), - ] = None - diagnostics: Annotated[ - Optional[str], - Field( - description="Additional diagnostic information about the issue.", - pattern="[ \\r\\n\\t\\S]+", - ), - ] = None - location: Optional[List[LocationItem]] = None - expression: Optional[List[ExpressionItem]] = None + extension: Annotated[ + List[ContentStabilityExtension], Field(max_length=1, min_length=1) + ] class DocumentReference(BaseModel): @@ -825,31 +894,6 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceContent(BaseModel): - id: Annotated[ - Optional[str], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", - pattern="[A-Za-z0-9\\-\\.]{1,64}", - ), - ] = None - attachment: Annotated[ - Attachment, - Field( - description="The document or URL of the document along with critical metadata to prove content has integrity." - ), - ] - format: Annotated[ - NRLFormatCode, - Field( - description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." - ), - ] - extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) - ] - - class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[str], @@ -870,38 +914,6 @@ class DocumentReferenceRelatesTo(BaseModel): ] -class CodeableConcept(BaseModel): - id: Annotated[ - Optional[str], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", - pattern="[A-Za-z0-9\\-\\.]{1,64}", - ), - ] = None - coding: Optional[List[Coding]] = None - text: Annotated[ - Optional[str], - Field( - 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.", - pattern="[ \\r\\n\\t\\S]+", - ), - ] = None - extension: Optional[List[Extension]] = None - - -class Extension(BaseModel): - valueCodeableConcept: Annotated[ - Optional[CodeableConcept], - Field( - description="A name which details the functional use for this link – see [http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1](http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1)." - ), - ] = None - url: Annotated[ - Optional[str], - Field(description="The reference details for the link.", pattern="\\S*"), - ] = None - - class Identifier(BaseModel): id: Annotated[ Optional[str], @@ -1035,27 +1047,9 @@ class Signature(BaseModel): ] = None -class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): - coding: Annotated[ - List[ContentStabilityExtensionCoding], Field(max_length=1, min_length=1) - ] - - -class ContentStabilityExtension(Extension): - url: Literal[ - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - ] - valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept - - -OperationOutcome.model_rebuild() -OperationOutcomeIssue.model_rebuild() DocumentReference.model_rebuild() Bundle.model_rebuild() BundleEntry.model_rebuild() DocumentReferenceContext.model_rebuild() -DocumentReferenceContent.model_rebuild() DocumentReferenceRelatesTo.model_rebuild() -CodeableConcept.model_rebuild() Identifier.model_rebuild() -ContentStabilityExtensionValueCodeableConcept.model_rebuild() diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index 17074f3b5..cfa6f3243 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2024-12-11T16:30:18+00:00 +# timestamp: 2024-12-12T13:19:55+00:00 from __future__ import annotations @@ -388,6 +388,40 @@ class RequestHeaderCorrelationId(RootModel[StrictStr]): root: Annotated[StrictStr, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] +class CodeableConcept(BaseModel): + id: Annotated[ + Optional[StrictStr], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + ), + ] = None + coding: Optional[List[Coding]] = None + text: Annotated[ + Optional[StrictStr], + Field( + 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." + ), + ] = None + + +class Extension(BaseModel): + valueCodeableConcept: Annotated[ + Optional[CodeableConcept], + Field( + description="A name which details the functional use for this link – see [http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1](http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1)." + ), + ] = None + url: Annotated[ + Optional[StrictStr], Field(description="The reference details for the link.") + ] = None + + +class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[ContentStabilityExtensionCoding], Field(max_length=1, min_length=1) + ] + + class RequestHeader(BaseModel): odsCode: RequestHeaderOdsCode @@ -403,6 +437,46 @@ class RequestParams(BaseModel): ] = None +class OperationOutcomeIssue(BaseModel): + id: Annotated[ + Optional[StrictStr], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." + ), + ] = None + severity: Annotated[ + StrictStr, + Field( + description="Indicates whether the issue indicates a variation from successful processing." + ), + ] + code: Annotated[ + StrictStr, + Field( + description="Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element." + ), + ] + details: Annotated[ + Optional[CodeableConcept], + Field( + description="Additional details about the error. This may be a text description of the error or a system code that identifies the error." + ), + ] = None + diagnostics: Annotated[ + Optional[StrictStr], + Field(description="Additional diagnostic information about the issue."), + ] = None + location: Optional[List[LocationItem]] = None + expression: Optional[List[ExpressionItem]] = None + + +class ContentStabilityExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ] + valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept + + class OperationOutcome(BaseModel): resourceType: Literal["OperationOutcome"] id: Annotated[ @@ -436,37 +510,28 @@ class OperationOutcome(BaseModel): issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)] -class OperationOutcomeIssue(BaseModel): +class DocumentReferenceContent(BaseModel): id: Annotated[ Optional[StrictStr], Field( description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." ), ] = None - severity: Annotated[ - StrictStr, + attachment: Annotated[ + Attachment, Field( - description="Indicates whether the issue indicates a variation from successful processing." + description="The document or URL of the document along with critical metadata to prove content has integrity." ), ] - code: Annotated[ - StrictStr, + format: Annotated[ + NRLFormatCode, Field( - description="Describes the type of the issue. The system that creates an OperationOutcome SHALL choose the most applicable code from the IssueType value set, and may additional provide its own code for the error in the details element." + description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." ), ] - details: Annotated[ - Optional[CodeableConcept], - Field( - description="Additional details about the error. This may be a text description of the error or a system code that identifies the error." - ), - ] = None - diagnostics: Annotated[ - Optional[StrictStr], - Field(description="Additional diagnostic information about the issue."), - ] = None - location: Optional[List[LocationItem]] = None - expression: Optional[List[ExpressionItem]] = None + extension: Annotated[ + List[ContentStabilityExtension], Field(max_length=1, min_length=1) + ] class DocumentReference(BaseModel): @@ -729,30 +794,6 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceContent(BaseModel): - id: Annotated[ - Optional[StrictStr], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - ), - ] = None - attachment: Annotated[ - Attachment, - Field( - description="The document or URL of the document along with critical metadata to prove content has integrity." - ), - ] - format: Annotated[ - NRLFormatCode, - Field( - description="An identifier of the document encoding, structure, and template that the document conforms to beyond the base format indicated in the mimeType." - ), - ] - extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) - ] - - class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[StrictStr], @@ -771,35 +812,6 @@ class DocumentReferenceRelatesTo(BaseModel): ] -class CodeableConcept(BaseModel): - id: Annotated[ - Optional[StrictStr], - Field( - description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces." - ), - ] = None - coding: Optional[List[Coding]] = None - text: Annotated[ - Optional[StrictStr], - Field( - 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." - ), - ] = None - extension: Optional[List[Extension]] = None - - -class Extension(BaseModel): - valueCodeableConcept: Annotated[ - Optional[CodeableConcept], - Field( - description="A name which details the functional use for this link – see [http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1](http://www.iana.org/assignments/link–relations/link–relations.xhtml#link–relations–1)." - ), - ] = None - url: Annotated[ - Optional[StrictStr], Field(description="The reference details for the link.") - ] = None - - class Identifier(BaseModel): id: Annotated[ Optional[StrictStr], @@ -914,27 +926,9 @@ class Signature(BaseModel): ] = None -class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): - coding: Annotated[ - List[ContentStabilityExtensionCoding], Field(max_length=1, min_length=1) - ] - - -class ContentStabilityExtension(Extension): - url: Literal[ - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - ] - valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept - - -OperationOutcome.model_rebuild() -OperationOutcomeIssue.model_rebuild() DocumentReference.model_rebuild() Bundle.model_rebuild() BundleEntry.model_rebuild() DocumentReferenceContext.model_rebuild() -DocumentReferenceContent.model_rebuild() DocumentReferenceRelatesTo.model_rebuild() -CodeableConcept.model_rebuild() Identifier.model_rebuild() -ContentStabilityExtensionValueCodeableConcept.model_rebuild() From 8b398a7466aad04e3c24084054b2b721d97aa49c Mon Sep 17 00:00:00 2001 From: eesa456 Date: Fri, 13 Dec 2024 07:19:38 +0000 Subject: [PATCH 40/40] NRL-518 add import for smoke tests --- tests/smoke/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index 4fb6d38da..f639e80f8 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -18,6 +18,7 @@ DocumentReferenceContext, DocumentReferenceRelatesTo, Identifier, + NRLFormatCode, Reference, ) from tests.utilities.api_clients import ProducerTestClient