From ac08c8316cabf25bc36e5655899d9b41dfee4b74 Mon Sep 17 00:00:00 2001 From: Kate Bobyn Date: Wed, 15 Jan 2025 13:49:16 +0000 Subject: [PATCH 01/10] SPII-1165 add stricter coding resources to model --- api/producer/swagger.yaml | 59 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index c927f4871..c7240a0d5 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1354,12 +1354,12 @@ components: enum: ["entered-in-error", "amended", "preliminary", "final"] description: The status of the underlying document. type: - $ref: "#/components/schemas/CodeableConcept" + $ref: "#/components/schemas/NRLCodeableConcept" description: Specifies the particular kind of document referenced (e.g. History and Physical, Discharge Summary, Progress Note). This usually equates to the purpose of making the document referenced. category: type: array items: - $ref: "#/components/schemas/CodeableConcept" + $ref: "#/components/schemas/NRLCodeableConcept" description: A categorization for the type of document referenced – helps for indexing and searching. This may be implied by or derived from the code specified in the DocumentReference.type. subject: $ref: "#/components/schemas/Reference" @@ -1607,7 +1607,7 @@ components: $ref: "#/components/schemas/CodeableConcept" description: The kind of facility where the patient was seen. practiceSetting: - $ref: "#/components/schemas/CodeableConcept" + $ref: "#/components/schemas/NRLCodeableConcept" description: This property may convey specifics about the practice setting where the content was created, often reflecting the clinical specialty. sourcePatientInfo: $ref: "#/components/schemas/Reference" @@ -1718,6 +1718,27 @@ 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. + NRLCodeableConcept: + type: object + properties: + id: + type: string + pattern: "[A-Za-z0-9\\-\\.]{1,64}" + description: Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces. + coding: + type: array + items: + $ref: "#/components/schemas/NRLCoding" + description: A reference to a code defined by a terminology system. + minItems: 1 + maxItems: 1 + text: + 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. + required: + - coding + Coding: type: object properties: @@ -1727,7 +1748,33 @@ components: description: Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces. system: type: string - pattern: \S* + pattern: \S+ + description: The identification of the code system that defines the meaning of the symbol in the code. + version: + type: string + pattern: "[ \\r\\n\\t\\S]+" + description: The version of the code system which was used when choosing this code. Note that a well–maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged. + code: + type: string + pattern: "[^\\s]+(\\s[^\\s]+)*" + description: A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post–coordination). + display: + type: string + pattern: "[ \\r\\n\\t\\S]+" + description: A representation of the meaning of the code in the system, following the rules of the system. + userSelected: + type: boolean + description: Indicates that this coding was chosen by a user directly – e.g. off a pick list of available items (codes or displays). + NRLCoding: + type: object + properties: + id: + type: string + pattern: "[A-Za-z0-9\\-\\.]{1,64}" + description: Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces. + system: + type: string + pattern: \S+ description: The identification of the code system that defines the meaning of the symbol in the code. version: type: string @@ -1744,6 +1791,10 @@ components: userSelected: type: boolean description: Indicates that this coding was chosen by a user directly – e.g. off a pick list of available items (codes or displays). + required: + - system + - code + - display Extension: type: object properties: From d0de21b57d39b9e55754a269cfa066014e2d39b8 Mon Sep 17 00:00:00 2001 From: Kate Bobyn Date: Mon, 27 Jan 2025 11:13:16 +0000 Subject: [PATCH 02/10] NRL-1165 reject whitespace-only strings in model, update tests for pydantic validation of codeable concept fields --- .../tests/test_create_document_reference.py | 30 ++ api/producer/swagger.yaml | 36 ++- .../tests/test_update_document_reference.py | 32 +- .../tests/test_upsert_document_reference.py | 30 ++ layer/nrlf/consumer/fhir/r4/model.py | 2 +- layer/nrlf/core/dynamodb/tests/test_model.py | 25 +- layer/nrlf/core/tests/test_pydantic_errors.py | 294 ++++++++++++++++++ layer/nrlf/core/tests/test_request.py | 17 + layer/nrlf/core/tests/test_validators.py | 225 +------------- layer/nrlf/producer/fhir/r4/model.py | 102 ++++-- layer/nrlf/producer/fhir/r4/strict_model.py | 65 +++- .../createDocumentReference-failure.feature | 58 ++++ 12 files changed, 625 insertions(+), 291 deletions(-) diff --git a/api/producer/createDocumentReference/tests/test_create_document_reference.py b/api/producer/createDocumentReference/tests/test_create_document_reference.py index 56daacfc6..f6a6238db 100644 --- a/api/producer/createDocumentReference/tests/test_create_document_reference.py +++ b/api/producer/createDocumentReference/tests/test_create_document_reference.py @@ -239,6 +239,36 @@ def test_create_document_reference_invalid_body(): "diagnostics": "Request body could not be parsed (status: Field required)", "expression": ["status"], }, + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + } + ], + }, + "diagnostics": "Request body could not be parsed (type: Field required)", + "expression": ["type"], + }, + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + } + ], + }, + "diagnostics": "Request body could not be parsed (category: Field required)", + "expression": ["category"], + }, { "severity": "error", "code": "invalid", diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index c7240a0d5..76d1f4284 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1386,7 +1386,7 @@ components: description: Relationships that this document has with other document references that already exist. description: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: Human–readable description of the source document. securityLabel: type: array @@ -1408,6 +1408,8 @@ components: - content - author - context + - type + - category Bundle: type: object properties: @@ -1693,7 +1695,7 @@ components: description: The calculated hash of the data using SHA–1. Represented using base64. title: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: A label or set of text to display in place of the data. creation: type: string @@ -1716,7 +1718,7 @@ components: description: A reference to a code defined by a terminology system. text: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\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. NRLCodeableConcept: type: object @@ -1734,7 +1736,7 @@ components: maxItems: 1 text: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\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. required: - coding @@ -1752,7 +1754,7 @@ components: description: The identification of the code system that defines the meaning of the symbol in the code. version: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: The version of the code system which was used when choosing this code. Note that a well–maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged. code: type: string @@ -1760,7 +1762,7 @@ components: description: A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post–coordination). display: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: A representation of the meaning of the code in the system, following the rules of the system. userSelected: type: boolean @@ -1778,7 +1780,7 @@ components: description: The identification of the code system that defines the meaning of the symbol in the code. version: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: The version of the code system which was used when choosing this code. Note that a well–maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged. code: type: string @@ -1786,7 +1788,7 @@ components: description: A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post–coordination). display: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: A representation of the meaning of the code in the system, following the rules of the system. userSelected: type: boolean @@ -1893,11 +1895,11 @@ components: description: A coded type for the identifier that can be used to determine which identifier to use for a specific purpose. system: type: string - pattern: \S* + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: Establishes the namespace for the value – that is, a URL that describes a set values that are unique. value: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: The portion of the identifier typically relevant to the user and which is unique within the context of the system. period: $ref: "#/components/schemas/Period" @@ -1936,11 +1938,11 @@ components: description: How the value should be understood and represented – whether the actual value is greater or less than the stated value due to measurement issues; e.g. if the comparator is "<" , then the real value is < stated value. unit: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: A human–readable form of the unit. system: type: string - pattern: \S* + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: The identification of the system that provides the coded form of the unit. code: type: string @@ -1955,11 +1957,11 @@ components: description: Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces. reference: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: A reference to a location at which the other resource is found. The reference may be a relative reference, in which case it is relative to the service base URL, or an absolute URL that resolves to the location where the resource is found. The reference may be version specific or not. If the reference is not to a FHIR RESTful server, then it should be assumed to be version specific. Internal fragment references (start with '#') refer to contained resources. type: type: string - pattern: \S* + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: |- The expected type of the target of the reference. If both Reference.type and Reference.reference are populated and Reference.reference is a FHIR URL, both SHALL be consistent. The type is the Canonical URL of Resource Definition that is the type this reference refers to. References are URLs that are relative to http://hl7.org/fhir/StructureDefinition/ e.g. "Patient" is a reference to http://hl7.org/fhir/StructureDefinition/Patient. Absolute URLs are only allowed for logical models (and can only be used in references in logical models, not resources). @@ -1968,7 +1970,7 @@ components: description: An identifier for the target resource. This is used when there is no way to reference the other resource directly, either because the entity it represents is not available through a FHIR server, or because there is no way for the author of the resource to convert a known identifier to an actual location. There is no requirement that a Reference.identifier point to something that is actually exposed as a FHIR instance, but it SHALL point to a business concept that would be expected to be exposed as a FHIR instance, and that instance would need to be of a FHIR resource type allowed by the reference. display: type: string - pattern: "[ \\r\\n\\t\\S]+" + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: Plain text narrative that identifies the resource in addition to the resource reference. Signature: type: object @@ -2026,13 +2028,13 @@ components: description: When the resource last changed – e.g. when the version changed. source: type: string - pattern: \S* + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc. profile: type: array items: type: string - pattern: \S* + pattern: "[\\S]+[ \\r\\n\\t\\S]*" description: A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition–definitions.html#StructureDefinition.url). security: type: array diff --git a/api/producer/updateDocumentReference/tests/test_update_document_reference.py b/api/producer/updateDocumentReference/tests/test_update_document_reference.py index 97b32207a..d60cfcd57 100644 --- a/api/producer/updateDocumentReference/tests/test_update_document_reference.py +++ b/api/producer/updateDocumentReference/tests/test_update_document_reference.py @@ -195,7 +195,7 @@ def test_create_document_reference_no_body(): } -def test_create_document_reference_invalid_body(): +def test_update_document_reference_invalid_body(): event = create_test_api_gateway_event( headers=create_headers(), path_parameters={"id": "Y05868-99999-99999-999999"}, @@ -246,6 +246,36 @@ def test_create_document_reference_invalid_body(): "diagnostics": "Request body could not be parsed (status: Field required)", "expression": ["status"], }, + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + } + ], + }, + "diagnostics": "Request body could not be parsed (type: Field required)", + "expression": ["type"], + }, + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + } + ], + }, + "diagnostics": "Request body could not be parsed (category: Field required)", + "expression": ["category"], + }, { "severity": "error", "code": "invalid", diff --git a/api/producer/upsertDocumentReference/tests/test_upsert_document_reference.py b/api/producer/upsertDocumentReference/tests/test_upsert_document_reference.py index 090542dae..35a087f55 100644 --- a/api/producer/upsertDocumentReference/tests/test_upsert_document_reference.py +++ b/api/producer/upsertDocumentReference/tests/test_upsert_document_reference.py @@ -324,6 +324,36 @@ def test_upsert_document_reference_invalid_body(): "diagnostics": "Request body could not be parsed (status: Field required)", "expression": ["status"], }, + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + } + ], + }, + "diagnostics": "Request body could not be parsed (type: Field required)", + "expression": ["type"], + }, + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + } + ], + }, + "diagnostics": "Request body could not be parsed (category: Field required)", + "expression": ["category"], + }, { "severity": "error", "code": "invalid", diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 2fd597b42..ffc1667f8 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-13T11:19:30+00:00 +# timestamp: 2025-01-27T09:26:33+00:00 from __future__ import annotations diff --git a/layer/nrlf/core/dynamodb/tests/test_model.py b/layer/nrlf/core/dynamodb/tests/test_model.py index 196d2ed17..951a8cf32 100644 --- a/layer/nrlf/core/dynamodb/tests/test_model.py +++ b/layer/nrlf/core/dynamodb/tests/test_model.py @@ -6,8 +6,7 @@ from nrlf.core.constants import PointerTypes from nrlf.core.dynamodb.model import DocumentPointer, DynamoDBModel from nrlf.core.utils import create_fhir_instant -from nrlf.producer.fhir.r4.model import DocumentReference -from nrlf.tests.data import load_document_reference, load_document_reference_json +from nrlf.tests.data import load_document_reference def test_dynamodb_model_init(): @@ -159,28 +158,6 @@ def test_document_pointer_from_document_reference_invalid(): assert str(error.value) == "'NoneType' object has no attribute 'coding'" -def test_document_pointer_from_document_reference_multiple_types(): - doc_ref_data = load_document_reference_json("Y05868-736253002-Valid") - doc_ref = DocumentReference.model_validate( - { - **doc_ref_data, - "type": { - "coding": [ - {"system": "http://snomed.info/sct", "code": "123456789"}, - {"system": "http://snomed.info/sct", "code": "987654321"}, - ] - }, - } - ) - - with pytest.raises(ValueError) as error: - DocumentPointer.from_document_reference(doc_ref) - - assert ( - str(error.value) == "DocumentReference.type.coding must have exactly one item" - ) - - def test_document_pointer_extract_custodian_suffix_no_suffix(): values = {"custodian": "X26", "custodian_suffix": None} diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 7ff69e6be..b3739b16d 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -301,3 +301,297 @@ def test_validate_content_missing_content_stability_coding(): "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"], } + + +def test_validate_multiple_codings(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["category"][0] = { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "734163000", + "display": "Care plan", + }, + { + "system": "http://snomed.info/sct", + "code": "734163000", + "display": "Care plan", + }, + { + "system": "http://snomed.info/sct", + "code": "734163000", + "display": "Care plan", + }, + ] + } + + 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 (category[0].coding: List should have at most 1 item after validation, not 3)", + "expression": ["category[0].coding"], + } + + +def test_validate_missing_coding(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["category"][0] = {"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 (category[0].coding: List should have at least 1 item after validation, not 0)", + "expression": ["category[0].coding"], + } + + +def test_validate_empty_strings(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["category"][0] = { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "734163000", + "display": "", + } + ] + } + + 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 (category[0].coding[0].display: String should match pattern '[\\S]+[ \\r\\n\\t\\S]*')", + "expression": ["category[0].coding[0].display"], + } + + +def test_validate_whitespace_strings(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["category"][0] = { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "734163000", + "display": " ", + } + ] + } + + 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 (category[0].coding[0].display: String should match pattern '[\\S]+[ \\r\\n\\t\\S]*')", + "expression": ["category[0].coding[0].display"], + } + + +def test_validate_no_coding_where_mandatory(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["context"]["practiceSetting"] = { + "text": "Description of the clinic in text" + } + + 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 (context.practiceSetting.coding: Field required)", + "expression": ["context.practiceSetting.coding"], + } + + +def test_validate_no_coding_where_optional(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["context"]["facilityType"] = { + "text": "Description of the facility type in text" + } + + result = validator.validate(document_ref_data) + + assert result.is_valid + + +def test_validate_missing_system_from_coding_where_mandatory(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["context"]["practiceSetting"] = { + "coding": [ + { + "code": "734163000", + "display": "Valid display string", + } + ] + } + + 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 (context.practiceSetting.coding[0].system: Field required)", + "expression": ["context.practiceSetting.coding[0].system"], + } + + +def test_validate_missing_code_from_coding_where_mandatory(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["context"]["practiceSetting"] = { + "coding": [ + { + "system": "http://snomed.info/sct", + "display": "Valid display string", + } + ] + } + + 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 (context.practiceSetting.coding[0].code: Field required)", + "expression": ["context.practiceSetting.coding[0].code"], + } + + +def test_validate_missing_display_from_coding_where_mandatory(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["context"]["practiceSetting"] = { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "788002001", + } + ] + } + + 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 (context.practiceSetting.coding[0].display: Field required)", + "expression": ["context.practiceSetting.coding[0].display"], + } diff --git a/layer/nrlf/core/tests/test_request.py b/layer/nrlf/core/tests/test_request.py index 6f4ec8d92..2c34c567a 100644 --- a/layer/nrlf/core/tests/test_request.py +++ b/layer/nrlf/core/tests/test_request.py @@ -274,6 +274,23 @@ def test_parse_body_invalid_json(): "diagnostics": "Request body could not be parsed (type: Input should be an object)", "expression": ["type"], }, + { + "code": "invalid", + "details": { + "coding": [ + { + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed", + "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", + }, + ], + }, + "diagnostics": "Request body could not be parsed (category: Field required)", + "expression": [ + "category", + ], + "severity": "error", + }, { "code": "invalid", "details": { diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 4b13e5d56..e28653576 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -204,7 +204,7 @@ def test_document_reference_validator_parse_invalid(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (type: Input should be a valid dictionary or instance of CodeableConcept)", + "diagnostics": "Failed to parse DocumentReference resource (type: Input should be a valid dictionary or instance of NRLCodeableConcept)", "expression": ["type"], } @@ -225,16 +225,14 @@ def test_validate_document_reference_missing_fields(): document_ref_data = load_document_reference_json("Y05868-736253002-Valid") del document_ref_data["id"] - del document_ref_data["type"] del document_ref_data["custodian"] del document_ref_data["subject"] - del document_ref_data["category"] result = validator.validate(document_ref_data) assert result.is_valid is False assert result.resource.id is None - assert len(result.issues) == 5 + assert len(result.issues) == 3 assert result.issues[0].model_dump(exclude_none=True) == { "severity": "error", "code": "required", @@ -255,9 +253,7 @@ def test_validate_document_reference_missing_fields(): assert diagnostics == [ "The required field 'custodian' is missing", "The required field 'id' is missing", - "The required field 'type' is missing", "The required field 'subject' is missing", - "The required field 'category' is missing", ] @@ -362,34 +358,6 @@ def test_validate_identifiers_no_subject_identifier(): } -def test_validate_category_no_category(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - del document_ref_data["category"] - - 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": "The required field 'category' is missing", - "expression": ["category"], - } - - def test_validate_category_too_many_category(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") @@ -497,52 +465,6 @@ def test_validate_category_coding_display_mismatch( } -def test_validate_category_coding_multiple_codings(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["category"][0] = { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "734163000", - "display": "Care plan", - }, - { - "system": "http://snomed.info/sct", - "code": "734163000", - "display": "Care plan", - }, - { - "system": "http://snomed.info/sct", - "code": "734163000", - "display": "Care plan", - }, - ] - } - - 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 category coding length: 3 Category Coding must only contain a single value", - "expression": ["category[0].coding"], - } - - def test_validate_category_coding_invalid_code(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") @@ -647,52 +569,6 @@ def test_validate_type_coding_invalid_code(): } -def test_validate_type_coding_multiple_codings(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["type"] = { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "736253002", - "display": "Mental health crisis plan", - }, - { - "system": "http://snomed.info/sct", - "code": "736253002", - "display": "Mental health crisis plan", - }, - { - "system": "http://snomed.info/sct", - "code": "736253002", - "display": "Mental health crisis plan", - }, - ] - } - - 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 type coding length: 3 Type Coding must only contain a single value", - "expression": ["type.coding"], - } - - def test_validate_type_coding_invalid_system(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") @@ -1363,35 +1239,6 @@ def test_validate_content_format_invalid_code_for_contact_details(): } -def test_validate_practiceSetting_no_coding(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["context"]["practiceSetting"] = { - "text": "Description of the clinic" - } - - 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 practice setting: must contain a Coding", - "expression": ["context.practiceSetting.coding"], - } - - def test_validate_practiceSetting_coding_invalid_system(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") @@ -1462,74 +1309,6 @@ def test_validate_practiceSetting_coding_invalid_code(): } -def test_validate_practiceSetting_coding_missing_code(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["context"]["practiceSetting"] = { - "coding": [ - { - "system": "http://snomed.info/sct", - "display": "Adult mental health service", - } - ] - } - - 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 practice setting code: None Practice Setting coding must be a member of value set https://fhir.nhs.uk/England/ValueSet/England-PracticeSetting", - "expression": ["context.practiceSetting.coding[0].code"], - } - - -def test_validate_practiceSetting_coding_missing_display(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - document_ref_data["context"]["practiceSetting"] = { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "788002001", - } - ] - } - - 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 practice setting coding: display None does not match the expected display for 788002001 Practice Setting coding is bound to value set https://fhir.nhs.uk/England/ValueSet/England-PracticeSetting", - "expression": ["context.practiceSetting.coding[0]"], - } - - def test_validate_practiceSetting_coding_mismatch_code_and_display(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 945b220ec..d2f9e3c06 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-13T11:19:26+00:00 +# timestamp: 2025-01-27T09:26:28+00:00 from __future__ import annotations @@ -174,7 +174,7 @@ class Attachment(BaseModel): Optional[str], Field( description="A label or set of text to display in place of the data.", - pattern="[ \\r\\n\\t\\S]+", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None creation: Annotated[ @@ -198,14 +198,14 @@ class Coding(BaseModel): Optional[str], Field( description="The identification of the code system that defines the meaning of the symbol in the code.", - pattern="\\S*", + pattern="\\S+", ), ] = None version: Annotated[ Optional[str], Field( description="The version of the code system which was used when choosing this code. Note that a well–maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged.", - pattern="[ \\r\\n\\t\\S]+", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None code: Annotated[ @@ -219,7 +219,7 @@ class Coding(BaseModel): Optional[str], Field( description="A representation of the meaning of the code in the system, following the rules of the system.", - pattern="[ \\r\\n\\t\\S]+", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None userSelected: Annotated[ @@ -230,6 +230,50 @@ class Coding(BaseModel): ] = None +class NRLCoding(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 + system: Annotated[ + str, + Field( + description="The identification of the code system that defines the meaning of the symbol in the code.", + pattern="\\S+", + ), + ] + version: Annotated[ + Optional[str], + Field( + description="The version of the code system which was used when choosing this code. Note that a well–maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged.", + pattern="[\\S]+[ \\r\\n\\t\\S]*", + ), + ] = None + code: Annotated[ + str, + Field( + description="A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post–coordination).", + pattern="[^\\s]+(\\s[^\\s]+)*", + ), + ] + display: Annotated[ + str, + Field( + description="A representation of the meaning of the code in the system, following the rules of the system.", + pattern="[\\S]+[ \\r\\n\\t\\S]*", + ), + ] + userSelected: Annotated[ + Optional[bool], + Field( + description="Indicates that this coding was chosen by a user directly – e.g. off a pick list of available items (codes or displays)." + ), + ] = None + + class ContentStabilityExtensionCoding(Coding): system: Literal[ "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" @@ -302,14 +346,14 @@ class Quantity(BaseModel): Optional[str], Field( description="A human–readable form of the unit.", - pattern="[ \\r\\n\\t\\S]+", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None system: Annotated[ Optional[str], Field( description="The identification of the system that provides the coded form of the unit.", - pattern="\\S*", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None code: Annotated[ @@ -326,7 +370,7 @@ class ProfileItem(RootModel[str]): str, Field( description="A list of profiles (references to [StructureDefinition](structuredefinition.html#) resources) that this resource claims to conform to. The URL is a reference to [StructureDefinition.url](structuredefinition–definitions.html#StructureDefinition.url).", - pattern="\\S*", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] @@ -357,7 +401,7 @@ class Meta(BaseModel): Optional[str], Field( description="A uri that identifies the source system of the resource. This provides a minimal amount of [Provenance](provenance.html#) information that can be used to track or differentiate the source of information in the resource. The source may identify another FHIR server, document, message, database, etc.", - pattern="\\S*", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None profile: Optional[List[ProfileItem]] = None @@ -453,7 +497,25 @@ class CodeableConcept(BaseModel): 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]+", + pattern="[\\S]+[ \\r\\n\\t\\S]*", + ), + ] = None + + +class NRLCodeableConcept(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: Annotated[List[NRLCoding], Field(max_length=1, min_length=1)] + 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="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None @@ -657,12 +719,12 @@ class DocumentReference(BaseModel): Field(description="The status of the underlying document."), ] = None type: Annotated[ - Optional[CodeableConcept], + NRLCodeableConcept, Field( description="Specifies the particular kind of document referenced (e.g. History and Physical, Discharge Summary, Progress Note). This usually equates to the purpose of making the document referenced." ), - ] = None - category: Optional[List[CodeableConcept]] = None + ] + category: List[NRLCodeableConcept] subject: Annotated[ Optional[Reference], Field( @@ -694,7 +756,7 @@ class DocumentReference(BaseModel): Optional[str], Field( description="Human–readable description of the source document.", - pattern="[ \\r\\n\\t\\S]+", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None securityLabel: Optional[List[CodeableConcept]] = None @@ -877,7 +939,7 @@ class DocumentReferenceContext(BaseModel): Field(description="The kind of facility where the patient was seen."), ] = None practiceSetting: Annotated[ - CodeableConcept, + NRLCodeableConcept, Field( description="This property may convey specifics about the practice setting where the content was created, often reflecting the clinical specialty." ), @@ -936,14 +998,14 @@ class Identifier(BaseModel): Optional[str], Field( description="Establishes the namespace for the value – that is, a URL that describes a set values that are unique.", - pattern="\\S*", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None value: Annotated[ Optional[str], Field( description="The portion of the identifier typically relevant to the user and which is unique within the context of the system.", - pattern="[ \\r\\n\\t\\S]+", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None period: Annotated[ @@ -968,14 +1030,14 @@ class Reference(BaseModel): Optional[str], Field( description="A reference to a location at which the other resource is found. The reference may be a relative reference, in which case it is relative to the service base URL, or an absolute URL that resolves to the location where the resource is found. The reference may be version specific or not. If the reference is not to a FHIR RESTful server, then it should be assumed to be version specific. Internal fragment references (start with '#') refer to contained resources.", - pattern="[ \\r\\n\\t\\S]+", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None type: Annotated[ Optional[str], Field( description='The expected type of the target of the reference. If both Reference.type and Reference.reference are populated and Reference.reference is a FHIR URL, both SHALL be consistent.\nThe type is the Canonical URL of Resource Definition that is the type this reference refers to. References are URLs that are relative to http://hl7.org/fhir/StructureDefinition/ e.g. "Patient" is a reference to http://hl7.org/fhir/StructureDefinition/Patient. Absolute URLs are only allowed for logical models (and can only be used in references in logical models, not resources).', - pattern="\\S*", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None identifier: Annotated[ @@ -988,7 +1050,7 @@ class Reference(BaseModel): Optional[str], Field( description="Plain text narrative that identifies the resource in addition to the resource reference.", - pattern="[ \\r\\n\\t\\S]+", + pattern="[\\S]+[ \\r\\n\\t\\S]*", ), ] = None diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index 0344821fc..c283c66a4 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-13T11:19:28+00:00 +# timestamp: 2025-01-27T09:26:31+00:00 from __future__ import annotations @@ -207,6 +207,45 @@ class Coding(BaseModel): ] = None +class NRLCoding(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 + system: Annotated[ + StrictStr, + Field( + description="The identification of the code system that defines the meaning of the symbol in the code." + ), + ] + version: Annotated[ + Optional[StrictStr], + Field( + description="The version of the code system which was used when choosing this code. Note that a well–maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged." + ), + ] = None + code: Annotated[ + StrictStr, + Field( + description="A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post–coordination)." + ), + ] + display: Annotated[ + StrictStr, + Field( + description="A representation of the meaning of the code in the system, following the rules of the system." + ), + ] + userSelected: Annotated[ + Optional[StrictBool], + Field( + description="Indicates that this coding was chosen by a user directly – e.g. off a pick list of available items (codes or displays)." + ), + ] = None + + class ContentStabilityExtensionCoding(Coding): system: Literal[ "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" @@ -404,6 +443,22 @@ class CodeableConcept(BaseModel): ] = None +class NRLCodeableConcept(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: Annotated[List[NRLCoding], Field(max_length=1, min_length=1)] + 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], @@ -582,12 +637,12 @@ class DocumentReference(BaseModel): Field(description="The status of the underlying document."), ] = None type: Annotated[ - Optional[CodeableConcept], + NRLCodeableConcept, Field( description="Specifies the particular kind of document referenced (e.g. History and Physical, Discharge Summary, Progress Note). This usually equates to the purpose of making the document referenced." ), - ] = None - category: Optional[List[CodeableConcept]] = None + ] + category: List[NRLCodeableConcept] subject: Annotated[ Optional[Reference], Field( @@ -781,7 +836,7 @@ class DocumentReferenceContext(BaseModel): Field(description="The kind of facility where the patient was seen."), ] = None practiceSetting: Annotated[ - CodeableConcept, + NRLCodeableConcept, Field( description="This property may convey specifics about the practice setting where the content was created, often reflecting the clinical specialty." ), diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index af22ae1bc..9be08756b 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -900,3 +900,61 @@ Feature: Producer - createDocumentReference - Failure Scenarios ] } """ + + 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 | + | 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" + }, + "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": "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 '[^\\s]+(\\s[^\\s]+)*')", + "expression": [ + "content[0].attachment.contentType" + ] + } + """ From a4dee9f20f850ea73d224775b848129ec5f9e32e Mon Sep 17 00:00:00 2001 From: Kate Bobyn Date: Thu, 30 Jan 2025 11:38:58 +0000 Subject: [PATCH 03/10] NRL-1165 reflect model changes in smoke test --- tests/smoke/setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index f639e80f8..8b37d642f 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -18,6 +18,7 @@ DocumentReferenceContext, DocumentReferenceRelatesTo, Identifier, + NRLCodeableConcept, NRLFormatCode, Reference, ) @@ -107,7 +108,7 @@ def build_document_reference( ) ], context=DocumentReferenceContext( - practiceSetting=CodeableConcept( + practiceSetting=NRLCodeableConcept( coding=[ Coding( system="http://snomed.info/sct", From cf199d54dcce0acf56d7e7903bd3de14b0890244 Mon Sep 17 00:00:00 2001 From: Kate Bobyn Date: Thu, 30 Jan 2025 12:51:54 +0000 Subject: [PATCH 04/10] NRL-1165 reflect model changes in smoke test --- tests/smoke/setup.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index 8b37d642f..9bceec028 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -8,8 +8,6 @@ ) from nrlf.producer.fhir.r4.model import ( Attachment, - CodeableConcept, - Coding, ContentStabilityExtension, ContentStabilityExtensionCoding, ContentStabilityExtensionValueCodeableConcept, @@ -19,6 +17,7 @@ DocumentReferenceRelatesTo, Identifier, NRLCodeableConcept, + NRLCoding, NRLFormatCode, Reference, ) @@ -66,9 +65,9 @@ def build_document_reference( ], ) ], - type=CodeableConcept( + type=NRLCodeableConcept( coding=[ - Coding( + NRLCoding( system="http://snomed.info/sct", code=type, display=TYPE_ATTRIBUTES.get(f"http://snomed.info/sct|{type}").get( @@ -95,9 +94,9 @@ def build_document_reference( ) ], category=[ - CodeableConcept( + NRLCodeableConcept( coding=[ - Coding( + NRLCoding( system="http://snomed.info/sct", code=category, display=( @@ -110,7 +109,7 @@ def build_document_reference( context=DocumentReferenceContext( practiceSetting=NRLCodeableConcept( coding=[ - Coding( + NRLCoding( system="http://snomed.info/sct", code="224891009", display="Healthcare services", From b370d84618b8b83d50a592c1ec3b6aadf1da9e50 Mon Sep 17 00:00:00 2001 From: Kate Bobyn Date: Fri, 31 Jan 2025 09:25:52 +0000 Subject: [PATCH 05/10] NRL-1165 reflect model changes in feature test utils --- tests/features/utils/data.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/features/utils/data.py b/tests/features/utils/data.py index 35c3eb393..33749da65 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -10,7 +10,6 @@ from nrlf.producer.fhir.r4.model import ( Attachment, CodeableConcept, - Coding, ContentStabilityExtension, ContentStabilityExtensionCoding, ContentStabilityExtensionValueCodeableConcept, @@ -19,6 +18,8 @@ DocumentReferenceContext, DocumentReferenceRelatesTo, Identifier, + NRLCodeableConcept, + NRLCoding, NRLFormatCode, Reference, ) @@ -80,9 +81,9 @@ def create_test_document_reference(items: dict) -> DocumentReference: ], ), context=DocumentReferenceContext( - practiceSetting=CodeableConcept( + practiceSetting=NRLCodeableConcept( coding=[ - Coding( + NRLCoding( system=SNOMED_SYSTEM_URL, code=str(practice_setting_code), display=practice_setting_display, @@ -103,7 +104,7 @@ def create_test_document_reference(items: dict) -> DocumentReference: ) base_doc_ref.type = CodeableConcept( - coding=[Coding(system=type_system, code=type_code, display=type_display)] + coding=[NRLCoding(system=type_system, code=type_code, display=type_display)] ) if items.get("subject"): @@ -136,9 +137,9 @@ def create_test_document_reference(items: dict) -> DocumentReference: f"{SNOMED_SYSTEM_URL}|{items['category']}", {} ).get("display") base_doc_ref.category = [ - CodeableConcept( + NRLCodeableConcept( coding=[ - Coding( + NRLCoding( system=SNOMED_SYSTEM_URL, code=items["category"], display=category_display, From bcd3eb6bea8c31713e63da1e8c7180afabb3a02c Mon Sep 17 00:00:00 2001 From: Kate Bobyn Date: Fri, 31 Jan 2025 10:13:44 +0000 Subject: [PATCH 06/10] NRL-1165 reflect model changes in feature test utils --- 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 33749da65..cd6a7262d 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -103,7 +103,7 @@ def create_test_document_reference(items: dict) -> DocumentReference: "type_display", TYPE_ATTRIBUTES.get(type_str, {}).get("display") ) - base_doc_ref.type = CodeableConcept( + base_doc_ref.type = NRLCodeableConcept( coding=[NRLCoding(system=type_system, code=type_code, display=type_display)] ) From 96a927a3ba825e97eda8060e524cc9609ad12486 Mon Sep 17 00:00:00 2001 From: Kate Bobyn Date: Sun, 2 Feb 2025 22:49:54 +0000 Subject: [PATCH 07/10] NRL-1165 reflect model changes in feature test scenarios --- .../createDocumentReference-failure.feature | 93 +++++++++---------- .../upsertDocumentReference-failure.feature | 41 ++++---- 2 files changed, 67 insertions(+), 67 deletions(-) diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 9be08756b..adea1d465 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -545,15 +545,16 @@ Feature: Producer - createDocumentReference - Failure Scenarios | system | value | | http://snomed.info/sct | 736253002 | When producer 'ANGY1' creates a DocumentReference with values: - | property | value | - | subject | 9278693472 | - | status | current | - | type_system | http://invalidsystem.info/sct | - | type | 736253002 | - | category | 734163000 | - | custodian | ANGY1 | - | author | HAR1 | - | url | https://example.org/my-doc.pdf | + | property | value | + | subject | 9278693472 | + | status | current | + | type_system | http://invalidsystem.info/sct | + | type_display | Mental health crisis plan | + | type | 736253002 | + | category | 734163000 | + | custodian | ANGY1 | + | author | HAR1 | + | url | https://example.org/my-doc.pdf | Then the response status code is 400 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: @@ -581,14 +582,15 @@ Feature: Producer - createDocumentReference - Failure Scenarios | system | value | | http://snomed.info/sct | 736253002 | When producer 'ANGY1' creates a DocumentReference with values: - | property | value | - | subject | 9999999999 | - | status | current | - | type | invalid | - | category | 734163000 | - | custodian | ANGY1 | - | author | HAR1 | - | url | https://example.org/my-doc.pdf | + | property | value | + | subject | 9999999999 | + | status | current | + | type | invalid | + | type_display | Mental health crisis plan | + | category | 734163000 | + | custodian | ANGY1 | + | author | HAR1 | + | url | https://example.org/my-doc.pdf | Then the response status code is 400 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: @@ -714,6 +716,8 @@ Feature: Producer - createDocumentReference - Failure Scenarios } } """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue Scenario: Missing content Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API @@ -901,43 +905,36 @@ Feature: Producer - createDocumentReference - Failure Scenarios } """ - Scenario: contentType empty string + Scenario: codings with empty string or leading whitespace 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: + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'context' 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" - }, - "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": { + "practiceSetting": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "788002001", + "display": "" + } + ] + }, + "facilityType": { + "coding": [ + { + "system": " system", + "code": "1234", + "display": "Tertiary care facility" + } ] + } + } """ Then the response status code is 400 - And the response is an OperationOutcome with 1 issue + And the response is an OperationOutcome with 2 issues And the OperationOutcome contains the issue: """ { @@ -952,9 +949,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 (context.practiceSetting.coding[0].display: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", "expression": [ - "content[0].attachment.contentType" + "context.practiceSetting.coding[0].display" ] } """ diff --git a/tests/features/producer/upsertDocumentReference-failure.feature b/tests/features/producer/upsertDocumentReference-failure.feature index 3855b1b34..b86cdf60c 100644 --- a/tests/features/producer/upsertDocumentReference-failure.feature +++ b/tests/features/producer/upsertDocumentReference-failure.feature @@ -83,16 +83,17 @@ Feature: Producer - upsertDocumentReference - Failure Scenarios | system | value | | http://snomed.info/sct | 736253002 | When producer 'ANGY1' upserts a DocumentReference with values: - | property | value | - | id | X26-testid-upsert-0001-0001 | - | subject | 9278693472 | - | status | current | - | type_system | http://invalidsystem.info/sct | - | type | 736253002 | - | category | 734163000 | - | custodian | ANGY1 | - | author | HAR1 | - | url | https://example.org/my-doc.pdf | + | property | value | + | id | X26-testid-upsert-0001-0001 | + | subject | 9278693472 | + | status | current | + | type_system | http://invalidsystem.info/sct | + | type_display | Mental health crisis plan | + | type | 736253002 | + | category | 734163000 | + | custodian | ANGY1 | + | author | HAR1 | + | url | https://example.org/my-doc.pdf | Then the response status code is 400 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: @@ -120,15 +121,17 @@ Feature: Producer - upsertDocumentReference - Failure Scenarios | system | value | | http://snomed.info/sct | 736253002 | When producer 'ANGY1' upserts a DocumentReference with values: - | property | value | - | id | X26-testid-upsert-0001-0001 | - | subject | 9999999999 | - | status | current | - | type | invalid | - | category | 734163000 | - | custodian | ANGY1 | - | author | HAR1 | - | url | https://example.org/my-doc.pdf | + | property | value | + | id | X26-testid-upsert-0001-0001 | + | subject | 9999999999 | + | status | current | + | type | invalid | + | type_system | http://snomed.info/sct | + | type_display | Mental health crisis plan | + | category | 734163000 | + | custodian | ANGY1 | + | author | HAR1 | + | url | https://example.org/my-doc.pdf | Then the response status code is 400 And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: From 8fa502508630c9f6300a4b161b455fede126aaba Mon Sep 17 00:00:00 2001 From: Kate Bobyn Date: Sun, 2 Feb 2025 23:07:40 +0000 Subject: [PATCH 08/10] NRL-1165 reflect model changes in feature test scenarios --- .../producer/createDocumentReference-failure.feature | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index adea1d465..0fe791004 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -927,14 +927,13 @@ Feature: Producer - createDocumentReference - Failure Scenarios { "system": " system", "code": "1234", - "display": "Tertiary care facility" } ] } } """ Then the response status code is 400 - And the response is an OperationOutcome with 2 issues + And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: """ { @@ -949,9 +948,9 @@ Feature: Producer - createDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Request body could not be parsed (context.practiceSetting.coding[0].display: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", + "diagnostics": "Request body could not be parsed (context.practiceSetting.coding[0].system: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", "expression": [ - "context.practiceSetting.coding[0].display" + "context.practiceSetting.coding[0].system" ] } """ From 4cc04fb5a9da79fa0d7f5a25fd5c19921f232c46 Mon Sep 17 00:00:00 2001 From: Kate Bobyn Date: Mon, 3 Feb 2025 01:41:01 +0000 Subject: [PATCH 09/10] NRL-1165 add feature test for regex validation on non-mandatory segments --- .../createDocumentReference-failure.feature | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 0fe791004..58fd95bf4 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -926,7 +926,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios "coding": [ { "system": " system", - "code": "1234", + "code": "1234" } ] } @@ -948,9 +948,29 @@ Feature: Producer - createDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Request body could not be parsed (context.practiceSetting.coding[0].system: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", + "diagnostics": "Request body could not be parsed (context.facilityType.coding[0].system: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", "expression": [ - "context.practiceSetting.coding[0].system" + "context.facilityType.coding[0].system" + ] + } + """ + 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 (context.practiceSetting.coding[0].display: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", + "expression": [ + "context.practiceSetting.coding[0].display" ] } """ From 64ae355318f7824e5d3655049a57882a25106f27 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Tue, 4 Feb 2025 08:09:42 +0000 Subject: [PATCH 10/10] [NRL-1165] Fix failing integ test. Fix sonarcloud warnings --- layer/nrlf/core/tests/test_pydantic_errors.py | 15 +++--- .../createDocumentReference-failure.feature | 54 ++++++------------- 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index b3739b16d..033961446 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -1,5 +1,6 @@ import pytest +from nrlf.core.constants import SNOMED_SYSTEM_URL from nrlf.core.errors import ParseError from nrlf.core.validators import DocumentReferenceValidator from nrlf.tests.data import load_document_reference_json @@ -310,17 +311,17 @@ def test_validate_multiple_codings(): document_ref_data["category"][0] = { "coding": [ { - "system": "http://snomed.info/sct", + "system": SNOMED_SYSTEM_URL, "code": "734163000", "display": "Care plan", }, { - "system": "http://snomed.info/sct", + "system": SNOMED_SYSTEM_URL, "code": "734163000", "display": "Care plan", }, { - "system": "http://snomed.info/sct", + "system": SNOMED_SYSTEM_URL, "code": "734163000", "display": "Care plan", }, @@ -384,7 +385,7 @@ def test_validate_empty_strings(): document_ref_data["category"][0] = { "coding": [ { - "system": "http://snomed.info/sct", + "system": SNOMED_SYSTEM_URL, "code": "734163000", "display": "", } @@ -420,7 +421,7 @@ def test_validate_whitespace_strings(): document_ref_data["category"][0] = { "coding": [ { - "system": "http://snomed.info/sct", + "system": SNOMED_SYSTEM_URL, "code": "734163000", "display": " ", } @@ -534,7 +535,7 @@ def test_validate_missing_code_from_coding_where_mandatory(): document_ref_data["context"]["practiceSetting"] = { "coding": [ { - "system": "http://snomed.info/sct", + "system": SNOMED_SYSTEM_URL, "display": "Valid display string", } ] @@ -569,7 +570,7 @@ def test_validate_missing_display_from_coding_where_mandatory(): document_ref_data["context"]["practiceSetting"] = { "coding": [ { - "system": "http://snomed.info/sct", + "system": SNOMED_SYSTEM_URL, "code": "788002001", } ] diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 58fd95bf4..61bd0db46 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -913,47 +913,27 @@ Feature: Producer - createDocumentReference - Failure Scenarios When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'context' is: """ "context": { - "practiceSetting": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "788002001", - "display": "" - } - ] - }, - "facilityType": { - "coding": [ - { - "system": " system", - "code": "1234" - } - ] - } - } - """ - 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": [ + "practiceSetting": { + "coding": [ { - "system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1", - "code": "MESSAGE_NOT_WELL_FORMED", - "display": "Message not well formed" + "system": "http://snomed.info/sct", + "code": "788002001", + "display": "" } - ] + ] }, - "diagnostics": "Request body could not be parsed (context.facilityType.coding[0].system: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", - "expression": [ - "context.facilityType.coding[0].system" - ] + "facilityType": { + "coding": [ + { + "system": " system", + "code": "1234" + } + ] + } } """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue And the OperationOutcome contains the issue: """ { @@ -968,7 +948,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Request body could not be parsed (context.practiceSetting.coding[0].display: String should match pattern '[^\\s]+(\\s[^\\s]+)*')", + "diagnostics": "Request body could not be parsed (context.practiceSetting.coding[0].display: String should match pattern '[\\S]+[ \\r\\n\\t\\S]*')", "expression": [ "context.practiceSetting.coding[0].display" ]