Skip to content

Commit 7affc4c

Browse files
committed
NRL-786 new parent class that does not allow extra fields for pydantic models
1 parent d3cd4a7 commit 7affc4c

File tree

8 files changed

+195
-116
lines changed

8 files changed

+195
-116
lines changed

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,18 +198,22 @@ generate-models: check-warn ## Generate Pydantic Models
198198
--input ./api/producer/swagger.yaml \
199199
--input-file-type openapi \
200200
--output ./layer/nrlf/producer/fhir/r4/model.py \
201-
--output-model-type "pydantic_v2.BaseModel"
201+
--output-model-type "pydantic_v2.BaseModel" \
202+
--base-class layer.nrlf.core.parent_model.Parent
202203
poetry run datamodel-codegen \
203204
--strict-types {str,bytes,int,float,bool} \
204205
--input ./api/producer/swagger.yaml \
205206
--input-file-type openapi \
206207
--output ./layer/nrlf/producer/fhir/r4/strict_model.py \
208+
--base-class layer.nrlf.core.parent_model.Parent \
207209
--output-model-type "pydantic_v2.BaseModel"
208210

211+
209212
@echo "Generating consumer model"
210213
mkdir -p ./layer/nrlf/consumer/fhir/r4
211214
poetry run datamodel-codegen \
212215
--input ./api/consumer/swagger.yaml \
213216
--input-file-type openapi \
214217
--output ./layer/nrlf/consumer/fhir/r4/model.py \
218+
--base-class layer.nrlf.core.parent_model.Parent \
215219
--output-model-type "pydantic_v2.BaseModel"

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

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# generated by datamodel-codegen:
22
# filename: swagger.yaml
3-
# timestamp: 2024-12-13T11:19:30+00:00
3+
# timestamp: 2024-12-19T15:23:29+00:00
44

55
from __future__ import annotations
66

77
from typing import Annotated, List, Literal, Optional
88

9-
from pydantic import BaseModel, Field, RootModel
9+
from pydantic import Field, RootModel
10+
11+
from layer.nrlf.core.parent_model import Parent
1012

1113

1214
class LocationItem(RootModel[str]):
@@ -29,7 +31,7 @@ class ExpressionItem(RootModel[str]):
2931
]
3032

3133

