From 66852c5d9317c8f3321887010fa22da229753625 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 6 Apr 2026 23:24:00 +0200 Subject: [PATCH 1/3] feat: Validate required fields inside slice elements Add validateSliceFields helper that checks required fields within matched slice elements. For constrained choice slices (e.g. BP component with value[x] restricted to valueQuantity), the variant is validated as required. Before: validate() only checked slice cardinality After: also reports "component[SystolicBP].valueQuantity is required" --- .../typescript/profile-helpers.ts | 25 +++++++++++++++ .../typescript/profile-validation.ts | 31 +++++++++++++++---- .../writer-generator/typescript/profile.ts | 1 + 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/assets/api/writer-generator/typescript/profile-helpers.ts b/assets/api/writer-generator/typescript/profile-helpers.ts index 2c159297..e02bbb12 100644 --- a/assets/api/writer-generator/typescript/profile-helpers.ts +++ b/assets/api/writer-generator/typescript/profile-helpers.ts @@ -378,6 +378,31 @@ export const validateSliceCardinality = ( return errors; }; +/** + * Validate required fields within matched slice elements. + * For each array item matching the discriminator, checks that the listed fields are present. + */ +export const validateSliceFields = ( + res: object, + profileName: string, + field: string, + match: Record, + sliceName: string, + requiredFields: string[], +): string[] => { + const items = (res as Record)[field] as unknown[] | undefined; + const errors: string[] = []; + for (const item of (items ?? []).filter((item) => matchesValue(item, match))) { + const obj = item as Record; + for (const rf of requiredFields) { + if (obj[rf] === undefined || obj[rf] === null) { + errors.push(`${profileName}.${field}[${sliceName}].${rf} is required`); + } + } + } + return errors; +}; + /** * Checks that at least one of the listed choice-type variants is present. * E.g. `["effectiveDateTime", "effectivePeriod"]`. diff --git a/src/api/writer-generator/typescript/profile-validation.ts b/src/api/writer-generator/typescript/profile-validation.ts index 22f6ebe6..d97da3df 100644 --- a/src/api/writer-generator/typescript/profile-validation.ts +++ b/src/api/writer-generator/typescript/profile-validation.ts @@ -17,6 +17,7 @@ export const collectRegularFieldValidation = ( field: RegularField | ChoiceFieldInstance, resolveRef: (ref: TypeIdentifier) => TypeIdentifier, canonicalUrlExpr?: { url: string; expr: string }, + tsIndex?: TypeSchemaIndex, ) => { if (field.excluded) { errors.push(`...validateExcluded(res, profileName, ${JSON.stringify(name)})`); @@ -48,14 +49,31 @@ export const collectRegularFieldValidation = ( if (field.slicing?.slices) { 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; - errors.push( - `...validateSliceCardinality(res, profileName, ${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; + errors.push( + `...validateSliceCardinality(res, profileName, ${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(rf); + } + // Constrained choice: the single variant is required + if (tsIndex && field.type && slice.elements) { + const cc = tsIndex.constrainedChoice(field.type.package, field.type, slice.elements); + if (cc) sliceRequiredFields.push(cc.variant); + } + if (sliceRequiredFields.length > 0) { + errors.push( + `...validateSliceFields(res, profileName, ${JSON.stringify(name)}, ${JSON.stringify(match)}, ${JSON.stringify(sliceName)}, ${JSON.stringify(sliceRequiredFields)})`, + ); + } } } }; @@ -94,6 +112,7 @@ export const generateValidateMethod = (w: TypeScript, tsIndex: TypeSchemaIndex, field, tsIndex.findLastSpecializationByIdentifier, canonicalUrlExpr, + tsIndex, ); } diff --git a/src/api/writer-generator/typescript/profile.ts b/src/api/writer-generator/typescript/profile.ts index 3d54a4d8..1bc77361 100644 --- a/src/api/writer-generator/typescript/profile.ts +++ b/src/api/writer-generator/typescript/profile.ts @@ -261,6 +261,7 @@ const generateProfileHelpersImport = ( "validateExcluded", "validateFixedValue", "validateSliceCardinality", + "validateSliceFields", "validateEnum", "validateReference", "validateChoiceRequired", From de8c6fc2ddfb8b3128a1e92e574f9706d83415bd Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 6 Apr 2026 23:24:00 +0200 Subject: [PATCH 2/3] test: Update snapshots for slice field validation --- .../__snapshots__/typescript.test.ts.snap | 13 +++++++++++++ .../__snapshots__/local-package.test.ts.snap | 1 + 2 files changed, 14 insertions(+) diff --git a/test/api/write-generator/__snapshots__/typescript.test.ts.snap b/test/api/write-generator/__snapshots__/typescript.test.ts.snap index a77ca9ab..e6153df9 100644 --- a/test/api/write-generator/__snapshots__/typescript.test.ts.snap +++ b/test/api/write-generator/__snapshots__/typescript.test.ts.snap @@ -338,6 +338,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -562,6 +563,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -810,7 +812,9 @@ export class observation_bpProfile { ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"code":"8480-6","system":"http://loinc.org"}]}}, "SystolicBP", 1, 1), + ...validateSliceFields(res, profileName, "component", {"code":{"coding":[{"code":"8480-6","system":"http://loinc.org"}]}}, "SystolicBP", ["valueQuantity"]), ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"code":"8462-4","system":"http://loinc.org"}]}}, "DiastolicBP", 1, 1), + ...validateSliceFields(res, profileName, "component", {"code":{"coding":[{"code":"8462-4","system":"http://loinc.org"}]}}, "DiastolicBP", ["valueQuantity"]), ], warnings: [ ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), @@ -861,6 +865,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -1119,6 +1124,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -1457,7 +1463,9 @@ export class USCoreBloodPressureProfile { ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", 1, 1), + ...validateSliceFields(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", ["valueQuantity"]), ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", 1, 1), + ...validateSliceFields(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", ["valueQuantity"]), ...validateReference(res, profileName, "performer", ["PractitionerRole","USCoreCareTeam","USCoreOrganizationProfile","Patient","USCorePractitionerProfile","USCoreRelatedPersonProfile"]), ], warnings: [ @@ -1505,6 +1513,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -1834,6 +1843,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -2047,7 +2057,10 @@ export class USCoreRaceExtensionProfile { errors: [ ...validateRequired(res, profileName, "extension"), ...validateSliceCardinality(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", 0, 6), + ...validateSliceFields(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", ["value","valueCoding"]), + ...validateSliceFields(res, profileName, "extension", {"url":"detailed"}, "detailed", ["value","valueCoding"]), ...validateSliceCardinality(res, profileName, "extension", {"url":"text"}, "text", 1, 1), + ...validateSliceFields(res, profileName, "extension", {"url":"text"}, "text", ["value","valueString"]), ...validateRequired(res, profileName, "url"), ...validateFixedValue(res, profileName, "url", USCoreRaceExtensionProfile.canonicalUrl), ], 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 1418558c..4ecf93c2 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 @@ -56,6 +56,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, From 07e565e0bcfc1c67540ff64578fc4a24aa4ff4c6 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 6 Apr 2026 23:24:00 +0200 Subject: [PATCH 3/3] test: Update BP example tests for slice field validation --- .../profiles/Extension_birthPlace.ts | 1 + .../profiles/Extension_birthTime.ts | 1 + .../profiles/Extension_nationality.ts | 1 + .../profiles/Extension_own_prefix.ts | 1 + .../Observation_observation_bodyweight.ts | 1 + .../profiles/Observation_observation_bp.ts | 3 +++ .../Observation_observation_vitalsigns.ts | 1 + .../fhir-types/profile-helpers.ts | 25 +++++++++++++++++++ examples/typescript-r4/profile-bp.test.ts | 8 ++++-- .../Observation_observation_vitalsigns.ts | 1 + .../Extension_USCoreEthnicityExtension.ts | 4 +++ .../Extension_USCoreIndividualSexExtension.ts | 1 + ...ension_USCoreInterpreterNeededExtension.ts | 1 + .../profiles/Extension_USCoreRaceExtension.ts | 4 +++ ...ension_USCoreTribalAffiliationExtension.ts | 3 +++ .../Observation_USCoreBloodPressureProfile.ts | 3 +++ .../Observation_USCoreBodyWeightProfile.ts | 1 + .../Observation_USCoreVitalSignsProfile.ts | 1 + .../profiles/Patient_USCorePatientProfile.ts | 1 + .../fhir-types/profile-helpers.ts | 25 +++++++++++++++++++ .../typescript-us-core/profile-bp.test.ts | 8 ++++-- 21 files changed, 91 insertions(+), 4 deletions(-) diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthPlace.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthPlace.ts index 0e5add04..e2729645 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthPlace.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthPlace.ts @@ -10,6 +10,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthTime.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthTime.ts index e9eecae6..088028b9 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthTime.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthTime.ts @@ -9,6 +9,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_nationality.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_nationality.ts index 063f86d9..3990ad4e 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_nationality.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_nationality.ts @@ -16,6 +16,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_own_prefix.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_own_prefix.ts index 74839752..eeb18eb6 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_own_prefix.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_own_prefix.ts @@ -9,6 +9,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bodyweight.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bodyweight.ts index 58c57126..a6fa90ea 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bodyweight.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bodyweight.ts @@ -24,6 +24,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bp.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bp.ts index 677fbff6..d303dd4a 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bp.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bp.ts @@ -32,6 +32,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -280,7 +281,9 @@ export class observation_bpProfile { ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"code":"8480-6","system":"http://loinc.org"}]}}, "SystolicBP", 1, 1), + ...validateSliceFields(res, profileName, "component", {"code":{"coding":[{"code":"8480-6","system":"http://loinc.org"}]}}, "SystolicBP", ["valueQuantity"]), ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"code":"8462-4","system":"http://loinc.org"}]}}, "DiastolicBP", 1, 1), + ...validateSliceFields(res, profileName, "component", {"code":{"coding":[{"code":"8462-4","system":"http://loinc.org"}]}}, "DiastolicBP", ["valueQuantity"]), ], warnings: [ ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts index a4c86d7d..146c333d 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts @@ -23,6 +23,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-r4/fhir-types/profile-helpers.ts b/examples/typescript-r4/fhir-types/profile-helpers.ts index 2c159297..e02bbb12 100644 --- a/examples/typescript-r4/fhir-types/profile-helpers.ts +++ b/examples/typescript-r4/fhir-types/profile-helpers.ts @@ -378,6 +378,31 @@ export const validateSliceCardinality = ( return errors; }; +/** + * Validate required fields within matched slice elements. + * For each array item matching the discriminator, checks that the listed fields are present. + */ +export const validateSliceFields = ( + res: object, + profileName: string, + field: string, + match: Record, + sliceName: string, + requiredFields: string[], +): string[] => { + const items = (res as Record)[field] as unknown[] | undefined; + const errors: string[] = []; + for (const item of (items ?? []).filter((item) => matchesValue(item, match))) { + const obj = item as Record; + for (const rf of requiredFields) { + if (obj[rf] === undefined || obj[rf] === null) { + errors.push(`${profileName}.${field}[${sliceName}].${rf} is required`); + } + } + } + return errors; +}; + /** * Checks that at least one of the listed choice-type variants is present. * E.g. `["effectiveDateTime", "effectivePeriod"]`. diff --git a/examples/typescript-r4/profile-bp.test.ts b/examples/typescript-r4/profile-bp.test.ts index 89b4eef7..34af41cd 100644 --- a/examples/typescript-r4/profile-bp.test.ts +++ b/examples/typescript-r4/profile-bp.test.ts @@ -14,9 +14,11 @@ describe("demo: create a blood pressure observation", () => { subject: { reference: "Patient/pt-1" }, }); - // Not yet valid: effective[x] is required by the vitalsigns base profile + // Not yet valid: effective[x] and component valueQuantity are required expect(profile.validate().errors).toEqual([ "observation-bp: at least one of effectiveDateTime, effectivePeriod is required", + "observation-bp.component[SystolicBP].valueQuantity is required", + "observation-bp.component[DiastolicBP].valueQuantity is required", ]); // Fill in the remaining required fields @@ -44,9 +46,11 @@ describe("demo: apply BP profile to an existing Observation", () => { // apply() adds meta.profile, sets fixed values (code), and auto-populates required slices const profile = bpProfile.apply(obs); - // Not yet valid: effective[x] is required + // Not yet valid: effective[x] and component valueQuantity are required expect(profile.validate().errors).toEqual([ "observation-bp: at least one of effectiveDateTime, effectivePeriod is required", + "observation-bp.component[SystolicBP].valueQuantity is required", + "observation-bp.component[DiastolicBP].valueQuantity is required", ]); // The profile mutates the original resource — no copy is made diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts index a4c86d7d..146c333d 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts @@ -23,6 +23,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreEthnicityExtension.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreEthnicityExtension.ts index cf40c2ef..6d734099 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreEthnicityExtension.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreEthnicityExtension.ts @@ -35,6 +35,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -248,7 +249,10 @@ export class USCoreEthnicityExtensionProfile { errors: [ ...validateRequired(res, profileName, "extension"), ...validateSliceCardinality(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", 0, 1), + ...validateSliceFields(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", ["value","valueCoding"]), + ...validateSliceFields(res, profileName, "extension", {"url":"detailed"}, "detailed", ["value","valueCoding"]), ...validateSliceCardinality(res, profileName, "extension", {"url":"text"}, "text", 1, 1), + ...validateSliceFields(res, profileName, "extension", {"url":"text"}, "text", ["value","valueString"]), ...validateRequired(res, profileName, "url"), ...validateFixedValue(res, profileName, "url", USCoreEthnicityExtensionProfile.canonicalUrl), ], diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreIndividualSexExtension.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreIndividualSexExtension.ts index 3f2d5d09..51942c3e 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreIndividualSexExtension.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreIndividualSexExtension.ts @@ -10,6 +10,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreInterpreterNeededExtension.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreInterpreterNeededExtension.ts index 7f76b566..bcc0496e 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreInterpreterNeededExtension.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreInterpreterNeededExtension.ts @@ -10,6 +10,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreRaceExtension.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreRaceExtension.ts index a6e85b68..b78f4234 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreRaceExtension.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreRaceExtension.ts @@ -35,6 +35,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -248,7 +249,10 @@ export class USCoreRaceExtensionProfile { errors: [ ...validateRequired(res, profileName, "extension"), ...validateSliceCardinality(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", 0, 6), + ...validateSliceFields(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", ["value","valueCoding"]), + ...validateSliceFields(res, profileName, "extension", {"url":"detailed"}, "detailed", ["value","valueCoding"]), ...validateSliceCardinality(res, profileName, "extension", {"url":"text"}, "text", 1, 1), + ...validateSliceFields(res, profileName, "extension", {"url":"text"}, "text", ["value","valueString"]), ...validateRequired(res, profileName, "url"), ...validateFixedValue(res, profileName, "url", USCoreRaceExtensionProfile.canonicalUrl), ], diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreTribalAffiliationExtension.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreTribalAffiliationExtension.ts index ceda1a02..0de39adc 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreTribalAffiliationExtension.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreTribalAffiliationExtension.ts @@ -30,6 +30,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -203,7 +204,9 @@ export class USCoreTribalAffiliationExtensionProfile { errors: [ ...validateRequired(res, profileName, "extension"), ...validateSliceCardinality(res, profileName, "extension", {"url":"tribalAffiliation"}, "tribalAffiliation", 1, 1), + ...validateSliceFields(res, profileName, "extension", {"url":"tribalAffiliation"}, "tribalAffiliation", ["value","valueCodeableConcept"]), ...validateSliceCardinality(res, profileName, "extension", {"url":"isEnrolled"}, "isEnrolled", 0, 1), + ...validateSliceFields(res, profileName, "extension", {"url":"isEnrolled"}, "isEnrolled", ["value","valueBoolean"]), ...validateRequired(res, profileName, "url"), ...validateFixedValue(res, profileName, "url", USCoreTribalAffiliationExtensionProfile.canonicalUrl), ], diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBloodPressureProfile.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBloodPressureProfile.ts index e3bf5938..8d75076e 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBloodPressureProfile.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBloodPressureProfile.ts @@ -35,6 +35,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, @@ -373,7 +374,9 @@ export class USCoreBloodPressureProfile { ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", 1, 1), + ...validateSliceFields(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", ["valueQuantity"]), ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", 1, 1), + ...validateSliceFields(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", ["valueQuantity"]), ...validateReference(res, profileName, "performer", ["PractitionerRole","USCoreCareTeam","USCoreOrganizationProfile","Patient","USCorePractitionerProfile","USCoreRelatedPersonProfile"]), ], warnings: [ diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBodyWeightProfile.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBodyWeightProfile.ts index 86ad76a4..e7256bd6 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBodyWeightProfile.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBodyWeightProfile.ts @@ -27,6 +27,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreVitalSignsProfile.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreVitalSignsProfile.ts index e1c90666..846a0aae 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreVitalSignsProfile.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreVitalSignsProfile.ts @@ -27,6 +27,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Patient_USCorePatientProfile.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Patient_USCorePatientProfile.ts index 5b6957d0..4a469b49 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Patient_USCorePatientProfile.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Patient_USCorePatientProfile.ts @@ -32,6 +32,7 @@ import { validateExcluded, validateFixedValue, validateSliceCardinality, + validateSliceFields, validateEnum, validateReference, validateChoiceRequired, diff --git a/examples/typescript-us-core/fhir-types/profile-helpers.ts b/examples/typescript-us-core/fhir-types/profile-helpers.ts index 2c159297..e02bbb12 100644 --- a/examples/typescript-us-core/fhir-types/profile-helpers.ts +++ b/examples/typescript-us-core/fhir-types/profile-helpers.ts @@ -378,6 +378,31 @@ export const validateSliceCardinality = ( return errors; }; +/** + * Validate required fields within matched slice elements. + * For each array item matching the discriminator, checks that the listed fields are present. + */ +export const validateSliceFields = ( + res: object, + profileName: string, + field: string, + match: Record, + sliceName: string, + requiredFields: string[], +): string[] => { + const items = (res as Record)[field] as unknown[] | undefined; + const errors: string[] = []; + for (const item of (items ?? []).filter((item) => matchesValue(item, match))) { + const obj = item as Record; + for (const rf of requiredFields) { + if (obj[rf] === undefined || obj[rf] === null) { + errors.push(`${profileName}.${field}[${sliceName}].${rf} is required`); + } + } + } + return errors; +}; + /** * Checks that at least one of the listed choice-type variants is present. * E.g. `["effectiveDateTime", "effectivePeriod"]`. diff --git a/examples/typescript-us-core/profile-bp.test.ts b/examples/typescript-us-core/profile-bp.test.ts index a973313d..9824d555 100644 --- a/examples/typescript-us-core/profile-bp.test.ts +++ b/examples/typescript-us-core/profile-bp.test.ts @@ -14,9 +14,11 @@ describe("demo: create a US Core blood pressure observation", () => { subject: { reference: "Patient/pt-1" }, }); - // Not yet valid: effective[x] is required by the vitalsigns base profile + // Not yet valid: effective[x] and component valueQuantity are required expect(profile.validate().errors).toEqual([ "USCoreBloodPressureProfile: at least one of effectiveDateTime, effectivePeriod is required", + "USCoreBloodPressureProfile.component[systolic].valueQuantity is required", + "USCoreBloodPressureProfile.component[diastolic].valueQuantity is required", ]); // Fill in the remaining required fields @@ -44,9 +46,11 @@ describe("demo: apply US Core BP profile to an existing Observation", () => { // apply() adds meta.profile, sets fixed values (code), and auto-populates required slices const profile = USCoreBloodPressureProfile.apply(obs); - // Not yet valid: effective[x] is required + // Not yet valid: effective[x] and component valueQuantity are required expect(profile.validate().errors).toEqual([ "USCoreBloodPressureProfile: at least one of effectiveDateTime, effectivePeriod is required", + "USCoreBloodPressureProfile.component[systolic].valueQuantity is required", + "USCoreBloodPressureProfile.component[diastolic].valueQuantity is required", ]); // The profile mutates the original resource — no copy is made