diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 632dca1bf..8d6947d7f 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1065,15 +1065,19 @@ 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 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: @@ -1130,6 +1134,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: @@ -1146,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: @@ -1187,6 +1189,78 @@ components: type: string pattern: \S* description: The reference details for the link. + ContentStabilityExtension: + 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: + $ref: "#/components/schemas/ContentStabilityExtensionCoding" + minItems: 1 + maxItems: 1 + required: + - coding + 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: + 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 642932212..d1b7801fa 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,15 +1630,19 @@ 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 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: @@ -1695,6 +1699,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: @@ -1711,12 +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: @@ -1753,6 +1754,78 @@ components: type: string pattern: \S* description: The reference details for the link. + ContentStabilityExtension: + 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: + $ref: "#/components/schemas/ContentStabilityExtensionCoding" + minItems: 1 + maxItems: 1 + required: + - coding + 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: + 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 4665533b5..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-11-20T09:43:58+00:00 +# timestamp: 2024-12-12T13:19:56+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]+)*", ), - ] = 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( @@ -230,6 +230,29 @@ class Coding(BaseModel): ] = None +class ContentStabilityExtensionCoding(Coding): + system: Literal[ + "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" + ] + code: Literal["static", "dynamic"] + display: Literal["Static", "Dynamic"] + + +class NRLFormatCode(Coding): + 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], @@ -427,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 @@ -451,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[ @@ -489,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( @@ -497,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): @@ -818,29 +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[ - Optional[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 - - class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[str], @@ -861,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], @@ -1026,13 +1063,9 @@ class Signature(BaseModel): ] = None -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() diff --git a/layer/nrlf/core/constants.py b/layer/nrlf/core/constants.py index 4280f7a6d..d7b94f366 100644 --- a/layer/nrlf/core/constants.py +++ b/layer/nrlf/core/constants.py @@ -657,3 +657,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 f5bdc3a47..615ef9a2a 100644 --- a/layer/nrlf/core/errors.py +++ b/layer/nrlf/core/errors.py @@ -3,22 +3,45 @@ 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 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 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 f". See ValueSet: {CONTENT_STABILITY_SYSTEM_URL}" + if "format" in loc_string: + return f". See ValueSet: {CONTENT_FORMAT_CODE_URL}" + + return "" + + def diag_for_error(error: ErrorDetails) -> str: - if error["loc"]: - loc_string = ".".join(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"]) + 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]: - return str(".".join(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 new file mode 100644 index 000000000..7ff69e6be --- /dev/null +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -0,0 +1,303 @@ +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_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. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode)", + "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. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", + "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'. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", + "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'. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", + "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. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", + "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. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", + "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 f41e45c2c..9041a3c6b 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -790,47 +790,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") @@ -965,195 +924,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") - - 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", - } - ] - }, - } - - 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 the same as code either '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") @@ -1533,6 +1303,144 @@ def test_validate_ssp_content_with_multiple_asids(): } +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_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"], + } + + +@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"], + } + + def test_validate_practiceSetting_no_coding(): 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 bef650299..57f685609 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -137,9 +137,9 @@ 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) + self._validate_content_extension(resource) self._validate_practiceSetting(resource) - if resource.content[0].extension: - self._validate_content_extension(resource) except StopValidationError: logger.log(LogReference.VALIDATOR003) @@ -486,72 +486,16 @@ 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].valueCodeableConcept.coding[0].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'", - field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].system", - ) - return - - if content.extension[0].valueCodeableConcept.coding[0].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'", - 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() - ): + coding = content.extension[0].valueCodeableConcept.coding[0] + if coding.code != coding.display.lower(): 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 the same as code either '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" - ): - 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 - def _validate_author(self, model: DocumentReference): """ Validate the author field contains an appropriate coding system and code. @@ -660,3 +604,35 @@ def _validate_practiceSetting(self, model: DocumentReference): field="context.practiceSetting.coding[0]", ) 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") + + 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( + 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", + ) + + # 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", + ) diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index d96b7ce73..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-11-20T10:10:52+00:00 +# timestamp: 2024-12-12T13:19:54+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]+)*", ), - ] = 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( @@ -230,6 +230,29 @@ class Coding(BaseModel): ] = None +class ContentStabilityExtensionCoding(Coding): + system: Literal[ + "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" + ] + code: Literal["static", "dynamic"] + display: Literal["Static", "Dynamic"] + + +class NRLFormatCode(Coding): + 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], @@ -417,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 @@ -432,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[ @@ -470,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( @@ -478,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): @@ -802,29 +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[ - Optional[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 - - class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[str], @@ -845,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], @@ -1010,13 +1047,9 @@ class Signature(BaseModel): ] = None -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() diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index e4edefc58..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-11-20T10:10:54+00:00 +# timestamp: 2024-12-12T13:19:55+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( @@ -208,6 +207,29 @@ class Coding(BaseModel): ] = None +class ContentStabilityExtensionCoding(Coding): + system: Literal[ + "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" + ] + code: Literal["static", "dynamic"] + display: Literal["Static", "Dynamic"] + + +class NRLFormatCode(Coding): + 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], @@ -366,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 @@ -381,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[ @@ -414,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): @@ -707,28 +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[ - Optional[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 - - class DocumentReferenceRelatesTo(BaseModel): id: Annotated[ Optional[StrictStr], @@ -747,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], @@ -890,13 +926,9 @@ class Signature(BaseModel): ] = None -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() 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" + } + ] + } + ] + } +} 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" + } + ] + } + ] + } +} 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/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 4341faf86..8380e0879 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 @@ -276,13 +276,40 @@ 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. (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 + * 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" + } + ] + } + } + ] } ] ``` diff --git a/tests/data/DocumentReference/RQI-736253002-Valid.json b/tests/data/DocumentReference/RQI-736253002-Valid.json index db7503fbf..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": [ { @@ -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-with-date-and-meta-lastupdated.json b/tests/data/DocumentReference/Y05868-736253002-Valid-with-date-and-meta-lastupdated.json index 2bf5f63df..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,8 +73,22 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" - } + "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..49e5484ca 100644 --- a/tests/data/DocumentReference/Y05868-736253002-Valid-with-date.json +++ b/tests/data/DocumentReference/Y05868-736253002-Valid-with-date.json @@ -70,8 +70,22 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" - } + "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..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,8 +72,22 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" - } + "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..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,8 +69,22 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" - } + "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.json b/tests/data/DocumentReference/Y05868-736253002-Valid.json index b0a18b1d5..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": [ { @@ -79,7 +79,7 @@ { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", "code": "static", - "display": "static" + "display": "Static" } ] } diff --git a/tests/features/consumer/readDocumentReference-success.feature b/tests/features/consumer/readDocumentReference-success.feature index f0854af16..aa91cd59f 100644 --- a/tests/features/consumer/readDocumentReference-success.feature +++ b/tests/features/consumer/readDocumentReference-success.feature @@ -69,7 +69,26 @@ 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" + }, + "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": { @@ -155,7 +174,26 @@ 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" + }, + "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 d407845b7..17c79b3a1 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -614,3 +614,189 @@ 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: 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" + ] + } + """ + + 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" + ] + } + """ + + 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/readDocumentReference-success.feature b/tests/features/producer/readDocumentReference-success.feature index 2eb19448b..9e4c2f8d7 100644 --- a/tests/features/producer/readDocumentReference-success.feature +++ b/tests/features/producer/readDocumentReference-success.feature @@ -71,7 +71,26 @@ 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" + }, + "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 b909b46cf..20a8bed4f 100644 --- a/tests/features/producer/updateDocumentReference-failure.feature +++ b/tests/features/producer/updateDocumentReference-failure.feature @@ -48,3 +48,261 @@ 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: 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 | + 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" + }, + "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" + ] + } + """ + + 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" + }, + "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 contentType: application/invalid. Must be 'application/pdf' or 'text/html'", + "expression": [ + "content[0].attachment.contentType" + ] + } + """ + + 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 3fb5d6ebd..c4fc4ea68 100644 --- a/tests/features/producer/upsertDocumentReference-failure.feature +++ b/tests/features/producer/upsertDocumentReference-failure.feature @@ -236,3 +236,190 @@ 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: 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 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" + }, + "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" + ] + } + """ + + 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" + ] + } + """ + + 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" + ] + } + """ diff --git a/tests/features/steps/2_request.py b/tests/features/steps/2_request.py index a008aed17..993fae1c7 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 @@ -94,17 +96,21 @@ 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, + 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(section, context.text) - context.response = client.create_text(doc_ref) + 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: doc_ref_id = context.response.headers["Location"].split("/")[-1] @@ -114,6 +120,42 @@ 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", 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", 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) diff --git a/tests/features/utils/constants.py b/tests/features/utils/constants.py index 7c656824e..2f0e4b2f3 100644 --- a/tests/features/utils/constants.py +++ b/tests/features/utils/constants.py @@ -112,8 +112,22 @@ "format": { "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", "code": "urn:nhs-ic:unstructured", - "display": "Unstructured document" - } + "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 5bc300585..94be4f427 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -1,5 +1,8 @@ from layer.nrlf.core.constants import ( CATEGORY_ATTRIBUTES, + CONTENT_FORMAT_CODE_URL, + CONTENT_STABILITY_EXTENSION_URL, + CONTENT_STABILITY_SYSTEM_URL, SNOMED_PRACTICE_SETTINGS, SNOMED_SYSTEM_URL, TYPE_ATTRIBUTES, @@ -8,11 +11,15 @@ Attachment, CodeableConcept, Coding, + ContentStabilityExtension, + ContentStabilityExtensionCoding, + ContentStabilityExtensionValueCodeableConcept, DocumentReference, DocumentReferenceContent, DocumentReferenceContext, DocumentReferenceRelatesTo, Identifier, + NRLFormatCode, Reference, ) from tests.features.utils.constants import ( @@ -39,14 +46,39 @@ 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/pdf"), + url=items["url"], + ), + format=NRLFormatCode( + system=items.get( + "formatSystem", + CONTENT_FORMAT_CODE_URL, + ), + code=items.get("formatCode", "urn:nhs-ic:unstructured"), + display=items.get("formatDisplay", "Unstructured Document"), + ), + extension=[ + ContentStabilityExtension( + url=CONTENT_STABILITY_EXTENSION_URL, + valueCodeableConcept=ContentStabilityExtensionValueCodeableConcept( + coding=[ + ContentStabilityExtensionCoding( + system=CONTENT_STABILITY_SYSTEM_URL, + code="static", + display="Static", + ) + ] + ), + ) + ], ) - ) - ], + ], + ), context=DocumentReferenceContext( practiceSetting=CodeableConcept( coding=[ diff --git a/tests/smoke/setup.py b/tests/smoke/setup.py index 69e1f6358..f639e80f8 100644 --- a/tests/smoke/setup.py +++ b/tests/smoke/setup.py @@ -1,13 +1,24 @@ -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, Coding, + ContentStabilityExtension, + ContentStabilityExtensionCoding, + ContentStabilityExtensionValueCodeableConcept, DocumentReference, DocumentReferenceContent, DocumentReferenceContext, DocumentReferenceRelatesTo, Identifier, + NRLFormatCode, Reference, ) from tests.utilities.api_clients import ProducerTestClient @@ -20,7 +31,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: @@ -32,7 +43,26 @@ def build_document_reference( attachment=Attachment( contentType=content_type, url=content_url, - ) + ), + format=NRLFormatCode( + system=CONTENT_FORMAT_CODE_URL, + code="urn:nhs-ic:unstructured", + display="Unstructured Document", + ), + extension=[ + ContentStabilityExtension( + url=CONTENT_STABILITY_EXTENSION_URL, + valueCodeableConcept=ContentStabilityExtensionValueCodeableConcept( + coding=[ + ContentStabilityExtensionCoding( + system=CONTENT_STABILITY_SYSTEM_URL, + code="static", + display="Static", + ) + ] + ), + ) + ], ) ], type=CodeableConcept( 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}",