From 6ea1231972151ce05da4189e764a7433359aa9a1 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 2 Apr 2026 12:04:15 +0200 Subject: [PATCH 1/6] fix: Profile apply() and create() improvements, fix slice match array wrapping - apply() sets fixed-value fields and auto-populates discriminator-only slice stubs, matching create() behavior - apply() skips redundant resourceType assignment - Only auto-populate slices where the stub is the complete content (e.g. category VSCat). Skip type discriminators (Bundle entry) and slices with required fields beyond match keys (BP component) - Fix computeMatchFromSchema to wrap sliced elements in arrays (e.g. component code.coding), matching the fhirschema 0.0.10 fix - Fixed-value fields get a getter but no setter - create() split into createResource + apply for readability - Extension apply() sets URL, uses upsert for single extensions --- .../typescript/profile-helpers.ts | 12 ++++ .../typescript/profile-extensions.ts | 55 ++++++++++++------- .../typescript/profile-slices.ts | 16 +++++- .../writer-generator/typescript/profile.ts | 50 +++++++++++++---- src/typeschema/core/field-builder.ts | 25 ++++++++- 5 files changed, 125 insertions(+), 33 deletions(-) diff --git a/assets/api/writer-generator/typescript/profile-helpers.ts b/assets/api/writer-generator/typescript/profile-helpers.ts index 9191c8ff9..05a0253ed 100644 --- a/assets/api/writer-generator/typescript/profile-helpers.ts +++ b/assets/api/writer-generator/typescript/profile-helpers.ts @@ -157,6 +157,18 @@ export const pushExtension = (target: { extension?: (target.extension ??= []).push(ext); }; +/** + * Insert or replace an extension by URL on `target.extension`. + * If an extension with the same `url` already exists it is replaced in place; + * otherwise the new extension is appended (like {@link pushExtension}). + */ +export const upsertExtension = (target: { extension?: E[] }, ext: E): void => { + const list = (target.extension ??= []); + const idx = list.findIndex((e) => e.url === ext.url); + if (idx >= 0) list[idx] = ext; + else list.push(ext); +}; + // --------------------------------------------------------------------------- // Extension helpers // --------------------------------------------------------------------------- diff --git a/src/api/writer-generator/typescript/profile-extensions.ts b/src/api/writer-generator/typescript/profile-extensions.ts index cf86a3ad4..e6dfe9344 100644 --- a/src/api/writer-generator/typescript/profile-extensions.ts +++ b/src/api/writer-generator/typescript/profile-extensions.ts @@ -119,24 +119,31 @@ export const resolveExtensionProfile = ( return { className, modulePath, flatProfile }; }; -/** Generate the body of a raw Extension branch: validate url, then push. */ -const generateRawExtensionBody = (w: TypeScript, ext: ProfileExtension, targetPath: string[], paramName = "input") => { +/** Generate the body of a raw Extension branch: validate url, then push/upsert. */ +const generateRawExtensionBody = ( + w: TypeScript, + ext: ProfileExtension, + targetPath: string[], + paramName = "input", + useUpsert = false, +) => { w.line( `if (${paramName}.url !== ${JSON.stringify(ext.url)}) throw new Error(\`Expected extension url '${ext.url}', got '\${${paramName}.url}'\`)`, ); - generateExtensionPush(w, targetPath, paramName); + generateExtensionPush(w, targetPath, paramName, useUpsert); }; -/** Generate the code that pushes an extension onto the target (root or nested path). */ -export const generateExtensionPush = (w: TypeScript, targetPath: string[], extExpr: string) => { +/** Generate the code that pushes (or upserts) an extension onto the target (root or nested path). */ +export const generateExtensionPush = (w: TypeScript, targetPath: string[], extExpr: string, useUpsert = false) => { + const fn = useUpsert ? "upsertExtension" : "pushExtension"; if (targetPath.length === 0) { - w.line(`pushExtension(this.resource, ${extExpr})`); + w.line(`${fn}(this.resource, ${extExpr})`); } else { w.line( `const target = ensurePath(this.resource as unknown as Record, ${JSON.stringify(targetPath)})`, ); w.line("if (!Array.isArray(target.extension)) target.extension = [] as Extension[]"); - w.line(`pushExtension(target as unknown as { extension?: Extension[] }, ${extExpr})`); + w.line(`${fn}(target as unknown as { extension?: Extension[] }, ${extExpr})`); } }; @@ -218,6 +225,7 @@ const generateComplexExtensionSetter = (w: TypeScript, info: ExtensionMethodInfo const extProfileHasFlatInput = extProfileInfo ? collectSubExtensionSlices(extProfileInfo.flatProfile).length > 0 : false; + const useUpsert = ext.max === "1"; if (extProfileInfo && extProfileHasFlatInput) { const paramType = `${extProfileInfo.className}Flat | ${extProfileInfo.className} | Extension`; @@ -226,14 +234,20 @@ const generateComplexExtensionSetter = (w: TypeScript, info: ExtensionMethodInfo [ { cond: `input instanceof ${extProfileInfo.className}`, - body: () => generateExtensionPush(w, targetPath, "input.toResource()"), + body: () => generateExtensionPush(w, targetPath, "input.toResource()", useUpsert), }, { cond: "isExtension(input)", - body: () => generateRawExtensionBody(w, ext, targetPath), + body: () => generateRawExtensionBody(w, ext, targetPath, "input", useUpsert), }, ], - () => generateExtensionPush(w, targetPath, `${extProfileInfo.className}.createResource(input)`), + () => + generateExtensionPush( + w, + targetPath, + `${extProfileInfo.className}.createResource(input)`, + useUpsert, + ), ); w.line("return this"); }); @@ -256,15 +270,16 @@ const generateComplexExtensionSetter = (w: TypeScript, info: ExtensionMethodInfo }); } } + const extLiteral = `{ url: "${ext.url}", extension: subExtensions }`; + const fn = useUpsert ? "upsertExtension" : "pushExtension"; if (targetPath.length === 0) { - w.line("const list = (this.resource.extension ??= [])"); - w.line(`list.push({ url: "${ext.url}", extension: subExtensions })`); + w.line(`${fn}(this.resource, ${extLiteral})`); } else { w.line( `const target = ensurePath(this.resource as unknown as Record, ${JSON.stringify(targetPath)})`, ); w.line("if (!Array.isArray(target.extension)) target.extension = [] as Extension[]"); - w.line(`(target.extension as Extension[]).push({ url: "${ext.url}", extension: subExtensions })`); + w.line(`${fn}(target as unknown as { extension?: Extension[] }, ${extLiteral})`); } w.line("return this"); }); @@ -299,6 +314,7 @@ const generateSingleValueExtensionSetter = (w: TypeScript, tsIndex: TypeSchemaIn if (!firstValueType) return; const valueType = tsTypeFromIdentifier(firstValueType); const valueField = tsValueFieldName(firstValueType); + const useUpsert = ext.max === "1"; if (extProfileInfo) { const paramType = `${extProfileInfo.className} | Extension | ${valueType}`; @@ -313,21 +329,21 @@ const generateSingleValueExtensionSetter = (w: TypeScript, tsIndex: TypeSchemaIn [ { cond: `value instanceof ${extProfileInfo.className}`, - body: () => generateExtensionPush(w, targetPath, "value.toResource()"), + body: () => generateExtensionPush(w, targetPath, "value.toResource()", useUpsert), }, { cond: "isExtension(value)", - body: () => generateRawExtensionBody(w, ext, targetPath, "value"), + body: () => generateRawExtensionBody(w, ext, targetPath, "value", useUpsert), }, ], - () => generateExtensionPush(w, targetPath, elseExpr), + () => generateExtensionPush(w, targetPath, elseExpr, useUpsert), ); w.line("return this"); }); } else { w.curlyBlock(["public", setMethodName, `(value: ${valueType}): this`], () => { const extLiteral = `{ url: "${ext.url}", ${valueField}: value } as Extension`; - generateExtensionPush(w, targetPath, extLiteral); + generateExtensionPush(w, targetPath, extLiteral, useUpsert); w.line("return this"); }); } @@ -349,16 +365,17 @@ const generateSingleValueExtensionGetter = (w: TypeScript, info: ExtensionMethod const generateGenericExtensionSetter = (w: TypeScript, info: ExtensionMethodInfo) => { const { ext, setMethodName, targetPath } = info; + const useUpsert = ext.max === "1"; w.curlyBlock(["public", setMethodName, `(value: Omit | Extension): this`], () => { w.ifElseChain( [ { cond: "isExtension(value)", - body: () => generateRawExtensionBody(w, ext, targetPath, "value"), + body: () => generateRawExtensionBody(w, ext, targetPath, "value", useUpsert), }, ], - () => generateExtensionPush(w, targetPath, `{ url: "${ext.url}", ...value } as Extension`), + () => generateExtensionPush(w, targetPath, `{ url: "${ext.url}", ...value } as Extension`, useUpsert), ); w.line("return this"); }); diff --git a/src/api/writer-generator/typescript/profile-slices.ts b/src/api/writer-generator/typescript/profile-slices.ts index 73611b617..6675e6e1b 100644 --- a/src/api/writer-generator/typescript/profile-slices.ts +++ b/src/api/writer-generator/typescript/profile-slices.ts @@ -72,10 +72,24 @@ export const collectTypesFromSlices = ( } }; +/** + * Returns names of required slices that can be auto-populated with just the discriminator match. + * Slices are excluded (need user-provided data) when: + * - They have required fields beyond the match keys (e.g. BP component.valueQuantity) + * - The field uses a type discriminator (e.g. Bundle entry.resource) — the stub only sets + * resourceType, the user must provide the actual typed resource + */ export const collectRequiredSliceNames = (field: RegularField): string[] | undefined => { if (!field.array || !field.slicing?.slices) return undefined; + const isTypeDisc = field.slicing.discriminator?.some((d) => d.type === "type") ?? false; + if (isTypeDisc) return undefined; const names = Object.entries(field.slicing.slices) - .filter(([_, s]) => s.min !== undefined && s.min >= 1 && s.match && Object.keys(s.match).length > 0) + .filter(([_, s]) => { + if (s.min === undefined || s.min < 1 || !s.match || Object.keys(s.match).length === 0) return false; + const matchKeys = new Set(Object.keys(s.match)); + const requiredBeyondMatch = (s.required ?? []).filter((name) => !matchKeys.has(name)); + return requiredBeyondMatch.length === 0; + }) .map(([name]) => name); return names.length > 0 ? names : undefined; }; diff --git a/src/api/writer-generator/typescript/profile.ts b/src/api/writer-generator/typescript/profile.ts index 78cbc3c9e..fd0858afe 100644 --- a/src/api/writer-generator/typescript/profile.ts +++ b/src/api/writer-generator/typescript/profile.ts @@ -56,6 +56,8 @@ type ProfileFactoryInfo = { sliceAutoFields: { name: string; tsType: string; typeId: TypeIdentifier; sliceNames: string[] }[]; params: { name: string; tsType: string; typeId: TypeIdentifier }[]; accessors: { name: string; tsType: string; typeId: TypeIdentifier }[]; + /** Accessor names that come from valueConstraint fields — skip generating setters for these */ + fixedFields: Set; }; const collectChoiceAccessors = ( @@ -98,6 +100,7 @@ export const collectProfileFactoryInfo = ( const sliceAutoFields: ProfileFactoryInfo["sliceAutoFields"] = []; const params: ProfileFactoryInfo["params"] = []; const autoAccessors: ProfileFactoryInfo["accessors"] = []; + const fixedFields = new Set(); const fields = flatProfile.fields ?? {}; const promotedChoices = new Set(); const resolveRef = tsIndex.findLastSpecializationByIdentifier; @@ -118,6 +121,7 @@ export const collectProfileFactoryInfo = ( if (field.valueConstraint) { const value = JSON.stringify(field.valueConstraint.value); autoFields.push({ name, value: field.array ? `[${value}]` : value }); + fixedFields.add(name); if (isNotChoiceDeclarationField(field) && field.type) { const tsType = fieldTsType(field, resolveRef); autoAccessors.push({ name, tsType, typeId: field.type }); @@ -156,7 +160,7 @@ export const collectProfileFactoryInfo = ( ]); const accessors = [...autoAccessors, ...collectChoiceAccessors(flatProfile, promotedChoices)]; - return { autoFields, sliceAutoFields, params, accessors }; + return { autoFields, sliceAutoFields, params, accessors, fixedFields }; }; /** Include base-type required fields not already covered by profile constraints */ @@ -227,7 +231,10 @@ const generateProfileHelpersImport = ( if (extensions.some((ext) => ext.isComplex && ext.subExtensions)) imports.push("extractComplexExtension"); if (sliceDefs.some((s) => !s.typeDiscriminator)) imports.push("stripMatchKeys"); if (sliceDefs.some((s) => s.constrainedChoice)) imports.push("wrapSliceChoice", "unwrapSliceChoice"); - if (extensions.some((ext) => ext.url)) imports.push("isExtension", "getExtensionValue", "pushExtension"); + if (extensions.some((ext) => ext.url)) { + imports.push("isExtension", "getExtensionValue", "pushExtension"); + if (extensions.some((ext) => ext.url && ext.max === "1")) imports.push("upsertExtension"); + } if (Object.keys(flatProfile.fields ?? {}).length > 0) imports.push( "validateRequired", @@ -362,6 +369,28 @@ const generateFactoryMethods = ( if (hasMeta) { w.lineSM(`ensureProfile(resource, ${profileClassName}.canonicalUrl)`); } + if (flatProfile.base.name === "Extension" && flatProfile.identifier.url) { + w.lineSM(`resource.url = ${profileClassName}.canonicalUrl`); + } + const applyAutoFields = factoryInfo.autoFields.filter((f) => f.name !== "resourceType"); + if (applyAutoFields.length > 0) { + w.curlyBlock(["Object.assign(resource,"], () => { + for (const f of applyAutoFields) { + w.line(`${f.name}: ${f.value},`); + } + }, [")"]); + } + for (const f of factoryInfo.sliceAutoFields) { + const matchRefs = f.sliceNames.map((s) => `${profileClassName}.${tsSliceStaticName(s)}SliceMatch`); + w.line(`resource.${f.name} = ensureSliceDefaults(`); + w.indentBlock(() => { + w.line(`[...(resource.${f.name} ?? [])],`); + for (const ref of matchRefs) { + w.line(`${ref},`); + } + }); + w.lineSM(")"); + } w.lineSM(`return new ${profileClassName}(resource)`); }); w.line(); @@ -488,9 +517,8 @@ const generateFactoryMethods = ( }); w.line(); w.curlyBlock(["static", "create", `(${paramSignature})`, `: ${profileClassName}`], () => { - w.lineSM( - `return ${profileClassName}.apply(${profileClassName}.createResource(${hasParams ? "args" : ""}))`, - ); + w.lineSM(`const resource = ${profileClassName}.createResource(${hasParams ? "args" : ""})`); + w.lineSM(`return ${profileClassName}.apply(resource)`); }); } w.line(); @@ -529,11 +557,13 @@ const generateFieldAccessors = ( w.lineSM(`return ${tsGet("this.resource", fieldAccess)} as ${a.tsType} | undefined`); }); w.line(); - w.curlyBlock([`set${methodBaseName}`, `(value: ${a.tsType})`, ": this"], () => { - w.lineSM(`Object.assign(this.resource, { ${fieldAccess}: value })`); - w.lineSM("return this"); - }); - w.line(); + if (!factoryInfo.fixedFields.has(a.name)) { + w.curlyBlock([`set${methodBaseName}`, `(value: ${a.tsType})`, ": this"], () => { + w.lineSM(`Object.assign(this.resource, { ${fieldAccess}: value })`); + w.lineSM("return this"); + }); + w.line(); + } } }; diff --git a/src/typeschema/core/field-builder.ts b/src/typeschema/core/field-builder.ts index cdbc3afd3..7c6de590a 100644 --- a/src/typeschema/core/field-builder.ts +++ b/src/typeschema/core/field-builder.ts @@ -137,6 +137,7 @@ const collectDiscriminatorValue = ( segments: string[], index: number, result: Record, + arrayPaths: Set, ): void => { if (index >= segments.length || !schema.elements) return; @@ -152,6 +153,8 @@ const collectDiscriminatorValue = ( // Element has slicing with sub-slices — collect match values from required slices if (element.slicing?.slices) { + // Slicing implies array — record the path for post-processing + arrayPaths.add(segments.slice(0, index + 1).join(".")); const remainingSegments = segments.slice(index + 1); for (const subSlice of Object.values(element.slicing.slices)) { if (!subSlice.min || subSlice.min < 1 || !subSlice.match || typeof subSlice.match !== "object") continue; @@ -169,7 +172,7 @@ const collectDiscriminatorValue = ( } // Continue navigating deeper - collectDiscriminatorValue(element, segments, index + 1, result); + collectDiscriminatorValue(element, segments, index + 1, result, arrayPaths); }; /** @@ -207,16 +210,32 @@ const computeMatchFromSchema = ( if (!schema || !discriminators || discriminators.length === 0) return undefined; const result: Record = {}; + const arrayPaths = new Set(); for (const disc of discriminators) { if (disc.type === "type") { computeTypeDiscriminatorMatch(disc.path, schema, result); } else { if (!schema.elements) continue; const segments = disc.path.split("."); - collectDiscriminatorValue(schema, segments, 0, result); + collectDiscriminatorValue(schema, segments, 0, result, arrayPaths); } } - return Object.keys(result).length > 0 ? result : undefined; + if (Object.keys(result).length === 0) return undefined; + // Wrap values at paths where sliced (array) elements were traversed + for (const path of arrayPaths) { + const segments = path.split("."); + let target: Record = result; + for (let i = 0; i < segments.length - 1; i++) { + const v = target[segments[i] as string]; + if (!v || typeof v !== "object" || Array.isArray(v)) break; + target = v as Record; + } + const key = segments[segments.length - 1] as string; + if (target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) { + target[key] = [target[key]]; + } + } + return result; }; const buildSlicing = (element: FHIRSchemaElement): FieldSlicing | undefined => { From 3e0431b893bbddbffc963eeb4224d2f6752a65c6 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 2 Apr 2026 12:04:21 +0200 Subject: [PATCH 2/6] chore: Update @atomic-ehr/fhirschema to 0.0.10 Slice match values now correctly wrap array elements (e.g. coding is Coding[] not a plain object). --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index dbe9b7cdc..af7fb4fab 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@atomic-ehr/codegen", "dependencies": { "@atomic-ehr/fhir-canonical-manager": "^0.0.21", - "@atomic-ehr/fhirschema": "0.0.9", + "@atomic-ehr/fhirschema": "0.0.10", "mustache": "^4.2.0", "picocolors": "^1.1.1", "yaml": "^2.8.3", @@ -34,7 +34,7 @@ "packages": { "@atomic-ehr/fhir-canonical-manager": ["@atomic-ehr/fhir-canonical-manager@0.0.21", "", { "peerDependencies": { "typescript": "^5" }, "bin": { "fcm": "dist/cli/index.js" } }, "sha512-MTmPXWixNJ6Wa2b9AqTeo4wg37w9u9ebDnVj0eRDwspDrNHhcgM/XYtEV3Oio7Mnzam3n9m0IPsto8iPQ87ilA=="], - "@atomic-ehr/fhirschema": ["@atomic-ehr/fhirschema@0.0.9", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-1RfyESETkGUy1VJbCF67T5jDEULMKuEMOjspxgINp+vnmTv+dr9ts1dgtvbgiWSTBdkrfDHFFKmCwHAYjatPyw=="], + "@atomic-ehr/fhirschema": ["@atomic-ehr/fhirschema@0.0.10", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-7yNkq6VwHFp7CC7KFFfxvi6k2LngfcWhBepS+BqZTNiXdbHurR+5PMbmmgZH3Bm3s11nZayl4ntlSPcjPcPmeA=="], "@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="], diff --git a/package.json b/package.json index 187bbb25f..166f16a0d 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "homepage": "https://github.com/atomic-ehr/codegen#readme", "dependencies": { "@atomic-ehr/fhir-canonical-manager": "^0.0.21", - "@atomic-ehr/fhirschema": "0.0.9", + "@atomic-ehr/fhirschema": "0.0.10", "mustache": "^4.2.0", "picocolors": "^1.1.1", "yaml": "^2.8.3", From cd3de1a8546b895d942b5b8e170724a3d7e522b6 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 2 Apr 2026 12:04:27 +0200 Subject: [PATCH 3/6] chore: Regenerate examples and snapshots --- .../profiles/Extension_birthPlace.ts | 12 +- .../profiles/Extension_birthTime.ts | 12 +- .../profiles/Extension_nationality.ts | 14 +- .../profiles/Extension_own_prefix.ts | 12 +- .../Observation_observation_bodyweight.ts | 19 ++- .../profiles/Observation_observation_bp.ts | 32 ++-- .../Observation_observation_vitalsigns.ts | 11 +- .../fhir-types/profile-helpers.ts | 12 ++ .../Observation_observation_vitalsigns.ts | 11 +- .../Extension_USCoreEthnicityExtension.ts | 26 ++-- .../Extension_USCoreIndividualSexExtension.ts | 12 +- ...ension_USCoreInterpreterNeededExtension.ts | 12 +- .../profiles/Extension_USCoreRaceExtension.ts | 22 ++- ...ension_USCoreTribalAffiliationExtension.ts | 26 ++-- .../Observation_USCoreBloodPressureProfile.ts | 24 ++- .../Observation_USCoreBodyWeightProfile.ts | 19 ++- .../Observation_USCoreVitalSignsProfile.ts | 11 +- .../profiles/Patient_USCorePatientProfile.ts | 28 ++-- .../fhir-types/profile-helpers.ts | 12 ++ .../__snapshots__/introspection.test.ts.snap | 2 +- .../__snapshots__/typescript.test.ts.snap | 144 ++++++++++-------- .../__snapshots__/local-package.test.ts.snap | 19 +-- 22 files changed, 274 insertions(+), 218 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 1dbdc3f7e..6d8f3a936 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 @@ -39,6 +39,10 @@ export class birthPlaceProfile { } static apply (resource: Extension) : birthPlaceProfile { + resource.url = birthPlaceProfile.canonicalUrl; + Object.assign(resource, { + url: "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", + }) return new birthPlaceProfile(resource); } @@ -51,7 +55,8 @@ export class birthPlaceProfile { } static create (args: birthPlaceProfileRaw) : birthPlaceProfile { - return birthPlaceProfile.apply(birthPlaceProfile.createResource(args)); + const resource = birthPlaceProfile.createResource(args); + return birthPlaceProfile.apply(resource); } toResource () : Extension { @@ -72,11 +77,6 @@ export class birthPlaceProfile { return this.resource.url as string | undefined; } - setUrl (value: string) : this { - Object.assign(this.resource, { url: value }); - return this; - } - // Extensions // Slices // Validation 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 0eb606652..c2961eab4 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 @@ -38,6 +38,10 @@ export class birthTimeProfile { } static apply (resource: Extension) : birthTimeProfile { + resource.url = birthTimeProfile.canonicalUrl; + Object.assign(resource, { + url: "http://hl7.org/fhir/StructureDefinition/patient-birthTime", + }) return new birthTimeProfile(resource); } @@ -50,7 +54,8 @@ export class birthTimeProfile { } static create (args: birthTimeProfileRaw) : birthTimeProfile { - return birthTimeProfile.apply(birthTimeProfile.createResource(args)); + const resource = birthTimeProfile.createResource(args); + return birthTimeProfile.apply(resource); } toResource () : Extension { @@ -71,11 +76,6 @@ export class birthTimeProfile { return this.resource.url as string | undefined; } - setUrl (value: string) : this { - Object.assign(this.resource, { url: value }); - return this; - } - // Extensions // Slices // Validation 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 14b8156e4..570702cfb 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 @@ -12,6 +12,7 @@ import { isExtension, getExtensionValue, pushExtension, + upsertExtension, validateRequired, validateExcluded, validateFixedValue, @@ -49,6 +50,10 @@ export class nationalityProfile { } static apply (resource: Extension) : nationalityProfile { + resource.url = nationalityProfile.canonicalUrl; + Object.assign(resource, { + url: "http://hl7.org/fhir/StructureDefinition/patient-nationality", + }) return new nationalityProfile(resource); } @@ -90,14 +95,9 @@ export class nationalityProfile { return this.resource.url as string | undefined; } - setUrl (value: string) : this { - Object.assign(this.resource, { url: value }); - return this; - } - // Extensions public setCode (value: CodeableConcept): this { - pushExtension(this.resource, { url: "code", valueCodeableConcept: value } as Extension) + upsertExtension(this.resource, { url: "code", valueCodeableConcept: value } as Extension) return this } @@ -112,7 +112,7 @@ export class nationalityProfile { } public setPeriod (value: Period): this { - pushExtension(this.resource, { url: "period", valuePeriod: value } as Extension) + upsertExtension(this.resource, { url: "period", valuePeriod: value } as Extension) return this } 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 b0c8a2aa5..353d6bdbd 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 @@ -38,6 +38,10 @@ export class own_prefixProfile { } static apply (resource: Extension) : own_prefixProfile { + resource.url = own_prefixProfile.canonicalUrl; + Object.assign(resource, { + url: "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix", + }) return new own_prefixProfile(resource); } @@ -50,7 +54,8 @@ export class own_prefixProfile { } static create (args: own_prefixProfileRaw) : own_prefixProfile { - return own_prefixProfile.apply(own_prefixProfile.createResource(args)); + const resource = own_prefixProfile.createResource(args); + return own_prefixProfile.apply(resource); } toResource () : Extension { @@ -71,11 +76,6 @@ export class own_prefixProfile { return this.resource.url as string | undefined; } - setUrl (value: string) : this { - Object.assign(this.resource, { url: value }); - return this; - } - // Extensions // Slices // Validation 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 1e97e7b9c..9e303edbf 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 @@ -39,7 +39,7 @@ export type observation_bodyweightProfileRaw = { export class observation_bodyweightProfile { static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/bodyweight"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; private resource: Observation; @@ -59,6 +59,13 @@ export class observation_bodyweightProfile { static apply (resource: Observation) : observation_bodyweightProfile { ensureProfile(resource, observation_bodyweightProfile.canonicalUrl); + Object.assign(resource, { + code: {"coding":[{"code":"29463-7","system":"http://loinc.org"}]}, + }) + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + observation_bodyweightProfile.VSCatSliceMatch, + ); return new observation_bodyweightProfile(resource); } @@ -80,7 +87,8 @@ export class observation_bodyweightProfile { } static create (args: observation_bodyweightProfileRaw) : observation_bodyweightProfile { - return observation_bodyweightProfile.apply(observation_bodyweightProfile.createResource(args)); + const resource = observation_bodyweightProfile.createResource(args); + return observation_bodyweightProfile.apply(resource); } toResource () : Observation { @@ -119,11 +127,6 @@ export class observation_bodyweightProfile { return this.resource.code as CodeableConcept<("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" | string)> | undefined; } - setCode (value: CodeableConcept<("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" | string)>) : this { - Object.assign(this.resource, { code: value }); - return this; - } - getEffectiveDateTime () : string | undefined { return this.resource.effectiveDateTime as string | undefined; } @@ -184,7 +187,7 @@ export class observation_bodyweightProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"29463-7","system":"http://loinc.org"}]}), ...validateRequired(res, profileName, "subject"), 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 4872a4023..1f20e9da2 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 @@ -44,9 +44,9 @@ export type observation_bpProfileRaw = { export class observation_bpProfile { static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/bp"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; - private static readonly SystolicBPSliceMatch: Record = {"code":{"coding":{"code":"8480-6","system":"http://loinc.org"}}}; - private static readonly DiastolicBPSliceMatch: Record = {"code":{"coding":{"code":"8462-4","system":"http://loinc.org"}}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; + private static readonly SystolicBPSliceMatch: Record = {"code":{"coding":[{"code":"8480-6","system":"http://loinc.org"}]}}; + private static readonly DiastolicBPSliceMatch: Record = {"code":{"coding":[{"code":"8462-4","system":"http://loinc.org"}]}}; private resource: Observation; @@ -66,6 +66,18 @@ export class observation_bpProfile { static apply (resource: Observation) : observation_bpProfile { ensureProfile(resource, observation_bpProfile.canonicalUrl); + Object.assign(resource, { + code: {"coding":[{"code":"85354-9","system":"http://loinc.org"}]}, + }) + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + observation_bpProfile.VSCatSliceMatch, + ); + resource.component = ensureSliceDefaults( + [...(resource.component ?? [])], + observation_bpProfile.SystolicBPSliceMatch, + observation_bpProfile.DiastolicBPSliceMatch, + ); return new observation_bpProfile(resource); } @@ -93,7 +105,8 @@ export class observation_bpProfile { } static create (args: observation_bpProfileRaw) : observation_bpProfile { - return observation_bpProfile.apply(observation_bpProfile.createResource(args)); + const resource = observation_bpProfile.createResource(args); + return observation_bpProfile.apply(resource); } toResource () : Observation { @@ -132,11 +145,6 @@ export class observation_bpProfile { return this.resource.code as CodeableConcept<("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" | string)> | undefined; } - setCode (value: CodeableConcept<("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" | string)>) : this { - Object.assign(this.resource, { code: value }); - return this; - } - getComponent () : ObservationComponent[] | undefined { return this.resource.component as ObservationComponent[] | undefined; } @@ -252,7 +260,7 @@ export class observation_bpProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"85354-9","system":"http://loinc.org"}]}), ...validateRequired(res, profileName, "subject"), @@ -260,8 +268,8 @@ export class observation_bpProfile { ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), ...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), - ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":{"code":"8462-4","system":"http://loinc.org"}}}, "DiastolicBP", 1, 1), + ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"code":"8480-6","system":"http://loinc.org"}]}}, "SystolicBP", 1, 1), + ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"code":"8462-4","system":"http://loinc.org"}]}}, "DiastolicBP", 1, 1), ], 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 b37de2412..280bc6ff2 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 @@ -39,7 +39,7 @@ export type observation_vitalsignsProfileRaw = { export class observation_vitalsignsProfile { static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/vitalsigns"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; private resource: Observation; @@ -59,6 +59,10 @@ export class observation_vitalsignsProfile { static apply (resource: Observation) : observation_vitalsignsProfile { ensureProfile(resource, observation_vitalsignsProfile.canonicalUrl); + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + observation_vitalsignsProfile.VSCatSliceMatch, + ); return new observation_vitalsignsProfile(resource); } @@ -80,7 +84,8 @@ export class observation_vitalsignsProfile { } static create (args: observation_vitalsignsProfileRaw) : observation_vitalsignsProfile { - return observation_vitalsignsProfile.apply(observation_vitalsignsProfile.createResource(args)); + const resource = observation_vitalsignsProfile.createResource(args); + return observation_vitalsignsProfile.apply(resource); } toResource () : Observation { @@ -175,7 +180,7 @@ export class observation_vitalsignsProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateRequired(res, profileName, "subject"), ...validateReference(res, profileName, "subject", ["Patient"]), diff --git a/examples/typescript-r4/fhir-types/profile-helpers.ts b/examples/typescript-r4/fhir-types/profile-helpers.ts index 9191c8ff9..05a0253ed 100644 --- a/examples/typescript-r4/fhir-types/profile-helpers.ts +++ b/examples/typescript-r4/fhir-types/profile-helpers.ts @@ -157,6 +157,18 @@ export const pushExtension = (target: { extension?: (target.extension ??= []).push(ext); }; +/** + * Insert or replace an extension by URL on `target.extension`. + * If an extension with the same `url` already exists it is replaced in place; + * otherwise the new extension is appended (like {@link pushExtension}). + */ +export const upsertExtension = (target: { extension?: E[] }, ext: E): void => { + const list = (target.extension ??= []); + const idx = list.findIndex((e) => e.url === ext.url); + if (idx >= 0) list[idx] = ext; + else list.push(ext); +}; + // --------------------------------------------------------------------------- // Extension helpers // --------------------------------------------------------------------------- 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 b37de2412..280bc6ff2 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 @@ -39,7 +39,7 @@ export type observation_vitalsignsProfileRaw = { export class observation_vitalsignsProfile { static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/vitalsigns"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; private resource: Observation; @@ -59,6 +59,10 @@ export class observation_vitalsignsProfile { static apply (resource: Observation) : observation_vitalsignsProfile { ensureProfile(resource, observation_vitalsignsProfile.canonicalUrl); + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + observation_vitalsignsProfile.VSCatSliceMatch, + ); return new observation_vitalsignsProfile(resource); } @@ -80,7 +84,8 @@ export class observation_vitalsignsProfile { } static create (args: observation_vitalsignsProfileRaw) : observation_vitalsignsProfile { - return observation_vitalsignsProfile.apply(observation_vitalsignsProfile.createResource(args)); + const resource = observation_vitalsignsProfile.createResource(args); + return observation_vitalsignsProfile.apply(resource); } toResource () : Observation { @@ -175,7 +180,7 @@ export class observation_vitalsignsProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateRequired(res, profileName, "subject"), ...validateReference(res, profileName, "subject", ["Patient"]), 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 60774bc2b..d15408b57 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 @@ -23,6 +23,7 @@ import { isExtension, getExtensionValue, pushExtension, + upsertExtension, validateRequired, validateExcluded, validateFixedValue, @@ -34,7 +35,7 @@ import { } from "../../profile-helpers"; export type USCoreEthnicityExtensionProfileRaw = { - extension?: Extension[]; + extension: Extension[]; } export type USCoreEthnicityExtensionProfileFlat = { @@ -65,6 +66,10 @@ export class USCoreEthnicityExtensionProfile { } static apply (resource: Extension) : USCoreEthnicityExtensionProfile { + resource.url = USCoreEthnicityExtensionProfile.canonicalUrl; + Object.assign(resource, { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + }) return new USCoreEthnicityExtensionProfile(resource); } @@ -90,14 +95,10 @@ export class USCoreEthnicityExtensionProfile { static createResource (args: USCoreEthnicityExtensionProfileRaw | USCoreEthnicityExtensionProfileFlat) : Extension { const resolvedExtensions = USCoreEthnicityExtensionProfile.resolveInput(args ?? {}); - const extensionWithDefaults = ensureSliceDefaults( - resolvedExtensions, - USCoreEthnicityExtensionProfile.textSliceMatch, - ); const resource = buildResource( { url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", - extension: extensionWithDefaults, + extension: resolvedExtensions, }) return resource; } @@ -124,18 +125,13 @@ export class USCoreEthnicityExtensionProfile { return this.resource.url as string | undefined; } - setUrl (value: string) : this { - Object.assign(this.resource, { url: value }); - return this; - } - // Extensions public setOmbCategory (value: Omit | Extension): this { if (isExtension(value)) { if (value.url !== "ombCategory") throw new Error(`Expected extension url 'ombCategory', got '${value.url}'`) - pushExtension(this.resource, value) + upsertExtension(this.resource, value) } else { - pushExtension(this.resource, { url: "ombCategory", ...value } as Extension) + upsertExtension(this.resource, { url: "ombCategory", ...value } as Extension) } return this } @@ -161,9 +157,9 @@ export class USCoreEthnicityExtensionProfile { public setText (value: Omit | Extension): this { if (isExtension(value)) { if (value.url !== "text") throw new Error(`Expected extension url 'text', got '${value.url}'`) - pushExtension(this.resource, value) + upsertExtension(this.resource, value) } else { - pushExtension(this.resource, { url: "text", ...value } as Extension) + upsertExtension(this.resource, { url: "text", ...value } as Extension) } return this } 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 4bf1c2a70..68ca39fb8 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 @@ -39,6 +39,10 @@ export class USCoreIndividualSexExtensionProfile { } static apply (resource: Extension) : USCoreIndividualSexExtensionProfile { + resource.url = USCoreIndividualSexExtensionProfile.canonicalUrl; + Object.assign(resource, { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex", + }) return new USCoreIndividualSexExtensionProfile(resource); } @@ -51,7 +55,8 @@ export class USCoreIndividualSexExtensionProfile { } static create (args: USCoreIndividualSexExtensionProfileRaw) : USCoreIndividualSexExtensionProfile { - return USCoreIndividualSexExtensionProfile.apply(USCoreIndividualSexExtensionProfile.createResource(args)); + const resource = USCoreIndividualSexExtensionProfile.createResource(args); + return USCoreIndividualSexExtensionProfile.apply(resource); } toResource () : Extension { @@ -72,11 +77,6 @@ export class USCoreIndividualSexExtensionProfile { return this.resource.url as string | undefined; } - setUrl (value: string) : this { - Object.assign(this.resource, { url: value }); - return this; - } - // Extensions // Slices // Validation 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 54141a3e1..d2ba5a35e 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 @@ -39,6 +39,10 @@ export class USCoreInterpreterNeededExtensionProfile { } static apply (resource: Extension) : USCoreInterpreterNeededExtensionProfile { + resource.url = USCoreInterpreterNeededExtensionProfile.canonicalUrl; + Object.assign(resource, { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed", + }) return new USCoreInterpreterNeededExtensionProfile(resource); } @@ -51,7 +55,8 @@ export class USCoreInterpreterNeededExtensionProfile { } static create (args: USCoreInterpreterNeededExtensionProfileRaw) : USCoreInterpreterNeededExtensionProfile { - return USCoreInterpreterNeededExtensionProfile.apply(USCoreInterpreterNeededExtensionProfile.createResource(args)); + const resource = USCoreInterpreterNeededExtensionProfile.createResource(args); + return USCoreInterpreterNeededExtensionProfile.apply(resource); } toResource () : Extension { @@ -72,11 +77,6 @@ export class USCoreInterpreterNeededExtensionProfile { return this.resource.url as string | undefined; } - setUrl (value: string) : this { - Object.assign(this.resource, { url: value }); - return this; - } - // Extensions // Slices // Validation 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 90b061a00..0e7186820 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 @@ -23,6 +23,7 @@ import { isExtension, getExtensionValue, pushExtension, + upsertExtension, validateRequired, validateExcluded, validateFixedValue, @@ -34,7 +35,7 @@ import { } from "../../profile-helpers"; export type USCoreRaceExtensionProfileRaw = { - extension?: Extension[]; + extension: Extension[]; } export type USCoreRaceExtensionProfileFlat = { @@ -65,6 +66,10 @@ export class USCoreRaceExtensionProfile { } static apply (resource: Extension) : USCoreRaceExtensionProfile { + resource.url = USCoreRaceExtensionProfile.canonicalUrl; + Object.assign(resource, { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + }) return new USCoreRaceExtensionProfile(resource); } @@ -90,14 +95,10 @@ export class USCoreRaceExtensionProfile { static createResource (args: USCoreRaceExtensionProfileRaw | USCoreRaceExtensionProfileFlat) : Extension { const resolvedExtensions = USCoreRaceExtensionProfile.resolveInput(args ?? {}); - const extensionWithDefaults = ensureSliceDefaults( - resolvedExtensions, - USCoreRaceExtensionProfile.textSliceMatch, - ); const resource = buildResource( { url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", - extension: extensionWithDefaults, + extension: resolvedExtensions, }) return resource; } @@ -124,11 +125,6 @@ export class USCoreRaceExtensionProfile { return this.resource.url as string | undefined; } - setUrl (value: string) : this { - Object.assign(this.resource, { url: value }); - return this; - } - // Extensions public setOmbCategory (value: Omit | Extension): this { if (isExtension(value)) { @@ -161,9 +157,9 @@ export class USCoreRaceExtensionProfile { public setText (value: Omit | Extension): this { if (isExtension(value)) { if (value.url !== "text") throw new Error(`Expected extension url 'text', got '${value.url}'`) - pushExtension(this.resource, value) + upsertExtension(this.resource, value) } else { - pushExtension(this.resource, { url: "text", ...value } as Extension) + upsertExtension(this.resource, { url: "text", ...value } as Extension) } return this } 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 b2db777a3..e79c5c853 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 @@ -22,6 +22,7 @@ import { isExtension, getExtensionValue, pushExtension, + upsertExtension, validateRequired, validateExcluded, validateFixedValue, @@ -33,7 +34,7 @@ import { } from "../../profile-helpers"; export type USCoreTribalAffiliationExtensionProfileRaw = { - extension?: Extension[]; + extension: Extension[]; } export type USCoreTribalAffiliationExtensionProfileFlat = { @@ -62,6 +63,10 @@ export class USCoreTribalAffiliationExtensionProfile { } static apply (resource: Extension) : USCoreTribalAffiliationExtensionProfile { + resource.url = USCoreTribalAffiliationExtensionProfile.canonicalUrl; + Object.assign(resource, { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation", + }) return new USCoreTribalAffiliationExtensionProfile(resource); } @@ -82,14 +87,10 @@ export class USCoreTribalAffiliationExtensionProfile { static createResource (args: USCoreTribalAffiliationExtensionProfileRaw | USCoreTribalAffiliationExtensionProfileFlat) : Extension { const resolvedExtensions = USCoreTribalAffiliationExtensionProfile.resolveInput(args ?? {}); - const extensionWithDefaults = ensureSliceDefaults( - resolvedExtensions, - USCoreTribalAffiliationExtensionProfile.tribalAffiliationSliceMatch, - ); const resource = buildResource( { url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation", - extension: extensionWithDefaults, + extension: resolvedExtensions, }) return resource; } @@ -116,18 +117,13 @@ export class USCoreTribalAffiliationExtensionProfile { return this.resource.url as string | undefined; } - setUrl (value: string) : this { - Object.assign(this.resource, { url: value }); - return this; - } - // Extensions public setTribalAffiliation (value: Omit | Extension): this { if (isExtension(value)) { if (value.url !== "tribalAffiliation") throw new Error(`Expected extension url 'tribalAffiliation', got '${value.url}'`) - pushExtension(this.resource, value) + upsertExtension(this.resource, value) } else { - pushExtension(this.resource, { url: "tribalAffiliation", ...value } as Extension) + upsertExtension(this.resource, { url: "tribalAffiliation", ...value } as Extension) } return this } @@ -139,9 +135,9 @@ export class USCoreTribalAffiliationExtensionProfile { public setIsEnrolled (value: Omit | Extension): this { if (isExtension(value)) { if (value.url !== "isEnrolled") throw new Error(`Expected extension url 'isEnrolled', got '${value.url}'`) - pushExtension(this.resource, value) + upsertExtension(this.resource, value) } else { - pushExtension(this.resource, { url: "isEnrolled", ...value } as Extension) + upsertExtension(this.resource, { url: "isEnrolled", ...value } as Extension) } return this } 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 5f1ffeb94..db1e68033 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 @@ -47,7 +47,7 @@ export type USCoreBloodPressureProfileRaw = { export class USCoreBloodPressureProfile { static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; private static readonly systolicSliceMatch: Record = {"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}; private static readonly diastolicSliceMatch: Record = {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}; @@ -69,6 +69,18 @@ export class USCoreBloodPressureProfile { static apply (resource: Observation) : USCoreBloodPressureProfile { ensureProfile(resource, USCoreBloodPressureProfile.canonicalUrl); + Object.assign(resource, { + code: {"coding":[{"system":"http://loinc.org","code":"85354-9"}]}, + }) + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + USCoreBloodPressureProfile.VSCatSliceMatch, + ); + resource.component = ensureSliceDefaults( + [...(resource.component ?? [])], + USCoreBloodPressureProfile.systolicSliceMatch, + USCoreBloodPressureProfile.diastolicSliceMatch, + ); return new USCoreBloodPressureProfile(resource); } @@ -96,7 +108,8 @@ export class USCoreBloodPressureProfile { } static create (args: USCoreBloodPressureProfileRaw) : USCoreBloodPressureProfile { - return USCoreBloodPressureProfile.apply(USCoreBloodPressureProfile.createResource(args)); + const resource = USCoreBloodPressureProfile.createResource(args); + return USCoreBloodPressureProfile.apply(resource); } toResource () : Observation { @@ -135,11 +148,6 @@ export class USCoreBloodPressureProfile { return this.resource.code as CodeableConcept<("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" | string)> | undefined; } - setCode (value: CodeableConcept<("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" | string)>) : this { - Object.assign(this.resource, { code: value }); - return this; - } - getComponent () : ObservationComponent[] | undefined { return this.resource.component as ObservationComponent[] | undefined; } @@ -345,7 +353,7 @@ export class USCoreBloodPressureProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateFixedValue(res, profileName, "code", {"coding":[{"system":"http://loinc.org","code":"85354-9"}]}), ...validateRequired(res, profileName, "subject"), 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 a2496de4e..a36184dc3 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 @@ -42,7 +42,7 @@ export type USCoreBodyWeightProfileRaw = { export class USCoreBodyWeightProfile { static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; private resource: Observation; @@ -62,6 +62,13 @@ export class USCoreBodyWeightProfile { static apply (resource: Observation) : USCoreBodyWeightProfile { ensureProfile(resource, USCoreBodyWeightProfile.canonicalUrl); + Object.assign(resource, { + code: {"coding":[{"system":"http://loinc.org","code":"29463-7"}]}, + }) + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + USCoreBodyWeightProfile.VSCatSliceMatch, + ); return new USCoreBodyWeightProfile(resource); } @@ -83,7 +90,8 @@ export class USCoreBodyWeightProfile { } static create (args: USCoreBodyWeightProfileRaw) : USCoreBodyWeightProfile { - return USCoreBodyWeightProfile.apply(USCoreBodyWeightProfile.createResource(args)); + const resource = USCoreBodyWeightProfile.createResource(args); + return USCoreBodyWeightProfile.apply(resource); } toResource () : Observation { @@ -122,11 +130,6 @@ export class USCoreBodyWeightProfile { return this.resource.code as CodeableConcept<("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" | string)> | undefined; } - setCode (value: CodeableConcept<("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" | string)>) : this { - Object.assign(this.resource, { code: value }); - return this; - } - getEffectiveDateTime () : string | undefined { return this.resource.effectiveDateTime as string | undefined; } @@ -277,7 +280,7 @@ export class USCoreBodyWeightProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateFixedValue(res, profileName, "code", {"coding":[{"system":"http://loinc.org","code":"29463-7"}]}), ...validateRequired(res, profileName, "subject"), 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 0e6288860..0f4d519fb 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 @@ -43,7 +43,7 @@ export type USCoreVitalSignsProfileRaw = { export class USCoreVitalSignsProfile { static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; private resource: Observation; @@ -63,6 +63,10 @@ export class USCoreVitalSignsProfile { static apply (resource: Observation) : USCoreVitalSignsProfile { ensureProfile(resource, USCoreVitalSignsProfile.canonicalUrl); + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + USCoreVitalSignsProfile.VSCatSliceMatch, + ); return new USCoreVitalSignsProfile(resource); } @@ -84,7 +88,8 @@ export class USCoreVitalSignsProfile { } static create (args: USCoreVitalSignsProfileRaw) : USCoreVitalSignsProfile { - return USCoreVitalSignsProfile.apply(USCoreVitalSignsProfile.createResource(args)); + const resource = USCoreVitalSignsProfile.createResource(args); + return USCoreVitalSignsProfile.apply(resource); } toResource () : Observation { @@ -278,7 +283,7 @@ export class USCoreVitalSignsProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateRequired(res, profileName, "subject"), ...validateReference(res, profileName, "subject", ["Patient"]), 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 64b37a783..3d2cc006c 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 @@ -28,6 +28,7 @@ import { isExtension, getExtensionValue, pushExtension, + upsertExtension, validateRequired, validateExcluded, validateFixedValue, @@ -79,7 +80,8 @@ export class USCorePatientProfile { } static create (args: USCorePatientProfileRaw) : USCorePatientProfile { - return USCorePatientProfile.apply(USCorePatientProfile.createResource(args)); + const resource = USCorePatientProfile.createResource(args); + return USCorePatientProfile.apply(resource); } toResource () : Patient { @@ -108,12 +110,12 @@ export class USCorePatientProfile { // Extensions public setRace (input: USCoreRaceExtensionProfileFlat | USCoreRaceExtensionProfile | Extension): this { if (input instanceof USCoreRaceExtensionProfile) { - pushExtension(this.resource, input.toResource()) + upsertExtension(this.resource, input.toResource()) } else if (isExtension(input)) { if (input.url !== "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race") throw new Error(`Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race', got '${input.url}'`) - pushExtension(this.resource, input) + upsertExtension(this.resource, input) } else { - pushExtension(this.resource, USCoreRaceExtensionProfile.createResource(input)) + upsertExtension(this.resource, USCoreRaceExtensionProfile.createResource(input)) } return this } @@ -133,12 +135,12 @@ export class USCorePatientProfile { public setEthnicity (input: USCoreEthnicityExtensionProfileFlat | USCoreEthnicityExtensionProfile | Extension): this { if (input instanceof USCoreEthnicityExtensionProfile) { - pushExtension(this.resource, input.toResource()) + upsertExtension(this.resource, input.toResource()) } else if (isExtension(input)) { if (input.url !== "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity") throw new Error(`Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity', got '${input.url}'`) - pushExtension(this.resource, input) + upsertExtension(this.resource, input) } else { - pushExtension(this.resource, USCoreEthnicityExtensionProfile.createResource(input)) + upsertExtension(this.resource, USCoreEthnicityExtensionProfile.createResource(input)) } return this } @@ -183,12 +185,12 @@ export class USCorePatientProfile { public setSex (value: USCoreIndividualSexExtensionProfile | Extension | Coding): this { if (value instanceof USCoreIndividualSexExtensionProfile) { - pushExtension(this.resource, value.toResource()) + upsertExtension(this.resource, value.toResource()) } else if (isExtension(value)) { if (value.url !== "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex") throw new Error(`Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex', got '${value.url}'`) - pushExtension(this.resource, value) + upsertExtension(this.resource, value) } else { - pushExtension(this.resource, USCoreIndividualSexExtensionProfile.createResource({ valueCoding: value as Coding })) + upsertExtension(this.resource, USCoreIndividualSexExtensionProfile.createResource({ valueCoding: value as Coding })) } return this } @@ -207,12 +209,12 @@ export class USCorePatientProfile { public setInterpreterRequired (value: USCoreInterpreterNeededExtensionProfile | Extension | Coding): this { if (value instanceof USCoreInterpreterNeededExtensionProfile) { - pushExtension(this.resource, value.toResource()) + upsertExtension(this.resource, value.toResource()) } else if (isExtension(value)) { if (value.url !== "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed") throw new Error(`Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed', got '${value.url}'`) - pushExtension(this.resource, value) + upsertExtension(this.resource, value) } else { - pushExtension(this.resource, USCoreInterpreterNeededExtensionProfile.createResource({ valueCoding: value as Coding })) + upsertExtension(this.resource, USCoreInterpreterNeededExtensionProfile.createResource({ valueCoding: value as Coding })) } return this } diff --git a/examples/typescript-us-core/fhir-types/profile-helpers.ts b/examples/typescript-us-core/fhir-types/profile-helpers.ts index 9191c8ff9..05a0253ed 100644 --- a/examples/typescript-us-core/fhir-types/profile-helpers.ts +++ b/examples/typescript-us-core/fhir-types/profile-helpers.ts @@ -157,6 +157,18 @@ export const pushExtension = (target: { extension?: (target.extension ??= []).push(ext); }; +/** + * Insert or replace an extension by URL on `target.extension`. + * If an extension with the same `url` already exists it is replaced in place; + * otherwise the new extension is appended (like {@link pushExtension}). + */ +export const upsertExtension = (target: { extension?: E[] }, ext: E): void => { + const list = (target.extension ??= []); + const idx = list.findIndex((e) => e.url === ext.url); + if (idx >= 0) list[idx] = ext; + else list.push(ext); +}; + // --------------------------------------------------------------------------- // Extension helpers // --------------------------------------------------------------------------- diff --git a/test/api/write-generator/__snapshots__/introspection.test.ts.snap b/test/api/write-generator/__snapshots__/introspection.test.ts.snap index 9d1d42651..694bea9e6 100644 --- a/test/api/write-generator/__snapshots__/introspection.test.ts.snap +++ b/test/api/write-generator/__snapshots__/introspection.test.ts.snap @@ -1396,7 +1396,7 @@ exports[`IntrospectionWriter - Fhir Schema Output Check all introspection data i {"name":"Variable","type":"Extension","kind":"complex-type","class":"extension","url":"http://hl7.org/fhir/StructureDefinition/variable","version":"4.0.1","description":"Variable specifying a logic to generate a variable for use in subsequent logic. The name of the variable will be added to FHIRPath's context when processing descendants of the element that contains this extension.","derivation":"constraint","base":"http://hl7.org/fhir/StructureDefinition/Extension","extensions":{},"elements":{"extension":{"index":0},"url":{"fixed":{"type":"uri","value":"http://hl7.org/fhir/StructureDefinition/variable"},"index":1},"value":{"choices":["valueExpression"],"index":3},"valueExpression":{"type":"Expression","choiceOf":"value","index":4}},"required":["value"],"package_meta":{"name":"hl7.fhir.r4.core","version":"4.0.1"}} {"name":"VerificationResult","type":"VerificationResult","kind":"resource","class":"resource","url":"http://hl7.org/fhir/StructureDefinition/VerificationResult","version":"4.0.1","description":"Describes validation requirements, source(s), status and dates for one or more elements.","derivation":"specialization","base":"http://hl7.org/fhir/StructureDefinition/DomainResource","elements":{"target":{"short":"A resource that was validated","type":"Reference","isSummary":true,"refers":["http://hl7.org/fhir/StructureDefinition/Resource"],"array":true,"index":0},"targetLocation":{"short":"The fhirpath location(s) within the resource that was validated","type":"string","isSummary":true,"array":true,"index":1},"need":{"short":"none | initial | periodic","type":"CodeableConcept","isSummary":true,"binding":{"strength":"preferred","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-need","bindingName":"need"},"index":2},"status":{"short":"attested | validated | in-process | req-revalid | val-fail | reval-fail","type":"code","isSummary":true,"binding":{"strength":"required","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-status|4.0.1","bindingName":"status"},"index":3},"statusDate":{"short":"When the validation status was updated","type":"dateTime","isSummary":true,"index":4},"validationType":{"short":"nothing | primary | multiple","type":"CodeableConcept","isSummary":true,"binding":{"strength":"preferred","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-validation-type","bindingName":"validation-type"},"index":5},"validationProcess":{"short":"The primary process by which the target is validated (edit check; value set; primary source; multiple sources; standalone; in context)","type":"CodeableConcept","isSummary":true,"binding":{"strength":"example","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-validation-process","bindingName":"validation-process"},"array":true,"index":6},"frequency":{"short":"Frequency of revalidation","type":"Timing","index":7},"lastPerformed":{"short":"The date/time validation was last completed (including failed validations)","type":"dateTime","index":8},"nextScheduled":{"short":"The date when target is next validated, if appropriate","type":"date","index":9},"failureAction":{"short":"fatal | warn | rec-only | none","type":"CodeableConcept","isSummary":true,"binding":{"strength":"preferred","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-failure-action","bindingName":"failure-action"},"index":10},"primarySource":{"short":"Information about the primary source(s) involved in validation","type":"BackboneElement","array":true,"index":11,"elements":{"who":{"short":"Reference to the primary source","type":"Reference","refers":["http://hl7.org/fhir/StructureDefinition/Organization","http://hl7.org/fhir/StructureDefinition/Practitioner","http://hl7.org/fhir/StructureDefinition/PractitionerRole"],"index":12},"type":{"short":"Type of primary source (License Board; Primary Education; Continuing Education; Postal Service; Relationship owner; Registration Authority; legal source; issuing source; authoritative source)","type":"CodeableConcept","isSummary":true,"binding":{"strength":"example","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-primary-source-type","bindingName":"primary-source-type"},"array":true,"index":13},"communicationMethod":{"short":"Method for exchanging information with the primary source","type":"CodeableConcept","isSummary":true,"binding":{"strength":"example","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-communication-method","bindingName":"communication-method"},"array":true,"index":14},"validationStatus":{"short":"successful | failed | unknown","type":"CodeableConcept","binding":{"strength":"preferred","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-validation-status","bindingName":"validation-status"},"index":15},"validationDate":{"short":"When the target was validated against the primary source","type":"dateTime","index":16},"canPushUpdates":{"short":"yes | no | undetermined","type":"CodeableConcept","isSummary":true,"binding":{"strength":"preferred","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-can-push-updates","bindingName":"can-push-updates"},"index":17},"pushTypeAvailable":{"short":"specific | any | source","type":"CodeableConcept","binding":{"strength":"preferred","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-push-type-available","bindingName":"push-type-available"},"array":true,"index":18}}},"attestation":{"short":"Information about the entity attesting to information","type":"BackboneElement","index":19,"elements":{"who":{"short":"The individual or organization attesting to information","type":"Reference","isSummary":true,"refers":["http://hl7.org/fhir/StructureDefinition/Organization","http://hl7.org/fhir/StructureDefinition/Practitioner","http://hl7.org/fhir/StructureDefinition/PractitionerRole"],"index":20},"onBehalfOf":{"short":"When the who is asserting on behalf of another (organization or individual)","type":"Reference","isSummary":true,"refers":["http://hl7.org/fhir/StructureDefinition/Organization","http://hl7.org/fhir/StructureDefinition/Practitioner","http://hl7.org/fhir/StructureDefinition/PractitionerRole"],"index":21},"communicationMethod":{"short":"The method by which attested information was submitted/retrieved","type":"CodeableConcept","isSummary":true,"binding":{"strength":"example","valueSet":"http://hl7.org/fhir/ValueSet/verificationresult-communication-method","bindingName":"communication-method"},"index":22},"date":{"short":"The date the information was attested to","type":"date","isSummary":true,"index":23},"sourceIdentityCertificate":{"short":"A digital identity certificate associated with the attestation source","type":"string","index":24},"proxyIdentityCertificate":{"short":"A digital identity certificate associated with the proxy entity submitting attested information on behalf of the attestation source","type":"string","index":25},"proxySignature":{"short":"Proxy signature","type":"Signature","index":26},"sourceSignature":{"short":"Attester signature","type":"Signature","index":27}}},"validator":{"short":"Information about the entity validating information","type":"BackboneElement","array":true,"index":28,"elements":{"organization":{"short":"Reference to the organization validating information","type":"Reference","refers":["http://hl7.org/fhir/StructureDefinition/Organization"],"index":29},"identityCertificate":{"short":"A digital identity certificate associated with the validator","type":"string","index":30},"attestationSignature":{"short":"Validator signature","type":"Signature","index":31}},"required":["organization"]}},"required":["status"],"package_meta":{"name":"hl7.fhir.r4.core","version":"4.0.1"}} {"name":"VisionPrescription","type":"VisionPrescription","kind":"resource","class":"resource","url":"http://hl7.org/fhir/StructureDefinition/VisionPrescription","version":"4.0.1","description":"An authorization for the provision of glasses and/or contact lenses to a patient.","derivation":"specialization","base":"http://hl7.org/fhir/StructureDefinition/DomainResource","elements":{"identifier":{"short":"Business Identifier for vision prescription","type":"Identifier","array":true,"index":0},"status":{"short":"active | cancelled | draft | entered-in-error","type":"code","isModifier":true,"isModifierReason":"This element is labelled as a modifier because it is a status element that contains status entered-in-error which means that the resource should not be treated as valid","isSummary":true,"binding":{"strength":"required","valueSet":"http://hl7.org/fhir/ValueSet/fm-status|4.0.1","bindingName":"VisionStatus"},"index":1},"created":{"short":"Response creation date","type":"dateTime","isSummary":true,"index":2},"patient":{"short":"Who prescription is for","type":"Reference","isSummary":true,"refers":["http://hl7.org/fhir/StructureDefinition/Patient"],"index":3},"encounter":{"short":"Created during encounter / admission / stay","type":"Reference","refers":["http://hl7.org/fhir/StructureDefinition/Encounter"],"index":4},"dateWritten":{"short":"When prescription was authorized","type":"dateTime","isSummary":true,"index":5},"prescriber":{"short":"Who authorized the vision prescription","type":"Reference","isSummary":true,"refers":["http://hl7.org/fhir/StructureDefinition/Practitioner","http://hl7.org/fhir/StructureDefinition/PractitionerRole"],"index":6},"lensSpecification":{"short":"Vision lens authorization","type":"BackboneElement","isSummary":true,"array":true,"min":1,"index":7,"elements":{"product":{"short":"Product to be supplied","type":"CodeableConcept","isSummary":true,"binding":{"strength":"example","valueSet":"http://hl7.org/fhir/ValueSet/vision-product","bindingName":"VisionProduct"},"index":8},"eye":{"short":"right | left","type":"code","isSummary":true,"binding":{"strength":"required","valueSet":"http://hl7.org/fhir/ValueSet/vision-eye-codes|4.0.1","bindingName":"VisionEyes"},"index":9},"sphere":{"short":"Power of the lens","type":"decimal","index":10},"cylinder":{"short":"Lens power for astigmatism","type":"decimal","index":11},"axis":{"short":"Lens meridian which contain no power for astigmatism","type":"integer","index":12},"prism":{"short":"Eye alignment compensation","type":"BackboneElement","array":true,"index":13,"elements":{"amount":{"short":"Amount of adjustment","type":"decimal","index":14},"base":{"short":"up | down | in | out","type":"code","binding":{"strength":"required","valueSet":"http://hl7.org/fhir/ValueSet/vision-base-codes|4.0.1","bindingName":"VisionBase"},"index":15}},"required":["amount","base"]},"add":{"short":"Added power for multifocal levels","type":"decimal","index":16},"power":{"short":"Contact lens power","type":"decimal","index":17},"backCurve":{"short":"Contact lens back curvature","type":"decimal","index":18},"diameter":{"short":"Contact lens diameter","type":"decimal","index":19},"duration":{"short":"Lens wear duration","type":"Quantity","index":20},"color":{"short":"Color required","type":"string","index":21},"brand":{"short":"Brand required","type":"string","index":22},"note":{"short":"Notes for coatings","type":"Annotation","array":true,"index":23}},"required":["eye","product"]}},"required":["created","dateWritten","lensSpecification","patient","prescriber","status"],"package_meta":{"name":"hl7.fhir.r4.core","version":"4.0.1"}} -{"name":"observation-vitalsigns","type":"Observation","kind":"resource","class":"profile","url":"http://hl7.org/fhir/StructureDefinition/vitalsigns","version":"4.0.1","description":"FHIR Vital Signs Profile","derivation":"constraint","base":"http://hl7.org/fhir/StructureDefinition/Observation","elements":{"status":{"type":"code","mustSupport":true,"binding":{"strength":"required","valueSet":"http://hl7.org/fhir/ValueSet/observation-status|4.0.1","bindingName":"Status"},"index":0},"category":{"type":"CodeableConcept","mustSupport":true,"array":true,"min":1,"index":1,"slicing":{"discriminator":[{"type":"value","path":"coding.code"},{"type":"value","path":"coding.system"}],"ordered":false,"rules":"open","min":1,"slices":{"VSCat":{"match":{"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}},"schema":{"type":"CodeableConcept","mustSupport":true,"_required":true,"index":2,"elements":{"coding":{"type":"Coding","mustSupport":true,"array":true,"min":1,"index":3,"elements":{"system":{"type":"uri","fixed":{"type":"uri","value":"http://terminology.hl7.org/CodeSystem/observation-category"},"mustSupport":true,"index":4},"code":{"type":"code","fixed":{"type":"code","value":"vital-signs"},"mustSupport":true,"index":5}},"required":["code","system"]}},"required":["coding"]},"min":1,"max":1}}}},"code":{"short":"Coded Responses from C-CDA Vital Sign Results","type":"CodeableConcept","mustSupport":true,"binding":{"strength":"extensible","valueSet":"http://hl7.org/fhir/ValueSet/observation-vitalsignresult","bindingName":"VitalSigns"},"index":6},"subject":{"type":"Reference","mustSupport":true,"refers":["http://hl7.org/fhir/StructureDefinition/Patient"],"index":7},"effective":{"short":"Often just a dateTime for Vital Signs","constraint":{"vs-1":{"expression":"($this as dateTime).toString().length() >= 8","human":"if Observation.effective[x] is dateTime and has a value then that value shall be precise to the day","severity":"error"}},"mustSupport":true,"choices":["effectiveDateTime","effectivePeriod"],"index":9},"effectiveDateTime":{"short":"Often just a dateTime for Vital Signs","type":"dateTime","constraint":{"vs-1":{"expression":"($this as dateTime).toString().length() >= 8","human":"if Observation.effective[x] is dateTime and has a value then that value shall be precise to the day","severity":"error"}},"mustSupport":true,"choiceOf":"effective","index":10},"effectivePeriod":{"short":"Often just a dateTime for Vital Signs","type":"Period","constraint":{"vs-1":{"expression":"($this as dateTime).toString().length() >= 8","human":"if Observation.effective[x] is dateTime and has a value then that value shall be precise to the day","severity":"error"}},"mustSupport":true,"choiceOf":"effective","index":11},"dataAbsentReason":{"type":"CodeableConcept","mustSupport":true,"index":13},"hasMember":{"short":"Used when reporting vital signs panel components","type":"Reference","refers":["http://hl7.org/fhir/StructureDefinition/MolecularSequence","http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse","http://hl7.org/fhir/StructureDefinition/vitalsigns"],"index":14},"derivedFrom":{"type":"Reference","refers":["http://hl7.org/fhir/StructureDefinition/DocumentReference","http://hl7.org/fhir/StructureDefinition/ImagingStudy","http://hl7.org/fhir/StructureDefinition/Media","http://hl7.org/fhir/StructureDefinition/MolecularSequence","http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse","http://hl7.org/fhir/StructureDefinition/vitalsigns"],"index":15},"component":{"short":"Used when reporting systolic and diastolic blood pressure.","constraint":{"vs-3":{"expression":"value.exists() or dataAbsentReason.exists()","human":"If there is no a value a data absent reason must be present","severity":"error"}},"mustSupport":true,"index":16,"elements":{"code":{"type":"CodeableConcept","mustSupport":true,"binding":{"strength":"extensible","valueSet":"http://hl7.org/fhir/ValueSet/observation-vitalsignresult","bindingName":"VitalSigns"},"index":17},"dataAbsentReason":{"type":"CodeableConcept","mustSupport":true,"index":19}},"required":["code"]}},"required":["category","code","effective","status","subject"],"package_meta":{"name":"hl7.fhir.r4.core","version":"4.0.1"}} +{"name":"observation-vitalsigns","type":"Observation","kind":"resource","class":"profile","url":"http://hl7.org/fhir/StructureDefinition/vitalsigns","version":"4.0.1","description":"FHIR Vital Signs Profile","derivation":"constraint","base":"http://hl7.org/fhir/StructureDefinition/Observation","elements":{"status":{"type":"code","mustSupport":true,"binding":{"strength":"required","valueSet":"http://hl7.org/fhir/ValueSet/observation-status|4.0.1","bindingName":"Status"},"index":0},"category":{"type":"CodeableConcept","mustSupport":true,"array":true,"min":1,"index":1,"slicing":{"discriminator":[{"type":"value","path":"coding.code"},{"type":"value","path":"coding.system"}],"ordered":false,"rules":"open","min":1,"slices":{"VSCat":{"match":{"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]},"schema":{"type":"CodeableConcept","mustSupport":true,"_required":true,"index":2,"elements":{"coding":{"type":"Coding","mustSupport":true,"array":true,"min":1,"index":3,"elements":{"system":{"type":"uri","fixed":{"type":"uri","value":"http://terminology.hl7.org/CodeSystem/observation-category"},"mustSupport":true,"index":4},"code":{"type":"code","fixed":{"type":"code","value":"vital-signs"},"mustSupport":true,"index":5}},"required":["code","system"]}},"required":["coding"]},"min":1,"max":1}}}},"code":{"short":"Coded Responses from C-CDA Vital Sign Results","type":"CodeableConcept","mustSupport":true,"binding":{"strength":"extensible","valueSet":"http://hl7.org/fhir/ValueSet/observation-vitalsignresult","bindingName":"VitalSigns"},"index":6},"subject":{"type":"Reference","mustSupport":true,"refers":["http://hl7.org/fhir/StructureDefinition/Patient"],"index":7},"effective":{"short":"Often just a dateTime for Vital Signs","constraint":{"vs-1":{"expression":"($this as dateTime).toString().length() >= 8","human":"if Observation.effective[x] is dateTime and has a value then that value shall be precise to the day","severity":"error"}},"mustSupport":true,"choices":["effectiveDateTime","effectivePeriod"],"index":9},"effectiveDateTime":{"short":"Often just a dateTime for Vital Signs","type":"dateTime","constraint":{"vs-1":{"expression":"($this as dateTime).toString().length() >= 8","human":"if Observation.effective[x] is dateTime and has a value then that value shall be precise to the day","severity":"error"}},"mustSupport":true,"choiceOf":"effective","index":10},"effectivePeriod":{"short":"Often just a dateTime for Vital Signs","type":"Period","constraint":{"vs-1":{"expression":"($this as dateTime).toString().length() >= 8","human":"if Observation.effective[x] is dateTime and has a value then that value shall be precise to the day","severity":"error"}},"mustSupport":true,"choiceOf":"effective","index":11},"dataAbsentReason":{"type":"CodeableConcept","mustSupport":true,"index":13},"hasMember":{"short":"Used when reporting vital signs panel components","type":"Reference","refers":["http://hl7.org/fhir/StructureDefinition/MolecularSequence","http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse","http://hl7.org/fhir/StructureDefinition/vitalsigns"],"index":14},"derivedFrom":{"type":"Reference","refers":["http://hl7.org/fhir/StructureDefinition/DocumentReference","http://hl7.org/fhir/StructureDefinition/ImagingStudy","http://hl7.org/fhir/StructureDefinition/Media","http://hl7.org/fhir/StructureDefinition/MolecularSequence","http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse","http://hl7.org/fhir/StructureDefinition/vitalsigns"],"index":15},"component":{"short":"Used when reporting systolic and diastolic blood pressure.","constraint":{"vs-3":{"expression":"value.exists() or dataAbsentReason.exists()","human":"If there is no a value a data absent reason must be present","severity":"error"}},"mustSupport":true,"index":16,"elements":{"code":{"type":"CodeableConcept","mustSupport":true,"binding":{"strength":"extensible","valueSet":"http://hl7.org/fhir/ValueSet/observation-vitalsignresult","bindingName":"VitalSigns"},"index":17},"dataAbsentReason":{"type":"CodeableConcept","mustSupport":true,"index":19}},"required":["code"]}},"required":["category","code","effective","status","subject"],"package_meta":{"name":"hl7.fhir.r4.core","version":"4.0.1"}} {"name":"observation-vitalspanel","type":"Observation","kind":"resource","class":"profile","url":"http://hl7.org/fhir/StructureDefinition/vitalspanel","version":"4.0.1","description":"FHIR Vital Signs Panel Profile","derivation":"constraint","base":"http://hl7.org/fhir/StructureDefinition/vitalsigns","elements":{"code":{"short":"Vital Signs Panel","index":0,"elements":{"coding":{"index":1,"slicing":{"discriminator":[{"type":"value","path":"code"},{"type":"value","path":"system"}],"ordered":false,"rules":"open","slices":{"VitalsPanelCode":{"match":{"code":"85353-1","system":"http://loinc.org"},"schema":{"index":2,"elements":{"system":{"type":"uri","fixed":{"type":"uri","value":"http://loinc.org"},"index":3},"code":{"type":"code","fixed":{"type":"code","value":"85353-1"},"index":4}}}}}}}}},"hasMember":{"mustSupport":true,"array":true,"min":1,"index":6}},"required":["hasMember"],"package_meta":{"name":"hl7.fhir.r4.core","version":"4.0.1"}} {"name":"episodeOfCare","type":"Extension","kind":"complex-type","class":"extension","url":"http://hl7.org/fhir/StructureDefinition/workflow-episodeOfCare","version":"4.0.1","description":"The episode(s) of care that establishes the context for this {{title}}.","derivation":"constraint","base":"http://hl7.org/fhir/StructureDefinition/Extension","extensions":{},"elements":{"extension":{"index":0},"url":{"fixed":{"type":"uri","value":"http://hl7.org/fhir/StructureDefinition/workflow-episodeOfCare"},"index":1},"value":{"choices":["valueReference"],"index":3},"valueReference":{"type":"Reference","choiceOf":"value","refers":["http://hl7.org/fhir/StructureDefinition/EpisodeOfCare"],"index":4}},"required":["value"],"package_meta":{"name":"hl7.fhir.r4.core","version":"4.0.1"}} {"name":"instantiatesCanonical","type":"Extension","kind":"complex-type","class":"extension","url":"http://hl7.org/fhir/StructureDefinition/workflow-instantiatesCanonical","version":"4.0.1","description":"The URL pointing to a FHIR-defined protocol, guideline, orderset or other definition that is adhered to in whole or in part by the event or request resource.","derivation":"constraint","base":"http://hl7.org/fhir/StructureDefinition/Extension","extensions":{},"elements":{"extension":{"index":0},"url":{"fixed":{"type":"uri","value":"http://hl7.org/fhir/StructureDefinition/workflow-instantiatesCanonical"},"index":1},"value":{"choices":["valueCanonical"],"index":3},"valueCanonical":{"type":"canonical","choiceOf":"value","index":4}},"required":["value"],"package_meta":{"name":"hl7.fhir.r4.core","version":"4.0.1"}} diff --git a/test/api/write-generator/__snapshots__/typescript.test.ts.snap b/test/api/write-generator/__snapshots__/typescript.test.ts.snap index ff9f09770..2a50335be 100644 --- a/test/api/write-generator/__snapshots__/typescript.test.ts.snap +++ b/test/api/write-generator/__snapshots__/typescript.test.ts.snap @@ -353,7 +353,7 @@ export type observation_bodyweightProfileRaw = { export class observation_bodyweightProfile { static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/bodyweight"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; private resource: Observation; @@ -373,6 +373,13 @@ export class observation_bodyweightProfile { static apply (resource: Observation) : observation_bodyweightProfile { ensureProfile(resource, observation_bodyweightProfile.canonicalUrl); + Object.assign(resource, { + code: {"coding":[{"code":"29463-7","system":"http://loinc.org"}]}, + }) + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + observation_bodyweightProfile.VSCatSliceMatch, + ); return new observation_bodyweightProfile(resource); } @@ -394,7 +401,8 @@ export class observation_bodyweightProfile { } static create (args: observation_bodyweightProfileRaw) : observation_bodyweightProfile { - return observation_bodyweightProfile.apply(observation_bodyweightProfile.createResource(args)); + const resource = observation_bodyweightProfile.createResource(args); + return observation_bodyweightProfile.apply(resource); } toResource () : Observation { @@ -433,11 +441,6 @@ export class observation_bodyweightProfile { return this.resource.code as CodeableConcept<("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" | string)> | undefined; } - setCode (value: CodeableConcept<("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" | string)>) : this { - Object.assign(this.resource, { code: value }); - return this; - } - getEffectiveDateTime () : string | undefined { return this.resource.effectiveDateTime as string | undefined; } @@ -498,7 +501,7 @@ export class observation_bodyweightProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"29463-7","system":"http://loinc.org"}]}), ...validateRequired(res, profileName, "subject"), @@ -568,9 +571,9 @@ export type observation_bpProfileRaw = { export class observation_bpProfile { static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/bp"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; - private static readonly SystolicBPSliceMatch: Record = {"code":{"coding":{"code":"8480-6","system":"http://loinc.org"}}}; - private static readonly DiastolicBPSliceMatch: Record = {"code":{"coding":{"code":"8462-4","system":"http://loinc.org"}}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; + private static readonly SystolicBPSliceMatch: Record = {"code":{"coding":[{"code":"8480-6","system":"http://loinc.org"}]}}; + private static readonly DiastolicBPSliceMatch: Record = {"code":{"coding":[{"code":"8462-4","system":"http://loinc.org"}]}}; private resource: Observation; @@ -590,6 +593,18 @@ export class observation_bpProfile { static apply (resource: Observation) : observation_bpProfile { ensureProfile(resource, observation_bpProfile.canonicalUrl); + Object.assign(resource, { + code: {"coding":[{"code":"85354-9","system":"http://loinc.org"}]}, + }) + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + observation_bpProfile.VSCatSliceMatch, + ); + resource.component = ensureSliceDefaults( + [...(resource.component ?? [])], + observation_bpProfile.SystolicBPSliceMatch, + observation_bpProfile.DiastolicBPSliceMatch, + ); return new observation_bpProfile(resource); } @@ -617,7 +632,8 @@ export class observation_bpProfile { } static create (args: observation_bpProfileRaw) : observation_bpProfile { - return observation_bpProfile.apply(observation_bpProfile.createResource(args)); + const resource = observation_bpProfile.createResource(args); + return observation_bpProfile.apply(resource); } toResource () : Observation { @@ -656,11 +672,6 @@ export class observation_bpProfile { return this.resource.code as CodeableConcept<("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" | string)> | undefined; } - setCode (value: CodeableConcept<("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" | string)>) : this { - Object.assign(this.resource, { code: value }); - return this; - } - getComponent () : ObservationComponent[] | undefined { return this.resource.component as ObservationComponent[] | undefined; } @@ -776,7 +787,7 @@ export class observation_bpProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"85354-9","system":"http://loinc.org"}]}), ...validateRequired(res, profileName, "subject"), @@ -784,8 +795,8 @@ export class observation_bpProfile { ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), ...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), - ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":{"code":"8462-4","system":"http://loinc.org"}}}, "DiastolicBP", 1, 1), + ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"code":"8480-6","system":"http://loinc.org"}]}}, "SystolicBP", 1, 1), + ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"code":"8462-4","system":"http://loinc.org"}]}}, "DiastolicBP", 1, 1), ], warnings: [ ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), @@ -832,6 +843,7 @@ import { isExtension, getExtensionValue, pushExtension, + upsertExtension, validateRequired, validateExcluded, validateFixedValue, @@ -883,7 +895,8 @@ export class USCorePatientProfile { } static create (args: USCorePatientProfileRaw) : USCorePatientProfile { - return USCorePatientProfile.apply(USCorePatientProfile.createResource(args)); + const resource = USCorePatientProfile.createResource(args); + return USCorePatientProfile.apply(resource); } toResource () : Patient { @@ -912,12 +925,12 @@ export class USCorePatientProfile { // Extensions public setRace (input: USCoreRaceExtensionProfileFlat | USCoreRaceExtensionProfile | Extension): this { if (input instanceof USCoreRaceExtensionProfile) { - pushExtension(this.resource, input.toResource()) + upsertExtension(this.resource, input.toResource()) } else if (isExtension(input)) { if (input.url !== "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race") throw new Error(\`Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race', got '\${input.url}'\`) - pushExtension(this.resource, input) + upsertExtension(this.resource, input) } else { - pushExtension(this.resource, USCoreRaceExtensionProfile.createResource(input)) + upsertExtension(this.resource, USCoreRaceExtensionProfile.createResource(input)) } return this } @@ -937,12 +950,12 @@ export class USCorePatientProfile { public setEthnicity (input: USCoreEthnicityExtensionProfileFlat | USCoreEthnicityExtensionProfile | Extension): this { if (input instanceof USCoreEthnicityExtensionProfile) { - pushExtension(this.resource, input.toResource()) + upsertExtension(this.resource, input.toResource()) } else if (isExtension(input)) { if (input.url !== "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity") throw new Error(\`Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity', got '\${input.url}'\`) - pushExtension(this.resource, input) + upsertExtension(this.resource, input) } else { - pushExtension(this.resource, USCoreEthnicityExtensionProfile.createResource(input)) + upsertExtension(this.resource, USCoreEthnicityExtensionProfile.createResource(input)) } return this } @@ -987,12 +1000,12 @@ export class USCorePatientProfile { public setSex (value: USCoreIndividualSexExtensionProfile | Extension | Coding): this { if (value instanceof USCoreIndividualSexExtensionProfile) { - pushExtension(this.resource, value.toResource()) + upsertExtension(this.resource, value.toResource()) } else if (isExtension(value)) { if (value.url !== "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex") throw new Error(\`Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex', got '\${value.url}'\`) - pushExtension(this.resource, value) + upsertExtension(this.resource, value) } else { - pushExtension(this.resource, USCoreIndividualSexExtensionProfile.createResource({ valueCoding: value as Coding })) + upsertExtension(this.resource, USCoreIndividualSexExtensionProfile.createResource({ valueCoding: value as Coding })) } return this } @@ -1011,12 +1024,12 @@ export class USCorePatientProfile { public setInterpreterRequired (value: USCoreInterpreterNeededExtensionProfile | Extension | Coding): this { if (value instanceof USCoreInterpreterNeededExtensionProfile) { - pushExtension(this.resource, value.toResource()) + upsertExtension(this.resource, value.toResource()) } else if (isExtension(value)) { if (value.url !== "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed") throw new Error(\`Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed', got '\${value.url}'\`) - pushExtension(this.resource, value) + upsertExtension(this.resource, value) } else { - pushExtension(this.resource, USCoreInterpreterNeededExtensionProfile.createResource({ valueCoding: value as Coding })) + upsertExtension(this.resource, USCoreInterpreterNeededExtensionProfile.createResource({ valueCoding: value as Coding })) } return this } @@ -1105,7 +1118,7 @@ export type USCoreBloodPressureProfileRaw = { export class USCoreBloodPressureProfile { static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; private static readonly systolicSliceMatch: Record = {"code":{"coding":[{"system":"http://loinc.org","code":"8480-6"}]}}; private static readonly diastolicSliceMatch: Record = {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}; @@ -1127,6 +1140,18 @@ export class USCoreBloodPressureProfile { static apply (resource: Observation) : USCoreBloodPressureProfile { ensureProfile(resource, USCoreBloodPressureProfile.canonicalUrl); + Object.assign(resource, { + code: {"coding":[{"system":"http://loinc.org","code":"85354-9"}]}, + }) + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + USCoreBloodPressureProfile.VSCatSliceMatch, + ); + resource.component = ensureSliceDefaults( + [...(resource.component ?? [])], + USCoreBloodPressureProfile.systolicSliceMatch, + USCoreBloodPressureProfile.diastolicSliceMatch, + ); return new USCoreBloodPressureProfile(resource); } @@ -1154,7 +1179,8 @@ export class USCoreBloodPressureProfile { } static create (args: USCoreBloodPressureProfileRaw) : USCoreBloodPressureProfile { - return USCoreBloodPressureProfile.apply(USCoreBloodPressureProfile.createResource(args)); + const resource = USCoreBloodPressureProfile.createResource(args); + return USCoreBloodPressureProfile.apply(resource); } toResource () : Observation { @@ -1193,11 +1219,6 @@ export class USCoreBloodPressureProfile { return this.resource.code as CodeableConcept<("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" | string)> | undefined; } - setCode (value: CodeableConcept<("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" | string)>) : this { - Object.assign(this.resource, { code: value }); - return this; - } - getComponent () : ObservationComponent[] | undefined { return this.resource.component as ObservationComponent[] | undefined; } @@ -1403,7 +1424,7 @@ export class USCoreBloodPressureProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateFixedValue(res, profileName, "code", {"coding":[{"system":"http://loinc.org","code":"85354-9"}]}), ...validateRequired(res, profileName, "subject"), @@ -1475,7 +1496,7 @@ export type USCoreBodyWeightProfileRaw = { export class USCoreBodyWeightProfile { static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight"; - private static readonly VSCatSliceMatch: Record = {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}; + private static readonly VSCatSliceMatch: Record = {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}; private resource: Observation; @@ -1495,6 +1516,13 @@ export class USCoreBodyWeightProfile { static apply (resource: Observation) : USCoreBodyWeightProfile { ensureProfile(resource, USCoreBodyWeightProfile.canonicalUrl); + Object.assign(resource, { + code: {"coding":[{"system":"http://loinc.org","code":"29463-7"}]}, + }) + resource.category = ensureSliceDefaults( + [...(resource.category ?? [])], + USCoreBodyWeightProfile.VSCatSliceMatch, + ); return new USCoreBodyWeightProfile(resource); } @@ -1516,7 +1544,8 @@ export class USCoreBodyWeightProfile { } static create (args: USCoreBodyWeightProfileRaw) : USCoreBodyWeightProfile { - return USCoreBodyWeightProfile.apply(USCoreBodyWeightProfile.createResource(args)); + const resource = USCoreBodyWeightProfile.createResource(args); + return USCoreBodyWeightProfile.apply(resource); } toResource () : Observation { @@ -1555,11 +1584,6 @@ export class USCoreBodyWeightProfile { return this.resource.code as CodeableConcept<("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" | string)> | undefined; } - setCode (value: CodeableConcept<("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" | string)>) : this { - Object.assign(this.resource, { code: value }); - return this; - } - getEffectiveDateTime () : string | undefined { return this.resource.effectiveDateTime as string | undefined; } @@ -1710,7 +1734,7 @@ export class USCoreBodyWeightProfile { ...validateRequired(res, profileName, "status"), ...validateEnum(res, profileName, "status", ["registered","preliminary","final","amended","corrected","cancelled","entered-in-error","unknown"]), ...validateRequired(res, profileName, "category"), - ...validateSliceCardinality(res, profileName, "category", {"coding":{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}}, "VSCat", 1, 1), + ...validateSliceCardinality(res, profileName, "category", {"coding":[{"code":"vital-signs","system":"http://terminology.hl7.org/CodeSystem/observation-category"}]}, "VSCat", 1, 1), ...validateRequired(res, profileName, "code"), ...validateFixedValue(res, profileName, "code", {"coding":[{"system":"http://loinc.org","code":"29463-7"}]}), ...validateRequired(res, profileName, "subject"), @@ -1771,6 +1795,7 @@ import { isExtension, getExtensionValue, pushExtension, + upsertExtension, validateRequired, validateExcluded, validateFixedValue, @@ -1782,7 +1807,7 @@ import { } from "../../profile-helpers"; export type USCoreRaceExtensionProfileRaw = { - extension?: Extension[]; + extension: Extension[]; } export type USCoreRaceExtensionProfileFlat = { @@ -1813,6 +1838,10 @@ export class USCoreRaceExtensionProfile { } static apply (resource: Extension) : USCoreRaceExtensionProfile { + resource.url = USCoreRaceExtensionProfile.canonicalUrl; + Object.assign(resource, { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + }) return new USCoreRaceExtensionProfile(resource); } @@ -1838,14 +1867,10 @@ export class USCoreRaceExtensionProfile { static createResource (args: USCoreRaceExtensionProfileRaw | USCoreRaceExtensionProfileFlat) : Extension { const resolvedExtensions = USCoreRaceExtensionProfile.resolveInput(args ?? {}); - const extensionWithDefaults = ensureSliceDefaults( - resolvedExtensions, - USCoreRaceExtensionProfile.textSliceMatch, - ); const resource = buildResource( { url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", - extension: extensionWithDefaults, + extension: resolvedExtensions, }) return resource; } @@ -1872,11 +1897,6 @@ export class USCoreRaceExtensionProfile { return this.resource.url as string | undefined; } - setUrl (value: string) : this { - Object.assign(this.resource, { url: value }); - return this; - } - // Extensions public setOmbCategory (value: Omit | Extension): this { if (isExtension(value)) { @@ -1909,9 +1929,9 @@ export class USCoreRaceExtensionProfile { public setText (value: Omit | Extension): this { if (isExtension(value)) { if (value.url !== "text") throw new Error(\`Expected extension url 'text', got '\${value.url}'\`) - pushExtension(this.resource, value) + upsertExtension(this.resource, value) } else { - pushExtension(this.resource, { url: "text", ...value } as Extension) + upsertExtension(this.resource, { url: "text", ...value } as Extension) } return this } 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 1ccf629b8..1d881bb49 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 @@ -60,7 +60,6 @@ import { export type ExampleTypedBundleProfileRaw = { type: ("document" | "message" | "transaction" | "transaction-response" | "batch" | "batch-response" | "history" | "searchset" | "collection"); - entry?: BundleEntry[]; } // CanonicalURL: http://example.org/fhir/StructureDefinition/ExampleTypedBundle (pkg: example.folder.structures#0.0.1) @@ -92,14 +91,8 @@ export class ExampleTypedBundleProfile { } static createResource (args: ExampleTypedBundleProfileRaw) : Bundle { - const entryWithDefaults = ensureSliceDefaults( - [...(args.entry ?? [])], - ExampleTypedBundleProfile.PatientEntrySliceMatch, - ); - const resource = buildResource( { resourceType: "Bundle", - entry: entryWithDefaults, type: args.type, meta: { profile: [ExampleTypedBundleProfile.canonicalUrl] }, }) @@ -107,7 +100,8 @@ export class ExampleTypedBundleProfile { } static create (args: ExampleTypedBundleProfileRaw) : ExampleTypedBundleProfile { - return ExampleTypedBundleProfile.apply(ExampleTypedBundleProfile.createResource(args)); + const resource = ExampleTypedBundleProfile.createResource(args); + return ExampleTypedBundleProfile.apply(resource); } toResource () : Bundle { @@ -124,15 +118,6 @@ export class ExampleTypedBundleProfile { return this; } - getEntry () : BundleEntry[] | undefined { - return this.resource.entry as BundleEntry[] | undefined; - } - - setEntry (value: BundleEntry[]) : this { - Object.assign(this.resource, { entry: value }); - return this; - } - // Extensions // Slices public setPatientEntry (input?: ExampleTypedBundle_Entry_PatientEntrySliceFlat | BundleEntry): this { From 451904d8aab88f51385ff112051d17ade2e4a9bc Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 2 Apr 2026 12:04:36 +0200 Subject: [PATCH 4/6] fix: Rewrite profile example tests with demo-first structure - Three demos on top: create (with validation flow), apply, from/read - Concise regression tests below for edge cases - Remove all as-any / as-never / as-unknown casts - Add resource snapshots for fully populated profiles --- .../profile-typed-bundle.test.ts | 132 ++------ .../profile-bodyweight.test.ts.snap | 81 +++++ .../__snapshots__/profile-bp.test.ts.snap | 69 +++++ .../typescript-r4/profile-bodyweight.test.ts | 280 +++++++---------- examples/typescript-r4/profile-bp.test.ts | 214 +++++-------- .../profile-bodyweight.test.ts.snap | 41 +++ .../__snapshots__/profile-bp.test.ts.snap | 69 +++++ .../profile-bodyweight.test.ts | 152 ++++----- .../typescript-us-core/profile-bp.test.ts | 288 ++++++------------ 9 files changed, 632 insertions(+), 694 deletions(-) create mode 100644 examples/typescript-r4/__snapshots__/profile-bodyweight.test.ts.snap create mode 100644 examples/typescript-r4/__snapshots__/profile-bp.test.ts.snap create mode 100644 examples/typescript-us-core/__snapshots__/profile-bodyweight.test.ts.snap create mode 100644 examples/typescript-us-core/__snapshots__/profile-bp.test.ts.snap diff --git a/examples/local-package-folder/profile-typed-bundle.test.ts b/examples/local-package-folder/profile-typed-bundle.test.ts index a0248d4ee..a47a5febd 100644 --- a/examples/local-package-folder/profile-typed-bundle.test.ts +++ b/examples/local-package-folder/profile-typed-bundle.test.ts @@ -11,54 +11,43 @@ import { describe, expect, test } from "bun:test"; import { ExampleTypedBundleProfile } from "./fhir-types/example-folder-structures/profiles/Bundle_ExampleTypedBundle"; -import type { BundleEntry } from "./fhir-types/hl7-fhir-r4-core/Bundle"; -import type { DomainResource } from "./fhir-types/hl7-fhir-r4-core/DomainResource"; import type { Organization } from "./fhir-types/hl7-fhir-r4-core/Organization"; import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient"; -const createBundle = () => ExampleTypedBundleProfile.create({ type: "collection" }); - const smithPatient: Patient = { resourceType: "Patient", name: [{ family: "Smith" }] }; -const jonesPatient: Patient = { resourceType: "Patient", name: [{ family: "Jones" }] }; const activePatient: Patient = { resourceType: "Patient", active: true }; -const acmeOrg: Organization = { resourceType: "Organization", name: "Acme Corp" }; const clinicOrg: Organization = { resourceType: "Organization", name: "Clinic" }; describe("type-discriminated bundle slices", () => { - test("create() auto-populates a PatientEntry stub (min: 1)", () => { - const bundle = createBundle(); - const entry = bundle.toResource().entry; - expect(entry).toHaveLength(1); - expect(entry![0]!.resource).toEqual({ resourceType: "Patient" }); + test("create() starts with no entry — PatientEntry must be set by user", () => { + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }); + expect(bundle.toResource().entry).toBeUndefined(); + expect(bundle.validate().errors).toEqual([ + "ExampleTypedBundle.entry: slice 'PatientEntry' requires at least 1 item(s), found 0", + ]); }); test("setPatientEntry inserts a typed patient entry", () => { - const bundle = createBundle(); + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }); bundle.setPatientEntry({ resource: smithPatient }); const entry = bundle.getPatientEntry()!; expect(entry.resource).toEqual(smithPatient); + expect(bundle.validate().errors).toEqual([]); }); test("setPatientEntry replaces existing patient entry (no duplicates)", () => { - const bundle = createBundle(); + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }); bundle.setPatientEntry({ resource: smithPatient }); - bundle.setPatientEntry({ resource: jonesPatient }); - - const patients = bundle.toResource().entry!.filter((e) => e.resource?.resourceType === "Patient"); - expect(patients).toHaveLength(1); - expect(bundle.getPatientEntry()!.resource).toEqual(jonesPatient); - }); - - test("setOrganizationEntry adds an organization entry", () => { - const bundle = createBundle(); - bundle.setOrganizationEntry({ resource: acmeOrg }); + bundle.setPatientEntry({ resource: activePatient }); - expect(bundle.getOrganizationEntry()!.resource).toEqual(acmeOrg); + const entries = bundle.toResource().entry!; + expect(entries).toHaveLength(1); + expect(entries[0]!.resource).toEqual(activePatient); }); test("getPatientEntry('flat') returns the entry as-is (no keys stripped)", () => { - const bundle = createBundle(); + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }); bundle.setPatientEntry({ fullUrl: "urn:uuid:patient-1", resource: activePatient }); const flat = bundle.getPatientEntry("flat")!; @@ -66,97 +55,24 @@ describe("type-discriminated bundle slices", () => { expect(flat.resource).toEqual(activePatient); }); - test("validate() checks PatientEntry cardinality", () => { - const bundle = ExampleTypedBundleProfile.apply({ - resourceType: "Bundle", - type: "collection", - }); - const { errors } = bundle.validate(); - expect(errors).toEqual(["ExampleTypedBundle.entry: slice 'PatientEntry' requires at least 1 item(s), found 0"]); - }); - test("fluent chaining across slice setters", () => { - const bundle = createBundle() + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }) .setPatientEntry({ resource: activePatient }) .setOrganizationEntry({ resource: clinicOrg }); + expect(bundle.toResource().entry).toHaveLength(2); expect(bundle.getPatientEntry()!.resource).toEqual(activePatient); expect(bundle.getOrganizationEntry()!.resource).toEqual(clinicOrg); - expect(bundle.toResource().entry).toHaveLength(2); }); - test("set/get PatientEntry with full BundleEntry input", () => { - const bundle = createBundle(); - const input: BundleEntry = { fullUrl: "urn:uuid:p1", resource: smithPatient }; - bundle.setPatientEntry(input); - - const raw = bundle.getPatientEntry("raw")!; - expect(raw.fullUrl).toBe("urn:uuid:p1"); - expect(raw.resource).toEqual(smithPatient); - - const flat = bundle.getPatientEntry("flat")!; - expect(flat.fullUrl).toBe("urn:uuid:p1"); - expect(flat.resource).toEqual(smithPatient); - }); - - test("set/get OrganizationEntry with full BundleEntry input", () => { - const bundle = createBundle(); - const input: BundleEntry = { fullUrl: "urn:uuid:o1", resource: acmeOrg }; - bundle.setOrganizationEntry(input); - - const raw = bundle.getOrganizationEntry("raw")!; - expect(raw.fullUrl).toBe("urn:uuid:o1"); - expect(raw.resource).toEqual(acmeOrg); - - const flat = bundle.getOrganizationEntry("flat")!; - expect(flat.fullUrl).toBe("urn:uuid:o1"); - expect(flat.resource).toEqual(acmeOrg); - }); -}); - -describe("generic type-family fields — compile-time narrowing", () => { - test("BundleEntry.resource is Patient (access Patient-specific fields without cast)", () => { - const bundle = createBundle(); - bundle.setPatientEntry({ resource: smithPatient }); - - const entry = bundle.getPatientEntry()!; - // entry.resource is Patient — .name is available directly, no cast needed - const family: string | undefined = entry.resource?.name?.[0]?.family; - expect(family).toBe("Smith"); - }); - - test("BundleEntry.resource is Organization (access Organization-specific fields without cast)", () => { - const bundle = createBundle(); - bundle.setOrganizationEntry({ resource: acmeOrg }); - - const entry = bundle.getOrganizationEntry()!; - // entry.resource is Organization — .name is string, not HumanName[] - const name: string | undefined = entry.resource?.name; - expect(name).toBe("Acme Corp"); - }); - - test("BundleEntry defaults to BundleEntry — unparameterized usage unchanged", () => { - const entry: BundleEntry = { resource: smithPatient }; - expect(entry.resource?.resourceType).toBe("Patient"); - }); - - test("DomainResource narrows contained to T[]", () => { - const container: DomainResource = { - resourceType: "Patient", - contained: [smithPatient, jonesPatient], - }; - // contained is Patient[] — .name available directly - const family: string | undefined = container.contained?.[0]?.name?.[0]?.family; - expect(family).toBe("Smith"); - }); - - test("BundleEntry rejects Organization at compile time", () => { - const patientEntry: BundleEntry = { resource: smithPatient }; - expect(patientEntry.resource?.resourceType).toBe("Patient"); + test("setOrganizationEntry replaces existing org entry (same discriminator)", () => { + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }) + .setPatientEntry({ resource: activePatient }) + .setOrganizationEntry({ resource: clinicOrg }) + .setOrganizationEntry({ resource: { resourceType: "Organization", name: "Acme" } }); - // Uncomment to verify compile error: - // @ts-expect-error — Organization is not assignable to Patient - const _bad: BundleEntry = { resource: acmeOrg }; - void _bad; + const entries = bundle.toResource().entry!; + expect(entries).toHaveLength(2); + expect(bundle.getOrganizationEntry()!.resource!.name).toBe("Acme"); }); }); diff --git a/examples/typescript-r4/__snapshots__/profile-bodyweight.test.ts.snap b/examples/typescript-r4/__snapshots__/profile-bodyweight.test.ts.snap new file mode 100644 index 000000000..f8af3cc9f --- /dev/null +++ b/examples/typescript-r4/__snapshots__/profile-bodyweight.test.ts.snap @@ -0,0 +1,81 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`demo: create a bodyweight observation build a valid bodyweight resource step by step 1`] = ` +{ + "category": [ + { + "coding": [ + { + "code": "vital-signs", + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + }, + ], + }, + ], + "code": { + "coding": [ + { + "code": "29463-7", + "system": "http://loinc.org", + }, + ], + }, + "effectiveDateTime": "2024-06-15", + "meta": { + "profile": [ + "http://hl7.org/fhir/StructureDefinition/bodyweight", + ], + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/pt-1", + }, + "valueQuantity": { + "code": "kg", + "system": "http://unitsofmeasure.org", + "unit": "kg", + "value": 75, + }, +} +`; + +exports[`bodyweight profile creation fully populated resource matches snapshot 1`] = ` +{ + "category": [ + { + "coding": [ + { + "code": "vital-signs", + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + }, + ], + }, + ], + "code": { + "coding": [ + { + "code": "29463-7", + "system": "http://loinc.org", + }, + ], + }, + "effectiveDateTime": "2024-06-15", + "meta": { + "profile": [ + "http://hl7.org/fhir/StructureDefinition/bodyweight", + ], + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/pt-1", + }, + "valueQuantity": { + "code": "kg", + "system": "http://unitsofmeasure.org", + "unit": "kg", + "value": 75, + }, +} +`; diff --git a/examples/typescript-r4/__snapshots__/profile-bp.test.ts.snap b/examples/typescript-r4/__snapshots__/profile-bp.test.ts.snap new file mode 100644 index 000000000..c916f470d --- /dev/null +++ b/examples/typescript-r4/__snapshots__/profile-bp.test.ts.snap @@ -0,0 +1,69 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`demo: create a blood pressure observation build a valid BP resource step by step 1`] = ` +{ + "category": [ + { + "coding": [ + { + "code": "vital-signs", + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + }, + ], + }, + ], + "code": { + "coding": [ + { + "code": "85354-9", + "system": "http://loinc.org", + }, + ], + }, + "component": [ + { + "code": { + "coding": [ + { + "code": "8480-6", + "system": "http://loinc.org", + }, + ], + }, + "valueQuantity": { + "code": "mm[Hg]", + "system": "http://unitsofmeasure.org", + "unit": "mmHg", + "value": 120, + }, + }, + { + "code": { + "coding": [ + { + "code": "8462-4", + "system": "http://loinc.org", + }, + ], + }, + "valueQuantity": { + "code": "mm[Hg]", + "system": "http://unitsofmeasure.org", + "unit": "mmHg", + "value": 80, + }, + }, + ], + "effectiveDateTime": "2024-06-15", + "meta": { + "profile": [ + "http://hl7.org/fhir/StructureDefinition/bp", + ], + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/pt-1", + }, +} +`; diff --git a/examples/typescript-r4/profile-bodyweight.test.ts b/examples/typescript-r4/profile-bodyweight.test.ts index 6ec8ab5c4..8de5b2ba3 100644 --- a/examples/typescript-r4/profile-bodyweight.test.ts +++ b/examples/typescript-r4/profile-bodyweight.test.ts @@ -6,229 +6,153 @@ import { describe, expect, test } from "bun:test"; import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation"; import { observation_bodyweightProfile as bodyweightProfile } from "./fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bodyweight"; -describe("bodyweight profile creation", () => { - let fromCreate: Observation; - let fromCreateResource: Observation; - let fromFrom: Observation; - - test("create() returns a profile wrapping the resource with auto-set code", () => { +describe("demo: create a bodyweight observation", () => { + test("build a valid bodyweight resource step by step", () => { + // Create a new bodyweight profile — auto-sets code and category const profile = bodyweightProfile.create({ status: "final", subject: { reference: "Patient/pt-1" }, }); - fromCreate = profile.toResource(); - expect(fromCreate.resourceType).toBe("Observation"); - expect(fromCreate.status).toBe("final"); - expect(fromCreate.code!.coding![0]!.code).toBe("29463-7"); - expect(fromCreate.code!.coding![0]!.system).toBe("http://loinc.org"); - expect(fromCreate.subject!.reference).toBe("Patient/pt-1"); - }); + // Not yet valid: effective[x] is required by the vitalsigns base profile + expect(profile.validate().errors).toEqual([ + "observation-bodyweight: at least one of effectiveDateTime, effectivePeriod is required", + ]); - test("createResource() returns a plain Observation with auto-set code", () => { - fromCreateResource = bodyweightProfile.createResource({ - status: "final", - subject: { reference: "Patient/pt-1" }, - }); + // Fill in the remaining required fields + profile + .setEffectiveDateTime("2024-06-15") + .setValueQuantity({ value: 75, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }); - expect(fromCreateResource.resourceType).toBe("Observation"); - expect(fromCreateResource.status).toBe("final"); - expect(fromCreateResource.code!.coding![0]!.code).toBe("29463-7"); - expect(fromCreateResource.subject!.reference).toBe("Patient/pt-1"); + // Now the resource is fully valid + expect(profile.validate().errors).toEqual([]); + expect(profile.toResource()).toMatchSnapshot(); }); +}); - test("apply() wraps an existing Observation", () => { - const obs: Observation = { resourceType: "Observation", code: {}, status: "preliminary" }; - const profile = bodyweightProfile.apply(obs); +describe("demo: apply bodyweight profile to an existing Observation", () => { + test("wrap a plain Observation and populate profiled fields", () => { + // Start with an existing Observation — e.g. received from another system + const obs: Observation = { + resourceType: "Observation", + status: "preliminary", + code: { coding: [{ code: "29463-7", system: "http://loinc.org" }] }, + subject: { reference: "Patient/pt-1" }, + }; - profile - .setStatus("final") - .setCode({ coding: [{ code: "29463-7", system: "http://loinc.org" }] }) - .setCategory([ - { - coding: { - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }, - } as any, - ]) - .setSubject({ reference: "Patient/pt-1" }); + // apply() adds meta.profile, sets fixed values (code), and auto-populates required slices + const profile = bodyweightProfile.apply(obs); - fromFrom = profile.toResource(); + // Not yet valid: effective[x] is required by the vitalsigns base profile + expect(profile.validate().errors).toEqual([ + "observation-bodyweight: at least one of effectiveDateTime, effectivePeriod is required", + ]); - expect(fromFrom).toBe(obs); // same reference - expect(profile.getStatus()).toBe("final"); - expect(profile.getCode()!.coding![0]!.code).toBe("29463-7"); - expect(profile.getSubject()!.reference).toBe("Patient/pt-1"); - }); + // The profile mutates the original resource — no copy is made + expect(profile.toResource()).toBe(obs); + expect(obs.meta?.profile).toContain("http://hl7.org/fhir/StructureDefinition/bodyweight"); - test("all three methods produce equal resources", () => { - expect(fromCreate).toEqual(fromCreateResource); - expect(fromCreate).toEqual(fromFrom); - }); + // Fill in the remaining fields via fluent setters + profile + .setStatus("final") + .setEffectiveDateTime("2024-06-15") + .setValueQuantity({ value: 75, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }); - test("all three methods set meta.profile", () => { - const expected = ["http://hl7.org/fhir/StructureDefinition/bodyweight"]; - expect(fromCreate.meta?.profile).toEqual(expected); - expect(fromCreateResource.meta?.profile).toEqual(expected); - expect(fromFrom.meta?.profile).toEqual(expected); + expect(profile.validate().errors).toEqual([]); }); }); -describe("bodyweight profile getters and setters", () => { - const profile = bodyweightProfile.create({ - status: "final", - subject: { reference: "Patient/pt-1" }, - }); +describe("demo: read a bodyweight observation from JSON", () => { + test("parse an API response and use typed getters", () => { + const json: Observation = { + resourceType: "Observation", + meta: { profile: ["http://hl7.org/fhir/StructureDefinition/bodyweight"] }, + status: "final", + category: [ + { + coding: [ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ], + text: "Vital Signs", + }, + ], + code: { coding: [{ code: "29463-7", system: "http://loinc.org" }] }, + subject: { reference: "Patient/pt-1" }, + effectiveDateTime: "2024-06-15", + valueQuantity: { value: 75, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }, + }; + + // from() validates the resource against the profile before wrapping + const profile = bodyweightProfile.from(json); - test("getStatus / setStatus", () => { + // Typed getters provide direct access to profiled fields expect(profile.getStatus()).toBe("final"); - profile.setStatus("amended"); - expect(profile.getStatus()).toBe("amended"); - expect(profile.toResource().status).toBe("amended"); - }); + expect(profile.getSubject()!.reference).toBe("Patient/pt-1"); + expect(profile.getEffectiveDateTime()).toBe("2024-06-15"); + expect(profile.getValueQuantity()!.value).toBe(75); + expect(profile.getValueQuantity()!.unit).toBe("kg"); - test("getCode / setCode", () => { - // Code is auto-set but still has getter/setter + // Code is a fixed value — auto-set by the profile, read-only (no setter) expect(profile.getCode()!.coding![0]!.code).toBe("29463-7"); - const newCode = { coding: [{ code: "3141-9", system: "http://loinc.org" }] }; - profile.setCode(newCode); - expect(profile.getCode()).toEqual(newCode); - }); - test("getCategory / setCategory", () => { - // category is auto-populated with VSCat discriminator - expect(profile.getCategory()).toHaveLength(1); - const newCategory = [{ text: "Laboratory" }]; - profile.setCategory(newCategory); - expect(profile.getCategory()).toEqual(newCategory); - }); - - test("getSubject / setSubject", () => { - expect(profile.getSubject()!.reference).toBe("Patient/pt-1"); - profile.setSubject({ reference: "Patient/pt-2" }); - expect(profile.getSubject()!.reference).toBe("Patient/pt-2"); + // Slice accessor strips discriminator keys (coding) by default — only user data remains + expect(profile.getVSCat()).toEqual({ text: "Vital Signs" }); + // Use "raw" mode to see the full element including discriminator + expect(profile.getVSCat("raw")!.coding).toEqual([ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ]); + expect(profile.getVSCat("raw")!.text).toBe("Vital Signs"); }); }); -describe("bodyweight profile slice accessors", () => { - const profile = bodyweightProfile.create({ - status: "final", - subject: { reference: "Patient/pt-1" }, - }); - - test("getVSCat returns empty simplified view from auto-populated stub", () => { - // category is auto-populated with VSCat discriminator match - expect(profile.getVSCat("raw")).toBeDefined(); - const raw = profile.getVSCat("raw")!; - expect(raw.coding as unknown).toEqual({ - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }); - // simplified view strips discriminator keys, leaving empty object - expect(profile.getVSCat()).toEqual({}); - }); +describe("factory method equivalence", () => { + test("create(), createResource(), and apply() produce equal resources", () => { + const args = { status: "final" as const, subject: { reference: "Patient/pt-1" as const } }; - test("setVSCat adds category with discriminator values", () => { - profile.setVSCat({ text: "Vital Signs" }); + const fromCreate = bodyweightProfile.create(args).toResource(); + const fromCreateResource = bodyweightProfile.createResource(args); + const fromApply = bodyweightProfile + .apply({ resourceType: "Observation", status: "preliminary", code: { text: "Body weight" } }) + .setStatus("final") + .setSubject({ reference: "Patient/pt-1" }) + .toResource(); - const raw = profile.getVSCat("raw")!; - expect(raw.text).toBe("Vital Signs"); - expect(raw.coding as unknown).toEqual({ - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }); + expect(fromCreate).toEqual(fromCreateResource); + expect(fromCreate).toEqual(fromApply); }); +}); - test("getVSCat returns simplified view without discriminator", () => { - const simplified = profile.getVSCat(); - expect(simplified).toEqual({ text: "Vital Signs" }); - expect("coding" in simplified!).toBe(false); - }); +describe("slice accessors", () => { + test("setVSCat merges discriminator and strips it in flat mode", () => { + const profile = bodyweightProfile.create({ status: "final", subject: { reference: "Patient/pt-1" } }); + profile.setVSCat({ text: "Vital Signs" }); - test("getVSCatRaw returns full element including discriminator", () => { - const raw = profile.getVSCat("raw")!; - expect(raw.text).toBe("Vital Signs"); - expect(raw.coding).toBeDefined(); + expect(profile.getVSCat()).toEqual({ text: "Vital Signs" }); + expect(profile.getVSCat("raw")!.coding).toBeDefined(); + expect(profile.getVSCat("raw")!.text).toBe("Vital Signs"); }); test("setVSCat replaces existing slice element", () => { - profile.setVSCat({ text: "Updated" }); + const profile = bodyweightProfile.create({ status: "final", subject: { reference: "Patient/pt-1" } }); + profile.setVSCat({ text: "First" }); + profile.setVSCat({ text: "Second" }); - expect(profile.getVSCat()!.text).toBe("Updated"); - expect(profile.toResource().category!.length).toBe(1); + expect(profile.getVSCat()!.text).toBe("Second"); + expect(profile.toResource().category).toHaveLength(1); }); }); -describe("bodyweight profile choice type accessors", () => { - const profile = bodyweightProfile.create({ - status: "final", - subject: { reference: "Patient/pt-1" }, - }); +describe("choice type accessors", () => { + test("effectiveDateTime and effectivePeriod are independent choices", () => { + const profile = bodyweightProfile.create({ status: "final", subject: { reference: "Patient/pt-1" } }); - test("choice accessors return undefined when not set", () => { expect(profile.getEffectiveDateTime()).toBeUndefined(); expect(profile.getEffectivePeriod()).toBeUndefined(); - expect(profile.getValueQuantity()).toBeUndefined(); - }); - test("setEffectiveDateTime / getEffectiveDateTime", () => { profile.setEffectiveDateTime("2024-01-15"); expect(profile.getEffectiveDateTime()).toBe("2024-01-15"); - expect(profile.toResource().effectiveDateTime).toBe("2024-01-15"); - }); - test("setEffectivePeriod / getEffectivePeriod", () => { profile.setEffectivePeriod({ start: "2024-01-15", end: "2024-01-16" }); expect(profile.getEffectivePeriod()).toEqual({ start: "2024-01-15", end: "2024-01-16" }); }); - - test("setValueQuantity / getValueQuantity", () => { - profile.setValueQuantity({ value: 75, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }); - expect(profile.getValueQuantity()!.value).toBe(75); - expect(profile.getValueQuantity()!.unit).toBe("kg"); - }); - - test("choice accessors support fluent chaining", () => { - const result = profile.setEffectiveDateTime("2024-02-01").setValueQuantity({ value: 80, unit: "kg" }); - expect(result).toBe(profile); - expect(profile.getEffectiveDateTime()).toBe("2024-02-01"); - expect(profile.getValueQuantity()!.value).toBe(80); - }); - - test("choice accessors mutate the underlying resource", () => { - const obs = bodyweightProfile.createResource({ - status: "final", - subject: { reference: "Patient/pt-1" }, - }); - const p = bodyweightProfile.apply(obs); - - p.setValueQuantity({ value: 90, unit: "kg" }); - expect((obs as any).valueQuantity.value).toBe(90); - - p.setEffectiveDateTime("2024-03-01"); - expect((obs as any).effectiveDateTime).toBe("2024-03-01"); - }); -}); - -describe("bodyweight profile static metadata", () => { - test("canonicalUrl is exposed as a static property", () => { - expect(bodyweightProfile.canonicalUrl).toBe("http://hl7.org/fhir/StructureDefinition/bodyweight"); - }); -}); - -describe("bodyweight profile mutability", () => { - test("profile mutates the underlying resource", () => { - const obs = bodyweightProfile.createResource({ - status: "final", - subject: { reference: "Patient/pt-1" }, - }); - const profile = bodyweightProfile.apply(obs); - - profile.setStatus("amended"); - expect(obs.status).toBe("amended"); - - profile.setVSCat({ text: "Vital Signs" }); - expect(obs.category!.length).toBe(1); - }); }); diff --git a/examples/typescript-r4/profile-bp.test.ts b/examples/typescript-r4/profile-bp.test.ts index 299564fd2..89b4eef70 100644 --- a/examples/typescript-r4/profile-bp.test.ts +++ b/examples/typescript-r4/profile-bp.test.ts @@ -3,187 +3,109 @@ */ import { describe, expect, test } from "bun:test"; +import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation"; import { observation_bpProfile as bpProfile } from "./fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bp"; -const createBp = () => - bpProfile.create({ - status: "final", - subject: { reference: "Patient/pt-1" }, - }); - -describe("blood pressure profile", () => { - const profile = bpProfile.create({ - status: "final", - subject: { reference: "Patient/pt-1" }, - }); - - test("canonicalUrl is exposed", () => { - expect(bpProfile.canonicalUrl).toBe("http://hl7.org/fhir/StructureDefinition/bp"); - }); - - test("create() auto-sets code and meta.profile", () => { - const obs = profile.toResource(); - expect(obs.resourceType).toBe("Observation"); - expect(obs.code!.coding![0]!.code).toBe("85354-9"); - expect(obs.code!.coding![0]!.system).toBe("http://loinc.org"); - expect(obs.meta?.profile).toEqual(["http://hl7.org/fhir/StructureDefinition/bp"]); - }); - - test("freshly created profile is not yet valid (missing effective)", () => { - const { errors } = profile.validate(); - expect(errors).toEqual(["observation-bp: at least one of effectiveDateTime, effectivePeriod is required"]); - }); - - test("create() auto-populates component with systolic/diastolic stubs", () => { - const fresh = createBp(); - const obs = fresh.toResource(); - expect(obs.component).toHaveLength(2); - // stubs contain only discriminator match values - expect(fresh.getSystolicBP("raw")).toBeDefined(); - expect(fresh.getDiastolicBP("raw")).toBeDefined(); - }); - - test("setSystolicBP / getSystolicBP / getSystolicBPRaw", () => { - profile.setSystolicBP({ value: 120, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }); - - expect(profile.getSystolicBP()).toEqual({ - value: 120, - unit: "mmHg", - system: "http://unitsofmeasure.org", - code: "mm[Hg]", - }); - - expect(profile.getSystolicBP("raw") as unknown).toEqual({ - valueQuantity: { value: 120, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }, - code: { coding: { code: "8480-6", system: "http://loinc.org" } }, +describe("demo: create a blood pressure observation", () => { + test("build a valid BP resource step by step", () => { + // Create a new BP profile — auto-sets code, category, and component stubs + const profile = bpProfile.create({ + status: "final", + subject: { reference: "Patient/pt-1" }, }); - }); - - test("setDiastolicBP / getDiastolicBP / getDiastolicBPRaw", () => { - profile.setDiastolicBP({ value: 80, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }); - expect(profile.getDiastolicBP()).toEqual({ - value: 80, - unit: "mmHg", - system: "http://unitsofmeasure.org", - code: "mm[Hg]", - }); + // Not yet valid: effective[x] is required by the vitalsigns base profile + expect(profile.validate().errors).toEqual([ + "observation-bp: at least one of effectiveDateTime, effectivePeriod is required", + ]); - expect(profile.getDiastolicBP("raw") as unknown).toEqual({ - valueQuantity: { value: 80, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }, - code: { coding: { code: "8462-4", system: "http://loinc.org" } }, - }); - }); + // Fill in the remaining required fields + profile + .setEffectiveDateTime("2024-06-15") + .setSystolicBP({ value: 120, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }) + .setDiastolicBP({ value: 80, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }); - test("both systolic and diastolic are in the component array", () => { - const obs = profile.toResource(); - // auto-populated stubs + set values = still 2 items (set replaces stubs) - expect(obs.component).toHaveLength(2); - - const systolicCode = profile.getSystolicBP("raw")!.code as Record; - const diastolicCode = profile.getDiastolicBP("raw")!.code as Record; - expect(systolicCode.coding).toEqual({ code: "8480-6", system: "http://loinc.org" }); - expect(diastolicCode.coding).toEqual({ code: "8462-4", system: "http://loinc.org" }); - expect(profile.getSystolicBP("raw")!.valueQuantity!.value).toBe(120); - expect(profile.getDiastolicBP("raw")!.valueQuantity!.value).toBe(80); + // Now the resource is fully valid + expect(profile.validate().errors).toEqual([]); + expect(profile.toResource()).toMatchSnapshot(); }); +}); - test("setSystolicBP replaces an existing systolic component", () => { - profile.setSystolicBP({ value: 130, unit: "mmHg" }); - - expect(profile.toResource().component).toHaveLength(2); - expect(profile.getSystolicBP("raw")!.valueQuantity!.value).toBe(130); - }); +describe("demo: apply BP profile to an existing Observation", () => { + test("wrap a plain Observation and populate profiled fields", () => { + // Start with an existing Observation — e.g. received from another system + const obs: Observation = { + resourceType: "Observation", + status: "preliminary", + code: { coding: [{ code: "85354-9", system: "http://loinc.org" }] }, + subject: { reference: "Patient/pt-1" }, + }; - test("setVSCat adds category with discriminator values", () => { - profile.setVSCat({ text: "Vital Signs" }); + // apply() adds meta.profile, sets fixed values (code), and auto-populates required slices + const profile = bpProfile.apply(obs); - const raw = profile.getVSCat("raw")!; - expect(raw.text).toBe("Vital Signs"); - expect(raw.coding as unknown).toEqual({ - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }); - }); + // Not yet valid: effective[x] is required + expect(profile.validate().errors).toEqual([ + "observation-bp: at least one of effectiveDateTime, effectivePeriod is required", + ]); - test("setEffectiveDateTime / getEffectiveDateTime", () => { - profile.setEffectiveDateTime("2024-06-15T10:30:00Z"); - expect(profile.getEffectiveDateTime()).toBe("2024-06-15T10:30:00Z"); - expect(profile.getValueQuantity()).toBeUndefined(); - }); + // The profile mutates the original resource — no copy is made + expect(profile.toResource()).toBe(obs); + expect(obs.meta?.profile).toContain("http://hl7.org/fhir/StructureDefinition/bp"); - test("fluent chaining across all accessor types", () => { - const result = profile + // Fill in the remaining fields via fluent setters + profile .setStatus("final") - .setVSCat({ text: "Vital Signs" }) .setEffectiveDateTime("2024-06-15") - .setSubject({ reference: "Patient/pt-2" }) .setSystolicBP({ value: 120, unit: "mmHg" }) .setDiastolicBP({ value: 80, unit: "mmHg" }); - expect(result).toBe(profile); - expect(profile.getStatus()).toBe("final"); - expect(profile.getVSCat()!.text).toBe("Vital Signs"); - expect(profile.getEffectiveDateTime()).toBe("2024-06-15"); - expect(profile.getSubject()!.reference).toBe("Patient/pt-2"); - expect(profile.getSystolicBP("raw")!.valueQuantity!.value).toBe(120); - expect(profile.getDiastolicBP("raw")!.valueQuantity!.value).toBe(80); + expect(profile.validate().errors).toEqual([]); }); +}); + +// Note: from() demo is omitted for R4 BP because the R4 component slice discriminator stores +// code.coding as a plain object (not an array) — a known upstream issue in the R4 FHIR schema. +// See the US Core BP test for a from() demo that works with correct array-shaped discriminators. - test("setSystolicBP with no args inserts discriminator-only component", () => { - const fresh = createBp(); - fresh.setSystolicBP(); +describe("component slice replacement", () => { + test("setSystolicBP replaces existing stub, component stays at 2", () => { + const profile = bpProfile.create({ status: "final", subject: { reference: "Patient/pt-1" } }); + expect(profile.toResource().component).toHaveLength(2); - const raw = fresh.getSystolicBP("raw")!; - const rawCode = raw.code as Record; - expect(rawCode.coding).toEqual({ code: "8480-6", system: "http://loinc.org" }); - expect(raw.valueQuantity).toBeUndefined(); + profile.setSystolicBP({ value: 130, unit: "mmHg" }); + expect(profile.toResource().component).toHaveLength(2); + expect(profile.getSystolicBP()!.value).toBe(130); }); +}); - test("create() with custom category preserves user values and adds required VSCat", () => { - const custom = bpProfile.create({ +describe("category auto-population", () => { + test("custom category is preserved, VSCat is appended", () => { + const profile = bpProfile.create({ status: "final", subject: { reference: "Patient/pt-1" }, category: [{ text: "My Category" }], }); - const obs = custom.toResource(); - // User category kept, VSCat auto-added + const obs = profile.toResource(); expect(obs.category).toHaveLength(2); expect(obs.category![0]!.text).toBe("My Category"); - expect((obs.category![1] as Record).coding).toEqual({ - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }); - }); - - test("create() with empty category still adds required VSCat", () => { - const custom = bpProfile.create({ - status: "final", - subject: { reference: "Patient/pt-1" }, - category: [], - }); - const obs = custom.toResource(); - expect(obs.category).toHaveLength(1); - expect((obs.category![0] as Record).coding).toEqual({ - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }); + expect(obs.category![1]!.coding).toEqual([ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ]); }); - test("create() with category already containing VSCat does not duplicate it", () => { - const custom = bpProfile.create({ + test("existing VSCat is not duplicated", () => { + const profile = bpProfile.create({ status: "final", subject: { reference: "Patient/pt-1" }, category: [ { - coding: { - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - } as unknown, - } as never, + coding: [ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ], + }, ], }); - const obs = custom.toResource(); - expect(obs.category).toHaveLength(1); + expect(profile.toResource().category).toHaveLength(1); }); }); diff --git a/examples/typescript-us-core/__snapshots__/profile-bodyweight.test.ts.snap b/examples/typescript-us-core/__snapshots__/profile-bodyweight.test.ts.snap new file mode 100644 index 000000000..78764ab6b --- /dev/null +++ b/examples/typescript-us-core/__snapshots__/profile-bodyweight.test.ts.snap @@ -0,0 +1,41 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`demo: create a US Core body weight observation build a valid body weight resource step by step 1`] = ` +{ + "category": [ + { + "coding": [ + { + "code": "vital-signs", + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + }, + ], + }, + ], + "code": { + "coding": [ + { + "code": "29463-7", + "system": "http://loinc.org", + }, + ], + }, + "effectiveDateTime": "2024-06-15", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight", + ], + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/pt-1", + }, + "valueQuantity": { + "code": "kg", + "system": "http://unitsofmeasure.org", + "unit": "kg", + "value": 75, + }, +} +`; diff --git a/examples/typescript-us-core/__snapshots__/profile-bp.test.ts.snap b/examples/typescript-us-core/__snapshots__/profile-bp.test.ts.snap new file mode 100644 index 000000000..3149e5956 --- /dev/null +++ b/examples/typescript-us-core/__snapshots__/profile-bp.test.ts.snap @@ -0,0 +1,69 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`demo: create a US Core blood pressure observation build a valid BP resource step by step 1`] = ` +{ + "category": [ + { + "coding": [ + { + "code": "vital-signs", + "system": "http://terminology.hl7.org/CodeSystem/observation-category", + }, + ], + }, + ], + "code": { + "coding": [ + { + "code": "85354-9", + "system": "http://loinc.org", + }, + ], + }, + "component": [ + { + "code": { + "coding": [ + { + "code": "8480-6", + "system": "http://loinc.org", + }, + ], + }, + "valueQuantity": { + "code": "mm[Hg]", + "system": "http://unitsofmeasure.org", + "unit": "mmHg", + "value": 120, + }, + }, + { + "code": { + "coding": [ + { + "code": "8462-4", + "system": "http://loinc.org", + }, + ], + }, + "valueQuantity": { + "code": "mm[Hg]", + "system": "http://unitsofmeasure.org", + "unit": "mmHg", + "value": 80, + }, + }, + ], + "effectiveDateTime": "2024-06-15", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure", + ], + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/pt-1", + }, +} +`; diff --git a/examples/typescript-us-core/profile-bodyweight.test.ts b/examples/typescript-us-core/profile-bodyweight.test.ts index 20312dc1b..65359e0c6 100644 --- a/examples/typescript-us-core/profile-bodyweight.test.ts +++ b/examples/typescript-us-core/profile-bodyweight.test.ts @@ -1,91 +1,122 @@ /** * US Core Body Weight Profile Class API Tests - * - * Feature coverage focus: from() / apply() / create(), slice getter modes. - * Factory methods, field accessors, slice accessors, choice types, validation, - * and mutability are tested on Patient and Blood Pressure profiles. */ import { describe, expect, test } from "bun:test"; import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation"; import { USCoreBodyWeightProfile } from "./fhir-types/hl7-fhir-us-core/profiles"; -describe("demo", () => { - test("import a profiled Observation from an API and read values", () => { - const apiResponse: Observation = { - resourceType: "Observation", - meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight"] }, +describe("demo: create a US Core body weight observation", () => { + test("build a valid body weight resource step by step", () => { + // Create a new body weight profile — auto-sets code and category + const profile = USCoreBodyWeightProfile.create({ status: "final", - // singular coding matches the slice discriminator format used by the profile - category: [ - { - coding: { - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }, - }, - ] as any, - code: { coding: [{ code: "29463-7", system: "http://loinc.org", display: "Body weight" }] }, subject: { reference: "Patient/pt-1" }, - effectiveDateTime: "2024-06-15", - valueQuantity: { value: 75, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }, - }; + }); - const profile = USCoreBodyWeightProfile.from(apiResponse); + // Not yet valid: effective[x] is required by the vitalsigns base profile + expect(profile.validate().errors).toEqual([ + "USCoreBodyWeightProfile: at least one of effectiveDateTime, effectivePeriod is required", + ]); - expect(profile.getStatus()).toBe("final"); - expect(profile.getValueQuantity()!.value).toBe(75); - expect(profile.getEffectiveDateTime()).toBe("2024-06-15"); - expect(profile.getSubject()!.reference).toBe("Patient/pt-1"); + // Fill in the remaining required fields + profile + .setEffectiveDateTime("2024-06-15") + .setValueQuantity({ value: 75, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }); + + // Now the resource is fully valid + expect(profile.validate().errors).toEqual([]); + expect(profile.toResource()).toMatchSnapshot(); }); +}); + +describe("demo: apply US Core body weight profile to an existing Observation", () => { + test("wrap a plain Observation and populate profiled fields", () => { + // Start with an existing Observation — e.g. received from another system + const obs: Observation = { + resourceType: "Observation", + status: "preliminary", + code: { coding: [{ code: "29463-7", system: "http://loinc.org" }] }, + subject: { reference: "Patient/pt-1" }, + }; + + // apply() adds meta.profile, sets fixed values (code), and auto-populates required slices + const profile = USCoreBodyWeightProfile.apply(obs); + + // Not yet valid: effective[x] is required + expect(profile.validate().errors).toEqual([ + "USCoreBodyWeightProfile: at least one of effectiveDateTime, effectivePeriod is required", + ]); - test("apply profile to a bare Observation and populate it", () => { - const bareObservation: Observation = { resourceType: "Observation", status: "preliminary", code: {} }; - const profile = USCoreBodyWeightProfile.apply(bareObservation); + // The profile mutates the original resource — no copy is made + expect(profile.toResource()).toBe(obs); + expect(obs.meta?.profile).toContain("http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight"); + // Fill in the remaining fields via fluent setters profile .setStatus("final") - .setCode({ coding: [{ code: "29463-7", system: "http://loinc.org" }] }) - .setSubject({ reference: "Patient/pt-1" }) - .setVSCat({}) .setEffectiveDateTime("2024-06-15") .setValueQuantity({ value: 75, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }); expect(profile.validate().errors).toEqual([]); - expect(profile.toResource().meta?.profile).toContain( - "http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight", - ); }); +}); - test("create() builds a resource with fixed code and required slice stubs", () => { - const profile = USCoreBodyWeightProfile.create({ +describe("demo: read a US Core body weight observation from JSON", () => { + test("parse an API response and use typed getters", () => { + const json: Observation = { + resourceType: "Observation", + meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight"] }, status: "final", - subject: { reference: "Patient/example" }, - }); + category: [ + { + coding: [ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ], + text: "Vital Signs", + }, + ], + code: { coding: [{ code: "29463-7", system: "http://loinc.org", display: "Body weight" }] }, + subject: { reference: "Patient/pt-1" }, + effectiveDateTime: "2024-06-15", + valueQuantity: { value: 75, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }, + }; - profile.setValueQuantity({ value: 70, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }); - profile.setEffectiveDateTime("2024-01-15"); + // from() validates the resource against the profile before wrapping + const profile = USCoreBodyWeightProfile.from(json); - const obs = profile.toResource(); - expect(obs.code!.coding![0]!.code).toBe("29463-7"); - expect(obs.valueQuantity!.value).toBe(70); - expect(obs.category).toHaveLength(1); - expect(profile.validate().errors).toEqual([]); + // Typed getters provide direct access to profiled fields + expect(profile.getStatus()).toBe("final"); + expect(profile.getSubject()!.reference).toBe("Patient/pt-1"); + expect(profile.getEffectiveDateTime()).toBe("2024-06-15"); + expect(profile.getValueQuantity()!.value).toBe(75); + expect(profile.getValueQuantity()!.unit).toBe("kg"); + + // Code is a fixed value — auto-set by the profile, read-only (no setter) + expect(profile.getCode()!.coding![0]!.code).toBe("29463-7"); + + // Slice accessor strips discriminator keys (coding) by default — only user data remains + expect(profile.getVSCat()).toEqual({ text: "Vital Signs" }); + // Use "raw" mode to see the full element including discriminator + expect(profile.getVSCat("raw")!.coding).toEqual([ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ]); }); +}); - test("validate() catches disallowed value[x] variants on raw resource", () => { +describe("validation", () => { + test("catches disallowed value[x] variants", () => { const resource: Observation = { resourceType: "Observation", meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight"] }, status: "final", category: [ { - coding: { - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }, + coding: [ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ], }, - ] as any, + ], code: { coding: [{ code: "29463-7", system: "http://loinc.org" }] }, subject: { reference: "Patient/pt-1" }, effectiveDateTime: "2024-06-15", @@ -96,19 +127,4 @@ describe("demo", () => { const { errors } = profile.validate(); expect(errors).toContain("USCoreBodyWeightProfile: field 'valueString' must not be present"); }); - - test("getVSCat() returns flat value, getVSCat('raw') includes discriminator", () => { - const profile = USCoreBodyWeightProfile.create({ - status: "final", - subject: { reference: "Patient/example" }, - }); - - const flat = profile.getVSCat(); - expect(flat).toBeDefined(); - expect(flat).not.toHaveProperty("coding"); - - const raw = profile.getVSCat("raw"); - expect(raw).toBeDefined(); - expect(raw!.coding).toBeDefined(); - }); }); diff --git a/examples/typescript-us-core/profile-bp.test.ts b/examples/typescript-us-core/profile-bp.test.ts index 1a2f75be8..a2aa57600 100644 --- a/examples/typescript-us-core/profile-bp.test.ts +++ b/examples/typescript-us-core/profile-bp.test.ts @@ -6,27 +6,78 @@ import { describe, expect, test } from "bun:test"; import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation"; import { USCoreBloodPressureProfile } from "./fhir-types/hl7-fhir-us-core/profiles"; -const createBp = () => - USCoreBloodPressureProfile.create({ - status: "final", - subject: { reference: "Patient/pt-1" }, +describe("demo: create a US Core blood pressure observation", () => { + test("build a valid BP resource step by step", () => { + // Create a new BP profile — auto-sets code, category, and component stubs + const profile = USCoreBloodPressureProfile.create({ + status: "final", + subject: { reference: "Patient/pt-1" }, + }); + + // Not yet valid: effective[x] is required by the vitalsigns base profile + expect(profile.validate().errors).toEqual([ + "USCoreBloodPressureProfile: at least one of effectiveDateTime, effectivePeriod is required", + ]); + + // Fill in the remaining required fields + profile + .setEffectiveDateTime("2024-06-15") + .setSystolic({ value: 120, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }) + .setDiastolic({ value: 80, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }); + + // Now the resource is fully valid + expect(profile.validate().errors).toEqual([]); + expect(profile.toResource()).toMatchSnapshot(); + }); +}); + +describe("demo: apply US Core BP profile to an existing Observation", () => { + test("wrap a plain Observation and populate profiled fields", () => { + // Start with an existing Observation — e.g. received from another system + const obs: Observation = { + resourceType: "Observation", + status: "preliminary", + code: { coding: [{ code: "85354-9", system: "http://loinc.org" }] }, + subject: { reference: "Patient/pt-1" }, + }; + + // 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 + expect(profile.validate().errors).toEqual([ + "USCoreBloodPressureProfile: at least one of effectiveDateTime, effectivePeriod is required", + ]); + + // The profile mutates the original resource — no copy is made + expect(profile.toResource()).toBe(obs); + expect(obs.meta?.profile).toContain("http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure"); + + // Fill in the remaining fields via fluent setters + profile + .setStatus("final") + .setEffectiveDateTime("2024-06-15") + .setSystolic({ value: 120, unit: "mmHg" }) + .setDiastolic({ value: 80, unit: "mmHg" }); + + expect(profile.validate().errors).toEqual([]); }); +}); -describe("demo", () => { - test("import a profiled Observation from an API and read components", () => { - const apiResponse: Observation = { +describe("demo: read a US Core blood pressure observation from JSON", () => { + test("parse an API response and use typed getters", () => { + const json: Observation = { resourceType: "Observation", meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure"] }, status: "final", - // singular coding matches the slice discriminator format used by the profile category: [ { - coding: { - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }, + coding: [ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ], + text: "Vital Signs", }, - ] as any, + ], code: { coding: [{ code: "85354-9", system: "http://loinc.org", display: "Blood pressure panel" }] }, subject: { reference: "Patient/pt-1" }, effectiveDateTime: "2024-06-15", @@ -42,208 +93,57 @@ describe("demo", () => { ], }; - const profile = USCoreBloodPressureProfile.from(apiResponse); + // from() validates the resource against the profile before wrapping + const profile = USCoreBloodPressureProfile.from(json); - expect(profile.getSystolic()).toEqual({ - value: 120, - unit: "mmHg", - system: "http://unitsofmeasure.org", - code: "mm[Hg]", - }); - expect(profile.getDiastolic()).toEqual({ - value: 80, - unit: "mmHg", - system: "http://unitsofmeasure.org", - code: "mm[Hg]", - }); + // Typed getters for profiled fields + expect(profile.getStatus()).toBe("final"); + expect(profile.getSubject()!.reference).toBe("Patient/pt-1"); expect(profile.getEffectiveDateTime()).toBe("2024-06-15"); - }); - - test("apply profile to a bare Observation and populate it", () => { - const bareObservation: Observation = { resourceType: "Observation", status: "preliminary", code: {} }; - const profile = USCoreBloodPressureProfile.apply(bareObservation); - - profile - .setStatus("final") - .setCode({ coding: [{ code: "85354-9", system: "http://loinc.org" }] }) - .setSubject({ reference: "Patient/pt-1" }) - .setVSCat({}) - .setEffectiveDateTime("2024-06-15") - .setSystolic({ value: 120, unit: "mmHg" }) - .setDiastolic({ value: 80, unit: "mmHg" }); - expect(profile.validate().errors).toEqual([]); - expect(profile.toResource().meta?.profile).toContain( - "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure", - ); - }); -}); - -describe("US Core blood pressure profile", () => { - const profile = createBp(); - - test("canonicalUrl is exposed", () => { - expect(USCoreBloodPressureProfile.canonicalUrl).toBe( - "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure", - ); - }); - - test("create() auto-sets code and meta.profile", () => { - const obs = profile.toResource(); - expect(obs.resourceType).toBe("Observation"); - expect(obs.code!.coding![0]!.code).toBe("85354-9"); - expect(obs.code!.coding![0]!.system).toBe("http://loinc.org"); - expect(obs.meta?.profile).toEqual(["http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure"]); - }); - - test("freshly created profile is not yet valid (missing effective)", () => { - const { errors } = profile.validate(); - expect(errors).toEqual([ - "USCoreBloodPressureProfile: at least one of effectiveDateTime, effectivePeriod is required", - ]); - }); - - test("create() auto-populates component with systolic/diastolic stubs", () => { - const fresh = createBp(); - const obs = fresh.toResource(); - expect(obs.component).toHaveLength(2); - expect(fresh.getSystolic("raw")).toBeDefined(); - expect(fresh.getDiastolic("raw")).toBeDefined(); - }); - - test("setSystolic / getSystolic / getSystolicRaw", () => { - profile.setSystolic({ value: 120, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }); - - expect(profile.getSystolic()).toEqual({ - value: 120, - unit: "mmHg", - system: "http://unitsofmeasure.org", - code: "mm[Hg]", - }); - - const raw = profile.getSystolic("raw")!; - expect(raw.valueQuantity!.value).toBe(120); - expect(raw.code?.coding?.[0]?.code).toBe("8480-6"); - }); - - test("setDiastolic / getDiastolic / getDiastolicRaw", () => { - profile.setDiastolic({ value: 80, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }); - - expect(profile.getDiastolic()).toEqual({ - value: 80, - unit: "mmHg", - system: "http://unitsofmeasure.org", - code: "mm[Hg]", - }); - - const raw = profile.getDiastolic("raw")!; - expect(raw.valueQuantity!.value).toBe(80); - expect(raw.code?.coding?.[0]?.code).toBe("8462-4"); - }); - - test("both systolic and diastolic are in the component array", () => { - const obs = profile.toResource(); - expect(obs.component).toHaveLength(2); + // Component slice accessors — flat mode returns valueQuantity directly + expect(profile.getSystolic()!.value).toBe(120); + expect(profile.getDiastolic()!.value).toBe(80); + // Raw mode returns the full component element including discriminator code expect(profile.getSystolic("raw")!.valueQuantity!.value).toBe(120); - expect(profile.getDiastolic("raw")!.valueQuantity!.value).toBe(80); - }); - - test("setSystolic replaces an existing systolic component", () => { - profile.setSystolic({ value: 130, unit: "mmHg" }); - - expect(profile.toResource().component).toHaveLength(2); - expect(profile.getSystolic("raw")!.valueQuantity!.value).toBe(130); - }); - - test("setVSCat adds category with discriminator values", () => { - profile.setVSCat({ text: "Vital Signs" }); - - const raw = profile.getVSCat("raw")!; - expect(raw.text).toBe("Vital Signs"); - expect(raw.coding as unknown).toEqual({ - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }); - }); + expect(profile.getSystolic("raw")!.code?.coding?.[0]?.code).toBe("8480-6"); - test("setEffectiveDateTime / getEffectiveDateTime", () => { - profile.setEffectiveDateTime("2024-06-15T10:30:00Z"); - expect(profile.getEffectiveDateTime()).toBe("2024-06-15T10:30:00Z"); - expect(profile.getValueQuantity()).toBeUndefined(); - }); - - test("fluent chaining across all accessor types", () => { - const result = profile - .setStatus("final") - .setVSCat({ text: "Vital Signs" }) - .setEffectiveDateTime("2024-06-15") - .setSubject({ reference: "Patient/pt-2" }) - .setSystolic({ value: 120, unit: "mmHg" }) - .setDiastolic({ value: 80, unit: "mmHg" }); - - expect(result).toBe(profile); - expect(profile.getStatus()).toBe("final"); - expect(profile.getVSCat()!.text).toBe("Vital Signs"); - expect(profile.getEffectiveDateTime()).toBe("2024-06-15"); - expect(profile.getSubject()!.reference).toBe("Patient/pt-2"); - expect(profile.getSystolic("raw")!.valueQuantity!.value).toBe(120); - expect(profile.getDiastolic("raw")!.valueQuantity!.value).toBe(80); - }); - - test("setSystolic with no args inserts discriminator-only component", () => { - const fresh = createBp(); - fresh.setSystolic(); - - const raw = fresh.getSystolic("raw")!; - expect(raw.code?.coding?.[0]?.code).toBe("8480-6"); - expect(raw.valueQuantity).toBeUndefined(); + // Category slice — flat strips discriminator, raw includes it + expect(profile.getVSCat()).toEqual({ text: "Vital Signs" }); + expect(profile.getVSCat("raw")!.coding).toEqual([ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ]); }); +}); - test("create() with custom category preserves user values and adds required VSCat", () => { - const custom = USCoreBloodPressureProfile.create({ +describe("category auto-population", () => { + test("custom category is preserved, VSCat is appended", () => { + const profile = USCoreBloodPressureProfile.create({ status: "final", subject: { reference: "Patient/pt-1" }, category: [{ text: "My Category" }], }); - const obs = custom.toResource(); + const obs = profile.toResource(); expect(obs.category).toHaveLength(2); expect(obs.category![0]!.text).toBe("My Category"); - expect((obs.category![1] as Record).coding).toEqual({ - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }); - }); - - test("create() with empty category still adds required VSCat", () => { - const custom = USCoreBloodPressureProfile.create({ - status: "final", - subject: { reference: "Patient/pt-1" }, - category: [], - }); - const obs = custom.toResource(); - expect(obs.category).toHaveLength(1); - expect((obs.category![0] as Record).coding).toEqual({ - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }); + expect(obs.category![1]!.coding).toEqual([ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ]); }); - test("create() with category already containing VSCat does not duplicate it", () => { - const custom = USCoreBloodPressureProfile.create({ + test("existing VSCat is not duplicated", () => { + const profile = USCoreBloodPressureProfile.create({ status: "final", subject: { reference: "Patient/pt-1" }, - // category with VSCat discriminator already present (singular coding matches internal format) category: [ { - coding: { - code: "vital-signs", - system: "http://terminology.hl7.org/CodeSystem/observation-category", - }, + coding: [ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ], }, - ] as any, + ], }); - const obs = custom.toResource(); - expect(obs.category).toHaveLength(1); + expect(profile.toResource().category).toHaveLength(1); }); }); From a3bf7f243e487e4a9ceb91d5e33b6dbf0b0b88bf Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 2 Apr 2026 12:04:41 +0200 Subject: [PATCH 5/6] docs: Add example test structure guidelines to CLAUDE.md --- CLAUDE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 5d370a241..48bd12b97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,6 +131,20 @@ FHIR Package → TypeSchema Generator → TypeSchema Format → Code Generators - Tests mirror source structure in `test/unit/` - API tests for high-level generators +### Example Test Structure + +Example test files in `examples/` follow a two-tier structure: + +1. **Demo tests** come first — readable, self-contained scenarios that show how the generated API is used. Each demo is a separate `describe("demo: ...")` block covering one use case. Demos should: + - Build **valid** resources (populate all required fields so `validate().errors` is empty) + - Use `toMatchSnapshot()` on the final resource to capture the full FHIR JSON + - Show the validation error → fix → valid flow when it makes the demo clearer + - Use comments to explain what the profile API does, not what the test asserts + +2. **Regression tests** follow — concise, focused tests for edge cases and mechanics not covered by demos (e.g. factory equivalence, slice replacement, choice type independence). Keep these minimal; don't duplicate what demos already prove. + +Reference example: `examples/typescript-r4/profile-bodyweight.test.ts` + ### Key Dependencies - `@atomic-ehr/fhir-canonical-manager`: FHIR package management - `@atomic-ehr/fhirschema`: FHIR schema definitions From 630949f0661ab590dbfec25170d9c36662ea6e09 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Thu, 2 Apr 2026 13:21:50 +0200 Subject: [PATCH 6/6] fix: Add snapshot test for compliant US Core Patient resource Adds gender, birthDate, and toMatchSnapshot() to the patient demo test to capture a fully valid US Core Patient with extensions. --- .../profile-patient.test.ts.snap | 78 +++++++++++++++++++ .../profile-patient.test.ts | 6 ++ 2 files changed, 84 insertions(+) create mode 100644 examples/typescript-us-core/__snapshots__/profile-patient.test.ts.snap diff --git a/examples/typescript-us-core/__snapshots__/profile-patient.test.ts.snap b/examples/typescript-us-core/__snapshots__/profile-patient.test.ts.snap new file mode 100644 index 000000000..b0e74ef3a --- /dev/null +++ b/examples/typescript-us-core/__snapshots__/profile-patient.test.ts.snap @@ -0,0 +1,78 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`demo three ways to set an extension: flat input, profile instance, raw Extension 1`] = ` +{ + "birthDate": "1985-03-15", + "extension": [ + { + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "code": "2106-3", + "display": "White", + "system": "urn:oid:2.16.840.1.113883.6.238", + }, + }, + { + "url": "text", + "valueString": "White", + }, + ], + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + }, + { + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "code": "2135-2", + "display": "Hispanic or Latino", + }, + }, + { + "url": "detailed", + "valueCoding": { + "code": "2148-5", + "display": "Mexican", + }, + }, + { + "url": "text", + "valueString": "Mexican", + }, + ], + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex", + "valueCoding": { + "code": "female", + "display": "Female", + }, + }, + ], + "gender": "female", + "identifier": [ + { + "system": "http://hospital.example.org/mrn", + "value": "MRN-12345", + }, + ], + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient", + ], + }, + "name": [ + { + "family": "Garcia", + "given": [ + "Maria", + "Elena", + ], + }, + ], + "resourceType": "Patient", +} +`; diff --git a/examples/typescript-us-core/profile-patient.test.ts b/examples/typescript-us-core/profile-patient.test.ts index 419503756..d34cfbbff 100644 --- a/examples/typescript-us-core/profile-patient.test.ts +++ b/examples/typescript-us-core/profile-patient.test.ts @@ -47,11 +47,17 @@ describe("demo", () => { }; patient.setSex(sexExtension); + // gender and birthDate are must-support base Patient fields — set directly on the resource + Object.assign(patient.toResource(), { gender: "female", birthDate: "1985-03-15" }); + expect(patient.validate().errors).toEqual([]); + expect(patient.toResource()).toMatchSnapshot(); expect(patient.toResource()).toEqual({ resourceType: "Patient", identifier: [{ system: "http://hospital.example.org/mrn", value: "MRN-12345" }], name: [{ family: "Garcia", given: ["Maria", "Elena"] }], + gender: "female", + birthDate: "1985-03-15", meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"] }, extension: [ {