diff --git a/assets/api/writer-generator/python/profile_helpers.py b/assets/api/writer-generator/python/profile_helpers.py index 0cc130a2..f3146147 100644 --- a/assets/api/writer-generator/python/profile_helpers.py +++ b/assets/api/writer-generator/python/profile_helpers.py @@ -444,6 +444,29 @@ def validate_slice_cardinality( return errors +def validate_slice_fields( + res: object, + profile_name: str, + field: str, + match: Mapping[str, Any], + slice_name: str, + required_fields: Sequence[str], +) -> list[str]: + """Validates required fields within matched slice elements. For each array + item matching the discriminator, checks that the listed fields are present.""" + items = _get_field(res, field) or [] + if not isinstance(items, Iterable): + items = [] + errors: list[str] = [] + for item in items: + if not matches_value(item, match): + continue + for rf in required_fields: + if _get_field(item, rf) is None: + errors.append(f"{profile_name}.{field}[{slice_name}].{rf} is required") + return errors + + def validate_choice_required(res: object, profile_name: str, choices: Sequence[str]) -> list[str]: """Checks that at least one of the listed choice-type variants is present.""" if any(_get_field(res, c) is not None for c in choices): @@ -451,6 +474,17 @@ def validate_choice_required(res: object, profile_name: str, choices: Sequence[s return [f"{profile_name}: at least one of {', '.join(choices)} is required"] +def validate_choice_prohibited(res: object, profile_name: str, prohibited: Sequence[str]) -> list[str]: + """Checks that none of the listed prohibited choice-type variants is present. + E.g. a profile narrowing ``value[x]`` to ``value_quantity`` prohibits every + other variant.""" + return [ + f"{profile_name}: field '{c}' must not be present" + for c in prohibited + if _get_field(res, c) is not None + ] + + def validate_enum(res: object, profile_name: str, field: str, allowed: Sequence[str]) -> list[str]: """Checks that the value of ``field`` has a code within ``allowed``. Handles plain strings, Coding, and CodeableConcept.""" diff --git a/examples/python-r4-us-core/fhir_types/example_folder_structures/profiles/bundle_example_typed_bundle.py b/examples/python-r4-us-core/fhir_types/example_folder_structures/profiles/bundle_example_typed_bundle.py index 8455acf8..b22bb1e6 100644 --- a/examples/python-r4-us-core/fhir_types/example_folder_structures/profiles/bundle_example_typed_bundle.py +++ b/examples/python-r4-us-core/fhir_types/example_folder_structures/profiles/bundle_example_typed_bundle.py @@ -13,7 +13,7 @@ from fhir_types.hl7_fhir_r4_core.resource import Resource from fhir_types.profile_helpers import ( build_resource, ensure_profile, get_array_slice, get_array_slices, set_array_slice, set_array_slices, \ - validate_slice_cardinality + validate_required, validate_slice_cardinality ) @@ -99,5 +99,6 @@ def validate(self) -> dict[str, list[str]]: errors: list[str] = [] warnings: list[str] = [] errors.extend(validate_slice_cardinality(self._resource, profile_name, "entry", {"resource":{"resourceType":"Patient"}}, "PatientEntry", 1, 1)) + errors.extend(validate_required(self._resource, profile_name, "type")) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/base.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/base.py index a8d255ca..a3b5662a 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/base.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/base.py @@ -61,7 +61,7 @@ class Age(Quantity): class Annotation(Element): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - authorReference: Reference | None = Field(None, alias="authorReference", serialization_alias="authorReference") + authorReference: Reference[Literal["Organization", "Patient", "Practitioner", "RelatedPerson"]] | None = Field(None, alias="authorReference", serialization_alias="authorReference") authorString: str | None = Field(None, alias="authorString", serialization_alias="authorString") authorStringExtension: Element | None = Field(None, alias="_authorString", serialization_alias="_authorString") text: str = Field(alias="text", serialization_alias="text") @@ -183,7 +183,7 @@ class DataRequirement(Element): profileExtension: PyList[Element | None] | None = Field(None, alias="_profile", serialization_alias="_profile") sort: PyList[DataRequirementSort] | None = Field(None, alias="sort", serialization_alias="sort") subjectCodeableConcept: CodeableConcept | None = Field(None, alias="subjectCodeableConcept", serialization_alias="subjectCodeableConcept") - subjectReference: Reference | None = Field(None, alias="subjectReference", serialization_alias="subjectReference") + subjectReference: Reference[Literal["Group"]] | None = Field(None, alias="subjectReference", serialization_alias="subjectReference") type: str = Field(alias="type", serialization_alias="type") typeExtension: Element | None = Field(None, alias="_type", serialization_alias="_type") @@ -346,7 +346,7 @@ class HumanName(Element): class Identifier(Element): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - assigner: Reference | None = Field(None, alias="assigner", serialization_alias="assigner") + assigner: Reference[Literal["Organization"]] | None = Field(None, alias="assigner", serialization_alias="assigner") period: Period | None = Field(None, alias="period", serialization_alias="period") system: str | None = Field(None, alias="system", serialization_alias="system") systemExtension: Element | None = Field(None, alias="_system", serialization_alias="_system") @@ -425,14 +425,14 @@ class Ratio(Element): numerator: Quantity | None = Field(None, alias="numerator", serialization_alias="numerator") -class Reference(Element): +class Reference(Element, Generic[T]): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") display: str | None = Field(None, alias="display", serialization_alias="display") displayExtension: Element | None = Field(None, alias="_display", serialization_alias="_display") identifier: Identifier | None = Field(None, alias="identifier", serialization_alias="identifier") reference: str | None = Field(None, alias="reference", serialization_alias="reference") referenceExtension: Element | None = Field(None, alias="_reference", serialization_alias="_reference") - type: str | None = Field(None, alias="type", serialization_alias="type") + type: T | None = Field(None, alias="type", serialization_alias="type") typeExtension: Element | None = Field(None, alias="_type", serialization_alias="_type") @@ -474,7 +474,7 @@ class Signature(Element): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") data: str | None = Field(None, alias="data", serialization_alias="data") dataExtension: Element | None = Field(None, alias="_data", serialization_alias="_data") - onBehalfOf: Reference | None = Field(None, alias="onBehalfOf", serialization_alias="onBehalfOf") + onBehalfOf: Reference[Literal["Device", "Organization", "Patient", "Practitioner", "PractitionerRole", "RelatedPerson"]] | None = Field(None, alias="onBehalfOf", serialization_alias="onBehalfOf") sigFormat: str | None = Field(None, alias="sigFormat", serialization_alias="sigFormat") sigFormatExtension: Element | None = Field(None, alias="_sigFormat", serialization_alias="_sigFormat") targetFormat: str | None = Field(None, alias="targetFormat", serialization_alias="targetFormat") @@ -482,7 +482,7 @@ class Signature(Element): type: PyList[Coding[Literal["1.2.840.10065.1.12.1.1", "1.2.840.10065.1.12.1.2", "1.2.840.10065.1.12.1.3", "1.2.840.10065.1.12.1.4", "1.2.840.10065.1.12.1.5", "1.2.840.10065.1.12.1.6", "1.2.840.10065.1.12.1.7", "1.2.840.10065.1.12.1.8", "1.2.840.10065.1.12.1.9", "1.2.840.10065.1.12.1.10", "1.2.840.10065.1.12.1.11", "1.2.840.10065.1.12.1.12", "1.2.840.10065.1.12.1.13", "1.2.840.10065.1.12.1.14", "1.2.840.10065.1.12.1.15", "1.2.840.10065.1.12.1.16", "1.2.840.10065.1.12.1.17", "1.2.840.10065.1.12.1.18"] | str]] = Field(alias="type", serialization_alias="type") when: str = Field(alias="when", serialization_alias="when") whenExtension: Element | None = Field(None, alias="_when", serialization_alias="_when") - who: Reference = Field(alias="who", serialization_alias="who") + who: Reference[Literal["Device", "Organization", "Patient", "Practitioner", "PractitionerRole", "RelatedPerson"]] = Field(alias="who", serialization_alias="who") class TimingRepeat(Element): @@ -524,7 +524,7 @@ class TriggerDefinition(Element): timingDateExtension: Element | None = Field(None, alias="_timingDate", serialization_alias="_timingDate") timingDateTime: str | None = Field(None, alias="timingDateTime", serialization_alias="timingDateTime") timingDateTimeExtension: Element | None = Field(None, alias="_timingDateTime", serialization_alias="_timingDateTime") - timingReference: Reference | None = Field(None, alias="timingReference", serialization_alias="timingReference") + timingReference: Reference[Literal["Schedule"]] | None = Field(None, alias="timingReference", serialization_alias="timingReference") timingTiming: Timing | None = Field(None, alias="timingTiming", serialization_alias="timingTiming") type: Literal["named-event", "periodic", "data-changed", "data-added", "data-modified", "data-removed", "data-accessed", "data-access-ended"] = Field(alias="type", serialization_alias="type") typeExtension: Element | None = Field(None, alias="_type", serialization_alias="_type") @@ -536,6 +536,6 @@ class UsageContext(Element): valueCodeableConcept: CodeableConcept | None = Field(None, alias="valueCodeableConcept", serialization_alias="valueCodeableConcept") valueQuantity: Quantity | None = Field(None, alias="valueQuantity", serialization_alias="valueQuantity") valueRange: Range | None = Field(None, alias="valueRange", serialization_alias="valueRange") - valueReference: Reference | None = Field(None, alias="valueReference", serialization_alias="valueReference") + valueReference: Reference[Literal["Group", "HealthcareService", "InsurancePlan", "Location", "Organization", "PlanDefinition", "ResearchStudy"]] | None = Field(None, alias="valueReference", serialization_alias="valueReference") diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/observation.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/observation.py index 2807d46f..f108ee6f 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/observation.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/observation.py @@ -52,36 +52,36 @@ class Observation(DomainResource): serialization_alias='resourceType', pattern='Observation' ) - basedOn: PyList[Reference] | None = Field(None, alias="basedOn", serialization_alias="basedOn") + basedOn: PyList[Reference[Literal["CarePlan", "DeviceRequest", "ImmunizationRecommendation", "MedicationRequest", "NutritionOrder", "ServiceRequest"]]] | None = Field(None, alias="basedOn", serialization_alias="basedOn") bodySite: CodeableConcept | None = Field(None, alias="bodySite", serialization_alias="bodySite") category: PyList[CodeableConcept[Literal["social-history", "vital-signs", "imaging", "laboratory", "procedure", "survey", "exam", "therapy", "activity"] | str]] | None = Field(None, alias="category", serialization_alias="category") code: CodeableConcept = Field(alias="code", serialization_alias="code") component: PyList[ObservationComponent] | None = Field(None, alias="component", serialization_alias="component") dataAbsentReason: CodeableConcept[Literal["unknown", "asked-unknown", "temp-unknown", "not-asked", "asked-declined", "masked", "not-applicable", "unsupported", "as-text", "error", "not-a-number", "negative-infinity", "positive-infinity", "not-performed", "not-permitted"] | str] | None = Field(None, alias="dataAbsentReason", serialization_alias="dataAbsentReason") - derivedFrom: PyList[Reference] | None = Field(None, alias="derivedFrom", serialization_alias="derivedFrom") - device: Reference | None = Field(None, alias="device", serialization_alias="device") + derivedFrom: PyList[Reference[Literal["DocumentReference", "ImagingStudy", "Media", "MolecularSequence", "Observation", "QuestionnaireResponse"]]] | None = Field(None, alias="derivedFrom", serialization_alias="derivedFrom") + device: Reference[Literal["Device", "DeviceMetric"]] | None = Field(None, alias="device", serialization_alias="device") effectiveDateTime: str | None = Field(None, alias="effectiveDateTime", serialization_alias="effectiveDateTime") effectiveDateTimeExtension: Element | None = Field(None, alias="_effectiveDateTime", serialization_alias="_effectiveDateTime") effectiveInstant: str | None = Field(None, alias="effectiveInstant", serialization_alias="effectiveInstant") effectiveInstantExtension: Element | None = Field(None, alias="_effectiveInstant", serialization_alias="_effectiveInstant") effectivePeriod: Period | None = Field(None, alias="effectivePeriod", serialization_alias="effectivePeriod") effectiveTiming: Timing | None = Field(None, alias="effectiveTiming", serialization_alias="effectiveTiming") - encounter: Reference | None = Field(None, alias="encounter", serialization_alias="encounter") + encounter: Reference[Literal["Encounter"]] | None = Field(None, alias="encounter", serialization_alias="encounter") focus: PyList[Reference] | None = Field(None, alias="focus", serialization_alias="focus") - hasMember: PyList[Reference] | None = Field(None, alias="hasMember", serialization_alias="hasMember") + hasMember: PyList[Reference[Literal["MolecularSequence", "Observation", "QuestionnaireResponse"]]] | None = Field(None, alias="hasMember", serialization_alias="hasMember") identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier") interpretation: PyList[CodeableConcept[Literal["_GeneticObservationInterpretation", "CAR", "Carrier", "_ObservationInterpretationChange", "B", "D", "U", "W", "_ObservationInterpretationExceptions", "<", ">", "AC", "IE", "QCF", "TOX", "_ObservationInterpretationNormality", "A", "AA", "HH", "LL", "H", "H>", "HU", "L", "L<", "LU", "N", "_ObservationInterpretationSusceptibility", "I", "MS", "NCL", "NS", "R", "SYN-R", "S", "SDD", "SYN-S", "VS", "EX", "HX", "LX", "HM", "ObservationInterpretationDetection", "IND", "E", "NEG", "ND", "POS", "DET", "ObservationInterpretationExpectation", "EXP", "UNE", "OBX", "ReactivityObservationInterpretation", "NR", "RR", "WR"] | str]] | None = Field(None, alias="interpretation", serialization_alias="interpretation") issued: str | None = Field(None, alias="issued", serialization_alias="issued") issuedExtension: Element | None = Field(None, alias="_issued", serialization_alias="_issued") method: CodeableConcept | None = Field(None, alias="method", serialization_alias="method") note: PyList[Annotation] | None = Field(None, alias="note", serialization_alias="note") - partOf: PyList[Reference] | None = Field(None, alias="partOf", serialization_alias="partOf") - performer: PyList[Reference] | None = Field(None, alias="performer", serialization_alias="performer") + partOf: PyList[Reference[Literal["ImagingStudy", "Immunization", "MedicationAdministration", "MedicationDispense", "MedicationStatement", "Procedure"]]] | None = Field(None, alias="partOf", serialization_alias="partOf") + performer: PyList[Reference[Literal["CareTeam", "Organization", "Patient", "Practitioner", "PractitionerRole", "RelatedPerson"]]] | None = Field(None, alias="performer", serialization_alias="performer") referenceRange: PyList[ObservationReferenceRange] | None = Field(None, alias="referenceRange", serialization_alias="referenceRange") - specimen: Reference | None = Field(None, alias="specimen", serialization_alias="specimen") + specimen: Reference[Literal["Specimen"]] | None = Field(None, alias="specimen", serialization_alias="specimen") status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"] = Field(alias="status", serialization_alias="status") statusExtension: Element | None = Field(None, alias="_status", serialization_alias="_status") - subject: Reference | None = Field(None, alias="subject", serialization_alias="subject") + subject: Reference[Literal["Device", "Group", "Location", "Patient"]] | None = Field(None, alias="subject", serialization_alias="subject") valueBoolean: bool | None = Field(None, alias="valueBoolean", serialization_alias="valueBoolean") valueBooleanExtension: Element | None = Field(None, alias="_valueBoolean", serialization_alias="_valueBoolean") valueCodeableConcept: CodeableConcept | None = Field(None, alias="valueCodeableConcept", serialization_alias="valueCodeableConcept") diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/organization.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/organization.py index cebea2a4..70c7b6f9 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/organization.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/organization.py @@ -37,11 +37,11 @@ class Organization(DomainResource): alias: PyList[str] | None = Field(None, alias="alias", serialization_alias="alias") aliasExtension: PyList[Element | None] | None = Field(None, alias="_alias", serialization_alias="_alias") contact: PyList[OrganizationContact] | None = Field(None, alias="contact", serialization_alias="contact") - endpoint: PyList[Reference] | None = Field(None, alias="endpoint", serialization_alias="endpoint") + endpoint: PyList[Reference[Literal["Endpoint"]]] | None = Field(None, alias="endpoint", serialization_alias="endpoint") identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier") name: str | None = Field(None, alias="name", serialization_alias="name") nameExtension: Element | None = Field(None, alias="_name", serialization_alias="_name") - partOf: Reference | None = Field(None, alias="partOf", serialization_alias="partOf") + partOf: Reference[Literal["Organization"]] | None = Field(None, alias="partOf", serialization_alias="partOf") telecom: PyList[ContactPoint] | None = Field(None, alias="telecom", serialization_alias="telecom") type: PyList[CodeableConcept] | None = Field(None, alias="type", serialization_alias="type") diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/patient.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/patient.py index d9691092..5e1ae42d 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/patient.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/patient.py @@ -25,14 +25,14 @@ class PatientContact(BackboneElement): address: Address | None = Field(None, alias="address", serialization_alias="address") gender: Literal["male", "female", "other", "unknown"] | None = Field(None, alias="gender", serialization_alias="gender") name: HumanName | None = Field(None, alias="name", serialization_alias="name") - organization: Reference | None = Field(None, alias="organization", serialization_alias="organization") + organization: Reference[Literal["Organization"]] | None = Field(None, alias="organization", serialization_alias="organization") period: Period | None = Field(None, alias="period", serialization_alias="period") relationship: PyList[CodeableConcept] | None = Field(None, alias="relationship", serialization_alias="relationship") telecom: PyList[ContactPoint] | None = Field(None, alias="telecom", serialization_alias="telecom") class PatientLink(BackboneElement): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - other: Reference = Field(alias="other", serialization_alias="other") + other: Reference[Literal["Patient", "RelatedPerson"]] = Field(alias="other", serialization_alias="other") type: Literal["replaced-by", "replaces", "refer", "seealso"] = Field(alias="type", serialization_alias="type") @@ -57,10 +57,10 @@ class Patient(DomainResource): deceasedDateTimeExtension: Element | None = Field(None, alias="_deceasedDateTime", serialization_alias="_deceasedDateTime") gender: Literal["male", "female", "other", "unknown"] | None = Field(None, alias="gender", serialization_alias="gender") genderExtension: Element | None = Field(None, alias="_gender", serialization_alias="_gender") - generalPractitioner: PyList[Reference] | None = Field(None, alias="generalPractitioner", serialization_alias="generalPractitioner") + generalPractitioner: PyList[Reference[Literal["Organization", "Practitioner", "PractitionerRole"]]] | None = Field(None, alias="generalPractitioner", serialization_alias="generalPractitioner") identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier") link: PyList[PatientLink] | None = Field(None, alias="link", serialization_alias="link") - managingOrganization: Reference | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization") + managingOrganization: Reference[Literal["Organization"]] | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization") maritalStatus: CodeableConcept[Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str] | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus") multipleBirthBoolean: bool | None = Field(None, alias="multipleBirthBoolean", serialization_alias="multipleBirthBoolean") multipleBirthBooleanExtension: Element | None = Field(None, alias="_multipleBirthBoolean", serialization_alias="_multipleBirthBoolean") diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_birth_place.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_birth_place.py index 5d7fd9e2..ca2d7e19 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_birth_place.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_birth_place.py @@ -9,7 +9,7 @@ from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.hl7_fhir_r4_core.base import Address from fhir_types.profile_helpers import ( - build_resource, validate_choice_required, validate_fixed_value, validate_required + build_resource, validate_choice_prohibited, validate_choice_required, validate_fixed_value, validate_required ) @@ -67,6 +67,13 @@ def validate(self) -> dict[str, list[str]]: warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "url")) errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/StructureDefinition/patient-birthPlace")) - errors.extend(validate_choice_required(self._resource, profile_name, ["valueAddress"])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "valueAddress" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "valueBase64binary","valueBoolean","valueCanonical","valueCode","valueDate","valueDateTime","valueDecimal","valueId","valueInstant","valueInteger","valueMarkdown","valueOid","valuePositiveInt","valueString","valueTime","valueUnsignedInt","valueUri","valueUrl","valueUuid","valueAge","valueAnnotation","valueAttachment","valueCodeableConcept","valueCoding","valueContactPoint","valueCount","valueDistance","valueDuration","valueHumanName","valueIdentifier","valueMoney","valuePeriod","valueQuantity","valueRange","valueRatio","valueReference","valueSampledData","valueSignature","valueTiming","valueContactDetail","valueContributor","valueDataRequirement","valueExpression","valueParameterDefinition","valueRelatedArtifact","valueTriggerDefinition","valueUsageContext","valueDosage","valueMeta" + ])) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_birth_time.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_birth_time.py index 6c6e0ee2..f3236787 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_birth_time.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_birth_time.py @@ -8,7 +8,7 @@ from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.profile_helpers import ( - build_resource, validate_choice_required, validate_fixed_value, validate_required + build_resource, validate_choice_prohibited, validate_choice_required, validate_fixed_value, validate_required ) @@ -66,6 +66,13 @@ def validate(self) -> dict[str, list[str]]: warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "url")) errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/StructureDefinition/patient-birthTime")) - errors.extend(validate_choice_required(self._resource, profile_name, ["valueDateTime"])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "valueDateTime" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "valueBase64binary","valueBoolean","valueCanonical","valueCode","valueDate","valueDecimal","valueId","valueInstant","valueInteger","valueMarkdown","valueOid","valuePositiveInt","valueString","valueTime","valueUnsignedInt","valueUri","valueUrl","valueUuid","valueAddress","valueAge","valueAnnotation","valueAttachment","valueCodeableConcept","valueCoding","valueContactPoint","valueCount","valueDistance","valueDuration","valueHumanName","valueIdentifier","valueMoney","valuePeriod","valueQuantity","valueRange","valueRatio","valueReference","valueSampledData","valueSignature","valueTiming","valueContactDetail","valueContributor","valueDataRequirement","valueExpression","valueParameterDefinition","valueRelatedArtifact","valueTriggerDefinition","valueUsageContext","valueDosage","valueMeta" + ])) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_own_prefix.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_own_prefix.py index 1daa8953..a99f8b95 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_own_prefix.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/extension_own_prefix.py @@ -8,7 +8,7 @@ from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.profile_helpers import ( - build_resource, validate_choice_required, validate_fixed_value, validate_required + build_resource, validate_choice_prohibited, validate_choice_required, validate_fixed_value, validate_required ) @@ -66,6 +66,13 @@ def validate(self) -> dict[str, list[str]]: warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "url")) errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix")) - errors.extend(validate_choice_required(self._resource, profile_name, ["valueString"])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "valueString" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "valueBase64binary","valueBoolean","valueCanonical","valueCode","valueDate","valueDateTime","valueDecimal","valueId","valueInstant","valueInteger","valueMarkdown","valueOid","valuePositiveInt","valueTime","valueUnsignedInt","valueUri","valueUrl","valueUuid","valueAddress","valueAge","valueAnnotation","valueAttachment","valueCodeableConcept","valueCoding","valueContactPoint","valueCount","valueDistance","valueDuration","valueHumanName","valueIdentifier","valueMoney","valuePeriod","valueQuantity","valueRange","valueRatio","valueReference","valueSampledData","valueSignature","valueTiming","valueContactDetail","valueContributor","valueDataRequirement","valueExpression","valueParameterDefinition","valueRelatedArtifact","valueTriggerDefinition","valueUsageContext","valueDosage","valueMeta" + ])) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_bodyweight.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_bodyweight.py index 05054783..5a8b04d0 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_bodyweight.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_bodyweight.py @@ -10,8 +10,8 @@ from fhir_types.hl7_fhir_r4_core.base import CodeableConcept, Period, Quantity, Reference from fhir_types.profile_helpers import ( apply_slice_match, build_resource, ensure_profile, ensure_slice_defaults, get_array_slice, matches_value, \ - set_array_slice, strip_match_keys, validate_choice_required, validate_enum, validate_fixed_value, validate_must_support, \ - validate_reference, validate_required, validate_slice_cardinality + set_array_slice, strip_match_keys, validate_choice_prohibited, validate_choice_required, validate_enum, \ + validate_fixed_value, validate_must_support, validate_reference, validate_required, validate_slice_cardinality ) @@ -46,7 +46,7 @@ def apply(cls, resource: Observation) -> "ObservationBodyweightProfile": return cls(resource) @classmethod - def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> Observation: + def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> Observation: category_with_defaults = ensure_slice_defaults(list(category or []), cls._vscat_slice_match) return build_resource( @@ -60,7 +60,7 @@ def create_resource(cls, *, category: list[CodeableConcept] | None = None, statu ) @classmethod - def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> "ObservationBodyweightProfile": + def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> "ObservationBodyweightProfile": return cls.apply(cls.create_resource(category=category, status=status, subject=subject)) def to_resource(self) -> Observation: @@ -73,10 +73,10 @@ def set_status(self, value: Literal["registered", "preliminary", "final", "amend setattr(self._resource, "status", value) return self - def get_subject(self) -> Reference | None: - return cast('Reference | None', getattr(self._resource, "subject", None)) + def get_subject(self) -> Reference[Literal["Patient"]] | None: + return cast('Reference[Literal["Patient"]] | None', getattr(self._resource, "subject", None)) - def set_subject(self, value: Reference) -> "ObservationBodyweightProfile": + def set_subject(self, value: Reference[Literal["Patient"]]) -> "ObservationBodyweightProfile": setattr(self._resource, "subject", value) return self @@ -143,19 +143,51 @@ def validate(self) -> dict[str, list[str]]: errors: list[str] = [] warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "status")) - errors.extend(validate_enum(self._resource, profile_name, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"])) + errors.extend( + validate_enum(self._resource, profile_name, "status", [ + "registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown" + ])) errors.extend(validate_required(self._resource, profile_name, "category")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1)) errors.extend(validate_required(self._resource, profile_name, "code")) errors.extend(validate_fixed_value(self._resource, profile_name, "code", {"coding":[{"code":"29463-7","system":"http://loinc.org"}]})) errors.extend(validate_required(self._resource, profile_name, "subject")) - errors.extend(validate_reference(self._resource, profile_name, "subject", ["Patient"])) - errors.extend(validate_choice_required(self._resource, profile_name, ["effectiveDateTime","effectivePeriod"])) - errors.extend(validate_reference(self._resource, profile_name, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"])) - warnings.extend(validate_enum(self._resource, profile_name, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"])) - warnings.extend(validate_enum(self._resource, profile_name, "code", ["85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0"])) - warnings.extend(validate_enum(self._resource, profile_name, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"])) + errors.extend( + validate_reference(self._resource, profile_name, "subject", [ + "Patient" + ])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "effectiveDateTime","effectivePeriod" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "effectiveTiming","effectiveInstant" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "hasMember", [ + "MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "derivedFrom", [ + "DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "valueCodeableConcept","valueString","valueBoolean","valueInteger","valueRange","valueRatio","valueSampledData","valueTime","valueDateTime","valuePeriod" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "category", [ + "social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "code", [ + "85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "dataAbsentReason", [ + "unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted" + ])) warnings.extend(validate_must_support(self._resource, profile_name, "dataAbsentReason")) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_vitalsigns.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_vitalsigns.py index b9c29cd0..12dbd1b6 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_vitalsigns.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/profiles/observation_observation_vitalsigns.py @@ -10,8 +10,8 @@ from fhir_types.hl7_fhir_r4_core.base import CodeableConcept, Period, Reference from fhir_types.profile_helpers import ( apply_slice_match, build_resource, ensure_profile, ensure_slice_defaults, get_array_slice, matches_value, \ - set_array_slice, strip_match_keys, validate_choice_required, validate_enum, validate_must_support, validate_reference, \ - validate_required, validate_slice_cardinality + set_array_slice, strip_match_keys, validate_choice_prohibited, validate_choice_required, validate_enum, \ + validate_must_support, validate_reference, validate_required, validate_slice_cardinality ) @@ -46,7 +46,7 @@ def apply(cls, resource: Observation) -> "ObservationVitalsignsProfile": return cls(resource) @classmethod - def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], code: CodeableConcept, subject: Reference) -> Observation: + def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], code: CodeableConcept, subject: Reference[Literal["Patient"]]) -> Observation: category_with_defaults = ensure_slice_defaults(list(category or []), cls._vscat_slice_match) return build_resource( @@ -60,7 +60,7 @@ def create_resource(cls, *, category: list[CodeableConcept] | None = None, statu ) @classmethod - def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], code: CodeableConcept, subject: Reference) -> "ObservationVitalsignsProfile": + def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], code: CodeableConcept, subject: Reference[Literal["Patient"]]) -> "ObservationVitalsignsProfile": return cls.apply(cls.create_resource(category=category, status=status, code=code, subject=subject)) def to_resource(self) -> Observation: @@ -80,10 +80,10 @@ def set_code(self, value: CodeableConcept) -> "ObservationVitalsignsProfile": setattr(self._resource, "code", value) return self - def get_subject(self) -> Reference | None: - return cast('Reference | None', getattr(self._resource, "subject", None)) + def get_subject(self) -> Reference[Literal["Patient"]] | None: + return cast('Reference[Literal["Patient"]] | None', getattr(self._resource, "subject", None)) - def set_subject(self, value: Reference) -> "ObservationVitalsignsProfile": + def set_subject(self, value: Reference[Literal["Patient"]]) -> "ObservationVitalsignsProfile": setattr(self._resource, "subject", value) return self @@ -136,18 +136,46 @@ def validate(self) -> dict[str, list[str]]: errors: list[str] = [] warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "status")) - errors.extend(validate_enum(self._resource, profile_name, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"])) + errors.extend( + validate_enum(self._resource, profile_name, "status", [ + "registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown" + ])) errors.extend(validate_required(self._resource, profile_name, "category")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1)) errors.extend(validate_required(self._resource, profile_name, "code")) errors.extend(validate_required(self._resource, profile_name, "subject")) - errors.extend(validate_reference(self._resource, profile_name, "subject", ["Patient"])) - errors.extend(validate_choice_required(self._resource, profile_name, ["effectiveDateTime","effectivePeriod"])) - errors.extend(validate_reference(self._resource, profile_name, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"])) - warnings.extend(validate_enum(self._resource, profile_name, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"])) - warnings.extend(validate_enum(self._resource, profile_name, "code", ["85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0"])) - warnings.extend(validate_enum(self._resource, profile_name, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"])) + errors.extend( + validate_reference(self._resource, profile_name, "subject", [ + "Patient" + ])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "effectiveDateTime","effectivePeriod" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "effectiveTiming","effectiveInstant" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "hasMember", [ + "MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "derivedFrom", [ + "DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "category", [ + "social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "code", [ + "85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "dataAbsentReason", [ + "unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted" + ])) warnings.extend(validate_must_support(self._resource, profile_name, "dataAbsentReason")) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_ethnicity_extension.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_ethnicity_extension.py index 45993ecb..df04873c 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_ethnicity_extension.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_ethnicity_extension.py @@ -11,7 +11,7 @@ from fhir_types.profile_helpers import ( _get_key, apply_slice_match, build_resource, ensure_slice_defaults, get_array_slice, get_extension_value, \ is_extension, is_record, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, \ - validate_fixed_value, validate_required, validate_slice_cardinality, wrap_slice_choice + validate_fixed_value, validate_required, validate_slice_cardinality, validate_slice_fields, wrap_slice_choice ) @@ -215,7 +215,19 @@ def validate(self) -> dict[str, list[str]]: warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "extension")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"ombCategory"}, "ombCategory", 0, 1)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"ombCategory"}, "ombCategory", [ + "value","valueCoding" + ])) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"detailed"}, "detailed", [ + "value","valueCoding" + ])) errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"text"}, "text", 1, 1)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"text"}, "text", [ + "value","valueString" + ])) errors.extend(validate_required(self._resource, profile_name, "url")) errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity")) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_individual_sex_extension.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_individual_sex_extension.py index b011b1df..60431896 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_individual_sex_extension.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_individual_sex_extension.py @@ -9,7 +9,7 @@ from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.hl7_fhir_r4_core.base import Coding from fhir_types.profile_helpers import ( - build_resource, validate_choice_required, validate_fixed_value, validate_required + build_resource, validate_choice_prohibited, validate_choice_required, validate_fixed_value, validate_required ) @@ -67,6 +67,13 @@ def validate(self) -> dict[str, list[str]]: warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "url")) errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex")) - errors.extend(validate_choice_required(self._resource, profile_name, ["valueCoding"])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "valueCoding" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "valueBase64binary","valueBoolean","valueCanonical","valueCode","valueDate","valueDateTime","valueDecimal","valueId","valueInstant","valueInteger","valueMarkdown","valueOid","valuePositiveInt","valueString","valueTime","valueUnsignedInt","valueUri","valueUrl","valueUuid","valueAddress","valueAge","valueAnnotation","valueAttachment","valueCodeableConcept","valueContactPoint","valueCount","valueDistance","valueDuration","valueHumanName","valueIdentifier","valueMoney","valuePeriod","valueQuantity","valueRange","valueRatio","valueReference","valueSampledData","valueSignature","valueTiming","valueContactDetail","valueContributor","valueDataRequirement","valueExpression","valueParameterDefinition","valueRelatedArtifact","valueTriggerDefinition","valueUsageContext","valueDosage","valueMeta" + ])) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_interpreter_needed_extension.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_interpreter_needed_extension.py index 9d9fdb47..a6237749 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_interpreter_needed_extension.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_interpreter_needed_extension.py @@ -9,7 +9,7 @@ from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.hl7_fhir_r4_core.base import Coding from fhir_types.profile_helpers import ( - build_resource, validate_choice_required, validate_fixed_value, validate_required + build_resource, validate_choice_prohibited, validate_choice_required, validate_fixed_value, validate_required ) @@ -67,6 +67,13 @@ def validate(self) -> dict[str, list[str]]: warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "url")) errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed")) - errors.extend(validate_choice_required(self._resource, profile_name, ["valueCoding"])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "valueCoding" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "valueBase64binary","valueBoolean","valueCanonical","valueCode","valueDate","valueDateTime","valueDecimal","valueId","valueInstant","valueInteger","valueMarkdown","valueOid","valuePositiveInt","valueString","valueTime","valueUnsignedInt","valueUri","valueUrl","valueUuid","valueAddress","valueAge","valueAnnotation","valueAttachment","valueCodeableConcept","valueContactPoint","valueCount","valueDistance","valueDuration","valueHumanName","valueIdentifier","valueMoney","valuePeriod","valueQuantity","valueRange","valueRatio","valueReference","valueSampledData","valueSignature","valueTiming","valueContactDetail","valueContributor","valueDataRequirement","valueExpression","valueParameterDefinition","valueRelatedArtifact","valueTriggerDefinition","valueUsageContext","valueDosage","valueMeta" + ])) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_race_extension.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_race_extension.py index 06ee4abe..34bb4b32 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_race_extension.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_race_extension.py @@ -11,7 +11,7 @@ from fhir_types.profile_helpers import ( _get_key, apply_slice_match, build_resource, ensure_slice_defaults, get_array_slice, get_extension_value, \ is_extension, is_record, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, \ - validate_fixed_value, validate_required, validate_slice_cardinality, wrap_slice_choice + validate_fixed_value, validate_required, validate_slice_cardinality, validate_slice_fields, wrap_slice_choice ) @@ -218,7 +218,19 @@ def validate(self) -> dict[str, list[str]]: warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "extension")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"ombCategory"}, "ombCategory", 0, 6)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"ombCategory"}, "ombCategory", [ + "value","valueCoding" + ])) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"detailed"}, "detailed", [ + "value","valueCoding" + ])) errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"text"}, "text", 1, 1)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"text"}, "text", [ + "value","valueString" + ])) errors.extend(validate_required(self._resource, profile_name, "url")) errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race")) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_tribal_affiliation_extension.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_tribal_affiliation_extension.py index fc418b50..de5dc3c9 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_tribal_affiliation_extension.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/extension_uscore_tribal_affiliation_extension.py @@ -11,7 +11,7 @@ from fhir_types.profile_helpers import ( _get_key, apply_slice_match, build_resource, ensure_slice_defaults, get_array_slice, get_extension_value, \ is_extension, is_record, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, \ - validate_fixed_value, validate_required, validate_slice_cardinality, wrap_slice_choice + validate_fixed_value, validate_required, validate_slice_cardinality, validate_slice_fields, wrap_slice_choice ) @@ -165,7 +165,15 @@ def validate(self) -> dict[str, list[str]]: warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "extension")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"tribalAffiliation"}, "tribalAffiliation", 1, 1)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"tribalAffiliation"}, "tribalAffiliation", [ + "value","valueCodeableConcept" + ])) errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"isEnrolled"}, "isEnrolled", 0, 1)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"isEnrolled"}, "isEnrolled", [ + "value","valueBoolean" + ])) errors.extend(validate_required(self._resource, profile_name, "url")) errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation")) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_blood_pressure_profile.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_blood_pressure_profile.py index 6ee1c5ea..36bb4253 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_blood_pressure_profile.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_blood_pressure_profile.py @@ -13,8 +13,9 @@ from fhir_types.hl7_fhir_r4_core.observation import ObservationComponent from fhir_types.profile_helpers import ( apply_slice_match, build_resource, ensure_profile, ensure_slice_defaults, get_array_slice, matches_value, \ - set_array_slice, strip_match_keys, unwrap_slice_choice, validate_choice_required, validate_enum, validate_fixed_value, \ - validate_must_support, validate_reference, validate_required, validate_slice_cardinality, wrap_slice_choice + set_array_slice, strip_match_keys, unwrap_slice_choice, validate_choice_prohibited, validate_choice_required, \ + validate_enum, validate_fixed_value, validate_must_support, validate_reference, validate_required, validate_slice_cardinality, \ + validate_slice_fields, wrap_slice_choice ) @@ -51,7 +52,7 @@ def apply(cls, resource: Observation) -> "UscoreBloodPressureProfile": return cls(resource) @classmethod - def create_resource(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> Observation: + def create_resource(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> Observation: category_with_defaults = ensure_slice_defaults(list(category or []), cls._vscat_slice_match) component_with_defaults = ensure_slice_defaults( list(component or []), @@ -71,7 +72,7 @@ def create_resource(cls, *, category: list[CodeableConcept] | None = None, compo ) @classmethod - def create(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> "UscoreBloodPressureProfile": + def create(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> "UscoreBloodPressureProfile": return cls.apply(cls.create_resource(category=category, component=component, status=status, subject=subject)) def to_resource(self) -> Observation: @@ -84,10 +85,10 @@ def set_status(self, value: Literal["registered", "preliminary", "final", "amend setattr(self._resource, "status", value) return self - def get_subject(self) -> Reference | None: - return cast('Reference | None', getattr(self._resource, "subject", None)) + def get_subject(self) -> Reference[Literal["Patient"]] | None: # http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient + return cast('Reference[Literal["Patient"]] | None', getattr(self._resource, "subject", None)) - def set_subject(self, value: Reference) -> "UscoreBloodPressureProfile": + def set_subject(self, value: Reference[Literal["Patient"]]) -> "UscoreBloodPressureProfile": # http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient setattr(self._resource, "subject", value) return self @@ -385,22 +386,61 @@ def validate(self) -> dict[str, list[str]]: errors: list[str] = [] warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "status")) - errors.extend(validate_enum(self._resource, profile_name, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"])) + errors.extend( + validate_enum(self._resource, profile_name, "status", [ + "registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown" + ])) errors.extend(validate_required(self._resource, profile_name, "category")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1)) errors.extend(validate_required(self._resource, profile_name, "code")) errors.extend(validate_fixed_value(self._resource, profile_name, "code", {"coding":[{"system":"http://loinc.org","code":"85354-9"}]})) errors.extend(validate_required(self._resource, profile_name, "subject")) - errors.extend(validate_reference(self._resource, profile_name, "subject", ["Patient"])) - errors.extend(validate_choice_required(self._resource, profile_name, ["effectiveDateTime","effectivePeriod"])) - errors.extend(validate_reference(self._resource, profile_name, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"])) + errors.extend( + validate_reference(self._resource, profile_name, "subject", [ + "Patient" + ])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "effectiveDateTime","effectivePeriod" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "effectiveTiming","effectiveInstant" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "hasMember", [ + "MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "derivedFrom", [ + "DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation" + ])) errors.extend(validate_slice_cardinality(self._resource, profile_name, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", 1, 1)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", [ + "valueQuantity" + ])) errors.extend(validate_slice_cardinality(self._resource, profile_name, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", 1, 1)) - errors.extend(validate_reference(self._resource, profile_name, "performer", ["PractitionerRole","CareTeam","Organization","Patient","Practitioner","RelatedPerson"])) - warnings.extend(validate_enum(self._resource, profile_name, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"])) - warnings.extend(validate_enum(self._resource, profile_name, "code", ["2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4"])) - warnings.extend(validate_enum(self._resource, profile_name, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"])) + errors.extend( + validate_slice_fields(self._resource, profile_name, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", [ + "valueQuantity" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "performer", [ + "PractitionerRole","CareTeam","Organization","Patient","Practitioner","RelatedPerson" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "category", [ + "social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "code", [ + "2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "dataAbsentReason", [ + "unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted" + ])) warnings.extend(validate_must_support(self._resource, profile_name, "dataAbsentReason")) warnings.extend(validate_must_support(self._resource, profile_name, "performer")) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_body_weight_profile.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_body_weight_profile.py index 00254658..eaf48475 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_body_weight_profile.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_body_weight_profile.py @@ -12,8 +12,8 @@ ) from fhir_types.profile_helpers import ( apply_slice_match, build_resource, ensure_profile, ensure_slice_defaults, get_array_slice, matches_value, \ - set_array_slice, strip_match_keys, validate_choice_required, validate_enum, validate_excluded, validate_fixed_value, \ - validate_must_support, validate_reference, validate_required, validate_slice_cardinality + set_array_slice, strip_match_keys, validate_choice_prohibited, validate_choice_required, validate_enum, \ + validate_fixed_value, validate_must_support, validate_reference, validate_required, validate_slice_cardinality ) @@ -48,7 +48,7 @@ def apply(cls, resource: Observation) -> "UscoreBodyWeightProfile": return cls(resource) @classmethod - def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> Observation: + def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> Observation: category_with_defaults = ensure_slice_defaults(list(category or []), cls._vscat_slice_match) return build_resource( @@ -62,7 +62,7 @@ def create_resource(cls, *, category: list[CodeableConcept] | None = None, statu ) @classmethod - def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> "UscoreBodyWeightProfile": + def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> "UscoreBodyWeightProfile": return cls.apply(cls.create_resource(category=category, status=status, subject=subject)) def to_resource(self) -> Observation: @@ -75,10 +75,10 @@ def set_status(self, value: Literal["registered", "preliminary", "final", "amend setattr(self._resource, "status", value) return self - def get_subject(self) -> Reference | None: - return cast('Reference | None', getattr(self._resource, "subject", None)) + def get_subject(self) -> Reference[Literal["Patient"]] | None: # http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient + return cast('Reference[Literal["Patient"]] | None', getattr(self._resource, "subject", None)) - def set_subject(self, value: Reference) -> "UscoreBodyWeightProfile": + def set_subject(self, value: Reference[Literal["Patient"]]) -> "UscoreBodyWeightProfile": # http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient setattr(self._resource, "subject", value) return self @@ -215,30 +215,55 @@ def validate(self) -> dict[str, list[str]]: errors: list[str] = [] warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "status")) - errors.extend(validate_enum(self._resource, profile_name, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"])) + errors.extend( + validate_enum(self._resource, profile_name, "status", [ + "registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown" + ])) errors.extend(validate_required(self._resource, profile_name, "category")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1)) errors.extend(validate_required(self._resource, profile_name, "code")) errors.extend(validate_fixed_value(self._resource, profile_name, "code", {"coding":[{"system":"http://loinc.org","code":"29463-7"}]})) errors.extend(validate_required(self._resource, profile_name, "subject")) - errors.extend(validate_reference(self._resource, profile_name, "subject", ["Patient"])) - errors.extend(validate_choice_required(self._resource, profile_name, ["effectiveDateTime","effectivePeriod"])) - errors.extend(validate_reference(self._resource, profile_name, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "performer", ["PractitionerRole","CareTeam","Organization","Patient","Practitioner","RelatedPerson"])) - errors.extend(validate_excluded(self._resource, profile_name, "valueCodeableConcept")) - errors.extend(validate_excluded(self._resource, profile_name, "valueString")) - errors.extend(validate_excluded(self._resource, profile_name, "valueBoolean")) - errors.extend(validate_excluded(self._resource, profile_name, "valueInteger")) - errors.extend(validate_excluded(self._resource, profile_name, "valueRange")) - errors.extend(validate_excluded(self._resource, profile_name, "valueRatio")) - errors.extend(validate_excluded(self._resource, profile_name, "valueSampledData")) - errors.extend(validate_excluded(self._resource, profile_name, "valueTime")) - errors.extend(validate_excluded(self._resource, profile_name, "valueDateTime")) - errors.extend(validate_excluded(self._resource, profile_name, "valuePeriod")) - warnings.extend(validate_enum(self._resource, profile_name, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"])) - warnings.extend(validate_enum(self._resource, profile_name, "code", ["2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4"])) - warnings.extend(validate_enum(self._resource, profile_name, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"])) + errors.extend( + validate_reference(self._resource, profile_name, "subject", [ + "Patient" + ])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "effectiveDateTime","effectivePeriod" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "effectiveTiming","effectiveInstant" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "hasMember", [ + "MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "derivedFrom", [ + "DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "performer", [ + "PractitionerRole","CareTeam","Organization","Patient","Practitioner","RelatedPerson" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "valueCodeableConcept","valueString","valueBoolean","valueInteger","valueRange","valueRatio","valueSampledData","valueTime","valueDateTime","valuePeriod" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "category", [ + "social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "code", [ + "2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "dataAbsentReason", [ + "unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted" + ])) warnings.extend(validate_must_support(self._resource, profile_name, "dataAbsentReason")) warnings.extend(validate_must_support(self._resource, profile_name, "performer")) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_vital_signs_profile.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_vital_signs_profile.py index bbbbb9e3..952d1347 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_vital_signs_profile.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_us_core/profiles/observation_uscore_vital_signs_profile.py @@ -12,8 +12,8 @@ ) from fhir_types.profile_helpers import ( apply_slice_match, build_resource, ensure_profile, ensure_slice_defaults, get_array_slice, matches_value, \ - set_array_slice, strip_match_keys, validate_choice_required, validate_enum, validate_must_support, validate_reference, \ - validate_required, validate_slice_cardinality + set_array_slice, strip_match_keys, validate_choice_prohibited, validate_choice_required, validate_enum, \ + validate_must_support, validate_reference, validate_required, validate_slice_cardinality ) @@ -48,7 +48,7 @@ def apply(cls, resource: Observation) -> "UscoreVitalSignsProfile": return cls(resource) @classmethod - def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], code: CodeableConcept, subject: Reference) -> Observation: + def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], code: CodeableConcept, subject: Reference[Literal["Patient"]]) -> Observation: category_with_defaults = ensure_slice_defaults(list(category or []), cls._vscat_slice_match) return build_resource( @@ -62,7 +62,7 @@ def create_resource(cls, *, category: list[CodeableConcept] | None = None, statu ) @classmethod - def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], code: CodeableConcept, subject: Reference) -> "UscoreVitalSignsProfile": + def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], code: CodeableConcept, subject: Reference[Literal["Patient"]]) -> "UscoreVitalSignsProfile": return cls.apply(cls.create_resource(category=category, status=status, code=code, subject=subject)) def to_resource(self) -> Observation: @@ -82,10 +82,10 @@ def set_code(self, value: CodeableConcept) -> "UscoreVitalSignsProfile": setattr(self._resource, "code", value) return self - def get_subject(self) -> Reference | None: - return cast('Reference | None', getattr(self._resource, "subject", None)) + def get_subject(self) -> Reference[Literal["Patient"]] | None: # http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient + return cast('Reference[Literal["Patient"]] | None', getattr(self._resource, "subject", None)) - def set_subject(self, value: Reference) -> "UscoreVitalSignsProfile": + def set_subject(self, value: Reference[Literal["Patient"]]) -> "UscoreVitalSignsProfile": # http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient setattr(self._resource, "subject", value) return self @@ -325,19 +325,50 @@ def validate(self) -> dict[str, list[str]]: errors: list[str] = [] warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "status")) - errors.extend(validate_enum(self._resource, profile_name, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"])) + errors.extend( + validate_enum(self._resource, profile_name, "status", [ + "registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown" + ])) errors.extend(validate_required(self._resource, profile_name, "category")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1)) errors.extend(validate_required(self._resource, profile_name, "code")) errors.extend(validate_required(self._resource, profile_name, "subject")) - errors.extend(validate_reference(self._resource, profile_name, "subject", ["Patient"])) - errors.extend(validate_choice_required(self._resource, profile_name, ["effectiveDateTime","effectivePeriod"])) - errors.extend(validate_reference(self._resource, profile_name, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "performer", ["PractitionerRole","CareTeam","Organization","Patient","Practitioner","RelatedPerson"])) - warnings.extend(validate_enum(self._resource, profile_name, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"])) - warnings.extend(validate_enum(self._resource, profile_name, "code", ["2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4"])) - warnings.extend(validate_enum(self._resource, profile_name, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"])) + errors.extend( + validate_reference(self._resource, profile_name, "subject", [ + "Patient" + ])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "effectiveDateTime","effectivePeriod" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "effectiveTiming","effectiveInstant" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "hasMember", [ + "MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "derivedFrom", [ + "DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "performer", [ + "PractitionerRole","CareTeam","Organization","Patient","Practitioner","RelatedPerson" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "category", [ + "social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "code", [ + "2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "dataAbsentReason", [ + "unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted" + ])) warnings.extend(validate_must_support(self._resource, profile_name, "dataAbsentReason")) warnings.extend(validate_must_support(self._resource, profile_name, "performer")) return {"errors": errors, "warnings": warnings} diff --git a/examples/python-r4-us-core/fhir_types/profile_helpers.py b/examples/python-r4-us-core/fhir_types/profile_helpers.py index 0cc130a2..f3146147 100644 --- a/examples/python-r4-us-core/fhir_types/profile_helpers.py +++ b/examples/python-r4-us-core/fhir_types/profile_helpers.py @@ -444,6 +444,29 @@ def validate_slice_cardinality( return errors +def validate_slice_fields( + res: object, + profile_name: str, + field: str, + match: Mapping[str, Any], + slice_name: str, + required_fields: Sequence[str], +) -> list[str]: + """Validates required fields within matched slice elements. For each array + item matching the discriminator, checks that the listed fields are present.""" + items = _get_field(res, field) or [] + if not isinstance(items, Iterable): + items = [] + errors: list[str] = [] + for item in items: + if not matches_value(item, match): + continue + for rf in required_fields: + if _get_field(item, rf) is None: + errors.append(f"{profile_name}.{field}[{slice_name}].{rf} is required") + return errors + + def validate_choice_required(res: object, profile_name: str, choices: Sequence[str]) -> list[str]: """Checks that at least one of the listed choice-type variants is present.""" if any(_get_field(res, c) is not None for c in choices): @@ -451,6 +474,17 @@ def validate_choice_required(res: object, profile_name: str, choices: Sequence[s return [f"{profile_name}: at least one of {', '.join(choices)} is required"] +def validate_choice_prohibited(res: object, profile_name: str, prohibited: Sequence[str]) -> list[str]: + """Checks that none of the listed prohibited choice-type variants is present. + E.g. a profile narrowing ``value[x]`` to ``value_quantity`` prohibits every + other variant.""" + return [ + f"{profile_name}: field '{c}' must not be present" + for c in prohibited + if _get_field(res, c) is not None + ] + + def validate_enum(res: object, profile_name: str, field: str, allowed: Sequence[str]) -> list[str]: """Checks that the value of ``field`` has a code within ``allowed``. Handles plain strings, Coding, and CodeableConcept.""" diff --git a/examples/python-r4-us-core/test_profile_bp.py b/examples/python-r4-us-core/test_profile_bp.py index 4b4f337f..25e094cb 100644 --- a/examples/python-r4-us-core/test_profile_bp.py +++ b/examples/python-r4-us-core/test_profile_bp.py @@ -116,6 +116,8 @@ def test_freshly_created_profile_is_not_yet_valid_missing_effective() -> None: errors = profile.validate()["errors"] assert errors == [ "UscoreBloodPressureProfile: at least one of effectiveDateTime, effectivePeriod is required", + "UscoreBloodPressureProfile.component[systolic].valueQuantity is required", + "UscoreBloodPressureProfile.component[diastolic].valueQuantity is required", ] diff --git a/src/api/writer-generator/python/naming-utils.ts b/src/api/writer-generator/python/naming-utils.ts index 6decb290..fe5f26c3 100644 --- a/src/api/writer-generator/python/naming-utils.ts +++ b/src/api/writer-generator/python/naming-utils.ts @@ -1,4 +1,6 @@ import { pascalCase, snakeCase, uppercaseFirstLetterOfEach } from "@root/api/writer-generator/utils"; +import type { TypeSchemaIndex } from "@root/typeschema/utils"; +import type { ChoiceFieldInstance, RegularField } from "@typeschema/types.ts"; import { isPrimitiveIdentifier, type TypeIdentifier } from "@typeschema/types.ts"; export const PRIMITIVE_TYPE_MAP: Record = { @@ -121,3 +123,23 @@ export const pyTypeFromIdentifier = (id: TypeIdentifier): string => { if (prim !== undefined) return prim; return deriveResourceName(id); }; + +/** `Literal[...]` type argument for a `Reference` field's targets, mirroring + * the TypeScript writer's `Reference<"Patient" | ...>`. Returns undefined + * when there are no targets or a target is a family type (e.g. `Resource`), + * where the bare `Reference` (defaulting to `str`) is the right annotation. */ +export const pyReferenceTypeParam = ( + field: RegularField | ChoiceFieldInstance, + tsIndex: TypeSchemaIndex, +): string | undefined => { + if (!field.reference || field.reference.resource.length === 0) return undefined; + const isFamilyType = (ref: TypeIdentifier): boolean => { + const schema = tsIndex.resolveType(ref); + if (!schema || !("typeFamily" in schema)) return false; + return (schema.typeFamily?.resources?.length ?? 0) > 0; + }; + const resolved = field.reference.resource.map((ref) => tsIndex.findLastSpecializationByIdentifier(ref)); + if (resolved.some(isFamilyType)) return undefined; + const names = [...new Set(resolved.map((ref) => ref.name))]; + return `Literal[${names.map((n) => JSON.stringify(n)).join(", ")}]`; +}; diff --git a/src/api/writer-generator/python/profile-factory.ts b/src/api/writer-generator/python/profile-factory.ts index 1e04370d..b7ca5f40 100644 --- a/src/api/writer-generator/python/profile-factory.ts +++ b/src/api/writer-generator/python/profile-factory.ts @@ -10,7 +10,7 @@ import { type TypeIdentifier, } from "@root/typeschema/types"; import type { TypeSchemaIndex } from "@root/typeschema/utils"; -import { pyTypeFromIdentifier } from "./naming-utils"; +import { pyReferenceTypeParam, pyTypeFromIdentifier } from "./naming-utils"; import { pyFieldName, pySliceStaticName, pySnakeName } from "./profile-naming"; import { collectRequiredSliceNames } from "./profile-slices"; import type { Python } from "./writer"; @@ -22,8 +22,14 @@ import type { Python } from "./writer"; export type ProfileFactoryInfo = { autoFields: { name: string; value: string }[]; sliceAutoFields: { name: string; pyType: string; typeId: TypeIdentifier; sliceNames: string[] }[]; - params: { name: string; pyType: string; typeId: TypeIdentifier }[]; - accessors: { name: string; pyType: string; typeId: TypeIdentifier; choiceSiblings?: string[] }[]; + params: { name: string; pyType: string; typeId: TypeIdentifier; refComment?: string }[]; + accessors: { + name: string; + pyType: string; + typeId: TypeIdentifier; + choiceSiblings?: string[]; + refComment?: string; + }[]; }; // --------------------------------------------------------------------------- @@ -38,16 +44,29 @@ export type ProfileFactoryInfo = { export const fieldPyType = ( field: RegularField | ChoiceFieldInstance, resolveRef?: TypeSchemaIndex["findLastSpecializationByIdentifier"], + tsIndex?: TypeSchemaIndex, ): string => { const resolved = resolveRef ? resolveRef(field.type) : field.type; - const base = pyTypeFromIdentifier(resolved); + let base = pyTypeFromIdentifier(resolved); if (base === "str" && field.enum && !field.enum.isOpen && field.enum.values.length > 0) { const literal = `Literal[${field.enum.values.map((v) => JSON.stringify(v)).join(", ")}]`; return field.array ? `list[${literal}]` : literal; } + if (base === "Reference" && tsIndex) { + const typeParam = pyReferenceTypeParam(field, tsIndex); + if (typeParam) base = `Reference[${typeParam}]`; + } return field.array ? `list[${base}]` : base; }; +/** Trailing `#` comment with the profile URLs of a reference field's targets. + * Profile targets are replaced by their base resource type in the annotation; + * the URLs are kept here to make server-side validation errors traceable. */ +export const pyReferenceComment = (field: RegularField | ChoiceFieldInstance): string | undefined => { + const urls = (field.reference?.profiles ?? []).map((profile) => profile.url); + return urls.length > 0 ? ` # ${urls.join(", ")}` : undefined; +}; + // --------------------------------------------------------------------------- // Factory info collection // --------------------------------------------------------------------------- @@ -58,17 +77,33 @@ const tryPromoteChoice = ( fields: Record, params: ProfileFactoryInfo["params"], promotedChoices: Set, + tsIndex: TypeSchemaIndex, ): void => { if (!isChoiceDeclarationField(field) || !field.required || field.choices.length !== 1) return; const choiceName = field.choices[0]; if (!choiceName) return; const choiceField = fields[choiceName]; if (!choiceField || !isChoiceInstanceField(choiceField)) return; - const pyType = pyTypeFromIdentifier(choiceField.type) + (choiceField.array ? "[]" : ""); - params.push({ name: choiceName, pyType, typeId: choiceField.type }); + const pyType = pyChoiceInstanceType(choiceField, tsIndex); + params.push({ + name: choiceName, + pyType, + typeId: choiceField.type, + refComment: pyReferenceComment(choiceField), + }); promotedChoices.add(choiceName); }; +/** Python type for a choice variant, with Reference targets parametrized. */ +const pyChoiceInstanceType = (field: ChoiceFieldInstance, tsIndex: TypeSchemaIndex): string => { + let base = pyTypeFromIdentifier(field.type); + if (base === "Reference") { + const typeParam = pyReferenceTypeParam(field, tsIndex); + if (typeParam) base = `Reference[${typeParam}]`; + } + return base + (field.array ? "[]" : ""); +}; + /** Include base-type required fields not already covered by profile constraints. */ const collectBaseRequiredParams = ( tsIndex: TypeSchemaIndex, @@ -86,8 +121,8 @@ const collectBaseRequiredParams = ( if (isChoiceInstanceField(field)) continue; if (isChoiceDeclarationField(field)) continue; if (isNotChoiceDeclarationField(field) && field.type) { - const pyType = fieldPyType(field, resolveRef); - params.push({ name, pyType, typeId: field.type }); + const pyType = fieldPyType(field, resolveRef, tsIndex); + params.push({ name, pyType, typeId: field.type, refComment: pyReferenceComment(field) }); } } }; @@ -124,7 +159,7 @@ export const collectProfileFactoryInfo = ( } if (isChoiceDeclarationField(field)) { - tryPromoteChoice(field, fields, params, promotedChoices); + tryPromoteChoice(field, fields, params, promotedChoices, tsIndex); continue; } @@ -132,8 +167,8 @@ export const collectProfileFactoryInfo = ( const value = JSON.stringify(field.valueConstraint.value); autoFields.push({ name, value: field.array ? `[${value}]` : value }); if (isNotChoiceDeclarationField(field) && field.type) { - const pyType = fieldPyType(field, resolveRef); - autoAccessors.push({ name, pyType, typeId: field.type }); + const pyType = fieldPyType(field, resolveRef, tsIndex); + autoAccessors.push({ name, pyType, typeId: field.type, refComment: pyReferenceComment(field) }); } continue; } @@ -142,17 +177,17 @@ export const collectProfileFactoryInfo = ( const sliceNames = collectRequiredSliceNames(field); if (sliceNames) { if (field.type) { - const pyType = fieldPyType(field, resolveRef); + const pyType = fieldPyType(field, resolveRef, tsIndex); sliceAutoFields.push({ name, pyType, typeId: field.type, sliceNames }); - autoAccessors.push({ name, pyType, typeId: field.type }); + autoAccessors.push({ name, pyType, typeId: field.type, refComment: pyReferenceComment(field) }); } continue; } } if (field.required) { - const pyType = fieldPyType(field, resolveRef); - params.push({ name, pyType, typeId: field.type }); + const pyType = fieldPyType(field, resolveRef, tsIndex); + params.push({ name, pyType, typeId: field.type, refComment: pyReferenceComment(field) }); } } @@ -166,9 +201,15 @@ export const collectProfileFactoryInfo = ( const choiceAccessors: ProfileFactoryInfo["accessors"] = []; for (const [name, field] of pendingChoiceInstances) { if (promotedChoices.has(name)) continue; - const pyType = pyTypeFromIdentifier(field.type) + (field.array ? "[]" : ""); + const pyType = pyChoiceInstanceType(field, tsIndex); const choiceSiblings = (choiceGroups.get(name) ?? []).filter((s) => s !== name && !promotedChoices.has(s)); - choiceAccessors.push({ name, pyType, typeId: field.type, choiceSiblings }); + choiceAccessors.push({ + name, + pyType, + typeId: field.type, + choiceSiblings, + refComment: pyReferenceComment(field), + }); } return { autoFields, sliceAutoFields, params, accessors: [...autoAccessors, ...choiceAccessors] }; @@ -280,12 +321,12 @@ export const generateFieldAccessors = ( for (const p of factoryInfo.params) { const fieldName = pyFieldName(p.name, fmt); const methodSuffix = pySnakeName(p.name); - w.line(`def get_${methodSuffix}(self) -> ${p.pyType} | None:`); + w.line(`def get_${methodSuffix}(self) -> ${p.pyType} | None:${p.refComment ?? ""}`); w.indentBlock(() => { w.line(`return cast('${p.pyType} | None', getattr(self._resource, ${JSON.stringify(fieldName)}, None))`); }); w.line(); - w.line(`def set_${methodSuffix}(self, value: ${p.pyType}) -> "${className}":`); + w.line(`def set_${methodSuffix}(self, value: ${p.pyType}) -> "${className}":${p.refComment ?? ""}`); w.indentBlock(() => { w.line(`setattr(self._resource, ${JSON.stringify(fieldName)}, value)`); w.line("return self"); @@ -297,12 +338,12 @@ export const generateFieldAccessors = ( const methodSuffix = pySnakeName(a.name); if (extSliceMethodBaseNames.has(methodSuffix)) continue; const fieldName = pyFieldName(a.name, fmt); - w.line(`def get_${methodSuffix}(self) -> ${a.pyType} | None:`); + w.line(`def get_${methodSuffix}(self) -> ${a.pyType} | None:${a.refComment ?? ""}`); w.indentBlock(() => { w.line(`return cast('${a.pyType} | None', getattr(self._resource, ${JSON.stringify(fieldName)}, None))`); }); w.line(); - w.line(`def set_${methodSuffix}(self, value: ${a.pyType}) -> "${className}":`); + w.line(`def set_${methodSuffix}(self, value: ${a.pyType}) -> "${className}":${a.refComment ?? ""}`); w.indentBlock(() => { if (a.choiceSiblings?.length) { for (const sibling of a.choiceSiblings) { diff --git a/src/api/writer-generator/python/profile-validation.ts b/src/api/writer-generator/python/profile-validation.ts index 6e4b8004..d00e5885 100644 --- a/src/api/writer-generator/python/profile-validation.ts +++ b/src/api/writer-generator/python/profile-validation.ts @@ -1,6 +1,5 @@ import { type ChoiceFieldInstance, - type Field, isChoiceDeclarationField, isChoiceInstanceField, isNotChoiceDeclarationField, @@ -14,11 +13,29 @@ import { pyFieldName } from "./profile-naming"; // Validation body collection // --------------------------------------------------------------------------- +/** Emit `.extend((self._resource, profile_name, , []))` + * with the call and its trailing list argument split across lines: + * + * errors.extend( + * validate_enum(self._resource, profile_name, "code", [ + * "85353-1","9279-1",... + * ])) + */ +const pushListValidation = (lines: string[], target: string, fn: string, args: string[], items: string[]): void => { + const argList = ["self._resource", "profile_name", ...args].join(", "); + lines.push( + `${target}.extend(`, + ` ${fn}(${argList}, [`, + ` ${items.map((i) => JSON.stringify(i)).join(",")}`, + "]))", + ); +}; + /** Walk fields once and emit validate() body lines into `out`, returning the * set of helper function names referenced. Pure: no writer side effects. */ export const collectValidateBody = ( flatProfile: SnapshotProfileTypeSchema, - resolveRef: TypeSchemaIndex["findLastSpecializationByIdentifier"], + tsIndex: TypeSchemaIndex, errorLines: string[], warningLines: string[], formatName: (s: string) => string, @@ -27,101 +44,124 @@ export const collectValidateBody = ( const fields = flatProfile.fields; for (const [name, field] of Object.entries(fields)) { const pyName = pyFieldName(name, formatName); - if (isChoiceInstanceField(field)) { - collectProhibitedChoiceValidation(fields, name, pyName, helpers, errorLines); - continue; - } + if (isChoiceInstanceField(field)) continue; if (isChoiceDeclarationField(field)) { if (field.required) { helpers.add("validate_choice_required"); const pyChoices = field.choices.map((c) => pyFieldName(c, formatName)); - errorLines.push( - `errors.extend(validate_choice_required(self._resource, profile_name, ${JSON.stringify(pyChoices)}))`, - ); - } - continue; - } - if (field.excluded) { - helpers.add("validate_excluded"); - errorLines.push( - `errors.extend(validate_excluded(self._resource, profile_name, ${JSON.stringify(pyName)}))`, - ); - continue; - } - if (field.required) { - helpers.add("validate_required"); - errorLines.push( - `errors.extend(validate_required(self._resource, profile_name, ${JSON.stringify(pyName)}))`, - ); - } - if (field.valueConstraint) { - helpers.add("validate_fixed_value"); - const value = JSON.stringify(field.valueConstraint.value); - errorLines.push( - `errors.extend(validate_fixed_value(self._resource, profile_name, ${JSON.stringify(pyName)}, ${value}))`, - ); - } - if (isNotChoiceDeclarationField(field)) { - if (field.enum) { - helpers.add("validate_enum"); - const target = field.enum.isOpen ? warningLines : errorLines; - const listName = field.enum.isOpen ? "warnings" : "errors"; - target.push( - `${listName}.extend(validate_enum(self._resource, profile_name, ${JSON.stringify(pyName)}, ${JSON.stringify(field.enum.values)}))`, - ); - } - if (field.mustSupport && !field.required) { - helpers.add("validate_must_support"); - warningLines.push( - `warnings.extend(validate_must_support(self._resource, profile_name, ${JSON.stringify(pyName)}))`, - ); + pushListValidation(errorLines, "errors", "validate_choice_required", [], pyChoices); } - if (field.reference && field.reference.resource.length > 0) { - helpers.add("validate_reference"); - const allowed = field.reference.resource.map((ref) => resolveRef(ref).name); - errorLines.push( - `errors.extend(validate_reference(self._resource, profile_name, ${JSON.stringify(pyName)}, ${JSON.stringify(allowed)}))`, - ); - } - if (field.slicing?.slices) { - collectSliceCardinalityValidation(field, pyName, helpers, errorLines); + if (field.prohibited?.length) { + helpers.add("validate_choice_prohibited"); + const pyProhibited = field.prohibited.map((c) => pyFieldName(c, formatName)); + pushListValidation(errorLines, "errors", "validate_choice_prohibited", [], pyProhibited); } + continue; } + collectRegularFieldValidation(field, pyName, helpers, errorLines, warningLines, tsIndex, formatName); + } + // Base-resource required fields the profile chain did not re-state. + // Emitted here (not via the regular field loop) because they intentionally + // live outside `fields` to avoid pulling unrelated base metadata into the + // profile's getter/setter surface. + for (const inheritedName of flatProfile.inheritedRequiredFields ?? []) { + helpers.add("validate_required"); + errorLines.push( + `errors.extend(validate_required(self._resource, profile_name, ${JSON.stringify(pyFieldName(inheritedName, formatName))}))`, + ); } return helpers; }; -const collectProhibitedChoiceValidation = ( - fields: Record, - name: string, +const collectRegularFieldValidation = ( + field: RegularField | ChoiceFieldInstance, pyName: string, helpers: Set, errorLines: string[], + warningLines: string[], + tsIndex: TypeSchemaIndex, + formatName: (s: string) => string, ): void => { - const field = fields[name]; - if (!field || !isChoiceInstanceField(field)) return; - const decl = fields[field.choiceOf]; - if (!decl || !isChoiceDeclarationField(decl) || !decl.prohibited?.includes(name)) return; - helpers.add("validate_excluded"); - errorLines.push(`errors.extend(validate_excluded(self._resource, profile_name, ${JSON.stringify(pyName)}))`); + if (field.excluded) { + helpers.add("validate_excluded"); + errorLines.push(`errors.extend(validate_excluded(self._resource, profile_name, ${JSON.stringify(pyName)}))`); + return; + } + if (field.required) { + helpers.add("validate_required"); + errorLines.push(`errors.extend(validate_required(self._resource, profile_name, ${JSON.stringify(pyName)}))`); + } + if (field.valueConstraint) { + helpers.add("validate_fixed_value"); + const value = JSON.stringify(field.valueConstraint.value); + errorLines.push( + `errors.extend(validate_fixed_value(self._resource, profile_name, ${JSON.stringify(pyName)}, ${value}))`, + ); + } + if (isNotChoiceDeclarationField(field)) { + if (field.enum) { + helpers.add("validate_enum"); + const target = field.enum.isOpen ? warningLines : errorLines; + const listName = field.enum.isOpen ? "warnings" : "errors"; + pushListValidation(target, listName, "validate_enum", [JSON.stringify(pyName)], field.enum.values); + } + if (field.mustSupport && !field.required) { + helpers.add("validate_must_support"); + warningLines.push( + `warnings.extend(validate_must_support(self._resource, profile_name, ${JSON.stringify(pyName)}))`, + ); + } + if (field.reference && field.reference.resource.length > 0) { + helpers.add("validate_reference"); + const allowed = field.reference.resource.map((ref) => tsIndex.findLastSpecializationByIdentifier(ref).name); + pushListValidation(errorLines, "errors", "validate_reference", [JSON.stringify(pyName)], allowed); + } + if (field.slicing?.slices) { + collectSliceValidation(field, pyName, helpers, errorLines, tsIndex, formatName); + } + } }; -const collectSliceCardinalityValidation = ( +const collectSliceValidation = ( field: RegularField | ChoiceFieldInstance, name: string, helpers: Set, errorLines: string[], + tsIndex: TypeSchemaIndex, + formatName: (s: string) => string, ): void => { if (!field.slicing?.slices) return; for (const [sliceName, slice] of Object.entries(field.slicing.slices)) { - if (slice.min === undefined && slice.max === undefined) continue; const match = slice.match ?? {}; if (Object.keys(match).length === 0) continue; - const min = slice.min ?? 0; - const max = slice.max ?? 0; - helpers.add("validate_slice_cardinality"); - errorLines.push( - `errors.extend(validate_slice_cardinality(self._resource, profile_name, ${JSON.stringify(name)}, ${JSON.stringify(match)}, ${JSON.stringify(sliceName)}, ${min}, ${max}))`, - ); + if (slice.min !== undefined || slice.max !== undefined) { + const min = slice.min ?? 0; + const max = slice.max ?? 0; + helpers.add("validate_slice_cardinality"); + errorLines.push( + `errors.extend(validate_slice_cardinality(self._resource, profile_name, ${JSON.stringify(name)}, ${JSON.stringify(match)}, ${JSON.stringify(sliceName)}, ${min}, ${max}))`, + ); + } + // Collect required fields within the slice element + const sliceRequiredFields: string[] = []; + const matchKeys = new Set(Object.keys(match)); + for (const rf of slice.required ?? []) { + if (!matchKeys.has(rf)) sliceRequiredFields.push(pyFieldName(rf, formatName)); + } + // Constrained choice: the single variant is required + if (field.type && slice.elements) { + const cc = tsIndex.constrainedChoice(field.type.package, field.type, slice.elements); + if (cc) sliceRequiredFields.push(pyFieldName(cc.variant, formatName)); + } + if (sliceRequiredFields.length > 0) { + helpers.add("validate_slice_fields"); + pushListValidation( + errorLines, + "errors", + "validate_slice_fields", + [JSON.stringify(name), JSON.stringify(match), JSON.stringify(sliceName)], + sliceRequiredFields, + ); + } } }; diff --git a/src/api/writer-generator/python/profile.ts b/src/api/writer-generator/python/profile.ts index 8b4b943f..11054591 100644 --- a/src/api/writer-generator/python/profile.ts +++ b/src/api/writer-generator/python/profile.ts @@ -403,13 +403,7 @@ const generateProfileModule = (w: Python, tsIndex: TypeSchemaIndex, flatProfile: const resolvedNames = resolveProfileMethodBaseNames(extensions, sliceDefs); const errorLines: string[] = []; const warningLines: string[] = []; - const validationHelpers = collectValidateBody( - flatProfile, - tsIndex.findLastSpecializationByIdentifier, - errorLines, - warningLines, - w.nameFormatFunction, - ); + const validationHelpers = collectValidateBody(flatProfile, tsIndex, errorLines, warningLines, w.nameFormatFunction); const helperImports = collectHelperImports(isResourceBase, factoryInfo, sliceDefs, extensions, validationHelpers); const typeImports = collectTypeImports( diff --git a/src/api/writer-generator/python/writer.ts b/src/api/writer-generator/python/writer.ts index bb8e4a9c..3ed991cb 100644 --- a/src/api/writer-generator/python/writer.ts +++ b/src/api/writer-generator/python/writer.ts @@ -24,6 +24,7 @@ import { type SpecializationTypeSchema, type TypeIdentifier, } from "@typeschema/types.ts"; +import { pyReferenceTypeParam } from "./naming-utils"; import { generateNewProfiles } from "./profile"; export const resolvePyAssets = (fn: string) => { @@ -48,6 +49,7 @@ const MAX_IMPORT_LINE_LENGTH = 100; const GENERIC_FIELD_REWRITES: Record> = { Coding: { code: "T" }, CodeableConcept: { coding: "Coding[T]" }, + Reference: { type: "T" }, }; const leafOf = (path: string[]): string => path[path.length - 1] ?? ""; @@ -590,6 +592,12 @@ export class Python extends Writer { } } + if (fieldType === "Reference" && "reference" in field && field.reference) { + assert(this.tsIndex !== undefined); + const typeParam = pyReferenceTypeParam(field, this.tsIndex); + if (typeParam) fieldType = `Reference[${typeParam}]`; + } + if (field.array) { fieldType = `PyList[${fieldType}]`; } diff --git a/test/api/write-generator/__snapshots__/python.test.ts.snap b/test/api/write-generator/__snapshots__/python.test.ts.snap index 895f7062..98e4389a 100644 --- a/test/api/write-generator/__snapshots__/python.test.ts.snap +++ b/test/api/write-generator/__snapshots__/python.test.ts.snap @@ -144,14 +144,14 @@ class PatientContact(BackboneElement): address: Address | None = Field(None, alias="address", serialization_alias="address") gender: Literal["male", "female", "other", "unknown"] | None = Field(None, alias="gender", serialization_alias="gender") name: HumanName | None = Field(None, alias="name", serialization_alias="name") - organization: Reference | None = Field(None, alias="organization", serialization_alias="organization") + organization: Reference[Literal["Organization"]] | None = Field(None, alias="organization", serialization_alias="organization") period: Period | None = Field(None, alias="period", serialization_alias="period") relationship: PyList[CodeableConcept] | None = Field(None, alias="relationship", serialization_alias="relationship") telecom: PyList[ContactPoint] | None = Field(None, alias="telecom", serialization_alias="telecom") class PatientLink(BackboneElement): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - other: Reference = Field(alias="other", serialization_alias="other") + other: Reference[Literal["Patient", "RelatedPerson"]] = Field(alias="other", serialization_alias="other") type: Literal["replaced-by", "replaces", "refer", "seealso"] = Field(alias="type", serialization_alias="type") @@ -172,10 +172,10 @@ class Patient(DomainResource): deceasedBoolean: bool | None = Field(None, alias="deceasedBoolean", serialization_alias="deceasedBoolean") deceasedDateTime: str | None = Field(None, alias="deceasedDateTime", serialization_alias="deceasedDateTime") gender: Literal["male", "female", "other", "unknown"] | None = Field(None, alias="gender", serialization_alias="gender") - generalPractitioner: PyList[Reference] | None = Field(None, alias="generalPractitioner", serialization_alias="generalPractitioner") + generalPractitioner: PyList[Reference[Literal["Organization", "Practitioner", "PractitionerRole"]]] | None = Field(None, alias="generalPractitioner", serialization_alias="generalPractitioner") identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier") link: PyList[PatientLink] | None = Field(None, alias="link", serialization_alias="link") - managingOrganization: Reference | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization") + managingOrganization: Reference[Literal["Organization"]] | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization") maritalStatus: CodeableConcept[Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str] | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus") multipleBirthBoolean: bool | None = Field(None, alias="multipleBirthBoolean", serialization_alias="multipleBirthBoolean") multipleBirthInteger: int | None = Field(None, alias="multipleBirthInteger", serialization_alias="multipleBirthInteger") @@ -423,8 +423,8 @@ from fhir_types.hl7_fhir_r4_core.observation import Observation from fhir_types.hl7_fhir_r4_core.base import CodeableConcept, Period, Quantity, Reference from fhir_types.profile_helpers import ( apply_slice_match, build_resource, ensure_profile, ensure_slice_defaults, get_array_slice, matches_value, \\ - set_array_slice, strip_match_keys, validate_choice_required, validate_enum, validate_fixed_value, validate_must_support, \\ - validate_reference, validate_required, validate_slice_cardinality + set_array_slice, strip_match_keys, validate_choice_prohibited, validate_choice_required, validate_enum, \\ + validate_fixed_value, validate_must_support, validate_reference, validate_required, validate_slice_cardinality ) @@ -459,7 +459,7 @@ class ObservationBodyweightProfile: return cls(resource) @classmethod - def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> Observation: + def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> Observation: category_with_defaults = ensure_slice_defaults(list(category or []), cls._vscat_slice_match) return build_resource( @@ -473,7 +473,7 @@ class ObservationBodyweightProfile: ) @classmethod - def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> "ObservationBodyweightProfile": + def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> "ObservationBodyweightProfile": return cls.apply(cls.create_resource(category=category, status=status, subject=subject)) def to_resource(self) -> Observation: @@ -486,10 +486,10 @@ class ObservationBodyweightProfile: setattr(self._resource, "status", value) return self - def get_subject(self) -> Reference | None: - return cast('Reference | None', getattr(self._resource, "subject", None)) + def get_subject(self) -> Reference[Literal["Patient"]] | None: + return cast('Reference[Literal["Patient"]] | None', getattr(self._resource, "subject", None)) - def set_subject(self, value: Reference) -> "ObservationBodyweightProfile": + def set_subject(self, value: Reference[Literal["Patient"]]) -> "ObservationBodyweightProfile": setattr(self._resource, "subject", value) return self @@ -556,19 +556,51 @@ class ObservationBodyweightProfile: errors: list[str] = [] warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "status")) - errors.extend(validate_enum(self._resource, profile_name, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"])) + errors.extend( + validate_enum(self._resource, profile_name, "status", [ + "registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown" + ])) errors.extend(validate_required(self._resource, profile_name, "category")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1)) errors.extend(validate_required(self._resource, profile_name, "code")) errors.extend(validate_fixed_value(self._resource, profile_name, "code", {"coding":[{"code":"29463-7","system":"http://loinc.org"}]})) errors.extend(validate_required(self._resource, profile_name, "subject")) - errors.extend(validate_reference(self._resource, profile_name, "subject", ["Patient"])) - errors.extend(validate_choice_required(self._resource, profile_name, ["effectiveDateTime","effectivePeriod"])) - errors.extend(validate_reference(self._resource, profile_name, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"])) - warnings.extend(validate_enum(self._resource, profile_name, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"])) - warnings.extend(validate_enum(self._resource, profile_name, "code", ["85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0"])) - warnings.extend(validate_enum(self._resource, profile_name, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"])) + errors.extend( + validate_reference(self._resource, profile_name, "subject", [ + "Patient" + ])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "effectiveDateTime","effectivePeriod" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "effectiveTiming","effectiveInstant" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "hasMember", [ + "MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "derivedFrom", [ + "DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "valueCodeableConcept","valueString","valueBoolean","valueInteger","valueRange","valueRatio","valueSampledData","valueTime","valueDateTime","valuePeriod" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "category", [ + "social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "code", [ + "85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "dataAbsentReason", [ + "unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted" + ])) warnings.extend(validate_must_support(self._resource, profile_name, "dataAbsentReason")) return {"errors": errors, "warnings": warnings} @@ -591,8 +623,9 @@ from fhir_types.hl7_fhir_r4_core.base import ( from fhir_types.hl7_fhir_r4_core.observation import ObservationComponent from fhir_types.profile_helpers import ( apply_slice_match, build_resource, ensure_profile, ensure_slice_defaults, get_array_slice, matches_value, \\ - set_array_slice, strip_match_keys, unwrap_slice_choice, validate_choice_required, validate_enum, validate_fixed_value, \\ - validate_must_support, validate_reference, validate_required, validate_slice_cardinality, wrap_slice_choice + set_array_slice, strip_match_keys, unwrap_slice_choice, validate_choice_prohibited, validate_choice_required, \\ + validate_enum, validate_fixed_value, validate_must_support, validate_reference, validate_required, validate_slice_cardinality, \\ + validate_slice_fields, wrap_slice_choice ) @@ -629,7 +662,7 @@ class ObservationBpProfile: return cls(resource) @classmethod - def create_resource(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> Observation: + def create_resource(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> Observation: category_with_defaults = ensure_slice_defaults(list(category or []), cls._vscat_slice_match) component_with_defaults = ensure_slice_defaults( list(component or []), @@ -649,7 +682,7 @@ class ObservationBpProfile: ) @classmethod - def create(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> "ObservationBpProfile": + def create(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> "ObservationBpProfile": return cls.apply(cls.create_resource(category=category, component=component, status=status, subject=subject)) def to_resource(self) -> Observation: @@ -662,10 +695,10 @@ class ObservationBpProfile: setattr(self._resource, "status", value) return self - def get_subject(self) -> Reference | None: - return cast('Reference | None', getattr(self._resource, "subject", None)) + def get_subject(self) -> Reference[Literal["Patient"]] | None: + return cast('Reference[Literal["Patient"]] | None', getattr(self._resource, "subject", None)) - def set_subject(self, value: Reference) -> "ObservationBpProfile": + def set_subject(self, value: Reference[Literal["Patient"]]) -> "ObservationBpProfile": setattr(self._resource, "subject", value) return self @@ -783,21 +816,61 @@ class ObservationBpProfile: errors: list[str] = [] warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "status")) - errors.extend(validate_enum(self._resource, profile_name, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"])) + errors.extend( + validate_enum(self._resource, profile_name, "status", [ + "registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown" + ])) errors.extend(validate_required(self._resource, profile_name, "category")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1)) errors.extend(validate_required(self._resource, profile_name, "code")) errors.extend(validate_fixed_value(self._resource, profile_name, "code", {"coding":[{"code":"85354-9","system":"http://loinc.org"}]})) errors.extend(validate_required(self._resource, profile_name, "subject")) - errors.extend(validate_reference(self._resource, profile_name, "subject", ["Patient"])) - errors.extend(validate_choice_required(self._resource, profile_name, ["effectiveDateTime","effectivePeriod"])) - errors.extend(validate_reference(self._resource, profile_name, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"])) + errors.extend( + validate_reference(self._resource, profile_name, "subject", [ + "Patient" + ])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "effectiveDateTime","effectivePeriod" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "effectiveTiming","effectiveInstant" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "hasMember", [ + "MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "derivedFrom", [ + "DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation" + ])) errors.extend(validate_slice_cardinality(self._resource, profile_name, "component", {"code":{"coding":[{"code":"8480-6","system":"http://loinc.org"}]}}, "SystolicBP", 1, 1)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "component", {"code":{"coding":[{"code":"8480-6","system":"http://loinc.org"}]}}, "SystolicBP", [ + "valueQuantity" + ])) errors.extend(validate_slice_cardinality(self._resource, profile_name, "component", {"code":{"coding":[{"code":"8462-4","system":"http://loinc.org"}]}}, "DiastolicBP", 1, 1)) - warnings.extend(validate_enum(self._resource, profile_name, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"])) - warnings.extend(validate_enum(self._resource, profile_name, "code", ["85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0"])) - warnings.extend(validate_enum(self._resource, profile_name, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"])) + errors.extend( + validate_slice_fields(self._resource, profile_name, "component", {"code":{"coding":[{"code":"8462-4","system":"http://loinc.org"}]}}, "DiastolicBP", [ + "valueQuantity" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "valueCodeableConcept","valueString","valueBoolean","valueInteger","valueRange","valueRatio","valueSampledData","valueTime","valueDateTime","valuePeriod" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "category", [ + "social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "code", [ + "85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "dataAbsentReason", [ + "unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted" + ])) warnings.extend(validate_must_support(self._resource, profile_name, "dataAbsentReason")) return {"errors": errors, "warnings": warnings} @@ -1087,8 +1160,9 @@ from fhir_types.hl7_fhir_r4_core.base import ( from fhir_types.hl7_fhir_r4_core.observation import ObservationComponent from fhir_types.profile_helpers import ( apply_slice_match, build_resource, ensure_profile, ensure_slice_defaults, get_array_slice, matches_value, \\ - set_array_slice, strip_match_keys, unwrap_slice_choice, validate_choice_required, validate_enum, validate_fixed_value, \\ - validate_must_support, validate_reference, validate_required, validate_slice_cardinality, wrap_slice_choice + set_array_slice, strip_match_keys, unwrap_slice_choice, validate_choice_prohibited, validate_choice_required, \\ + validate_enum, validate_fixed_value, validate_must_support, validate_reference, validate_required, validate_slice_cardinality, \\ + validate_slice_fields, wrap_slice_choice ) @@ -1125,7 +1199,7 @@ class UscoreBloodPressureProfile: return cls(resource) @classmethod - def create_resource(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> Observation: + def create_resource(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> Observation: category_with_defaults = ensure_slice_defaults(list(category or []), cls._vscat_slice_match) component_with_defaults = ensure_slice_defaults( list(component or []), @@ -1145,7 +1219,7 @@ class UscoreBloodPressureProfile: ) @classmethod - def create(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> "UscoreBloodPressureProfile": + def create(cls, *, category: list[CodeableConcept] | None = None, component: list[BackboneElement] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> "UscoreBloodPressureProfile": return cls.apply(cls.create_resource(category=category, component=component, status=status, subject=subject)) def to_resource(self) -> Observation: @@ -1158,10 +1232,10 @@ class UscoreBloodPressureProfile: setattr(self._resource, "status", value) return self - def get_subject(self) -> Reference | None: - return cast('Reference | None', getattr(self._resource, "subject", None)) + def get_subject(self) -> Reference[Literal["Patient"]] | None: # http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient + return cast('Reference[Literal["Patient"]] | None', getattr(self._resource, "subject", None)) - def set_subject(self, value: Reference) -> "UscoreBloodPressureProfile": + def set_subject(self, value: Reference[Literal["Patient"]]) -> "UscoreBloodPressureProfile": # http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient setattr(self._resource, "subject", value) return self @@ -1459,22 +1533,61 @@ class UscoreBloodPressureProfile: errors: list[str] = [] warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "status")) - errors.extend(validate_enum(self._resource, profile_name, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"])) + errors.extend( + validate_enum(self._resource, profile_name, "status", [ + "registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown" + ])) errors.extend(validate_required(self._resource, profile_name, "category")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1)) errors.extend(validate_required(self._resource, profile_name, "code")) errors.extend(validate_fixed_value(self._resource, profile_name, "code", {"coding":[{"system":"http://loinc.org","code":"85354-9"}]})) errors.extend(validate_required(self._resource, profile_name, "subject")) - errors.extend(validate_reference(self._resource, profile_name, "subject", ["Patient"])) - errors.extend(validate_choice_required(self._resource, profile_name, ["effectiveDateTime","effectivePeriod"])) - errors.extend(validate_reference(self._resource, profile_name, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"])) + errors.extend( + validate_reference(self._resource, profile_name, "subject", [ + "Patient" + ])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "effectiveDateTime","effectivePeriod" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "effectiveTiming","effectiveInstant" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "hasMember", [ + "MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "derivedFrom", [ + "DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation" + ])) errors.extend(validate_slice_cardinality(self._resource, profile_name, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", 1, 1)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", [ + "valueQuantity" + ])) errors.extend(validate_slice_cardinality(self._resource, profile_name, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", 1, 1)) - errors.extend(validate_reference(self._resource, profile_name, "performer", ["PractitionerRole","CareTeam","Organization","Patient","Practitioner","RelatedPerson"])) - warnings.extend(validate_enum(self._resource, profile_name, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"])) - warnings.extend(validate_enum(self._resource, profile_name, "code", ["2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4"])) - warnings.extend(validate_enum(self._resource, profile_name, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"])) + errors.extend( + validate_slice_fields(self._resource, profile_name, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", [ + "valueQuantity" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "performer", [ + "PractitionerRole","CareTeam","Organization","Patient","Practitioner","RelatedPerson" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "category", [ + "social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "code", [ + "2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "dataAbsentReason", [ + "unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted" + ])) warnings.extend(validate_must_support(self._resource, profile_name, "dataAbsentReason")) warnings.extend(validate_must_support(self._resource, profile_name, "performer")) return {"errors": errors, "warnings": warnings} @@ -1497,8 +1610,8 @@ from fhir_types.hl7_fhir_r4_core.base import ( ) from fhir_types.profile_helpers import ( apply_slice_match, build_resource, ensure_profile, ensure_slice_defaults, get_array_slice, matches_value, \\ - set_array_slice, strip_match_keys, validate_choice_required, validate_enum, validate_excluded, validate_fixed_value, \\ - validate_must_support, validate_reference, validate_required, validate_slice_cardinality + set_array_slice, strip_match_keys, validate_choice_prohibited, validate_choice_required, validate_enum, \\ + validate_fixed_value, validate_must_support, validate_reference, validate_required, validate_slice_cardinality ) @@ -1533,7 +1646,7 @@ class UscoreBodyWeightProfile: return cls(resource) @classmethod - def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> Observation: + def create_resource(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> Observation: category_with_defaults = ensure_slice_defaults(list(category or []), cls._vscat_slice_match) return build_resource( @@ -1547,7 +1660,7 @@ class UscoreBodyWeightProfile: ) @classmethod - def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference) -> "UscoreBodyWeightProfile": + def create(cls, *, category: list[CodeableConcept] | None = None, status: Literal["registered", "preliminary", "final", "amended", "corrected", "cancelled", "entered-in-error", "unknown"], subject: Reference[Literal["Patient"]]) -> "UscoreBodyWeightProfile": return cls.apply(cls.create_resource(category=category, status=status, subject=subject)) def to_resource(self) -> Observation: @@ -1560,10 +1673,10 @@ class UscoreBodyWeightProfile: setattr(self._resource, "status", value) return self - def get_subject(self) -> Reference | None: - return cast('Reference | None', getattr(self._resource, "subject", None)) + def get_subject(self) -> Reference[Literal["Patient"]] | None: # http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient + return cast('Reference[Literal["Patient"]] | None', getattr(self._resource, "subject", None)) - def set_subject(self, value: Reference) -> "UscoreBodyWeightProfile": + def set_subject(self, value: Reference[Literal["Patient"]]) -> "UscoreBodyWeightProfile": # http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient setattr(self._resource, "subject", value) return self @@ -1700,30 +1813,55 @@ class UscoreBodyWeightProfile: errors: list[str] = [] warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "status")) - errors.extend(validate_enum(self._resource, profile_name, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"])) + errors.extend( + validate_enum(self._resource, profile_name, "status", [ + "registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown" + ])) errors.extend(validate_required(self._resource, profile_name, "category")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1)) errors.extend(validate_required(self._resource, profile_name, "code")) errors.extend(validate_fixed_value(self._resource, profile_name, "code", {"coding":[{"system":"http://loinc.org","code":"29463-7"}]})) errors.extend(validate_required(self._resource, profile_name, "subject")) - errors.extend(validate_reference(self._resource, profile_name, "subject", ["Patient"])) - errors.extend(validate_choice_required(self._resource, profile_name, ["effectiveDateTime","effectivePeriod"])) - errors.extend(validate_reference(self._resource, profile_name, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"])) - errors.extend(validate_reference(self._resource, profile_name, "performer", ["PractitionerRole","CareTeam","Organization","Patient","Practitioner","RelatedPerson"])) - errors.extend(validate_excluded(self._resource, profile_name, "valueCodeableConcept")) - errors.extend(validate_excluded(self._resource, profile_name, "valueString")) - errors.extend(validate_excluded(self._resource, profile_name, "valueBoolean")) - errors.extend(validate_excluded(self._resource, profile_name, "valueInteger")) - errors.extend(validate_excluded(self._resource, profile_name, "valueRange")) - errors.extend(validate_excluded(self._resource, profile_name, "valueRatio")) - errors.extend(validate_excluded(self._resource, profile_name, "valueSampledData")) - errors.extend(validate_excluded(self._resource, profile_name, "valueTime")) - errors.extend(validate_excluded(self._resource, profile_name, "valueDateTime")) - errors.extend(validate_excluded(self._resource, profile_name, "valuePeriod")) - warnings.extend(validate_enum(self._resource, profile_name, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"])) - warnings.extend(validate_enum(self._resource, profile_name, "code", ["2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4"])) - warnings.extend(validate_enum(self._resource, profile_name, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"])) + errors.extend( + validate_reference(self._resource, profile_name, "subject", [ + "Patient" + ])) + errors.extend( + validate_choice_required(self._resource, profile_name, [ + "effectiveDateTime","effectivePeriod" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "effectiveTiming","effectiveInstant" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "hasMember", [ + "MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "derivedFrom", [ + "DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation" + ])) + errors.extend( + validate_reference(self._resource, profile_name, "performer", [ + "PractitionerRole","CareTeam","Organization","Patient","Practitioner","RelatedPerson" + ])) + errors.extend( + validate_choice_prohibited(self._resource, profile_name, [ + "valueCodeableConcept","valueString","valueBoolean","valueInteger","valueRange","valueRatio","valueSampledData","valueTime","valueDateTime","valuePeriod" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "category", [ + "social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "code", [ + "2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4" + ])) + warnings.extend( + validate_enum(self._resource, profile_name, "dataAbsentReason", [ + "unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted" + ])) warnings.extend(validate_must_support(self._resource, profile_name, "dataAbsentReason")) warnings.extend(validate_must_support(self._resource, profile_name, "performer")) return {"errors": errors, "warnings": warnings} @@ -1745,7 +1883,7 @@ from fhir_types.hl7_fhir_r4_core.base import Extension from fhir_types.profile_helpers import ( _get_key, apply_slice_match, build_resource, ensure_slice_defaults, get_array_slice, get_extension_value, \\ is_extension, is_record, matches_value, push_extension, set_array_slice, strip_match_keys, unwrap_slice_choice, \\ - validate_fixed_value, validate_required, validate_slice_cardinality, wrap_slice_choice + validate_fixed_value, validate_required, validate_slice_cardinality, validate_slice_fields, wrap_slice_choice ) @@ -1952,7 +2090,19 @@ class UscoreRaceExtension: warnings: list[str] = [] errors.extend(validate_required(self._resource, profile_name, "extension")) errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"ombCategory"}, "ombCategory", 0, 6)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"ombCategory"}, "ombCategory", [ + "value","valueCoding" + ])) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"detailed"}, "detailed", [ + "value","valueCoding" + ])) errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"text"}, "text", 1, 1)) + errors.extend( + validate_slice_fields(self._resource, profile_name, "extension", {"url":"text"}, "text", [ + "value","valueString" + ])) errors.extend(validate_required(self._resource, profile_name, "url")) errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race")) return {"errors": errors, "warnings": warnings} diff --git a/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap b/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap index 94a6b778..fdb15d85 100644 --- a/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap +++ b/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap @@ -217,7 +217,7 @@ class ExampleNotebook(DomainResource): serialization_alias='resourceType', pattern='ExampleNotebook' ) - author: Reference = Field(alias="author", serialization_alias="author") + author: Reference[Literal["Patient", "Practitioner"]] = Field(alias="author", serialization_alias="author") content: str = Field(alias="content", serialization_alias="content") identifier: Identifier = Field(alias="identifier", serialization_alias="identifier") tag: PyList[Coding] | None = Field(None, alias="tag", serialization_alias="tag") @@ -313,14 +313,14 @@ class PatientContact(BackboneElement): address: Address | None = Field(None, alias="address", serialization_alias="address") gender: Literal["male", "female", "other", "unknown"] | None = Field(None, alias="gender", serialization_alias="gender") name: HumanName | None = Field(None, alias="name", serialization_alias="name") - organization: Reference | None = Field(None, alias="organization", serialization_alias="organization") + organization: Reference[Literal["Organization"]] | None = Field(None, alias="organization", serialization_alias="organization") period: Period | None = Field(None, alias="period", serialization_alias="period") relationship: PyList[CodeableConcept] | None = Field(None, alias="relationship", serialization_alias="relationship") telecom: PyList[ContactPoint] | None = Field(None, alias="telecom", serialization_alias="telecom") class PatientLink(BackboneElement): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - other: Reference = Field(alias="other", serialization_alias="other") + other: Reference[Literal["Patient", "RelatedPerson"]] = Field(alias="other", serialization_alias="other") type: Literal["replaced-by", "replaces", "refer", "seealso"] = Field(alias="type", serialization_alias="type") @@ -340,10 +340,10 @@ class Patient(DomainResource): deceasedBoolean: bool | None = Field(None, alias="deceasedBoolean", serialization_alias="deceasedBoolean") deceasedDateTime: str | None = Field(None, alias="deceasedDateTime", serialization_alias="deceasedDateTime") gender: Literal["male", "female", "other", "unknown"] | None = Field(None, alias="gender", serialization_alias="gender") - generalPractitioner: PyList[Reference] | None = Field(None, alias="generalPractitioner", serialization_alias="generalPractitioner") + generalPractitioner: PyList[Reference[Literal["Organization", "Practitioner", "PractitionerRole"]]] | None = Field(None, alias="generalPractitioner", serialization_alias="generalPractitioner") identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier") link: PyList[PatientLink] | None = Field(None, alias="link", serialization_alias="link") - managingOrganization: Reference | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization") + managingOrganization: Reference[Literal["Organization"]] | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization") maritalStatus: CodeableConcept[Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str] | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus") multipleBirthBoolean: bool | None = Field(None, alias="multipleBirthBoolean", serialization_alias="multipleBirthBoolean") multipleBirthInteger: int | None = Field(None, alias="multipleBirthInteger", serialization_alias="multipleBirthInteger") diff --git a/test/api/write-generator/profile-inherited-required.test.ts b/test/api/write-generator/profile-inherited-required.test.ts index 4f1674e7..ce4cbf24 100644 --- a/test/api/write-generator/profile-inherited-required.test.ts +++ b/test/api/write-generator/profile-inherited-required.test.ts @@ -72,3 +72,41 @@ describe("Profile inherited base-required fields (codegen-8iw)", async () => { expect(file).toMatch(/import\s*\{[^}]*\bvalidateRequired\b/); }); }); + +describe("Profile inherited base-required fields, Python writer (codegen-8iw)", async () => { + const result = await new APIBuilder({ logger: mkSilentLogger() }) + .localStructureDefinitions({ + package: { name: "cognovis.test.praxis", version: "0.0.1" }, + path: FIXTURE_PATH, + dependencies: [{ name: "hl7.fhir.r4.core", version: "4.0.1" }], + }) + .python({ inMemoryOnly: true, generateProfile: true, client: "none" }) + .generate(); + + const profileKey = Object.keys(result.filesGenerated.python ?? {}).find((k) => + k.includes("provenance_praxis_proposal_provenance"), + ); + const profileFile = () => (profileKey ? result.filesGenerated.python![profileKey] : undefined); + + it("should succeed", () => { + expect(result.success).toBeTrue(); + }); + + it("generates the Provenance profile class", () => { + expect(profileKey).toBeDefined(); + }); + + it("emits validate_required() for the differential-stated required fields", () => { + const file = profileFile(); + expect(file).toBeDefined(); + expect(file).toContain('validate_required(self._resource, profile_name, "activity")'); + expect(file).toContain('validate_required(self._resource, profile_name, "agent")'); + }); + + it("emits validate_required() for base-R4 required fields the profile inherits", () => { + const file = profileFile(); + expect(file).toBeDefined(); + expect(file).toContain('validate_required(self._resource, profile_name, "target")'); + expect(file).toContain('validate_required(self._resource, profile_name, "recorded")'); + }); +});