32-
class BundleEntryRequest(BaseModel):
34+
class BundleEntryRequest(Parent):
3335
id: Annotated[
3436
Optional[str],
3537
Field(
@@ -81,7 +83,7 @@ class BundleEntryRequest(BaseModel):
8183
] = None
8284

8385

84-
class BundleEntrySearch(BaseModel):
86+
class BundleEntrySearch(Parent):
8587
id: Annotated[
8688
Optional[str],
8789
Field(
@@ -104,7 +106,7 @@ class BundleEntrySearch(BaseModel):
104106
] = None
105107

106108

107-
class BundleLink(BaseModel):
109+
class BundleLink(Parent):
108110
id: Annotated[
109111
Optional[str],
110112
Field(
@@ -124,7 +126,7 @@ class BundleLink(BaseModel):
124126
]
125127

126128

127-
class Attachment(BaseModel):
129+
class Attachment(Parent):
128130
id: Annotated[
129131
Optional[str],
130132
Field(
@@ -186,7 +188,7 @@ class Attachment(BaseModel):
186188
] = None
187189

188190

189-
class Coding(BaseModel):
191+
class Coding(Parent):
190192
id: Annotated[
191193
Optional[str],
192194
Field(
@@ -253,7 +255,7 @@ class NRLFormatCode(Coding):
253255
]
254256

255257

256-
class Period(BaseModel):
258+
class Period(Parent):
257259
id: Annotated[
258260
Optional[str],
259261
Field(
@@ -277,7 +279,7 @@ class Period(BaseModel):
277279
] = None
278280

279281

280-
class Quantity(BaseModel):
282+
class Quantity(Parent):
281283
id: Annotated[
282284
Optional[str],
283285
Field(
@@ -331,7 +333,7 @@ class ProfileItem(RootModel[str]):
331333
]
332334

333335

334-
class Meta(BaseModel):
336+
class Meta(Parent):
335337
id: Annotated[
336338
Optional[str],
337339
Field(
@@ -365,7 +367,7 @@ class Meta(BaseModel):
365367
tag: Optional[List[Coding]] = None
366368

367369

368-
class Narrative(BaseModel):
370+
class Narrative(Parent):
369371
id: Annotated[
370372
Optional[str],
371373
Field(
@@ -392,7 +394,7 @@ class DocumentId(RootModel[str]):
392394
root: Annotated[str, Field(pattern="[A-Za-z0-9\\-\\.]{1,64}")]
393395

394396

395-
class RequestPathParams(BaseModel):
397+
class RequestPathParams(Parent):
396398
id: DocumentId
397399

398400

@@ -450,7 +452,7 @@ class RequestHeaderCorrelationId(RootModel[str]):
450452
root: Annotated[str, Field(examples=["11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"])]
451453

452454

453-
class CodeableConcept(BaseModel):
455+
class CodeableConcept(Parent):
454456
id: Annotated[
455457
Optional[str],
456458
Field(
@@ -468,7 +470,7 @@ class CodeableConcept(BaseModel):
468470
] = None
469471

470472

471-
class Extension(BaseModel):
473+
class Extension(Parent):
472474
valueCodeableConcept: Annotated[
473475
Optional[CodeableConcept],
474476
Field(
@@ -487,11 +489,11 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept):
487489
]
488490

489491

490-
class RequestHeader(BaseModel):
492+
class RequestHeader(Parent):
491493
odsCode: RequestHeaderOdsCode
492494

493495

494-
class RequestParams(BaseModel):
496+
class RequestParams(Parent):
495497
subject_identifier: Annotated[
496498
RequestQuerySubject, Field(alias="subject:identifier")
497499
]
@@ -505,13 +507,13 @@ class RequestParams(BaseModel):
505507
] = None
506508

507509

508-
class CountRequestParams(BaseModel):
510+
class CountRequestParams(Parent):
509511
subject_identifier: Annotated[
510512
RequestQuerySubject, Field(alias="subject:identifier")
511513
]
512514

513515

514-
class OperationOutcomeIssue(BaseModel):
516+
class OperationOutcomeIssue(Parent):
515517
id: Annotated[
516518
Optional[str],
517519
Field(
@@ -557,7 +559,7 @@ class ContentStabilityExtension(Extension):
557559
valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept
558560

559561

560-
class OperationOutcome(BaseModel):
562+
class OperationOutcome(Parent):
561563
resourceType: Literal["OperationOutcome"]
562564
id: Annotated[
563565
Optional[str],
@@ -595,7 +597,7 @@ class OperationOutcome(BaseModel):
595597
issue: Annotated[List[OperationOutcomeIssue], Field(min_length=1)]
596598

597599

598-
class DocumentReferenceContent(BaseModel):
600+
class DocumentReferenceContent(Parent):
599601
id: Annotated[
600602
Optional[str],
601603
Field(
@@ -620,7 +622,7 @@ class DocumentReferenceContent(BaseModel):
620622
]
621623

622624

623-
class DocumentReference(BaseModel):
625+
class DocumentReference(Parent):
624626
resourceType: Literal["DocumentReference"]
625627
id: Annotated[
626628
Optional[str],
@@ -721,7 +723,7 @@ class DocumentReference(BaseModel):
721723
] = None
722724

723725

724-
class Bundle(BaseModel):
726+
class Bundle(Parent):
725727
resourceType: Literal["Bundle"]
726728
id: Annotated[
727729
Optional[str],
@@ -786,7 +788,7 @@ class Bundle(BaseModel):
786788
] = None
787789

788790

789-
class BundleEntry(BaseModel):
791+
class BundleEntry(Parent):
790792
id: Annotated[
791793
Optional[str],
792794
Field(
@@ -828,7 +830,7 @@ class BundleEntry(BaseModel):
828830
] = None
829831

830832

831-
class BundleEntryResponse(BaseModel):
833+
class BundleEntryResponse(Parent):
832834
id: Annotated[
833835
Optional[str],
834836
Field(
@@ -872,7 +874,7 @@ class BundleEntryResponse(BaseModel):
872874
] = None
873875

874876

875-
class DocumentReferenceContext(BaseModel):
877+
class DocumentReferenceContext(Parent):
876878
id: Annotated[
877879
Optional[str],
878880
Field(
@@ -907,7 +909,7 @@ class DocumentReferenceContext(BaseModel):
907909
related: Optional[List[Reference]] = None
908910

909911

910-
class DocumentReferenceRelatesTo(BaseModel):
912+
class DocumentReferenceRelatesTo(Parent):
911913
id: Annotated[
912914
Optional[str],
913915
Field(
@@ -927,7 +929,7 @@ class DocumentReferenceRelatesTo(BaseModel):
927929
]
928930

929931

930-
class Identifier(BaseModel):
932+
class Identifier(Parent):
931933
id: Annotated[
932934
Optional[str],
933935
Field(
@@ -972,7 +974,7 @@ class Identifier(BaseModel):
972974
] = None
973975

974976

975-
class Reference(BaseModel):
977+
class Reference(Parent):
976978
id: Annotated[
977979
Optional[str],
978980
Field(
@@ -1009,7 +1011,7 @@ class Reference(BaseModel):
10091011
] = None
10101012

10111013

1012-
class Signature(BaseModel):
1014+
class Signature(Parent):
10131015
id: Annotated[
10141016
Optional[str],
10151017
Field(

layer/nrlf/core/parent_model.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from pydantic import BaseModel, ConfigDict
2+
3+
4+
class Parent(BaseModel):
5+
model_config = ConfigDict(regex_engine="python-re", extra="forbid")

layer/nrlf/core/tests/test_validators.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,12 +285,40 @@ def test_validate_document_reference_extra_fields():
285285

286286
document_ref_data["extra_field"] = "extra_value"
287287

288-
result = validator.validate(document_ref_data)
288+
with pytest.raises(ParseError) as error:
289+
validator.validate(document_ref_data)
289290

290-
assert result.is_valid is False
291-
assert result.resource.id == "Y05868-99999-99999-999999"
292-
assert len(result.issues) == 1
293-
assert result.issues[0].model_dump(exclude_none=True) == {
291+
exc = error.value
292+
assert len(exc.issues) == 1
293+
assert exc.issues[0].model_dump(exclude_none=True) == {
294+
"severity": "error",
295+
"code": "invalid",
296+
"details": {
297+
"coding": [
298+
{
299+
"system": "https://fhir.nhs.uk/ValueSet/Spine-ErrorOrWarningCode-1",
300+
"code": "INVALID_RESOURCE",
301+
"display": "Invalid validation of resource",
302+
}
303+
]
304+
},
305+
"diagnostics": "Failed to parse DocumentReference resource (extra_field: Extra inputs are not permitted)",
306+
"expression": ["extra_field"],
307+
}
308+
309+
310+
def test_validate_document_reference_extra_fields_content():
311+
validator = DocumentReferenceValidator()
312+
document_ref_data = load_document_reference_json("Y05868-736253002-Valid")
313+
314+
document_ref_data["content"][0]["extra_field"] = "extra_value"
315+
316+
with pytest.raises(ParseError) as error:
317+
validator.validate(document_ref_data)
318+
319+
exc = error.value
320+
assert len(exc.issues) == 1
321+
assert exc.issues[0].model_dump(exclude_none=True) == {
294322
"severity": "error",
295323
"code": "invalid",
296324
"details": {
@@ -302,7 +330,8 @@ def test_validate_document_reference_extra_fields():
302330
}
303331
]
304332
},
305-
"diagnostics": "The resource contains extra fields",
333+
"diagnostics": "Failed to parse DocumentReference resource (content[0].extra_field: Extra inputs are not permitted)",
334+
"expression": ["content[0].extra_field"],
306335
}
307336

308337

layer/nrlf/core/validators.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ def validate(self, data: Dict[str, Any] | DocumentReference):
129129

130130
try:
131131
self._validate_required_fields(resource)
132-
self._validate_no_extra_fields(resource, data)
133132
self._validate_identifiers(resource)
134133
self._validate_relates_to(resource)
135134
self._validate_ssp_asid(resource)
@@ -174,29 +173,6 @@ def _validate_required_fields(self, model: DocumentReference):
174173
if not self.result.is_valid:
175174
raise StopValidationError()
176175

177-
def _validate_no_extra_fields(
178-
self, resource: DocumentReference, data: Dict[str, Any] | DocumentReference
179-
):
180-
"""
181-
Validate that there are no extra fields
182-
"""
183-
logger.log(LogReference.VALIDATOR001, step="no_extra_fields")
184-
has_extra_fields = False
185-
186-
if isinstance(data, DocumentReference):
187-
has_extra_fields = (
188-
len(set(resource.__dict__) - set(resource.model_fields)) > 0
189-
)
190-
else:
191-
has_extra_fields = data != resource.model_dump(exclude_none=True)
192-
193-
if has_extra_fields:
194-
self.result.add_error(
195-
issue_code="invalid",
196-
error_code="INVALID_RESOURCE",
197-
diagnostics="The resource contains extra fields",
198-
)
199-
200176
def _validate_identifiers(self, model: DocumentReference):
201177
""" """
202178
logger.log(LogReference.VALIDATOR001, step="identifiers")

0 commit comments

Comments
 (0)