diff --git a/Makefile b/Makefile index bf2003ca6..af0ba0a71 100644 --- a/Makefile +++ b/Makefile @@ -198,18 +198,22 @@ generate-models: check-warn ## Generate Pydantic Models --input ./api/producer/swagger.yaml \ --input-file-type openapi \ --output ./layer/nrlf/producer/fhir/r4/model.py \ - --output-model-type "pydantic_v2.BaseModel" + --output-model-type "pydantic_v2.BaseModel" \ + --base-class nrlf.core.parent_model.Parent poetry run datamodel-codegen \ --strict-types {str,bytes,int,float,bool} \ --input ./api/producer/swagger.yaml \ --input-file-type openapi \ --output ./layer/nrlf/producer/fhir/r4/strict_model.py \ + --base-class nrlf.core.parent_model.Parent \ --output-model-type "pydantic_v2.BaseModel" + @echo "Generating consumer model" mkdir -p ./layer/nrlf/consumer/fhir/r4 poetry run datamodel-codegen \ --input ./api/consumer/swagger.yaml \ --input-file-type openapi \ --output ./layer/nrlf/consumer/fhir/r4/model.py \ + --base-class nrlf.core.parent_model.Parent \ --output-model-type "pydantic_v2.BaseModel" diff --git a/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py b/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py index 88d1757c8..9643dad18 100644 --- a/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py +++ b/api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py @@ -3,6 +3,7 @@ from moto import mock_aws from api.consumer.searchDocumentReference.search_document_reference import handler +from nrlf.consumer.fhir.r4.model import CodeableConcept, Identifier from nrlf.core.constants import ( CATEGORY_ATTRIBUTES, TYPE_ATTRIBUTES, @@ -66,7 +67,9 @@ def test_search_document_reference_accession_number_in_pointer( ): doc_ref = load_document_reference("Y05868-736253002-Valid") doc_ref.identifier = [ - {"type": {"text": "Accession-Number"}, "value": "Y05868.123456789"} + Identifier( + type=CodeableConcept(text="Accession-Number"), value="Y05868.123456789" + ) ] doc_pointer = DocumentPointer.from_document_reference(doc_ref) repository.create(doc_pointer) diff --git a/api/producer/updateDocumentReference/tests/test_update_document_reference.py b/api/producer/updateDocumentReference/tests/test_update_document_reference.py index d60cfcd57..d2f06b669 100644 --- a/api/producer/updateDocumentReference/tests/test_update_document_reference.py +++ b/api/producer/updateDocumentReference/tests/test_update_document_reference.py @@ -592,7 +592,6 @@ def test_update_document_reference_immutable_fields(repository): ) ], text=None, - extension=None, ) event = create_test_api_gateway_event( diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index ffc1667f8..5f9b911a6 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,12 +1,14 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-01-27T09:26:33+00:00 +# timestamp: 2025-02-07T14:10:39+00:00 from __future__ import annotations from typing import Annotated, List, Literal, Optional -from pydantic import BaseModel, Field, RootModel +from pydantic import Field, RootModel + +from nrlf.core.parent_model import Parent class LocationItem(RootModel[str]): @@ -29,7 +31,7 @@ class ExpressionItem(RootModel[str]): ] -class BundleEntryRequest(BaseModel): +class BundleEntryRequest(Parent): id: Annotated[ Optional[str], Field( @@ -81,7 +83,7 @@ class BundleEntryRequest(BaseModel): ] = None -class BundleEntrySearch(BaseModel): +class BundleEntrySearch(Parent): id: Annotated[ Optional[str], Field( @@ -104,7 +106,7 @@ class BundleEntrySearch(BaseModel): ] = None -class BundleLink(BaseModel): +class BundleLink(Parent): id: Annotated[ Optional[str], Field( @@ -124,7 +126,7 @@ class BundleLink(BaseModel): ] -class Attachment(BaseModel): +class Attachment(Parent): id: Annotated[ Optional[str], Field( @@ -186,7 +188,7 @@ class Attachment(BaseModel): ] = None -class Coding(BaseModel): +class Coding(Parent): id: Annotated[ Optional[str], Field( @@ -253,7 +255,7 @@ class NRLFormatCode(Coding): ] -class Period(BaseModel): +class Period(Parent): id: Annotated[ Optional[str], Field( @@ -277,7 +279,7 @@ class Period(BaseModel): ] = None -class Quantity(BaseModel): +class Quantity(Parent): id: Annotated[ Optional[str], Field( @@ -331,7 +333,7 @@ class ProfileItem(RootModel[str]): ] -class Meta(BaseModel): +class Meta(Parent): id: Annotated[ Optional[str], Field( @@ -365,7 +367,7 @@ class Meta(BaseModel): tag: Optional[List[Coding]] = None -class Narrative(BaseModel): +class Narrative(Parent): id: Annotated[ Optional[str], Field( @@ -392,7 +394,7 @@ class DocumentId(RootModel[str]): root: Annotated[str, Field(pattern="[A-Za-z0-9\\-\\.]{1,64}")] -class RequestPathParams(BaseModel): +class RequestPathParams(Parent): id: DocumentId @@ -450,7 +452,7 @@ class RequestHeaderCorrelationId(RootModel[str]): root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] -class CodeableConcept(BaseModel): +class CodeableConcept(Parent): id: Annotated[ Optional[str], Field( @@ -468,7 +470,7 @@ class CodeableConcept(BaseModel): ] = None -class Extension(BaseModel): +class Extension(Parent): valueCodeableConcept: Annotated[ Optional[CodeableConcept], Field( @@ -487,11 +489,11 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] -class RequestHeader(BaseModel): +class RequestHeader(Parent): odsCode: RequestHeaderOdsCode -class RequestParams(BaseModel): +class RequestParams(Parent): subject_identifier: Annotated[ RequestQuerySubject, Field(alias="subject:identifier") ] @@ -505,13 +507,13 @@ class RequestParams(BaseModel): ] = None -class CountRequestParams(BaseModel): +class CountRequestParams(Parent): subject_identifier: Annotated[ RequestQuerySubject, Field(alias="subject:identifier") ] -class OperationOutcomeIssue(BaseModel): +class OperationOutcomeIssue(Parent): id: Annotated[ Optional[str], Field( @@ -557,7 +559,7 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept -class OperationOutcome(BaseModel): +class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ Optional[str], @@ -595,7 +597,7 @@ class OperationOutcome(BaseModel): issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)] -class DocumentReferenceContent(BaseModel): +class DocumentReferenceContent(Parent): id: Annotated[ Optional[str], Field( @@ -620,7 +622,7 @@ class DocumentReferenceContent(BaseModel): ] -class DocumentReference(BaseModel): +class DocumentReference(Parent): resourceType: Literal["DocumentReference"] id: Annotated[ Optional[str], @@ -721,7 +723,7 @@ class DocumentReference(BaseModel): ] = None -class Bundle(BaseModel): +class Bundle(Parent): resourceType: Literal["Bundle"] id: Annotated[ Optional[str], @@ -786,7 +788,7 @@ class Bundle(BaseModel): ] = None -class BundleEntry(BaseModel): +class BundleEntry(Parent): id: Annotated[ Optional[str], Field( @@ -828,7 +830,7 @@ class BundleEntry(BaseModel): ] = None -class BundleEntryResponse(BaseModel): +class BundleEntryResponse(Parent): id: Annotated[ Optional[str], Field( @@ -872,7 +874,7 @@ class BundleEntryResponse(BaseModel): ] = None -class DocumentReferenceContext(BaseModel): +class DocumentReferenceContext(Parent): id: Annotated[ Optional[str], Field( @@ -907,7 +909,7 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceRelatesTo(BaseModel): +class DocumentReferenceRelatesTo(Parent): id: Annotated[ Optional[str], Field( @@ -927,7 +929,7 @@ class DocumentReferenceRelatesTo(BaseModel): ] -class Identifier(BaseModel): +class Identifier(Parent): id: Annotated[ Optional[str], Field( @@ -972,7 +974,7 @@ class Identifier(BaseModel): ] = None -class Reference(BaseModel): +class Reference(Parent): id: Annotated[ Optional[str], Field( @@ -1009,7 +1011,7 @@ class Reference(BaseModel): ] = None -class Signature(BaseModel): +class Signature(Parent): id: Annotated[ Optional[str], Field( diff --git a/layer/nrlf/core/parent_model.py b/layer/nrlf/core/parent_model.py new file mode 100644 index 000000000..3c18be72c --- /dev/null +++ b/layer/nrlf/core/parent_model.py @@ -0,0 +1,88 @@ +from typing import Annotated, List, Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class ParentCoding(BaseModel): + model_config = ConfigDict(regex_engine="python-re", extra="forbid") + id: Annotated[ + Optional[str], + Field( + description="Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + pattern="[A-Za-z0-9\\-\\.]{1,64}", + ), + ] = None + system: Annotated[ + Optional[str], + Field( + description="The identification of the code system that defines the meaning of the symbol in the code.", + pattern="\\S*", + ), + ] = None + version: Annotated[ + Optional[str], + Field( + description="The version of the code system which was used when choosing this code. Note that a well–maintained code system does not need the version reported, because the meaning of codes is consistent across versions. However this cannot consistently be assured, and when the meaning is not guaranteed to be consistent, the version SHOULD be exchanged.", + pattern="[ \\r\\n\\t\\S]+", + ), + ] = None + code: Annotated[ + Optional[str], + Field( + description="A symbol in syntax defined by the system. The symbol may be a predefined code or an expression in a syntax defined by the coding system (e.g. post–coordination).", + pattern="[^\\s]+(\\s[^\\s]+)*", + ), + ] = None + display: Annotated[ + Optional[str], + Field( + description="A representation of the meaning of the code in the system, following the rules of the system.", + pattern="[ \\r\\n\\t\\S]+", + ), + ] = None + userSelected: Annotated[ + Optional[bool], + Field( + description="Indicates that this coding was chosen by a user directly – e.g. off a pick list of available items (codes or displays)." + ), + ] = None + + +class ParentCodeableConcept(BaseModel): + model_config = ConfigDict(regex_engine="python-re", extra="forbid") + 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[ParentCoding]] = 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 ParentExtension(BaseModel): + valueCodeableConcept: Annotated[ + Optional[ParentCodeableConcept], + 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 Parent(BaseModel): + model_config = ConfigDict(regex_engine="python-re", extra="forbid") + extension: Annotated[ + Optional[List[ParentExtension]], + Field(description="A list of relevant extensions"), + ] = None diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index e28653576..8c5c36e0f 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -281,12 +281,40 @@ def test_validate_document_reference_extra_fields(): document_ref_data["extra_field"] = "extra_value" - result = validator.validate(document_ref_data) + with pytest.raises(ParseError) as error: + 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) == { + 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 (extra_field: Extra inputs are not permitted)", + "expression": ["extra_field"], + } + + +def test_validate_document_reference_extra_fields_content(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["extra_field"] = "extra_value" + + 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": { @@ -298,7 +326,8 @@ def test_validate_document_reference_extra_fields(): } ] }, - "diagnostics": "The resource contains extra fields", + "diagnostics": "Failed to parse DocumentReference resource (content[0].extra_field: Extra inputs are not permitted)", + "expression": ["content[0].extra_field"], } diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index cb8140f1e..94c086afa 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -129,7 +129,6 @@ def validate(self, data: Dict[str, Any] | DocumentReference): try: self._validate_required_fields(resource) - self._validate_no_extra_fields(resource, data) self._validate_identifiers(resource) self._validate_relates_to(resource) self._validate_ssp_asid(resource) @@ -174,29 +173,6 @@ def _validate_required_fields(self, model: DocumentReference): if not self.result.is_valid: raise StopValidationError() - def _validate_no_extra_fields( - self, resource: DocumentReference, data: Dict[str, Any] | DocumentReference - ): - """ - Validate that there are no extra fields - """ - logger.log(LogReference.VALIDATOR001, step="no_extra_fields") - has_extra_fields = False - - if isinstance(data, DocumentReference): - has_extra_fields = ( - len(set(resource.__dict__) - set(resource.model_fields)) > 0 - ) - else: - has_extra_fields = data != resource.model_dump(exclude_none=True) - - if has_extra_fields: - self.result.add_error( - issue_code="invalid", - error_code="INVALID_RESOURCE", - diagnostics="The resource contains extra fields", - ) - def _validate_identifiers(self, model: DocumentReference): """ """ logger.log(LogReference.VALIDATOR001, step="identifiers") diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index d2f9e3c06..715a7e12e 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,12 +1,14 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-01-27T09:26:28+00:00 +# timestamp: 2025-02-07T14:10:35+00:00 from __future__ import annotations from typing import Annotated, List, Literal, Optional -from pydantic import BaseModel, ConfigDict, Field, RootModel +from pydantic import ConfigDict, Field, RootModel + +from nrlf.core.parent_model import Parent class LocationItem(RootModel[str]): @@ -29,7 +31,7 @@ class ExpressionItem(RootModel[str]): ] -class BundleEntryRequest(BaseModel): +class BundleEntryRequest(Parent): id: Annotated[ Optional[str], Field( @@ -81,7 +83,7 @@ class BundleEntryRequest(BaseModel): ] = None -class BundleEntrySearch(BaseModel): +class BundleEntrySearch(Parent): id: Annotated[ Optional[str], Field( @@ -104,7 +106,7 @@ class BundleEntrySearch(BaseModel): ] = None -class BundleLink(BaseModel): +class BundleLink(Parent): id: Annotated[ Optional[str], Field( @@ -124,7 +126,7 @@ class BundleLink(BaseModel): ] -class Attachment(BaseModel): +class Attachment(Parent): id: Annotated[ Optional[str], Field( @@ -186,7 +188,7 @@ class Attachment(BaseModel): ] = None -class Coding(BaseModel): +class Coding(Parent): id: Annotated[ Optional[str], Field( @@ -230,7 +232,7 @@ class Coding(BaseModel): ] = None -class NRLCoding(BaseModel): +class NRLCoding(Parent): id: Annotated[ Optional[str], Field( @@ -297,7 +299,7 @@ class NRLFormatCode(Coding): ] -class Period(BaseModel): +class Period(Parent): id: Annotated[ Optional[str], Field( @@ -321,7 +323,7 @@ class Period(BaseModel): ] = None -class Quantity(BaseModel): +class Quantity(Parent): id: Annotated[ Optional[str], Field( @@ -375,7 +377,7 @@ class ProfileItem(RootModel[str]): ] -class Meta(BaseModel): +class Meta(Parent): id: Annotated[ Optional[str], Field( @@ -409,7 +411,7 @@ class Meta(BaseModel): tag: Optional[List[Coding]] = None -class Narrative(BaseModel): +class Narrative(Parent): id: Annotated[ Optional[str], Field( @@ -436,7 +438,7 @@ class DocumentId(RootModel[str]): root: Annotated[str, Field(pattern="[A-Za-z0-9\\-\\.]{1,64}")] -class RequestPathParams(BaseModel): +class RequestPathParams(Parent): id: DocumentId @@ -484,7 +486,7 @@ class RequestHeaderCorrelationId(RootModel[str]): root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] -class CodeableConcept(BaseModel): +class CodeableConcept(Parent): id: Annotated[ Optional[str], Field( @@ -502,7 +504,7 @@ class CodeableConcept(BaseModel): ] = None -class NRLCodeableConcept(BaseModel): +class NRLCodeableConcept(Parent): id: Annotated[ Optional[str], Field( @@ -520,7 +522,7 @@ class NRLCodeableConcept(BaseModel): ] = None -class Extension(BaseModel): +class Extension(Parent): valueCodeableConcept: Annotated[ Optional[CodeableConcept], Field( @@ -539,11 +541,11 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] -class RequestHeader(BaseModel): +class RequestHeader(Parent): odsCode: RequestHeaderOdsCode -class RequestParams(BaseModel): +class RequestParams(Parent): subject_identifier: Annotated[ Optional[RequestQuerySubject], Field(alias="subject:identifier") ] = None @@ -554,7 +556,7 @@ class RequestParams(BaseModel): ] = None -class OperationOutcomeIssue(BaseModel): +class OperationOutcomeIssue(Parent): id: Annotated[ Optional[str], Field( @@ -600,7 +602,7 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept -class OperationOutcome(BaseModel): +class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ Optional[str], @@ -638,7 +640,7 @@ class OperationOutcome(BaseModel): issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)] -class DocumentReferenceContent(BaseModel): +class DocumentReferenceContent(Parent): id: Annotated[ Optional[str], Field( @@ -663,7 +665,7 @@ class DocumentReferenceContent(BaseModel): ] -class DocumentReference(BaseModel): +class DocumentReference(Parent): model_config = ConfigDict( regex_engine="python-re", ) @@ -767,7 +769,7 @@ class DocumentReference(BaseModel): ] -class Bundle(BaseModel): +class Bundle(Parent): resourceType: Literal["Bundle"] id: Annotated[ Optional[str], @@ -832,7 +834,7 @@ class Bundle(BaseModel): ] = None -class BundleEntry(BaseModel): +class BundleEntry(Parent): id: Annotated[ Optional[str], Field( @@ -874,7 +876,7 @@ class BundleEntry(BaseModel): ] = None -class BundleEntryResponse(BaseModel): +class BundleEntryResponse(Parent): id: Annotated[ Optional[str], Field( @@ -918,7 +920,7 @@ class BundleEntryResponse(BaseModel): ] = None -class DocumentReferenceContext(BaseModel): +class DocumentReferenceContext(Parent): id: Annotated[ Optional[str], Field( @@ -953,7 +955,7 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceRelatesTo(BaseModel): +class DocumentReferenceRelatesTo(Parent): id: Annotated[ Optional[str], Field( @@ -973,7 +975,7 @@ class DocumentReferenceRelatesTo(BaseModel): ] -class Identifier(BaseModel): +class Identifier(Parent): id: Annotated[ Optional[str], Field( @@ -1018,7 +1020,7 @@ class Identifier(BaseModel): ] = None -class Reference(BaseModel): +class Reference(Parent): id: Annotated[ Optional[str], Field( @@ -1055,7 +1057,7 @@ class Reference(BaseModel): ] = None -class Signature(BaseModel): +class Signature(Parent): id: Annotated[ Optional[str], Field( diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index c283c66a4..d7849f154 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,13 +1,12 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-01-27T09:26:31+00:00 +# timestamp: 2025-02-07T14:10:37+00:00 from __future__ import annotations from typing import Annotated, List, Literal, Optional from pydantic import ( - BaseModel, ConfigDict, Field, RootModel, @@ -17,6 +16,8 @@ StrictStr, ) +from nrlf.core.parent_model import Parent + class LocationItem(RootModel[StrictStr]): root: Annotated[ @@ -36,7 +37,7 @@ class ExpressionItem(RootModel[StrictStr]): ] -class BundleEntryRequest(BaseModel): +class BundleEntryRequest(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -81,7 +82,7 @@ class BundleEntryRequest(BaseModel): ] = None -class BundleEntrySearch(BaseModel): +class BundleEntrySearch(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -102,7 +103,7 @@ class BundleEntrySearch(BaseModel): ] = None -class BundleLink(BaseModel): +class BundleLink(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -118,7 +119,7 @@ class BundleLink(BaseModel): url: Annotated[StrictStr, Field(description="The reference details for the link.")] -class Attachment(BaseModel): +class Attachment(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -168,7 +169,7 @@ class Attachment(BaseModel): ] = None -class Coding(BaseModel): +class Coding(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -207,7 +208,7 @@ class Coding(BaseModel): ] = None -class NRLCoding(BaseModel): +class NRLCoding(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -269,7 +270,7 @@ class NRLFormatCode(Coding): ] -class Period(BaseModel): +class Period(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -288,7 +289,7 @@ class Period(BaseModel): ] = None -class Quantity(BaseModel): +class Quantity(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -334,7 +335,7 @@ class ProfileItem(RootModel[StrictStr]): ] -class Meta(BaseModel): +class Meta(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -364,7 +365,7 @@ class Meta(BaseModel): tag: Optional[List[Coding]] = None -class Narrative(BaseModel): +class Narrative(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -389,7 +390,7 @@ class DocumentId(RootModel[StrictStr]): root: StrictStr -class RequestPathParams(BaseModel): +class RequestPathParams(Parent): id: DocumentId @@ -427,7 +428,7 @@ class RequestHeaderCorrelationId(RootModel[StrictStr]): root: Annotated[StrictStr, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])] -class CodeableConcept(BaseModel): +class CodeableConcept(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -443,7 +444,7 @@ class CodeableConcept(BaseModel): ] = None -class NRLCodeableConcept(BaseModel): +class NRLCodeableConcept(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -459,7 +460,7 @@ class NRLCodeableConcept(BaseModel): ] = None -class Extension(BaseModel): +class Extension(Parent): valueCodeableConcept: Annotated[ Optional[CodeableConcept], Field( @@ -477,11 +478,11 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] -class RequestHeader(BaseModel): +class RequestHeader(Parent): odsCode: RequestHeaderOdsCode -class RequestParams(BaseModel): +class RequestParams(Parent): subject_identifier: Annotated[ Optional[RequestQuerySubject], Field(alias="subject:identifier") ] = None @@ -492,7 +493,7 @@ class RequestParams(BaseModel): ] = None -class OperationOutcomeIssue(BaseModel): +class OperationOutcomeIssue(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -532,7 +533,7 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept -class OperationOutcome(BaseModel): +class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ Optional[StrictStr], @@ -565,7 +566,7 @@ class OperationOutcome(BaseModel): issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)] -class DocumentReferenceContent(BaseModel): +class DocumentReferenceContent(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -589,7 +590,7 @@ class DocumentReferenceContent(BaseModel): ] -class DocumentReference(BaseModel): +class DocumentReference(Parent): model_config = ConfigDict( regex_engine="python-re", ) @@ -679,7 +680,7 @@ class DocumentReference(BaseModel): ] -class Bundle(BaseModel): +class Bundle(Parent): resourceType: Literal["Bundle"] id: Annotated[ Optional[StrictStr], @@ -737,7 +738,7 @@ class Bundle(BaseModel): ] = None -class BundleEntry(BaseModel): +class BundleEntry(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -777,7 +778,7 @@ class BundleEntry(BaseModel): ] = None -class BundleEntryResponse(BaseModel): +class BundleEntryResponse(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -816,7 +817,7 @@ class BundleEntryResponse(BaseModel): ] = None -class DocumentReferenceContext(BaseModel): +class DocumentReferenceContext(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -850,7 +851,7 @@ class DocumentReferenceContext(BaseModel): related: Optional[List[Reference]] = None -class DocumentReferenceRelatesTo(BaseModel): +class DocumentReferenceRelatesTo(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -868,7 +869,7 @@ class DocumentReferenceRelatesTo(BaseModel): ] -class Identifier(BaseModel): +class Identifier(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -906,7 +907,7 @@ class Identifier(BaseModel): ] = None -class Reference(BaseModel): +class Reference(Parent): id: Annotated[ Optional[StrictStr], Field( @@ -939,7 +940,7 @@ class Reference(BaseModel): ] = None -class Signature(BaseModel): +class Signature(Parent): id: Annotated[ Optional[StrictStr], Field( diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 61bd0db46..1510543a0 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -905,6 +905,66 @@ Feature: Producer - createDocumentReference - Failure Scenarios } """ + Scenario: Invalid content has extra field + 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 | 1363501000000100 | + | 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": "someContact.co.uk" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extra_field": "hello", + "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].extra_field: Extra inputs are not permitted)", + "expression": [ + "content[0].extra_field" + ] + } + """ + Scenario: codings with empty string or leading whitespace Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API And the organisation 'TSTCUS' is authorised to access pointer types: