diff --git a/CLAUDE.md b/CLAUDE.md index 96409a5e1..dc2efd61c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,6 +116,9 @@ FHIR Package → TypeSchema Generator → TypeSchema Format → Code Generators - Avoid `function foo() { ... }` declarations in new code - Avoid re-exports inside project - Avoid `interface Foo { ... }` declarations in new code, prefer type syntax if it is possible +- In code generators (writer-generator): use `curlyBlock` and `squareBlock` helpers for writing structured output instead of manual indent/deindent or string concatenation +- Use `Record` instead of `Map` unless there is a significant reason for `Map` (e.g. non-string keys, iteration order guarantees, frequent deletion) +- Prefer single-line guard clauses without braces: `if (!x) throw new Error("...");` instead of wrapping in `{ }` ### Testing Strategy - Uses Bun's built-in test runner diff --git a/README.md b/README.md index 69520613e..411e4be72 100644 --- a/README.md +++ b/README.md @@ -343,11 +343,11 @@ const obs = bp.toResource(); **Slicing & Choice Type Flattening:** ```typescript -// Simplified getter — discriminator stripped, choice type flattened -bp.getSystolicBP(); // { value: 120, unit: "mmHg" } +// Flat getter (default) — discriminator stripped, choice type flattened +bp.getSystolicBP(); // { value: 120, unit: "mmHg" } // Raw getter — full FHIR element including discriminator values -bp.getSystolicBPRaw(); // { code: { coding: [...] }, valueQuantity: { value: 120, ... } } +bp.getSystolicBP('raw'); // { code: { coding: [...] }, valueQuantity: { value: 120, ... } } ``` **Wrapping Existing Resources:** diff --git a/assets/api/writer-generator/typescript/profile-helpers.ts b/assets/api/writer-generator/typescript/profile-helpers.ts index 4f45db45b..9191c8ff9 100644 --- a/assets/api/writer-generator/typescript/profile-helpers.ts +++ b/assets/api/writer-generator/typescript/profile-helpers.ts @@ -121,6 +121,42 @@ export const matchesValue = (value: unknown, match: unknown): boolean => { return value === match; }; +/** + * Type guard that discriminates a raw extension input (with an `extension` + * array) from a flat-API input object. Using a custom type guard instead of + * a bare `"extension" in args` lets TypeScript narrow *both* branches of the + * union — the plain `in` check cannot eliminate a type whose `extension` + * property is optional. + */ +export const isRawExtensionInput = (input: object): input is TRaw => "extension" in input; + +/** + * Type guard that tests whether an unknown setter input is a raw Extension + * (i.e. an object with a `url` property). When `url` is provided, also + * checks that the extension's URL matches the expected value. + */ +export const isExtension = (input: unknown, url?: string): input is E => + typeof input === "object" && input !== null && "url" in input && (url === undefined || input.url === url); + +/** + * Read a single typed value field from an Extension, returning `undefined` + * when the extension itself is absent or the field is not set. + * + * This avoids the double-cast `(ext as Record<…>)?.field as T` that would + * otherwise be needed for value fields not declared on the base Extension type. + */ +export const getExtensionValue = (ext: { url?: string } | undefined, field: string): T | undefined => { + if (!ext) return undefined; + return (ext as Record)[field] as T | undefined; +}; + +/** + * Push an extension onto `target.extension`, creating the array if absent. + */ +export const pushExtension = (target: { extension?: E[] }, ext: E): void => { + (target.extension ??= []).push(ext); +}; + // --------------------------------------------------------------------------- // Extension helpers // --------------------------------------------------------------------------- @@ -134,10 +170,10 @@ export const matchesValue = (value: unknown, match: unknown): boolean => { * @returns A record keyed by sub-extension URL, or `undefined` if the * extension has no nested children. */ -export const extractComplexExtension = ( - extension: { extension?: Array<{ url?: string; [key: string]: unknown }> } | undefined, +export const extractComplexExtension = >( + extension: { extension?: Array<{ url?: string }> } | undefined, config: Array<{ name: string; valueField: string; isArray: boolean }>, -): Record | undefined => { +): T | undefined => { if (!extension?.extension) return undefined; const result: Record = {}; for (const { name, valueField, isArray } of config) { @@ -148,7 +184,7 @@ export const extractComplexExtension = ( result[name] = (subExts[0] as Record)[valueField]; } } - return result; + return result as T; }; // --------------------------------------------------------------------------- @@ -219,6 +255,13 @@ export const ensureSliceDefaults = (items: T[], ...matches: Record(obj: object): T => obj as unknown as T; + /** * Add `canonicalUrl` to `resource.meta.profile` if not already present. * Creates `meta` and `profile` when missing. @@ -256,25 +299,31 @@ export const getArraySlice = (list: readonly T[] | undefined, match: Record, profileName: string, field: string): string[] => { - return res[field] === undefined || res[field] === null +export const validateRequired = (res: object, profileName: string, field: string): string[] => { + const rec = res as Record; + return rec[field] === undefined || rec[field] === null ? [`${profileName}: required field '${field}' is missing`] : []; }; +/** Checks that a must-support field is populated (warning, not error). */ +export const validateMustSupport = (res: object, profileName: string, field: string): string[] => { + const rec = res as Record; + return rec[field] === undefined || rec[field] === null + ? [`${profileName}: must-support field '${field}' is not populated`] + : []; +}; + /** Checks that `field` is absent (profiles may exclude base fields). */ -export const validateExcluded = (res: Record, profileName: string, field: string): string[] => { - return res[field] !== undefined ? [`${profileName}: field '${field}' must not be present`] : []; +export const validateExcluded = (res: object, profileName: string, field: string): string[] => { + return (res as Record)[field] !== undefined + ? [`${profileName}: field '${field}' must not be present`] + : []; }; /** Checks that `field` structurally contains the expected fixed value. */ -export const validateFixedValue = ( - res: Record, - profileName: string, - field: string, - expected: unknown, -): string[] => { - return matchesValue(res[field], expected) +export const validateFixedValue = (res: object, profileName: string, field: string, expected: unknown): string[] => { + return matchesValue((res as Record)[field], expected) ? [] : [`${profileName}: field '${field}' does not match expected fixed value`]; }; @@ -284,7 +333,7 @@ export const validateFixedValue = ( * discriminator) falls within [`min`, `max`]. Pass `max = 0` for unbounded. */ export const validateSliceCardinality = ( - res: Record, + res: object, profileName: string, field: string, match: Record, @@ -292,7 +341,7 @@ export const validateSliceCardinality = ( min: number, max: number, ): string[] => { - const items = res[field] as unknown[] | undefined; + const items = (res as Record)[field] as unknown[] | undefined; const count = (items ?? []).filter((item) => matchesValue(item, match)).length; const errors: string[] = []; if (count < min) { @@ -308,12 +357,9 @@ export const validateSliceCardinality = ( * Checks that at least one of the listed choice-type variants is present. * E.g. `["effectiveDateTime", "effectivePeriod"]`. */ -export const validateChoiceRequired = ( - res: Record, - profileName: string, - choices: string[], -): string[] => { - return choices.some((c) => res[c] !== undefined) +export const validateChoiceRequired = (res: object, profileName: string, choices: string[]): string[] => { + const rec = res as Record; + return choices.some((c) => rec[c] !== undefined) ? [] : [`${profileName}: at least one of ${choices.join(", ")} is required`]; }; @@ -323,13 +369,8 @@ export const validateChoiceRequired = ( * Handles plain strings, Coding objects, and CodeableConcept objects. * Skips validation when the field is absent. */ -export const validateEnum = ( - res: Record, - profileName: string, - field: string, - allowed: string[], -): string[] => { - const value = res[field]; +export const validateEnum = (res: object, profileName: string, field: string, allowed: string[]): string[] => { + const value = (res as Record)[field]; if (value === undefined || value === null) return []; if (typeof value === "string") { return allowed.includes(value) @@ -357,13 +398,8 @@ export const validateEnum = ( * types. Extracts the type from the `reference` string (the part before * the first `/`). Skips validation when the field or reference is absent. */ -export const validateReference = ( - res: Record, - profileName: string, - field: string, - allowed: string[], -): string[] => { - const value = res[field]; +export const validateReference = (res: object, profileName: string, field: string, allowed: string[]): string[] => { + const value = (res as Record)[field]; if (value === undefined || value === null) return []; const ref = (value as Record).reference as string | undefined; if (!ref) return []; diff --git a/docs/design/profiles.md b/docs/design/profiles.md index aa2c6cbb2..7e5ce6a62 100644 --- a/docs/design/profiles.md +++ b/docs/design/profiles.md @@ -1,6 +1,6 @@ # FHIR Profiles Representation -Status: Implemented (TypeScript). See `examples/typescript-r4/` for working examples. +Status: Implemented (TypeScript). See `examples/typescript-r4/` and `examples/typescript-us-core/` for working examples. This document covers the representation of FHIR profiles in generated code: resource profiles, extension profiles, and their relationship to base resources. @@ -82,16 +82,21 @@ Problem: if we have several levels of profiles on top of the resource how they s Arbitrary JSON can be converted to: 1. Resource type, independently from the profiles. -1. Profile type via `ProfileClass.from(resource)`. +1. Profile type via `ProfileClass.from(resource)` or `ProfileClass.apply(resource)`. ```typescript // Parse as resource const obs: Observation = JSON.parse(json) -// Wrap with profile +// from() validates meta.profile and runs validate(), throws on errors const bodyweight = observation_bodyweightProfile.from(obs) -bodyweight.getVSCat() // access slice -bodyweight.toResource() // back to Observation (same object) + +// apply() stamps meta.profile without validation, for incremental construction +const bodyweight = observation_bodyweightProfile.apply(obs) + +bodyweight.getVSCat() // access slice (flat by default) +bodyweight.getVSCat("raw") // access raw FHIR element +bodyweight.toResource() // back to Observation (same object) ``` ### Mutable/Immutable Representation @@ -100,7 +105,7 @@ The profile class holds a mutable reference to the underlying resource. Mutation ```typescript const obs: Observation = { resourceType: "Observation", status: "preliminary", ... } -const profile = observation_bodyweightProfile.from(obs) +const profile = observation_bodyweightProfile.apply(obs) profile.setStatus("final") obs.status // "final" — same object ``` @@ -120,38 +125,44 @@ export interface observation_bodyweight extends Observation { } ``` -2. **Profile class** — wraps the resource with typed getters/setters and slice accessors: +2. **Profile class** — wraps the resource with factory methods, typed getters/setters, slice accessors, extension accessors, and validation: ```typescript export class observation_bodyweightProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/bodyweight" private resource: Observation constructor(resource: Observation) { ... } - static from(resource: Observation): observation_bodyweightProfile { ... } - static createResource(args: observation_bodyweightProfileParams): Observation { ... } - static create(args: observation_bodyweightProfileParams): observation_bodyweightProfile { ... } + + // Factory methods + static from(resource: Observation): observation_bodyweightProfile { ... } // validates, throws on error + static apply(resource: Observation): observation_bodyweightProfile { ... } // stamps meta.profile, no validation + static createResource(args: observation_bodyweightProfileRaw): Observation { ... } + static create(args: observation_bodyweightProfileRaw): observation_bodyweightProfile { ... } // Typed getters/setters for constrained fields getStatus(): (...) | undefined { ... } setStatus(value: ...): this { ... } - // Slice accessors (see slices.md) - setVSCat(input?: Observation_bodyweight_Category_VSCatSliceInput): this { ... } - getVSCat(): Observation_bodyweight_Category_VSCatSliceInput | undefined { ... } - getVSCatRaw(): CodeableConcept | undefined { ... } + // Slice accessors with mode overloads (see slices.md) + setVSCat(input?: VSCatSliceFlat | CodeableConcept): this { ... } + getVSCat(): VSCatSliceFlat | undefined { ... } // flat (default) + getVSCat(mode: 'flat'): VSCatSliceFlat | undefined { ... } + getVSCat(mode: 'raw'): CodeableConcept | undefined { ... } // Conversion toResource(): Observation { ... } - toProfile(): observation_bodyweight { ... } + + // Validation + validate(): { errors: string[]; warnings: string[] } { ... } } ``` 3. **Params type** — lists fields for the `createResource`/`create` factory methods. Array fields with required slices are optional -- stubs are auto-merged: ```typescript -export type observation_bodyweightProfileParams = { +export type observation_bodyweightProfileRaw = { status: (...); - code: CodeableConcept<(...)>; subject: Reference<"Patient">; category?: CodeableConcept<(...)>[]; // optional -- required slice stubs auto-merged } @@ -166,27 +177,24 @@ Extension profiles constrain the `Extension` type. They come in two forms: A simple extension carries one `value[x]` field (e.g., `patient-birthPlace` carries `valueAddress`): ```typescript -export type birthPlaceProfileParams = { +export type birthPlaceProfileRaw = { valueAddress: Address; } export class birthPlaceProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/patient-birthPlace" private resource: Extension - static createResource(args: birthPlaceProfileParams): Extension { - return { - url: "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", - valueAddress: args.valueAddress, - } as unknown as Extension - } - - static create(args: birthPlaceProfileParams): birthPlaceProfile { ... } - static from(resource: Extension): birthPlaceProfile { ... } + static from(resource: Extension): birthPlaceProfile { ... } // validates + static apply(resource: Extension): birthPlaceProfile { ... } // no validation + static createResource(args: birthPlaceProfileRaw): Extension { ... } + static create(args: birthPlaceProfileRaw): birthPlaceProfile { ... } getValueAddress(): Address | undefined { ... } setValueAddress(value: Address): this { ... } toResource(): Extension { ... } + validate(): { errors: string[]; warnings: string[] } { ... } } ``` @@ -206,36 +214,70 @@ const patient: Patient = { A complex extension has nested extension elements instead of a single value (e.g., `patient-nationality` has `code` and `period` sub-extensions): ```typescript +// Raw input — pass extension[] directly +export type nationalityProfileRaw = { extension?: Extension[] } + +// Flat input — typed sub-extension fields +export type nationalityProfileFlat = { code?: CodeableConcept; period?: Period } + export class nationalityProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/patient-nationality" private resource: Extension - static createResource(): Extension { - return { - url: "http://hl7.org/fhir/StructureDefinition/patient-nationality", - } as unknown as Extension - } + static from(resource: Extension): nationalityProfile { ... } + static apply(resource: Extension): nationalityProfile { ... } + + // create/createResource accept both raw and flat input + static createResource(args?: nationalityProfileRaw | nationalityProfileFlat): Extension { ... } + static create(args?: nationalityProfileRaw | nationalityProfileFlat): nationalityProfile { ... } - // Sub-extension accessors + // Sub-extension accessors with mode overloads setCode(value: CodeableConcept): this { ... } + getCode(): CodeableConcept | undefined { ... } // flat (default) + getCode(mode: 'flat'): CodeableConcept | undefined { ... } + getCode(mode: 'raw'): Extension | undefined { ... } // raw sub-extension + setPeriod(value: Period): this { ... } - getCode(): CodeableConcept | undefined { ... } - getCodeExtension(): Extension | undefined { ... } // raw access getPeriod(): Period | undefined { ... } - getPeriodExtension(): Extension | undefined { ... } + getPeriod(mode: 'raw'): Extension | undefined { ... } toResource(): Extension { ... } + validate(): { errors: string[]; warnings: string[] } { ... } } ``` Usage: ```typescript -const profile = nationalityProfile.create() - .setCode({ coding: [{ system: "urn:iso:std:iso:3166", code: "US" }] }) - .setPeriod({ start: "2000-01-01" }) +// Flat input +const profile = nationalityProfile.create({ + code: { coding: [{ system: "urn:iso:std:iso:3166", code: "US" }] }, + period: { start: "2000-01-01" }, +}) + +// Read values back +profile.getCode() // { coding: [...] } +profile.getCode("raw") // { url: "code", valueCodeableConcept: { coding: [...] } } + const ext: Extension = profile.toResource() ``` +### Extension Accessors on Resource Profiles + +Resource profiles that declare extensions (e.g., US Core Patient with `us-core-race`) generate multi-form setters and overloaded getters: + +```typescript +// Setter accepts flat input, profile instance, or raw Extension +patient.setRace({ ombCategory: { code: "2028-9" }, text: "Asian" }) // flat input +patient.setRace(USCoreRaceExtensionProfile.create({ ... })) // profile instance +patient.setRace({ url: "http://.../us-core-race", extension: [...] }) // raw Extension + +// Getter with mode overloads +patient.getRace() // flat: { ombCategory: ..., text: "Asian" } +patient.getRace("profile") // USCoreRaceExtensionProfile instance +patient.getRace("raw") // raw FHIR Extension +``` + ## TypeSchema Representation Profiles use `kind = "constraint"` in TypeSchema. @@ -246,19 +288,38 @@ Current approach: to collect all profile elements we traverse the inheritance tr Profile classes depend on a generated `profile-helpers.ts` module that provides: +Slice helpers: - `applySliceMatch(input, match)` — merges discriminator values into a slice element -- `matchesSlice(value, match)` — checks if an element matches a slice discriminator -- `extractSliceSimplified(slice, matchKeys)` — strips discriminator keys from a slice for the simplified input type -- `wrapSliceChoice(input, choiceVariant)` — wraps flat input fields under a single choice variant key (for setter) -- `flattenSliceChoice(slice, matchKeys, choiceVariant)` — strips discriminator keys and flattens a single choice variant into parent (for getter) -- `mergeMatch(target, match)` — deep-merges match values into target +- `matchesValue(value, match)` — recursive structural match test +- `setArraySlice(list, match, value)` — find-or-insert in array by discriminator +- `getArraySlice(list, match)` — find first matching element +- `ensureSliceDefaults(items, ...matches)` — ensure required slices have stubs +- `stripMatchKeys(slice, matchKeys)` — remove discriminator keys from getter result +- `wrapSliceChoice(input, choiceVariant)` — wrap flat input fields under a single choice variant key (for setter) +- `unwrapSliceChoice(slice, matchKeys, choiceVariant)` — inverse of wrap + +Extension helpers: +- `ensurePath(root, path)` — navigate/create nested paths for deep extensions - `extractComplexExtension(extension, config)` — extracts typed values from nested extension elements -- `validateRequired(r, field, path)` — checks that a required field is present -- `validateExcluded(r, field, path)` — checks that a forbidden field is absent -- `validateFixedValue(r, field, expected, path)` — checks that a field matches a fixed/pattern value -- `validateSliceCardinality(items, match, sliceName, min, max, path)` — checks min/max counts for a named slice -- `validateEnum(value, allowed, field, path)` — checks that a value is within a required value set (supports primitives, Coding, CodeableConcept) -- `validateReference(value, allowed, field, path)` — checks that a reference targets an allowed resource type +- `isExtension(input, url?)` — type guard for raw Extension detection +- `isRawExtensionInput(input)` — discriminate raw vs flat input for extension profile factories +- `getExtensionValue(ext, field)` — read a typed value field from Extension +- `pushExtension(target, ext)` — push extension onto target.extension array + +Factory helpers: +- `buildResource(obj)` — cast object to resource type +- `ensureProfile(resource, canonicalUrl)` — add profile URL to meta.profile +- `mergeMatch(target, match)` — deep-merges match values into target + +Validation helpers: +- `validateRequired(res, profileName, field)` — checks that a required field is present +- `validateMustSupport(res, profileName, field)` — checks that a must-support field is populated (warning, not error) +- `validateExcluded(res, profileName, field)` — checks that a forbidden field is absent +- `validateFixedValue(res, profileName, field, expected)` — checks that a field matches a fixed/pattern value +- `validateSliceCardinality(res, profileName, field, match, sliceName, min, max)` — checks min/max counts for a named slice +- `validateChoiceRequired(res, profileName, choices)` — checks that at least one choice variant is present +- `validateEnum(res, profileName, field, allowed)` — checks that a value is within a value set (supports primitives, Coding, CodeableConcept) +- `validateReference(res, profileName, field, allowed)` — checks that a reference targets an allowed resource type ## Configuration @@ -280,9 +341,9 @@ new APIBuilder() ## Runtime Validation -Profile classes generate a `validate(): string[]` method that checks the wrapped resource against the profile's constraints. An empty array means the resource conforms; each string describes one violation. +Profile classes generate a `validate(): { errors: string[]; warnings: string[] }` method that checks the wrapped resource against the profile's constraints. Empty arrays mean the resource conforms; each string describes one violation. -Checks performed: +Errors (hard constraint violations): - **Required fields** — fields that the profile marks as mandatory (min >= 1) - **Excluded fields** — fields that the profile forbids (max = 0) - **Fixed/pattern values** — fields constrained to specific values (e.g., `code.coding` must contain a specific LOINC code) @@ -291,17 +352,30 @@ Checks performed: - **Reference types** — reference targets restricted to specific resource types - **Choice type requirements** — at least one variant must be present when the choice group is required +Warnings (soft checks): +- **Extensible binding mismatches** — values outside an extensible value set +- **Must-support fields** — fields marked `mustSupport: true` in the profile that are not populated (only for non-required fields; required fields already produce errors) + ```typescript const bp = observation_bpProfile.create({ status: "final", subject: { reference: "Patient/pt-1" }, }); -const errors = bp.validate(); -// ["effective: at least one of effectiveDateTime, effectivePeriod is required"] +const { errors, warnings } = bp.validate(); +// errors: ["effective: at least one of effectiveDateTime, effectivePeriod is required"] +// warnings: ["observation_bp: must-support field 'dataAbsentReason' is not populated"] // Required slices (VSCat, SystolicBP, DiastolicBP) are auto-populated by create() ``` +`from()` uses `validate()` internally — it throws on errors but allows warnings: + +```typescript +const profile = observation_bodyweightProfile.from(obs) +// throws if meta.profile is missing or validate().errors is non-empty +// warnings are not thrown — retrieve them via profile.validate().warnings +``` + Validation helpers are emitted into `profile-helpers.ts` alongside the existing slice helpers. ## Future Work diff --git a/docs/posts/typescript-profiles-deep-dive.md b/docs/posts/typescript-profiles-deep-dive.md index d061e640c..cee698760 100644 --- a/docs/posts/typescript-profiles-deep-dive.md +++ b/docs/posts/typescript-profiles-deep-dive.md @@ -327,7 +327,7 @@ A few choices worth understanding: ## Runtime Validation -Profile classes generate a `validate()` method that checks the wrapped resource against profile constraints. It returns an array of error strings -- empty means valid: +Profile classes generate a `validate()` method that checks the wrapped resource against profile constraints. It returns `{ errors, warnings }` -- errors are hard constraint violations, warnings are soft checks like extensible binding mismatches and unpopulated must-support fields: ```typescript const bp = observation_bpProfile.create({ @@ -335,8 +335,9 @@ const bp = observation_bpProfile.create({ subject: { reference: "Patient/pt-1" }, }); -bp.validate(); -// ["effective: at least one of effectiveDateTime, effectivePeriod is required"] +const { errors, warnings } = bp.validate(); +// errors: ["effective: at least one of effectiveDateTime, effectivePeriod is required"] +// warnings: ["observation_bp: must-support field 'dataAbsentReason' is not populated"] // Required slices (VSCat, SystolicBP, DiastolicBP) are auto-populated by create() ``` @@ -348,10 +349,10 @@ bp.setVSCat({ text: "Vital Signs" }) .setSystolicBP({ value: 120, unit: "mmHg" }) .setDiastolicBP({ value: 80, unit: "mmHg" }); -bp.validate(); // [] — valid +bp.validate(); // { errors: [], warnings: [...] } ``` -The method checks required fields, excluded fields, fixed/pattern values, slice cardinality, closed enum bindings, reference types, and choice type requirements. +The method checks required fields, excluded fields, fixed/pattern values, slice cardinality, closed enum bindings, reference types, choice type requirements, and must-support field population (as warnings). ## What's Next diff --git a/docs/posts/typescript-profiles-quick.md b/docs/posts/typescript-profiles-quick.md index ec8fb07f2..40732a4e2 100644 --- a/docs/posts/typescript-profiles-quick.md +++ b/docs/posts/typescript-profiles-quick.md @@ -18,7 +18,7 @@ bp.setVSCat({ text: "Vital Signs" }) .setDiastolicBP({ value: 80, unit: "mmHg" }) .setEffectiveDateTime("2024-06-15"); -bp.validate(); // [] -- valid +bp.validate(); // { errors: [], warnings: [...] } // Plain FHIR JSON -- ready for API calls, storage, etc. const obs = bp.toResource(); diff --git a/docs/posts/typescript-us-core-profiles-quick.md b/docs/posts/typescript-us-core-profiles-quick.md new file mode 100644 index 000000000..2f1495388 --- /dev/null +++ b/docs/posts/typescript-us-core-profiles-quick.md @@ -0,0 +1,95 @@ +# `@atomic-ehr/codegen` adds US Core profile support + +New release of [`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) generates typed profile classes for **US Core IG**. Extensions get a flat, typed API -- no manual `extension[]` wrangling: + +Import a profiled Patient from an API response and read extensions via typed getters: + +```typescript +import { USCorePatientProfile } from "./profiles/Patient_USCorePatientProfile"; + +// from() validates the resource conforms to the profile (meta.profile + required fields) +const patient = USCorePatientProfile.from(apiResponse); + +patient.getName(); // [{ family: "Smith", given: ["John"] }] +patient.getRace(); // flat input: { ombCategory: { code: "2054-5", ... }, text: "Black or African American" } +patient.getSex("profile"); // profile instance: USCoreIndividualSexExtensionProfile +patient.getRace("extension"); // { url: ".../us-core-race", extension: [{ url: "ombCategory", ... }, ...] } +``` + +Apply the profile to a bare resource and populate it -- each extension setter accepts a flat input, a profile instance, or a raw FHIR Extension: + +```typescript +import type { Extension } from "./fhir-types/hl7-fhir-r4-core/Extension"; +import { USCoreEthnicityExtensionProfile, USCorePatientProfile } from "./fhir-types/hl7-fhir-us-core/profiles"; +import type { USCoreRaceExtensionProfileInput } from "./fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreRaceExtension"; + +// apply() attaches meta.profile without validation -- useful for incremental construction +const patient = USCorePatientProfile.apply({ resourceType: "Patient" }); + +patient.setIdentifier([{ system: "http://hospital.example.org/mrn", value: "MRN-00001" }]); +patient.setName([{ family: "Chen", given: ["Wei"] }]); + +// 1. Flat input -- the most common way +const race: USCoreRaceExtensionProfileInput = { + ombCategory: { code: "2028-9", display: "Asian" }, + text: "Chinese", +}; +patient.setRace(race); + +// 2. Profile instance -- when you already have one from another source +const ethnicity: USCoreEthnicityExtensionProfile = USCoreEthnicityExtensionProfile.create({ + ombCategory: { code: "2135-2", display: "Hispanic or Latino" }, + text: "Hispanic or Latino", +}); +patient.setEthnicity(ethnicity); + +// 3. Raw FHIR Extension -- for pass-through from external sources +const sex: Extension = { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex", + valueCoding: { code: "female", display: "Female" }, +}; +patient.setSex(sex); + +patient.validate(); // { errors: [], warnings: [...] } +patient.toResource(); +// { +// resourceType: "Patient", +// meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"] }, +// identifier: [{ system: "http://hospital.example.org/mrn", value: "MRN-00001" }], +// name: [{ family: "Chen", given: ["Wei"] }], +// extension: [ +// { url: ".../us-core-race", extension: [ +// { url: "ombCategory", valueCoding: { code: "2028-9", display: "Asian" } }, +// { url: "text", valueString: "Chinese" }, +// ]}, +// { url: ".../us-core-ethnicity", extension: [ +// { url: "ombCategory", valueCoding: { code: "2135-2", display: "Hispanic or Latino" } }, +// { url: "text", valueString: "Hispanic or Latino" }, +// ]}, +// { url: ".../us-core-individual-sex", valueCoding: { code: "female", display: "Female" } }, +// ], +// } +``` + +Each profile class provides: + +- **Field accessors** -- typed get/set for profiled fields with fluent chaining +- **Fixed values** -- `code`, `meta.profile` auto-set on `create()` +- **Slices** -- category and component slices with discriminator values applied automatically +- **Choice types** -- `effective[x]`, `value[x]` with per-branch accessors +- **Extensions** -- flat API for complex and simple extensions, multi-form setters (flat input, profile instance, raw Extension) +- **Factory methods** -- `from()` (validates), `apply()` (stamps), `create()` (builds from typed input) +- **Validation** -- `validate()` returns `{ errors, warnings }` — checks required fields, choice constraints, and must-support field population + +See the [generate script](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/generate.ts) and [example README](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/README.md) for setup. + +Working examples: + +- Patient: [base type](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Patient.ts), [profile class](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Patient_USCorePatientProfile.ts), [tests](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/profile-patient.test.ts) +- Extensions ([base type](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Extension.ts)): [race](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreRaceExtension.ts), [ethnicity](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreEthnicityExtension.ts), [tribal affiliation](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreTribalAffiliationExtension.ts) +- Blood pressure: [base type](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Observation.ts), [profile class](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBloodPressureProfile.ts), [tests](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/profile-bp.test.ts) +- Body weight: [base type](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Observation.ts), [profile class](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBodyWeightProfile.ts), [tests](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-us-core/profile-bodyweight.test.ts) + +Feedback welcome on [GitHub](https://github.com/atomic-ehr/codegen). + +NPM: [`@atomic-ehr/codegen`](https://www.npmjs.com/package/@atomic-ehr/codegen) diff --git a/examples/typescript-r4/extension-profile.test.ts b/examples/typescript-r4/extension-profile.test.ts index 892084a85..1dee28115 100644 --- a/examples/typescript-r4/extension-profile.test.ts +++ b/examples/typescript-r4/extension-profile.test.ts @@ -34,9 +34,9 @@ test("Patient with extensions built from profiles", () => { expect(patient).toMatchSnapshot(); }); -test("from() wraps existing resource", () => { +test("apply() wraps existing resource", () => { const ext = birthPlaceProfile.createResource({ valueAddress: { city: "Boston" } }); - const profile = birthPlaceProfile.from(ext); + const profile = birthPlaceProfile.apply(ext); expect(profile.toResource()).toBe(ext); }); 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 82c139fe0..1dbdc3f7e 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 @@ -5,72 +5,92 @@ import type { Address } from "../../hl7-fhir-r4-core/Address"; import type { Extension } from "../../hl7-fhir-r4-core/Extension"; -import { validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type birthPlaceProfileParams = { +import { + buildResource, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type birthPlaceProfileRaw = { valueAddress: Address; } // CanonicalURL: http://hl7.org/fhir/StructureDefinition/patient-birthPlace (pkg: hl7.fhir.r4.core#4.0.1) export class birthPlaceProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/patient-birthPlace" + static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/patient-birthPlace"; - private resource: Extension + private resource: Extension; constructor (resource: Extension) { - this.resource = resource + this.resource = resource; } static from (resource: Extension) : birthPlaceProfile { - return new birthPlaceProfile(resource) + const profile = new birthPlaceProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Extension) : birthPlaceProfile { + return new birthPlaceProfile(resource); } - static createResource (args: birthPlaceProfileParams) : Extension { - const resource = { + static createResource (args: birthPlaceProfileRaw) : Extension { + const resource = buildResource( { url: "http://hl7.org/fhir/StructureDefinition/patient-birthPlace", valueAddress: args.valueAddress, - } as unknown as Extension - return resource + }) + return resource; } - static create (args: birthPlaceProfileParams) : birthPlaceProfile { - return birthPlaceProfile.from(birthPlaceProfile.createResource(args)) + static create (args: birthPlaceProfileRaw) : birthPlaceProfile { + return birthPlaceProfile.apply(birthPlaceProfile.createResource(args)); } toResource () : Extension { - return this.resource + return this.resource; } // Field accessors - getValueAddress () : Address | undefined { - return this.resource.valueAddress as Address | undefined + return this.resource.valueAddress as Address | undefined; } setValueAddress (value: Address) : this { - Object.assign(this.resource, { valueAddress: value }) - return this + Object.assign(this.resource, { valueAddress: value }); + return this; } getUrl () : string | undefined { - return this.resource.url as string | undefined + return this.resource.url as string | undefined; } setUrl (value: string) : this { - Object.assign(this.resource, { url: value }) - return this + Object.assign(this.resource, { url: value }); + return this; } + // Extensions + // Slices // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "birthPlace" - const res = this.resource as unknown as Record - return [ - ...validateRequired(res, profileName, "url"), - ...validateFixedValue(res, profileName, "url", "http://hl7.org/fhir/StructureDefinition/patient-birthPlace"), - ...validateChoiceRequired(res, profileName, ["valueAddress"]), - ] + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "url"), + ...validateFixedValue(res, profileName, "url", birthPlaceProfile.canonicalUrl), + ...validateChoiceRequired(res, profileName, ["valueAddress"]), + ], + warnings: [], + } } } 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 53e7203bb..0eb606652 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 @@ -4,72 +4,92 @@ import type { Extension } from "../../hl7-fhir-r4-core/Extension"; -import { validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type birthTimeProfileParams = { +import { + buildResource, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type birthTimeProfileRaw = { valueDateTime: string; } // CanonicalURL: http://hl7.org/fhir/StructureDefinition/patient-birthTime (pkg: hl7.fhir.r4.core#4.0.1) export class birthTimeProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/patient-birthTime" + static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/patient-birthTime"; - private resource: Extension + private resource: Extension; constructor (resource: Extension) { - this.resource = resource + this.resource = resource; } static from (resource: Extension) : birthTimeProfile { - return new birthTimeProfile(resource) + const profile = new birthTimeProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Extension) : birthTimeProfile { + return new birthTimeProfile(resource); } - static createResource (args: birthTimeProfileParams) : Extension { - const resource = { + static createResource (args: birthTimeProfileRaw) : Extension { + const resource = buildResource( { url: "http://hl7.org/fhir/StructureDefinition/patient-birthTime", valueDateTime: args.valueDateTime, - } as unknown as Extension - return resource + }) + return resource; } - static create (args: birthTimeProfileParams) : birthTimeProfile { - return birthTimeProfile.from(birthTimeProfile.createResource(args)) + static create (args: birthTimeProfileRaw) : birthTimeProfile { + return birthTimeProfile.apply(birthTimeProfile.createResource(args)); } toResource () : Extension { - return this.resource + return this.resource; } // Field accessors - getValueDateTime () : string | undefined { - return this.resource.valueDateTime as string | undefined + return this.resource.valueDateTime as string | undefined; } setValueDateTime (value: string) : this { - Object.assign(this.resource, { valueDateTime: value }) - return this + Object.assign(this.resource, { valueDateTime: value }); + return this; } getUrl () : string | undefined { - return this.resource.url as string | undefined + return this.resource.url as string | undefined; } setUrl (value: string) : this { - Object.assign(this.resource, { url: value }) - return this + Object.assign(this.resource, { url: value }); + return this; } + // Extensions + // Slices // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "birthTime" - const res = this.resource as unknown as Record - return [ - ...validateRequired(res, profileName, "url"), - ...validateFixedValue(res, profileName, "url", "http://hl7.org/fhir/StructureDefinition/patient-birthTime"), - ...validateChoiceRequired(res, profileName, ["valueDateTime"]), - ] + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "url"), + ...validateFixedValue(res, profileName, "url", birthTimeProfile.canonicalUrl), + ...validateChoiceRequired(res, profileName, ["valueDateTime"]), + ], + warnings: [], + } } } 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 07c3ec146..14b8156e4 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 @@ -6,91 +6,138 @@ import type { CodeableConcept } from "../../hl7-fhir-r4-core/CodeableConcept"; import type { Extension } from "../../hl7-fhir-r4-core/Extension"; import type { Period } from "../../hl7-fhir-r4-core/Period"; -import { validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; +import { + buildResource, + isRawExtensionInput, + isExtension, + getExtensionValue, + pushExtension, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type nationalityProfileRaw = { + extension?: Extension[]; +} + +export type nationalityProfileFlat = { + code?: CodeableConcept; + period?: Period; +} // CanonicalURL: http://hl7.org/fhir/StructureDefinition/patient-nationality (pkg: hl7.fhir.r4.core#4.0.1) export class nationalityProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/patient-nationality" + static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/patient-nationality"; - private resource: Extension + private resource: Extension; constructor (resource: Extension) { - this.resource = resource + this.resource = resource; } static from (resource: Extension) : nationalityProfile { - return new nationalityProfile(resource) + const profile = new nationalityProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; } - static createResource () : Extension { - const resource = { + static apply (resource: Extension) : nationalityProfile { + return new nationalityProfile(resource); + } + + private static resolveInput (args: nationalityProfileRaw | nationalityProfileFlat) : Extension[] { + if (isRawExtensionInput(args)) { + return args.extension ?? []; + } else { + const result: Extension[] = []; + if (args.code !== undefined) { + result.push({ url: "code", valueCodeableConcept: args.code } as Extension); + } + if (args.period !== undefined) { + result.push({ url: "period", valuePeriod: args.period } as Extension); + } + return result; + } + } + + static createResource (args?: nationalityProfileRaw | nationalityProfileFlat) : Extension { + const resolvedExtensions = nationalityProfile.resolveInput(args ?? {}); + + const resource = buildResource( { url: "http://hl7.org/fhir/StructureDefinition/patient-nationality", - } as unknown as Extension - return resource + extension: resolvedExtensions, + }) + return resource; } - static create () : nationalityProfile { - return nationalityProfile.from(nationalityProfile.createResource()) + static create (args?: nationalityProfileRaw | nationalityProfileFlat) : nationalityProfile { + return nationalityProfile.apply(nationalityProfile.createResource(args)); } toResource () : Extension { - return this.resource + return this.resource; } // Field accessors - getUrl () : string | undefined { - return this.resource.url as string | undefined + return this.resource.url as string | undefined; } setUrl (value: string) : this { - Object.assign(this.resource, { url: value }) - return this + Object.assign(this.resource, { url: value }); + return this; } - // Slices and extensions - + // Extensions public setCode (value: CodeableConcept): this { - const list = (this.resource.extension ??= []) - list.push({ url: "code", valueCodeableConcept: value } as Extension) + pushExtension(this.resource, { url: "code", valueCodeableConcept: value } as Extension) return this } - public setPeriod (value: Period): this { - const list = (this.resource.extension ??= []) - list.push({ url: "period", valuePeriod: value } as Extension) - return this - } - - public getCode (): CodeableConcept | undefined { + public getCode(mode: 'flat'): CodeableConcept | undefined; + public getCode(mode: 'raw'): Extension | undefined; + public getCode(): CodeableConcept | undefined; + public getCode (mode: 'flat' | 'raw' = 'flat'): CodeableConcept | Extension | undefined { const ext = this.resource.extension?.find(e => e.url === "code") - return (ext as Record | undefined)?.valueCodeableConcept as CodeableConcept | undefined + if (!ext) return undefined + if (mode === 'raw') return ext + return getExtensionValue(ext, "valueCodeableConcept") } - public getCodeExtension (): Extension | undefined { - const ext = this.resource.extension?.find(e => e.url === "code") - return ext - } - - public getPeriod (): Period | undefined { - const ext = this.resource.extension?.find(e => e.url === "period") - return (ext as Record | undefined)?.valuePeriod as Period | undefined + public setPeriod (value: Period): this { + pushExtension(this.resource, { url: "period", valuePeriod: value } as Extension) + return this } - public getPeriodExtension (): Extension | undefined { + public getPeriod(mode: 'flat'): Period | undefined; + public getPeriod(mode: 'raw'): Extension | undefined; + public getPeriod(): Period | undefined; + public getPeriod (mode: 'flat' | 'raw' = 'flat'): Period | Extension | undefined { const ext = this.resource.extension?.find(e => e.url === "period") - return ext + if (!ext) return undefined + if (mode === 'raw') return ext + return getExtensionValue(ext, "valuePeriod") } + // Slices // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "nationality" - const res = this.resource as unknown as Record - return [ - ...validateRequired(res, profileName, "url"), - ...validateFixedValue(res, profileName, "url", "http://hl7.org/fhir/StructureDefinition/patient-nationality"), - ] + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "url"), + ...validateFixedValue(res, profileName, "url", nationalityProfile.canonicalUrl), + ], + warnings: [], + } } } 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 b3da99461..b0c8a2aa5 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 @@ -4,72 +4,92 @@ import type { Extension } from "../../hl7-fhir-r4-core/Extension"; -import { validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type own_prefixProfileParams = { +import { + buildResource, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type own_prefixProfileRaw = { valueString: string; } // CanonicalURL: http://hl7.org/fhir/StructureDefinition/humanname-own-prefix (pkg: hl7.fhir.r4.core#4.0.1) export class own_prefixProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix" + static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix"; - private resource: Extension + private resource: Extension; constructor (resource: Extension) { - this.resource = resource + this.resource = resource; } static from (resource: Extension) : own_prefixProfile { - return new own_prefixProfile(resource) + const profile = new own_prefixProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Extension) : own_prefixProfile { + return new own_prefixProfile(resource); } - static createResource (args: own_prefixProfileParams) : Extension { - const resource = { + static createResource (args: own_prefixProfileRaw) : Extension { + const resource = buildResource( { url: "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix", valueString: args.valueString, - } as unknown as Extension - return resource + }) + return resource; } - static create (args: own_prefixProfileParams) : own_prefixProfile { - return own_prefixProfile.from(own_prefixProfile.createResource(args)) + static create (args: own_prefixProfileRaw) : own_prefixProfile { + return own_prefixProfile.apply(own_prefixProfile.createResource(args)); } toResource () : Extension { - return this.resource + return this.resource; } // Field accessors - getValueString () : string | undefined { - return this.resource.valueString as string | undefined + return this.resource.valueString as string | undefined; } setValueString (value: string) : this { - Object.assign(this.resource, { valueString: value }) - return this + Object.assign(this.resource, { valueString: value }); + return this; } getUrl () : string | undefined { - return this.resource.url as string | undefined + return this.resource.url as string | undefined; } setUrl (value: string) : this { - Object.assign(this.resource, { url: value }) - return this + Object.assign(this.resource, { url: value }); + return this; } + // Extensions + // Slices // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "own-prefix" - const res = this.resource as unknown as Record - return [ - ...validateRequired(res, profileName, "url"), - ...validateFixedValue(res, profileName, "url", "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix"), - ...validateChoiceRequired(res, profileName, ["valueString"]), - ] + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "url"), + ...validateFixedValue(res, profileName, "url", own_prefixProfile.canonicalUrl), + ...validateChoiceRequired(res, profileName, ["valueString"]), + ], + warnings: [], + } } } 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 58b8c5418..6d8aae43c 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 @@ -13,11 +13,28 @@ export interface observation_bodyweight extends Observation { subject: Reference<"Patient">; } -export type Observation_bodyweight_Category_VSCatSliceInput = Omit; - -import { ensureProfile, applySliceMatch, matchesValue, setArraySlice, getArraySlice, ensureSliceDefaults, stripMatchKeys, validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type observation_bodyweightProfileParams = { +export type Observation_bodyweight_Category_VSCatSliceFlat = Omit; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type observation_bodyweightProfileRaw = { status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); subject: Reference<"Patient">; category?: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]; @@ -25,155 +42,169 @@ export type observation_bodyweightProfileParams = { // CanonicalURL: http://hl7.org/fhir/StructureDefinition/bodyweight (pkg: hl7.fhir.r4.core#4.0.1) export class observation_bodyweightProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/bodyweight" + 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 + private resource: Observation; constructor (resource: Observation) { - this.resource = resource - ensureProfile(resource, "http://hl7.org/fhir/StructureDefinition/bodyweight") + this.resource = resource; } static from (resource: Observation) : observation_bodyweightProfile { - return new observation_bodyweightProfile(resource) + if (!resource.meta?.profile?.includes(observation_bodyweightProfile.canonicalUrl)) { + throw new Error(`observation_bodyweightProfile: meta.profile must include ${observation_bodyweightProfile.canonicalUrl}`) + } + const profile = new observation_bodyweightProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; } - static createResource (args: observation_bodyweightProfileParams) : Observation { + static apply (resource: Observation) : observation_bodyweightProfile { + ensureProfile(resource, observation_bodyweightProfile.canonicalUrl); + return new observation_bodyweightProfile(resource); + } + + static createResource (args: observation_bodyweightProfileRaw) : Observation { const categoryWithDefaults = ensureSliceDefaults( [...(args.category ?? [])], observation_bodyweightProfile.VSCatSliceMatch, - ) + ); - const resource = { + const resource = buildResource( { resourceType: "Observation", code: {"coding":[{"code":"29463-7","system":"http://loinc.org"}]}, category: categoryWithDefaults, status: args.status, subject: args.subject, meta: { profile: [observation_bodyweightProfile.canonicalUrl] }, - } as unknown as Observation - return resource + }) + return resource; } - static create (args: observation_bodyweightProfileParams) : observation_bodyweightProfile { - return observation_bodyweightProfile.from(observation_bodyweightProfile.createResource(args)) + static create (args: observation_bodyweightProfileRaw) : observation_bodyweightProfile { + return observation_bodyweightProfile.apply(observation_bodyweightProfile.createResource(args)); } toResource () : Observation { - return this.resource + return this.resource; } // Field accessors - getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { - return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; } setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { - Object.assign(this.resource, { status: value }) - return this + Object.assign(this.resource, { status: value }); + return this; } getSubject () : Reference<"Patient"> | undefined { - return this.resource.subject as Reference<"Patient"> | undefined + return this.resource.subject as Reference<"Patient"> | undefined; } setSubject (value: Reference<"Patient">) : this { - Object.assign(this.resource, { subject: value }) - return this + Object.assign(this.resource, { subject: value }); + return this; } getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { - return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; } setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { - Object.assign(this.resource, { category: value }) - return this + Object.assign(this.resource, { category: value }); + return this; } getCode () : 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 { - 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 + 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 + Object.assign(this.resource, { code: value }); + return this; } getEffectiveDateTime () : string | undefined { - return this.resource.effectiveDateTime as string | undefined + return this.resource.effectiveDateTime as string | undefined; } setEffectiveDateTime (value: string) : this { - Object.assign(this.resource, { effectiveDateTime: value }) - return this + Object.assign(this.resource, { effectiveDateTime: value }); + return this; } getEffectivePeriod () : Period | undefined { - return this.resource.effectivePeriod as Period | undefined + return this.resource.effectivePeriod as Period | undefined; } setEffectivePeriod (value: Period) : this { - Object.assign(this.resource, { effectivePeriod: value }) - return this + Object.assign(this.resource, { effectivePeriod: value }); + return this; } getValueQuantity () : Quantity | undefined { - return this.resource.valueQuantity as Quantity | undefined + return this.resource.valueQuantity as Quantity | undefined; } setValueQuantity (value: Quantity) : this { - Object.assign(this.resource, { valueQuantity: value }) - return this + Object.assign(this.resource, { valueQuantity: value }); + return this; } - toProfile () : observation_bodyweight { - return this.resource as observation_bodyweight - } - - // Slices and extensions - - public setVSCat (input?: Observation_bodyweight_Category_VSCatSliceInput): this { + // Extensions + // Slices + public setVSCat (input?: Observation_bodyweight_Category_VSCatSliceFlat | CodeableConcept): this { const match = observation_bodyweightProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.category ??= [], match, value) return this } - public getVSCat (): Observation_bodyweight_Category_VSCatSliceInput | undefined { + public getVSCat(mode: 'flat'): Observation_bodyweight_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): Observation_bodyweight_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): Observation_bodyweight_Category_VSCatSliceFlat | CodeableConcept | undefined { const match = observation_bodyweightProfile.VSCatSliceMatch const item = getArraySlice(this.resource.category, match) if (!item) return undefined - return stripMatchKeys(item, ["coding"]) - } - - public getVSCatRaw (): CodeableConcept | undefined { - const match = observation_bodyweightProfile.VSCatSliceMatch - const item = getArraySlice(this.resource.category, match) - return item + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) } // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "observation-bodyweight" - const res = this.resource as unknown as Record - return [ - ...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), - ...validateRequired(res, profileName, "code"), - ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"29463-7","system":"http://loinc.org"}]}), - ...validateRequired(res, profileName, "subject"), - ...validateReference(res, profileName, "subject", ["Patient"]), - ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), - ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), - ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), - ] + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"29463-7","system":"http://loinc.org"}]}), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), + ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), + ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ], + } } } 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 d670fad75..a42430a92 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 @@ -3,8 +3,7 @@ // Any manual changes made to this file may be overwritten. import type { CodeableConcept } from "../../hl7-fhir-r4-core/CodeableConcept"; -import type { Observation } from "../../hl7-fhir-r4-core/Observation"; -import type { ObservationComponent } from "../../hl7-fhir-r4-core/Observation"; +import type { Observation, ObservationComponent } from "../../hl7-fhir-r4-core/Observation"; import type { Period } from "../../hl7-fhir-r4-core/Period"; import type { Quantity } from "../../hl7-fhir-r4-core/Quantity"; import type { Reference } from "../../hl7-fhir-r4-core/Reference"; @@ -14,13 +13,32 @@ export interface observation_bp extends Observation { subject: Reference<"Patient">; } -export type Observation_bp_Category_VSCatSliceInput = Omit; -export type Observation_bp_Component_SystolicBPSliceInput = Omit & Quantity; -export type Observation_bp_Component_DiastolicBPSliceInput = Omit & Quantity; - -import { ensureProfile, applySliceMatch, matchesValue, setArraySlice, getArraySlice, ensureSliceDefaults, stripMatchKeys, wrapSliceChoice, unwrapSliceChoice, validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type observation_bpProfileParams = { +export type Observation_bp_Category_VSCatSliceFlat = Omit; +export type Observation_bp_Component_SystolicBPSliceFlat = Omit & Quantity; +export type Observation_bp_Component_DiastolicBPSliceFlat = Omit & Quantity; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + wrapSliceChoice, + unwrapSliceChoice, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type observation_bpProfileRaw = { status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); subject: Reference<"Patient">; category?: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]; @@ -29,35 +47,45 @@ export type observation_bpProfileParams = { // CanonicalURL: http://hl7.org/fhir/StructureDefinition/bp (pkg: hl7.fhir.r4.core#4.0.1) export class observation_bpProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/bp" + 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 + private resource: Observation; constructor (resource: Observation) { - this.resource = resource - ensureProfile(resource, "http://hl7.org/fhir/StructureDefinition/bp") + this.resource = resource; } static from (resource: Observation) : observation_bpProfile { - return new observation_bpProfile(resource) + if (!resource.meta?.profile?.includes(observation_bpProfile.canonicalUrl)) { + throw new Error(`observation_bpProfile: meta.profile must include ${observation_bpProfile.canonicalUrl}`) + } + const profile = new observation_bpProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Observation) : observation_bpProfile { + ensureProfile(resource, observation_bpProfile.canonicalUrl); + return new observation_bpProfile(resource); } - static createResource (args: observation_bpProfileParams) : Observation { + static createResource (args: observation_bpProfileRaw) : Observation { const categoryWithDefaults = ensureSliceDefaults( [...(args.category ?? [])], observation_bpProfile.VSCatSliceMatch, - ) + ); const componentWithDefaults = ensureSliceDefaults( [...(args.component ?? [])], observation_bpProfile.SystolicBPSliceMatch, observation_bpProfile.DiastolicBPSliceMatch, - ) + ); - const resource = { + const resource = buildResource( { resourceType: "Observation", code: {"coding":[{"code":"85354-9","system":"http://loinc.org"}]}, category: categoryWithDefaults, @@ -65,180 +93,188 @@ export class observation_bpProfile { status: args.status, subject: args.subject, meta: { profile: [observation_bpProfile.canonicalUrl] }, - } as unknown as Observation - return resource + }) + return resource; } - static create (args: observation_bpProfileParams) : observation_bpProfile { - return observation_bpProfile.from(observation_bpProfile.createResource(args)) + static create (args: observation_bpProfileRaw) : observation_bpProfile { + return observation_bpProfile.apply(observation_bpProfile.createResource(args)); } toResource () : Observation { - return this.resource + return this.resource; } // Field accessors - getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { - return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; } setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { - Object.assign(this.resource, { status: value }) - return this + Object.assign(this.resource, { status: value }); + return this; } getSubject () : Reference<"Patient"> | undefined { - return this.resource.subject as Reference<"Patient"> | undefined + return this.resource.subject as Reference<"Patient"> | undefined; } setSubject (value: Reference<"Patient">) : this { - Object.assign(this.resource, { subject: value }) - return this + Object.assign(this.resource, { subject: value }); + return this; } getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { - return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; } setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { - Object.assign(this.resource, { category: value }) - return this + Object.assign(this.resource, { category: value }); + return this; } getCode () : 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 { - 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 + 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 + Object.assign(this.resource, { code: value }); + return this; } getComponent () : ObservationComponent[] | undefined { - return this.resource.component as ObservationComponent[] | undefined + return this.resource.component as ObservationComponent[] | undefined; } setComponent (value: ObservationComponent[]) : this { - Object.assign(this.resource, { component: value }) - return this + Object.assign(this.resource, { component: value }); + return this; } getEffectiveDateTime () : string | undefined { - return this.resource.effectiveDateTime as string | undefined + return this.resource.effectiveDateTime as string | undefined; } setEffectiveDateTime (value: string) : this { - Object.assign(this.resource, { effectiveDateTime: value }) - return this + Object.assign(this.resource, { effectiveDateTime: value }); + return this; } getEffectivePeriod () : Period | undefined { - return this.resource.effectivePeriod as Period | undefined + return this.resource.effectivePeriod as Period | undefined; } setEffectivePeriod (value: Period) : this { - Object.assign(this.resource, { effectivePeriod: value }) - return this + Object.assign(this.resource, { effectivePeriod: value }); + return this; } getValueQuantity () : Quantity | undefined { - return this.resource.valueQuantity as Quantity | undefined + return this.resource.valueQuantity as Quantity | undefined; } setValueQuantity (value: Quantity) : this { - Object.assign(this.resource, { valueQuantity: value }) - return this - } - - toProfile () : observation_bp { - return this.resource as observation_bp + Object.assign(this.resource, { valueQuantity: value }); + return this; } - // Slices and extensions - - public setVSCat (input?: Observation_bp_Category_VSCatSliceInput): this { + // Extensions + // Slices + public setVSCat (input?: Observation_bp_Category_VSCatSliceFlat | CodeableConcept): this { const match = observation_bpProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.category ??= [], match, value) return this } - public setSystolicBP (input?: Observation_bp_Component_SystolicBPSliceInput): this { + public setSystolicBP (input?: Observation_bp_Component_SystolicBPSliceFlat | ObservationComponent): this { const match = observation_bpProfile.SystolicBPSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) + return this + } const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") const value = applySliceMatch(wrapped, match) setArraySlice(this.resource.component ??= [], match, value) return this } - public setDiastolicBP (input?: Observation_bp_Component_DiastolicBPSliceInput): this { + public setDiastolicBP (input?: Observation_bp_Component_DiastolicBPSliceFlat | ObservationComponent): this { const match = observation_bpProfile.DiastolicBPSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) + return this + } const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") const value = applySliceMatch(wrapped, match) setArraySlice(this.resource.component ??= [], match, value) return this } - public getVSCat (): Observation_bp_Category_VSCatSliceInput | undefined { + public getVSCat(mode: 'flat'): Observation_bp_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): Observation_bp_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): Observation_bp_Category_VSCatSliceFlat | CodeableConcept | undefined { const match = observation_bpProfile.VSCatSliceMatch const item = getArraySlice(this.resource.category, match) if (!item) return undefined - return stripMatchKeys(item, ["coding"]) + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) } - public getVSCatRaw (): CodeableConcept | undefined { - const match = observation_bpProfile.VSCatSliceMatch - const item = getArraySlice(this.resource.category, match) - return item - } - - public getSystolicBP (): Observation_bp_Component_SystolicBPSliceInput | undefined { + public getSystolicBP(mode: 'flat'): Observation_bp_Component_SystolicBPSliceFlat | undefined; + public getSystolicBP(mode: 'raw'): ObservationComponent | undefined; + public getSystolicBP(): Observation_bp_Component_SystolicBPSliceFlat | undefined; + public getSystolicBP (mode: 'flat' | 'raw' = 'flat'): Observation_bp_Component_SystolicBPSliceFlat | ObservationComponent | undefined { const match = observation_bpProfile.SystolicBPSliceMatch const item = getArraySlice(this.resource.component, match) if (!item) return undefined - return unwrapSliceChoice(item, ["code"], "valueQuantity") - } - - public getSystolicBPRaw (): ObservationComponent | undefined { - const match = observation_bpProfile.SystolicBPSliceMatch - const item = getArraySlice(this.resource.component, match) - return item + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["code"], "valueQuantity") } - public getDiastolicBP (): Observation_bp_Component_DiastolicBPSliceInput | undefined { + public getDiastolicBP(mode: 'flat'): Observation_bp_Component_DiastolicBPSliceFlat | undefined; + public getDiastolicBP(mode: 'raw'): ObservationComponent | undefined; + public getDiastolicBP(): Observation_bp_Component_DiastolicBPSliceFlat | undefined; + public getDiastolicBP (mode: 'flat' | 'raw' = 'flat'): Observation_bp_Component_DiastolicBPSliceFlat | ObservationComponent | undefined { const match = observation_bpProfile.DiastolicBPSliceMatch const item = getArraySlice(this.resource.component, match) if (!item) return undefined - return unwrapSliceChoice(item, ["code"], "valueQuantity") - } - - public getDiastolicBPRaw (): ObservationComponent | undefined { - const match = observation_bpProfile.DiastolicBPSliceMatch - const item = getArraySlice(this.resource.component, match) - return item + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["code"], "valueQuantity") } // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "observation-bp" - const res = this.resource as unknown as Record - return [ - ...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), - ...validateRequired(res, profileName, "code"), - ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"85354-9","system":"http://loinc.org"}]}), - ...validateRequired(res, profileName, "subject"), - ...validateReference(res, profileName, "subject", ["Patient"]), - ...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), - ] + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"85354-9","system":"http://loinc.org"}]}), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...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), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ], + } } } 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 8f87ef5a4..95164109a 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 @@ -12,11 +12,28 @@ export interface observation_vitalsigns extends Observation { subject: Reference<"Patient">; } -export type Observation_vitalsigns_Category_VSCatSliceInput = Omit; - -import { ensureProfile, applySliceMatch, matchesValue, setArraySlice, getArraySlice, ensureSliceDefaults, stripMatchKeys, validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type observation_vitalsignsProfileParams = { +export type Observation_vitalsigns_Category_VSCatSliceFlat = Omit; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type observation_vitalsignsProfileRaw = { status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); code: 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)>; subject: Reference<"Patient">; @@ -25,145 +42,159 @@ export type observation_vitalsignsProfileParams = { // CanonicalURL: http://hl7.org/fhir/StructureDefinition/vitalsigns (pkg: hl7.fhir.r4.core#4.0.1) export class observation_vitalsignsProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/vitalsigns" + 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 + private resource: Observation; constructor (resource: Observation) { - this.resource = resource - ensureProfile(resource, "http://hl7.org/fhir/StructureDefinition/vitalsigns") + this.resource = resource; } static from (resource: Observation) : observation_vitalsignsProfile { - return new observation_vitalsignsProfile(resource) + if (!resource.meta?.profile?.includes(observation_vitalsignsProfile.canonicalUrl)) { + throw new Error(`observation_vitalsignsProfile: meta.profile must include ${observation_vitalsignsProfile.canonicalUrl}`) + } + const profile = new observation_vitalsignsProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; } - static createResource (args: observation_vitalsignsProfileParams) : Observation { + static apply (resource: Observation) : observation_vitalsignsProfile { + ensureProfile(resource, observation_vitalsignsProfile.canonicalUrl); + return new observation_vitalsignsProfile(resource); + } + + static createResource (args: observation_vitalsignsProfileRaw) : Observation { const categoryWithDefaults = ensureSliceDefaults( [...(args.category ?? [])], observation_vitalsignsProfile.VSCatSliceMatch, - ) + ); - const resource = { + const resource = buildResource( { resourceType: "Observation", category: categoryWithDefaults, status: args.status, code: args.code, subject: args.subject, meta: { profile: [observation_vitalsignsProfile.canonicalUrl] }, - } as unknown as Observation - return resource + }) + return resource; } - static create (args: observation_vitalsignsProfileParams) : observation_vitalsignsProfile { - return observation_vitalsignsProfile.from(observation_vitalsignsProfile.createResource(args)) + static create (args: observation_vitalsignsProfileRaw) : observation_vitalsignsProfile { + return observation_vitalsignsProfile.apply(observation_vitalsignsProfile.createResource(args)); } toResource () : Observation { - return this.resource + return this.resource; } // Field accessors - getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { - return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; } setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { - Object.assign(this.resource, { status: value }) - return this + Object.assign(this.resource, { status: value }); + return this; } getCode () : 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 { - 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 + 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 + Object.assign(this.resource, { code: value }); + return this; } getSubject () : Reference<"Patient"> | undefined { - return this.resource.subject as Reference<"Patient"> | undefined + return this.resource.subject as Reference<"Patient"> | undefined; } setSubject (value: Reference<"Patient">) : this { - Object.assign(this.resource, { subject: value }) - return this + Object.assign(this.resource, { subject: value }); + return this; } getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { - return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; } setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { - Object.assign(this.resource, { category: value }) - return this + Object.assign(this.resource, { category: value }); + return this; } getEffectiveDateTime () : string | undefined { - return this.resource.effectiveDateTime as string | undefined + return this.resource.effectiveDateTime as string | undefined; } setEffectiveDateTime (value: string) : this { - Object.assign(this.resource, { effectiveDateTime: value }) - return this + Object.assign(this.resource, { effectiveDateTime: value }); + return this; } getEffectivePeriod () : Period | undefined { - return this.resource.effectivePeriod as Period | undefined + return this.resource.effectivePeriod as Period | undefined; } setEffectivePeriod (value: Period) : this { - Object.assign(this.resource, { effectivePeriod: value }) - return this - } - - toProfile () : observation_vitalsigns { - return this.resource as observation_vitalsigns + Object.assign(this.resource, { effectivePeriod: value }); + return this; } - // Slices and extensions - - public setVSCat (input?: Observation_vitalsigns_Category_VSCatSliceInput): this { + // Extensions + // Slices + public setVSCat (input?: Observation_vitalsigns_Category_VSCatSliceFlat | CodeableConcept): this { const match = observation_vitalsignsProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.category ??= [], match, value) return this } - public getVSCat (): Observation_vitalsigns_Category_VSCatSliceInput | undefined { + public getVSCat(mode: 'flat'): Observation_vitalsigns_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): Observation_vitalsigns_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): Observation_vitalsigns_Category_VSCatSliceFlat | CodeableConcept | undefined { const match = observation_vitalsignsProfile.VSCatSliceMatch const item = getArraySlice(this.resource.category, match) if (!item) return undefined - return stripMatchKeys(item, ["coding"]) - } - - public getVSCatRaw (): CodeableConcept | undefined { - const match = observation_vitalsignsProfile.VSCatSliceMatch - const item = getArraySlice(this.resource.category, match) - return item + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) } // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "observation-vitalsigns" - const res = this.resource as unknown as Record - return [ - ...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), - ...validateRequired(res, profileName, "code"), - ...validateRequired(res, profileName, "subject"), - ...validateReference(res, profileName, "subject", ["Patient"]), - ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), - ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), - ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), - ] + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), + ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), + ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ], + } } } diff --git a/examples/typescript-r4/fhir-types/profile-helpers.ts b/examples/typescript-r4/fhir-types/profile-helpers.ts index 4f45db45b..9191c8ff9 100644 --- a/examples/typescript-r4/fhir-types/profile-helpers.ts +++ b/examples/typescript-r4/fhir-types/profile-helpers.ts @@ -121,6 +121,42 @@ export const matchesValue = (value: unknown, match: unknown): boolean => { return value === match; }; +/** + * Type guard that discriminates a raw extension input (with an `extension` + * array) from a flat-API input object. Using a custom type guard instead of + * a bare `"extension" in args` lets TypeScript narrow *both* branches of the + * union — the plain `in` check cannot eliminate a type whose `extension` + * property is optional. + */ +export const isRawExtensionInput = (input: object): input is TRaw => "extension" in input; + +/** + * Type guard that tests whether an unknown setter input is a raw Extension + * (i.e. an object with a `url` property). When `url` is provided, also + * checks that the extension's URL matches the expected value. + */ +export const isExtension = (input: unknown, url?: string): input is E => + typeof input === "object" && input !== null && "url" in input && (url === undefined || input.url === url); + +/** + * Read a single typed value field from an Extension, returning `undefined` + * when the extension itself is absent or the field is not set. + * + * This avoids the double-cast `(ext as Record<…>)?.field as T` that would + * otherwise be needed for value fields not declared on the base Extension type. + */ +export const getExtensionValue = (ext: { url?: string } | undefined, field: string): T | undefined => { + if (!ext) return undefined; + return (ext as Record)[field] as T | undefined; +}; + +/** + * Push an extension onto `target.extension`, creating the array if absent. + */ +export const pushExtension = (target: { extension?: E[] }, ext: E): void => { + (target.extension ??= []).push(ext); +}; + // --------------------------------------------------------------------------- // Extension helpers // --------------------------------------------------------------------------- @@ -134,10 +170,10 @@ export const matchesValue = (value: unknown, match: unknown): boolean => { * @returns A record keyed by sub-extension URL, or `undefined` if the * extension has no nested children. */ -export const extractComplexExtension = ( - extension: { extension?: Array<{ url?: string; [key: string]: unknown }> } | undefined, +export const extractComplexExtension = >( + extension: { extension?: Array<{ url?: string }> } | undefined, config: Array<{ name: string; valueField: string; isArray: boolean }>, -): Record | undefined => { +): T | undefined => { if (!extension?.extension) return undefined; const result: Record = {}; for (const { name, valueField, isArray } of config) { @@ -148,7 +184,7 @@ export const extractComplexExtension = ( result[name] = (subExts[0] as Record)[valueField]; } } - return result; + return result as T; }; // --------------------------------------------------------------------------- @@ -219,6 +255,13 @@ export const ensureSliceDefaults = (items: T[], ...matches: Record(obj: object): T => obj as unknown as T; + /** * Add `canonicalUrl` to `resource.meta.profile` if not already present. * Creates `meta` and `profile` when missing. @@ -256,25 +299,31 @@ export const getArraySlice = (list: readonly T[] | undefined, match: Record, profileName: string, field: string): string[] => { - return res[field] === undefined || res[field] === null +export const validateRequired = (res: object, profileName: string, field: string): string[] => { + const rec = res as Record; + return rec[field] === undefined || rec[field] === null ? [`${profileName}: required field '${field}' is missing`] : []; }; +/** Checks that a must-support field is populated (warning, not error). */ +export const validateMustSupport = (res: object, profileName: string, field: string): string[] => { + const rec = res as Record; + return rec[field] === undefined || rec[field] === null + ? [`${profileName}: must-support field '${field}' is not populated`] + : []; +}; + /** Checks that `field` is absent (profiles may exclude base fields). */ -export const validateExcluded = (res: Record, profileName: string, field: string): string[] => { - return res[field] !== undefined ? [`${profileName}: field '${field}' must not be present`] : []; +export const validateExcluded = (res: object, profileName: string, field: string): string[] => { + return (res as Record)[field] !== undefined + ? [`${profileName}: field '${field}' must not be present`] + : []; }; /** Checks that `field` structurally contains the expected fixed value. */ -export const validateFixedValue = ( - res: Record, - profileName: string, - field: string, - expected: unknown, -): string[] => { - return matchesValue(res[field], expected) +export const validateFixedValue = (res: object, profileName: string, field: string, expected: unknown): string[] => { + return matchesValue((res as Record)[field], expected) ? [] : [`${profileName}: field '${field}' does not match expected fixed value`]; }; @@ -284,7 +333,7 @@ export const validateFixedValue = ( * discriminator) falls within [`min`, `max`]. Pass `max = 0` for unbounded. */ export const validateSliceCardinality = ( - res: Record, + res: object, profileName: string, field: string, match: Record, @@ -292,7 +341,7 @@ export const validateSliceCardinality = ( min: number, max: number, ): string[] => { - const items = res[field] as unknown[] | undefined; + const items = (res as Record)[field] as unknown[] | undefined; const count = (items ?? []).filter((item) => matchesValue(item, match)).length; const errors: string[] = []; if (count < min) { @@ -308,12 +357,9 @@ export const validateSliceCardinality = ( * Checks that at least one of the listed choice-type variants is present. * E.g. `["effectiveDateTime", "effectivePeriod"]`. */ -export const validateChoiceRequired = ( - res: Record, - profileName: string, - choices: string[], -): string[] => { - return choices.some((c) => res[c] !== undefined) +export const validateChoiceRequired = (res: object, profileName: string, choices: string[]): string[] => { + const rec = res as Record; + return choices.some((c) => rec[c] !== undefined) ? [] : [`${profileName}: at least one of ${choices.join(", ")} is required`]; }; @@ -323,13 +369,8 @@ export const validateChoiceRequired = ( * Handles plain strings, Coding objects, and CodeableConcept objects. * Skips validation when the field is absent. */ -export const validateEnum = ( - res: Record, - profileName: string, - field: string, - allowed: string[], -): string[] => { - const value = res[field]; +export const validateEnum = (res: object, profileName: string, field: string, allowed: string[]): string[] => { + const value = (res as Record)[field]; if (value === undefined || value === null) return []; if (typeof value === "string") { return allowed.includes(value) @@ -357,13 +398,8 @@ export const validateEnum = ( * types. Extracts the type from the `reference` string (the part before * the first `/`). Skips validation when the field or reference is absent. */ -export const validateReference = ( - res: Record, - profileName: string, - field: string, - allowed: string[], -): string[] => { - const value = res[field]; +export const validateReference = (res: object, profileName: string, field: string, allowed: string[]): string[] => { + const value = (res as Record)[field]; if (value === undefined || value === null) return []; const ref = (value as Record).reference as string | undefined; if (!ref) return []; diff --git a/examples/typescript-r4/profile-bodyweight.test.ts b/examples/typescript-r4/profile-bodyweight.test.ts index 15c95eb20..6ec8ab5c4 100644 --- a/examples/typescript-r4/profile-bodyweight.test.ts +++ b/examples/typescript-r4/profile-bodyweight.test.ts @@ -37,9 +37,9 @@ describe("bodyweight profile creation", () => { expect(fromCreateResource.subject!.reference).toBe("Patient/pt-1"); }); - test("from() wraps an existing Observation", () => { + test("apply() wraps an existing Observation", () => { const obs: Observation = { resourceType: "Observation", code: {}, status: "preliminary" }; - const profile = bodyweightProfile.from(obs); + const profile = bodyweightProfile.apply(obs); profile .setStatus("final") @@ -119,8 +119,8 @@ describe("bodyweight profile slice accessors", () => { test("getVSCat returns empty simplified view from auto-populated stub", () => { // category is auto-populated with VSCat discriminator match - expect(profile.getVSCatRaw()).toBeDefined(); - const raw = profile.getVSCatRaw()!; + 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", @@ -132,7 +132,7 @@ describe("bodyweight profile slice accessors", () => { test("setVSCat adds category with discriminator values", () => { profile.setVSCat({ text: "Vital Signs" }); - const raw = profile.getVSCatRaw()!; + const raw = profile.getVSCat("raw")!; expect(raw.text).toBe("Vital Signs"); expect(raw.coding as unknown).toEqual({ code: "vital-signs", @@ -147,7 +147,7 @@ describe("bodyweight profile slice accessors", () => { }); test("getVSCatRaw returns full element including discriminator", () => { - const raw = profile.getVSCatRaw()!; + const raw = profile.getVSCat("raw")!; expect(raw.text).toBe("Vital Signs"); expect(raw.coding).toBeDefined(); }); @@ -201,7 +201,7 @@ describe("bodyweight profile choice type accessors", () => { status: "final", subject: { reference: "Patient/pt-1" }, }); - const p = bodyweightProfile.from(obs); + const p = bodyweightProfile.apply(obs); p.setValueQuantity({ value: 90, unit: "kg" }); expect((obs as any).valueQuantity.value).toBe(90); @@ -223,7 +223,7 @@ describe("bodyweight profile mutability", () => { status: "final", subject: { reference: "Patient/pt-1" }, }); - const profile = bodyweightProfile.from(obs); + const profile = bodyweightProfile.apply(obs); profile.setStatus("amended"); expect(obs.status).toBe("amended"); diff --git a/examples/typescript-r4/profile-bp.test.ts b/examples/typescript-r4/profile-bp.test.ts index ad42f32c9..299564fd2 100644 --- a/examples/typescript-r4/profile-bp.test.ts +++ b/examples/typescript-r4/profile-bp.test.ts @@ -30,7 +30,7 @@ describe("blood pressure profile", () => { }); test("freshly created profile is not yet valid (missing effective)", () => { - const errors = profile.validate(); + const { errors } = profile.validate(); expect(errors).toEqual(["observation-bp: at least one of effectiveDateTime, effectivePeriod is required"]); }); @@ -39,8 +39,8 @@ describe("blood pressure profile", () => { const obs = fresh.toResource(); expect(obs.component).toHaveLength(2); // stubs contain only discriminator match values - expect(fresh.getSystolicBPRaw()).toBeDefined(); - expect(fresh.getDiastolicBPRaw()).toBeDefined(); + expect(fresh.getSystolicBP("raw")).toBeDefined(); + expect(fresh.getDiastolicBP("raw")).toBeDefined(); }); test("setSystolicBP / getSystolicBP / getSystolicBPRaw", () => { @@ -53,7 +53,7 @@ describe("blood pressure profile", () => { code: "mm[Hg]", }); - expect(profile.getSystolicBPRaw() as unknown).toEqual({ + 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" } }, }); @@ -69,7 +69,7 @@ describe("blood pressure profile", () => { code: "mm[Hg]", }); - expect(profile.getDiastolicBPRaw() as unknown).toEqual({ + 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" } }, }); @@ -80,25 +80,25 @@ describe("blood pressure profile", () => { // auto-populated stubs + set values = still 2 items (set replaces stubs) expect(obs.component).toHaveLength(2); - const systolicCode = profile.getSystolicBPRaw()!.code as Record; - const diastolicCode = profile.getDiastolicBPRaw()!.code as Record; + 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.getSystolicBPRaw()!.valueQuantity!.value).toBe(120); - expect(profile.getDiastolicBPRaw()!.valueQuantity!.value).toBe(80); + expect(profile.getSystolicBP("raw")!.valueQuantity!.value).toBe(120); + expect(profile.getDiastolicBP("raw")!.valueQuantity!.value).toBe(80); }); test("setSystolicBP replaces an existing systolic component", () => { profile.setSystolicBP({ value: 130, unit: "mmHg" }); expect(profile.toResource().component).toHaveLength(2); - expect(profile.getSystolicBPRaw()!.valueQuantity!.value).toBe(130); + expect(profile.getSystolicBP("raw")!.valueQuantity!.value).toBe(130); }); test("setVSCat adds category with discriminator values", () => { profile.setVSCat({ text: "Vital Signs" }); - const raw = profile.getVSCatRaw()!; + const raw = profile.getVSCat("raw")!; expect(raw.text).toBe("Vital Signs"); expect(raw.coding as unknown).toEqual({ code: "vital-signs", @@ -126,15 +126,15 @@ describe("blood pressure profile", () => { expect(profile.getVSCat()!.text).toBe("Vital Signs"); expect(profile.getEffectiveDateTime()).toBe("2024-06-15"); expect(profile.getSubject()!.reference).toBe("Patient/pt-2"); - expect(profile.getSystolicBPRaw()!.valueQuantity!.value).toBe(120); - expect(profile.getDiastolicBPRaw()!.valueQuantity!.value).toBe(80); + expect(profile.getSystolicBP("raw")!.valueQuantity!.value).toBe(120); + expect(profile.getDiastolicBP("raw")!.valueQuantity!.value).toBe(80); }); test("setSystolicBP with no args inserts discriminator-only component", () => { const fresh = createBp(); fresh.setSystolicBP(); - const raw = fresh.getSystolicBPRaw()!; + 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(); diff --git a/examples/typescript-us-core/.gitignore b/examples/typescript-us-core/.gitignore new file mode 100644 index 000000000..f3c53d9af --- /dev/null +++ b/examples/typescript-us-core/.gitignore @@ -0,0 +1 @@ +fhir-types/ts/ diff --git a/examples/typescript-us-core/README.md b/examples/typescript-us-core/README.md index 13de17e44..db8f257a2 100644 --- a/examples/typescript-us-core/README.md +++ b/examples/typescript-us-core/README.md @@ -2,202 +2,54 @@ US Core FHIR profile generation with type-safe profile wrapper classes. -## Overview - -This example demonstrates how to generate TypeScript types for the HL7 US Core Implementation Guide, including generated profile classes that provide a fluent API for working with extensions and slices. It includes: - -- US Core 8.0.1 type definitions (tree-shaken to Patient, Blood Pressure, and Body Weight) -- Profile wrapper classes for type-safe extension handling -- Automatic discriminator application for slices -- Flat API for complex extensions (race, ethnicity, etc.) - ## Generating Types -To generate TypeScript types for US Core: - ```bash bun run examples/typescript-us-core/generate.ts ``` -This will output to `./examples/typescript-us-core/fhir-types/` - -## Configuration - -Edit `generate.ts` to customize: - -```typescript -.typeSchema({ - treeShake: { - "hl7.fhir.us.core": { - "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient": {}, - "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure": {}, - "http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight": {}, - }, - }, -}) -.typescript({ - withDebugComment: false, // Include generation metadata - generateProfile: true, // Generate profile wrapper classes - openResourceTypeSet: false // Closed resource type union -}) -``` - -Tree shaking keeps only the specified profiles and their transitive dependencies, significantly reducing the generated output. - -## Using Profile Classes - -### US Core Patient with Extensions - -Profile classes provide a fluent API for setting extensions without dealing with the complex FHIR extension structure: - -```typescript -import type { Patient } from './fhir-types/hl7-fhir-r4-core/Patient'; -import { USCorePatientProfileProfile } from './fhir-types/hl7-fhir-us-core/profiles/UscorePatientProfile'; - -// Create a new patient profile builder -const patientProfile = new USCorePatientProfileProfile({ resourceType: 'Patient' }); - -// Use flat API - no nested extension arrays needed! -patientProfile - .setRace({ - ombCategory: { - system: 'urn:oid:2.16.840.1.113883.6.238', - code: '2106-3', - display: 'White' - }, - text: 'White' - }) - .setEthnicity({ - ombCategory: { - system: 'urn:oid:2.16.840.1.113883.6.238', - code: '2186-5', - display: 'Not Hispanic or Latino' - }, - text: 'Not Hispanic or Latino' - }) - .setSex({ - system: 'http://hl7.org/fhir/us/core/CodeSystem/birthsex', - code: 'M', - display: 'Male' - }); - -// Get the underlying FHIR resource -const patient = patientProfile.toResource(); -patient.name = [{ family: 'Smith', given: ['John'] }]; -``` - -### US Core Blood Pressure with Slices - -Profile classes automatically apply discriminator values for slices: - -```typescript -import type { Observation } from './fhir-types/hl7-fhir-r4-core/Observation'; -import { USCoreBloodPressureProfileProfile } from './fhir-types/hl7-fhir-us-core/profiles/UscoreBloodPressureProfile'; - -const bpProfile = new USCoreBloodPressureProfileProfile({ - resourceType: 'Observation' -} as Observation); - -// Discriminator codes (8480-6 for systolic, 8462-4 for diastolic) are auto-applied -bpProfile - .setVscat() - .setSystolic({ - valueQuantity: { - value: 120, - unit: 'mmHg', - system: 'http://unitsofmeasure.org', - code: 'mm[Hg]' - } - }) - .setDiastolic({ - valueQuantity: { - value: 80, - unit: 'mmHg', - system: 'http://unitsofmeasure.org', - code: 'mm[Hg]' - } - }); - -const observation = bpProfile.toResource(); -observation.status = 'final'; -observation.code = { - coding: [{ - system: 'http://loinc.org', - code: '85354-9', - display: 'Blood pressure panel' - }] -}; -``` - -### Wrapping Existing Resources - -You can wrap existing FHIR resources to add profile-specific extensions: +Edit [`generate.ts`](generate.ts) to customize which profiles to include. Tree shaking keeps only the specified profiles and their transitive dependencies. Output goes to `./fhir-types/`. -```typescript -// Existing patient from API -const existingPatient: Patient = { - resourceType: 'Patient', - id: 'existing-patient', - name: [{ family: 'Doe', given: ['Jane'] }], - gender: 'female' -}; +## Profile Class API -// Wrap with profile class -const patientProfile = new USCorePatientProfileProfile(existingPatient); +Each profile class provides: -// Add US Core extensions -patientProfile.setSex({ - system: 'http://hl7.org/fhir/us/core/CodeSystem/birthsex', - code: 'F', - display: 'Female' -}); +- **`from(resource)`** -- validate the resource conforms to the profile (meta.profile + required fields), throw on errors +- **`apply(resource)`** -- attach meta.profile without validation, useful for incremental construction +- **`create(args)`** -- build a new resource from typed input, auto-sets fixed values +- **`validate()`** -- check required fields, return error list +- **`toResource()`** -- get the underlying FHIR resource -const updatedPatient = patientProfile.toResource(); -``` +Generated accessors depend on what the profile defines: -## Running the Demo +- **Fields** -- `getStatus()` / `setStatus(value)` for profile-constrained fields with narrowed types +- **Choice types** -- `getEffectiveDateTime()` / `setEffectiveDateTime(value)`, `getEffectivePeriod()` / `setEffectivePeriod(value)` etc. +- **Fixed values** -- auto-set by `create()` (e.g. `code` on body weight is always LOINC 29463-7) +- **Slices** -- `setSystolic(value)` / `getSystolic()` for component slices; discriminator values auto-applied; `getSystolic('raw')` returns the full element +- **Extensions** -- `setRace(value)` accepts flat input, profile instance, or raw FHIR Extension; `getRace()` / `getRace("profile")` / `getRace("raw")` for three return modes -After generating types, run the included demos: +## Tests ```bash -# Full profile demo with multiple examples -bun run examples/typescript-us-core/profile-demo.ts - -# Multi-profile demo -bun run examples/typescript-us-core/multi-profile-demo.ts +cd examples/typescript-us-core && bun test ``` +- [profile-patient.test.ts](profile-patient.test.ts) -- Patient profile with extensions (race, ethnicity, sex) +- [profile-bp.test.ts](profile-bp.test.ts) -- Blood Pressure with component slices +- [profile-bodyweight.test.ts](profile-bodyweight.test.ts) -- Body Weight with choice types, slice getter modes + ## File Structure ``` typescript-us-core/ -├── README.md # This file ├── generate.ts # Type generation script -├── profile-demo.ts # Profile class usage demo -├── multi-profile-demo.ts # Multi-profile examples -├── multi-profile.test.ts # Profile tests -├── tsconfig.json # TypeScript config -├── fhir-types/ # Generated types (after generation) -│ ├── hl7-fhir-r4-core/ # FHIR R4 core types (tree-shaken dependencies) -│ ├── hl7-fhir-us-core/ # US Core profiles -│ │ └── profiles/ # Profile wrapper classes (Patient, BP, BodyWeight) -│ └── profile-helpers.ts # Runtime helpers for profile classes -└── type-tree.yaml # Dependency tree (debug output) +├── profile-patient.test.ts # Patient profile tests +├── profile-bp.test.ts # Blood pressure tests +├── profile-bodyweight.test.ts # Body weight tests +├── fhir-types/ # Generated output +│ ├── hl7-fhir-r4-core/ # FHIR R4 base types +│ ├── hl7-fhir-us-core/ # US Core types +│ │ └── profiles/ # Profile wrapper classes +│ └── profile-helpers.ts # Runtime helpers +└── type-tree.yaml # Dependency tree (debug) ``` - -## Profile Class API - -Each profile class provides: - -- **Constructor**: `new ProfileClass(resource)` - Wrap a FHIR resource -- **Setters**: `setExtensionName(value)` - Set extension with flat API -- **Getters**: `getExtensionName()` - Get extension value (flat) -- **Raw Getters**: `getExtensionNameExtension()` - Get raw FHIR Extension -- **Reset**: `resetExtensionName()` - Remove extension -- **toResource()**: Get the underlying FHIR resource - -## Next Steps - -- See [typescript-r4/](../typescript-r4/) for basic FHIR R4 generation -- See [examples/](../) overview for other language examples -- Check [../../docs/guides/writer-generator.md](../../docs/guides/writer-generator.md) for generator documentation 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 8f87ef5a4..95164109a 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 @@ -12,11 +12,28 @@ export interface observation_vitalsigns extends Observation { subject: Reference<"Patient">; } -export type Observation_vitalsigns_Category_VSCatSliceInput = Omit; - -import { ensureProfile, applySliceMatch, matchesValue, setArraySlice, getArraySlice, ensureSliceDefaults, stripMatchKeys, validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type observation_vitalsignsProfileParams = { +export type Observation_vitalsigns_Category_VSCatSliceFlat = Omit; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type observation_vitalsignsProfileRaw = { status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); code: 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)>; subject: Reference<"Patient">; @@ -25,145 +42,159 @@ export type observation_vitalsignsProfileParams = { // CanonicalURL: http://hl7.org/fhir/StructureDefinition/vitalsigns (pkg: hl7.fhir.r4.core#4.0.1) export class observation_vitalsignsProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/vitalsigns" + 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 + private resource: Observation; constructor (resource: Observation) { - this.resource = resource - ensureProfile(resource, "http://hl7.org/fhir/StructureDefinition/vitalsigns") + this.resource = resource; } static from (resource: Observation) : observation_vitalsignsProfile { - return new observation_vitalsignsProfile(resource) + if (!resource.meta?.profile?.includes(observation_vitalsignsProfile.canonicalUrl)) { + throw new Error(`observation_vitalsignsProfile: meta.profile must include ${observation_vitalsignsProfile.canonicalUrl}`) + } + const profile = new observation_vitalsignsProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; } - static createResource (args: observation_vitalsignsProfileParams) : Observation { + static apply (resource: Observation) : observation_vitalsignsProfile { + ensureProfile(resource, observation_vitalsignsProfile.canonicalUrl); + return new observation_vitalsignsProfile(resource); + } + + static createResource (args: observation_vitalsignsProfileRaw) : Observation { const categoryWithDefaults = ensureSliceDefaults( [...(args.category ?? [])], observation_vitalsignsProfile.VSCatSliceMatch, - ) + ); - const resource = { + const resource = buildResource( { resourceType: "Observation", category: categoryWithDefaults, status: args.status, code: args.code, subject: args.subject, meta: { profile: [observation_vitalsignsProfile.canonicalUrl] }, - } as unknown as Observation - return resource + }) + return resource; } - static create (args: observation_vitalsignsProfileParams) : observation_vitalsignsProfile { - return observation_vitalsignsProfile.from(observation_vitalsignsProfile.createResource(args)) + static create (args: observation_vitalsignsProfileRaw) : observation_vitalsignsProfile { + return observation_vitalsignsProfile.apply(observation_vitalsignsProfile.createResource(args)); } toResource () : Observation { - return this.resource + return this.resource; } // Field accessors - getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { - return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; } setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { - Object.assign(this.resource, { status: value }) - return this + Object.assign(this.resource, { status: value }); + return this; } getCode () : 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 { - 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 + 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 + Object.assign(this.resource, { code: value }); + return this; } getSubject () : Reference<"Patient"> | undefined { - return this.resource.subject as Reference<"Patient"> | undefined + return this.resource.subject as Reference<"Patient"> | undefined; } setSubject (value: Reference<"Patient">) : this { - Object.assign(this.resource, { subject: value }) - return this + Object.assign(this.resource, { subject: value }); + return this; } getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { - return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; } setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { - Object.assign(this.resource, { category: value }) - return this + Object.assign(this.resource, { category: value }); + return this; } getEffectiveDateTime () : string | undefined { - return this.resource.effectiveDateTime as string | undefined + return this.resource.effectiveDateTime as string | undefined; } setEffectiveDateTime (value: string) : this { - Object.assign(this.resource, { effectiveDateTime: value }) - return this + Object.assign(this.resource, { effectiveDateTime: value }); + return this; } getEffectivePeriod () : Period | undefined { - return this.resource.effectivePeriod as Period | undefined + return this.resource.effectivePeriod as Period | undefined; } setEffectivePeriod (value: Period) : this { - Object.assign(this.resource, { effectivePeriod: value }) - return this - } - - toProfile () : observation_vitalsigns { - return this.resource as observation_vitalsigns + Object.assign(this.resource, { effectivePeriod: value }); + return this; } - // Slices and extensions - - public setVSCat (input?: Observation_vitalsigns_Category_VSCatSliceInput): this { + // Extensions + // Slices + public setVSCat (input?: Observation_vitalsigns_Category_VSCatSliceFlat | CodeableConcept): this { const match = observation_vitalsignsProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.category ??= [], match, value) return this } - public getVSCat (): Observation_vitalsigns_Category_VSCatSliceInput | undefined { + public getVSCat(mode: 'flat'): Observation_vitalsigns_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): Observation_vitalsigns_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): Observation_vitalsigns_Category_VSCatSliceFlat | CodeableConcept | undefined { const match = observation_vitalsignsProfile.VSCatSliceMatch const item = getArraySlice(this.resource.category, match) if (!item) return undefined - return stripMatchKeys(item, ["coding"]) - } - - public getVSCatRaw (): CodeableConcept | undefined { - const match = observation_vitalsignsProfile.VSCatSliceMatch - const item = getArraySlice(this.resource.category, match) - return item + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) } // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "observation-vitalsigns" - const res = this.resource as unknown as Record - return [ - ...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), - ...validateRequired(res, profileName, "code"), - ...validateRequired(res, profileName, "subject"), - ...validateReference(res, profileName, "subject", ["Patient"]), - ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), - ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), - ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), - ] + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), + ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), + ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ], + } } } 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 new file mode 100644 index 000000000..60774bc2b --- /dev/null +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreEthnicityExtension.ts @@ -0,0 +1,261 @@ +// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { Coding } from "../../hl7-fhir-r4-core/Coding"; +import type { Extension } from "../../hl7-fhir-r4-core/Extension"; + +export type USCoreEthnicityExtension_Extension_OmbCategorySliceFlat = Omit & Coding; +export type USCoreEthnicityExtension_Extension_DetailedSliceFlat = Omit & Coding; +export type USCoreEthnicityExtension_Extension_TextSliceFlat = Omit; + +import { + buildResource, + isRawExtensionInput, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + wrapSliceChoice, + unwrapSliceChoice, + isExtension, + getExtensionValue, + pushExtension, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreEthnicityExtensionProfileRaw = { + extension?: Extension[]; +} + +export type USCoreEthnicityExtensionProfileFlat = { + ombCategory?: Coding; + detailed?: Coding[]; + text: string; +} + +// CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity (pkg: hl7.fhir.us.core#8.0.1) +export class USCoreEthnicityExtensionProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"; + + private static readonly ombCategorySliceMatch: Record = {"url":"ombCategory"}; + private static readonly detailedSliceMatch: Record = {"url":"detailed"}; + private static readonly textSliceMatch: Record = {"url":"text"}; + + private resource: Extension; + + constructor (resource: Extension) { + this.resource = resource; + } + + static from (resource: Extension) : USCoreEthnicityExtensionProfile { + const profile = new USCoreEthnicityExtensionProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Extension) : USCoreEthnicityExtensionProfile { + return new USCoreEthnicityExtensionProfile(resource); + } + + private static resolveInput (args: USCoreEthnicityExtensionProfileRaw | USCoreEthnicityExtensionProfileFlat) : Extension[] { + if (isRawExtensionInput(args)) { + return args.extension ?? []; + } else { + const result: Extension[] = []; + if (args.ombCategory !== undefined) { + result.push({ url: "ombCategory", valueCoding: args.ombCategory } as Extension); + } + if (args.detailed) { + for (const item of args.detailed) { + result.push({ url: "detailed", valueCoding: item } as Extension); + } + } + if (args.text !== undefined) { + result.push({ url: "text", valueString: args.text } as Extension); + } + return result; + } + } + + 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, + }) + return resource; + } + + static create (args: USCoreEthnicityExtensionProfileRaw | USCoreEthnicityExtensionProfileFlat) : USCoreEthnicityExtensionProfile { + return USCoreEthnicityExtensionProfile.apply(USCoreEthnicityExtensionProfile.createResource(args)); + } + + toResource () : Extension { + return this.resource; + } + + // Field accessors + getExtension () : Extension[] | undefined { + return this.resource.extension as Extension[] | undefined; + } + + setExtension (value: Extension[]) : this { + Object.assign(this.resource, { extension: value }); + return this; + } + + getUrl () : string | undefined { + 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) + } else { + pushExtension(this.resource, { url: "ombCategory", ...value } as Extension) + } + return this + } + + public getOmbCategory (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "ombCategory") + } + + public setDetailed (value: Omit | Extension): this { + if (isExtension(value)) { + if (value.url !== "detailed") throw new Error(`Expected extension url 'detailed', got '${value.url}'`) + pushExtension(this.resource, value) + } else { + pushExtension(this.resource, { url: "detailed", ...value } as Extension) + } + return this + } + + public getDetailed (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "detailed") + } + + 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) + } else { + pushExtension(this.resource, { url: "text", ...value } as Extension) + } + return this + } + + public getText (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "text") + } + + // Slices + public setExtensionOmbCategory (input?: USCoreEthnicityExtension_Extension_OmbCategorySliceFlat | Extension): this { + const match = USCoreEthnicityExtensionProfile.ombCategorySliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const wrapped = wrapSliceChoice(input ?? {}, "valueCoding") + const value = applySliceMatch(wrapped, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public setExtensionDetailed (input?: USCoreEthnicityExtension_Extension_DetailedSliceFlat | Extension): this { + const match = USCoreEthnicityExtensionProfile.detailedSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const wrapped = wrapSliceChoice(input ?? {}, "valueCoding") + const value = applySliceMatch(wrapped, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public setExtensionText (input?: USCoreEthnicityExtension_Extension_TextSliceFlat | Extension): this { + const match = USCoreEthnicityExtensionProfile.textSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const value = applySliceMatch(input ?? {}, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public getExtensionOmbCategory(mode: 'flat'): USCoreEthnicityExtension_Extension_OmbCategorySliceFlat | undefined; + public getExtensionOmbCategory(mode: 'raw'): Extension | undefined; + public getExtensionOmbCategory(): USCoreEthnicityExtension_Extension_OmbCategorySliceFlat | undefined; + public getExtensionOmbCategory (mode: 'flat' | 'raw' = 'flat'): USCoreEthnicityExtension_Extension_OmbCategorySliceFlat | Extension | undefined { + const match = USCoreEthnicityExtensionProfile.ombCategorySliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["url"], "valueCoding") + } + + public getExtensionDetailed(mode: 'flat'): USCoreEthnicityExtension_Extension_DetailedSliceFlat | undefined; + public getExtensionDetailed(mode: 'raw'): Extension | undefined; + public getExtensionDetailed(): USCoreEthnicityExtension_Extension_DetailedSliceFlat | undefined; + public getExtensionDetailed (mode: 'flat' | 'raw' = 'flat'): USCoreEthnicityExtension_Extension_DetailedSliceFlat | Extension | undefined { + const match = USCoreEthnicityExtensionProfile.detailedSliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["url"], "valueCoding") + } + + public getExtensionText(mode: 'flat'): USCoreEthnicityExtension_Extension_TextSliceFlat | undefined; + public getExtensionText(mode: 'raw'): Extension | undefined; + public getExtensionText(): USCoreEthnicityExtension_Extension_TextSliceFlat | undefined; + public getExtensionText (mode: 'flat' | 'raw' = 'flat'): USCoreEthnicityExtension_Extension_TextSliceFlat | Extension | undefined { + const match = USCoreEthnicityExtensionProfile.textSliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return stripMatchKeys(item, ["url"]) + } + + // Validation + validate(): { errors: string[]; warnings: string[] } { + const profileName = "USCoreEthnicityExtension" + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "extension"), + ...validateSliceCardinality(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", 0, 1), + ...validateSliceCardinality(res, profileName, "extension", {"url":"text"}, "text", 1, 1), + ...validateRequired(res, profileName, "url"), + ...validateFixedValue(res, profileName, "url", USCoreEthnicityExtensionProfile.canonicalUrl), + ], + warnings: [], + } + } + +} + 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 new file mode 100644 index 000000000..4bf1c2a70 --- /dev/null +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreIndividualSexExtension.ts @@ -0,0 +1,97 @@ +// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { Coding } from "../../hl7-fhir-r4-core/Coding"; +import type { Extension } from "../../hl7-fhir-r4-core/Extension"; + +import { + buildResource, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreIndividualSexExtensionProfileRaw = { + valueCoding: Coding; +} + +// CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex (pkg: hl7.fhir.us.core#8.0.1) +export class USCoreIndividualSexExtensionProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex"; + + private resource: Extension; + + constructor (resource: Extension) { + this.resource = resource; + } + + static from (resource: Extension) : USCoreIndividualSexExtensionProfile { + const profile = new USCoreIndividualSexExtensionProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Extension) : USCoreIndividualSexExtensionProfile { + return new USCoreIndividualSexExtensionProfile(resource); + } + + static createResource (args: USCoreIndividualSexExtensionProfileRaw) : Extension { + const resource = buildResource( { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex", + valueCoding: args.valueCoding, + }) + return resource; + } + + static create (args: USCoreIndividualSexExtensionProfileRaw) : USCoreIndividualSexExtensionProfile { + return USCoreIndividualSexExtensionProfile.apply(USCoreIndividualSexExtensionProfile.createResource(args)); + } + + toResource () : Extension { + return this.resource; + } + + // Field accessors + getValueCoding () : Coding | undefined { + return this.resource.valueCoding as Coding | undefined; + } + + setValueCoding (value: Coding) : this { + Object.assign(this.resource, { valueCoding: value }); + return this; + } + + getUrl () : string | undefined { + return this.resource.url as string | undefined; + } + + setUrl (value: string) : this { + Object.assign(this.resource, { url: value }); + return this; + } + + // Extensions + // Slices + // Validation + validate(): { errors: string[]; warnings: string[] } { + const profileName = "USCoreIndividualSexExtension" + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "url"), + ...validateFixedValue(res, profileName, "url", USCoreIndividualSexExtensionProfile.canonicalUrl), + ...validateChoiceRequired(res, profileName, ["valueCoding"]), + ], + warnings: [], + } + } + +} + 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 new file mode 100644 index 000000000..54141a3e1 --- /dev/null +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreInterpreterNeededExtension.ts @@ -0,0 +1,97 @@ +// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { Coding } from "../../hl7-fhir-r4-core/Coding"; +import type { Extension } from "../../hl7-fhir-r4-core/Extension"; + +import { + buildResource, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreInterpreterNeededExtensionProfileRaw = { + valueCoding: Coding; +} + +// CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed (pkg: hl7.fhir.us.core#8.0.1) +export class USCoreInterpreterNeededExtensionProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed"; + + private resource: Extension; + + constructor (resource: Extension) { + this.resource = resource; + } + + static from (resource: Extension) : USCoreInterpreterNeededExtensionProfile { + const profile = new USCoreInterpreterNeededExtensionProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Extension) : USCoreInterpreterNeededExtensionProfile { + return new USCoreInterpreterNeededExtensionProfile(resource); + } + + static createResource (args: USCoreInterpreterNeededExtensionProfileRaw) : Extension { + const resource = buildResource( { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed", + valueCoding: args.valueCoding, + }) + return resource; + } + + static create (args: USCoreInterpreterNeededExtensionProfileRaw) : USCoreInterpreterNeededExtensionProfile { + return USCoreInterpreterNeededExtensionProfile.apply(USCoreInterpreterNeededExtensionProfile.createResource(args)); + } + + toResource () : Extension { + return this.resource; + } + + // Field accessors + getValueCoding () : Coding | undefined { + return this.resource.valueCoding as Coding | undefined; + } + + setValueCoding (value: Coding) : this { + Object.assign(this.resource, { valueCoding: value }); + return this; + } + + getUrl () : string | undefined { + return this.resource.url as string | undefined; + } + + setUrl (value: string) : this { + Object.assign(this.resource, { url: value }); + return this; + } + + // Extensions + // Slices + // Validation + validate(): { errors: string[]; warnings: string[] } { + const profileName = "USCoreInterpreterNeededExtension" + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "url"), + ...validateFixedValue(res, profileName, "url", USCoreInterpreterNeededExtensionProfile.canonicalUrl), + ...validateChoiceRequired(res, profileName, ["valueCoding"]), + ], + warnings: [], + } + } + +} + 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 new file mode 100644 index 000000000..90b061a00 --- /dev/null +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreRaceExtension.ts @@ -0,0 +1,261 @@ +// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { Coding } from "../../hl7-fhir-r4-core/Coding"; +import type { Extension } from "../../hl7-fhir-r4-core/Extension"; + +export type USCoreRaceExtension_Extension_OmbCategorySliceFlat = Omit & Coding; +export type USCoreRaceExtension_Extension_DetailedSliceFlat = Omit & Coding; +export type USCoreRaceExtension_Extension_TextSliceFlat = Omit; + +import { + buildResource, + isRawExtensionInput, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + wrapSliceChoice, + unwrapSliceChoice, + isExtension, + getExtensionValue, + pushExtension, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreRaceExtensionProfileRaw = { + extension?: Extension[]; +} + +export type USCoreRaceExtensionProfileFlat = { + ombCategory?: Coding; + detailed?: Coding[]; + text: string; +} + +// CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-race (pkg: hl7.fhir.us.core#8.0.1) +export class USCoreRaceExtensionProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"; + + private static readonly ombCategorySliceMatch: Record = {"url":"ombCategory"}; + private static readonly detailedSliceMatch: Record = {"url":"detailed"}; + private static readonly textSliceMatch: Record = {"url":"text"}; + + private resource: Extension; + + constructor (resource: Extension) { + this.resource = resource; + } + + static from (resource: Extension) : USCoreRaceExtensionProfile { + const profile = new USCoreRaceExtensionProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Extension) : USCoreRaceExtensionProfile { + return new USCoreRaceExtensionProfile(resource); + } + + private static resolveInput (args: USCoreRaceExtensionProfileRaw | USCoreRaceExtensionProfileFlat) : Extension[] { + if (isRawExtensionInput(args)) { + return args.extension ?? []; + } else { + const result: Extension[] = []; + if (args.ombCategory !== undefined) { + result.push({ url: "ombCategory", valueCoding: args.ombCategory } as Extension); + } + if (args.detailed) { + for (const item of args.detailed) { + result.push({ url: "detailed", valueCoding: item } as Extension); + } + } + if (args.text !== undefined) { + result.push({ url: "text", valueString: args.text } as Extension); + } + return result; + } + } + + 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, + }) + return resource; + } + + static create (args: USCoreRaceExtensionProfileRaw | USCoreRaceExtensionProfileFlat) : USCoreRaceExtensionProfile { + return USCoreRaceExtensionProfile.apply(USCoreRaceExtensionProfile.createResource(args)); + } + + toResource () : Extension { + return this.resource; + } + + // Field accessors + getExtension () : Extension[] | undefined { + return this.resource.extension as Extension[] | undefined; + } + + setExtension (value: Extension[]) : this { + Object.assign(this.resource, { extension: value }); + return this; + } + + getUrl () : string | undefined { + 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) + } else { + pushExtension(this.resource, { url: "ombCategory", ...value } as Extension) + } + return this + } + + public getOmbCategory (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "ombCategory") + } + + public setDetailed (value: Omit | Extension): this { + if (isExtension(value)) { + if (value.url !== "detailed") throw new Error(`Expected extension url 'detailed', got '${value.url}'`) + pushExtension(this.resource, value) + } else { + pushExtension(this.resource, { url: "detailed", ...value } as Extension) + } + return this + } + + public getDetailed (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "detailed") + } + + 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) + } else { + pushExtension(this.resource, { url: "text", ...value } as Extension) + } + return this + } + + public getText (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "text") + } + + // Slices + public setExtensionOmbCategory (input?: USCoreRaceExtension_Extension_OmbCategorySliceFlat | Extension): this { + const match = USCoreRaceExtensionProfile.ombCategorySliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const wrapped = wrapSliceChoice(input ?? {}, "valueCoding") + const value = applySliceMatch(wrapped, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public setExtensionDetailed (input?: USCoreRaceExtension_Extension_DetailedSliceFlat | Extension): this { + const match = USCoreRaceExtensionProfile.detailedSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const wrapped = wrapSliceChoice(input ?? {}, "valueCoding") + const value = applySliceMatch(wrapped, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public setExtensionText (input?: USCoreRaceExtension_Extension_TextSliceFlat | Extension): this { + const match = USCoreRaceExtensionProfile.textSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const value = applySliceMatch(input ?? {}, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public getExtensionOmbCategory(mode: 'flat'): USCoreRaceExtension_Extension_OmbCategorySliceFlat | undefined; + public getExtensionOmbCategory(mode: 'raw'): Extension | undefined; + public getExtensionOmbCategory(): USCoreRaceExtension_Extension_OmbCategorySliceFlat | undefined; + public getExtensionOmbCategory (mode: 'flat' | 'raw' = 'flat'): USCoreRaceExtension_Extension_OmbCategorySliceFlat | Extension | undefined { + const match = USCoreRaceExtensionProfile.ombCategorySliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["url"], "valueCoding") + } + + public getExtensionDetailed(mode: 'flat'): USCoreRaceExtension_Extension_DetailedSliceFlat | undefined; + public getExtensionDetailed(mode: 'raw'): Extension | undefined; + public getExtensionDetailed(): USCoreRaceExtension_Extension_DetailedSliceFlat | undefined; + public getExtensionDetailed (mode: 'flat' | 'raw' = 'flat'): USCoreRaceExtension_Extension_DetailedSliceFlat | Extension | undefined { + const match = USCoreRaceExtensionProfile.detailedSliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["url"], "valueCoding") + } + + public getExtensionText(mode: 'flat'): USCoreRaceExtension_Extension_TextSliceFlat | undefined; + public getExtensionText(mode: 'raw'): Extension | undefined; + public getExtensionText(): USCoreRaceExtension_Extension_TextSliceFlat | undefined; + public getExtensionText (mode: 'flat' | 'raw' = 'flat'): USCoreRaceExtension_Extension_TextSliceFlat | Extension | undefined { + const match = USCoreRaceExtensionProfile.textSliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return stripMatchKeys(item, ["url"]) + } + + // Validation + validate(): { errors: string[]; warnings: string[] } { + const profileName = "USCoreRaceExtension" + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "extension"), + ...validateSliceCardinality(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", 0, 6), + ...validateSliceCardinality(res, profileName, "extension", {"url":"text"}, "text", 1, 1), + ...validateRequired(res, profileName, "url"), + ...validateFixedValue(res, profileName, "url", USCoreRaceExtensionProfile.canonicalUrl), + ], + warnings: [], + } + } + +} + 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 new file mode 100644 index 000000000..b2db777a3 --- /dev/null +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreTribalAffiliationExtension.ts @@ -0,0 +1,216 @@ +// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { CodeableConcept } from "../../hl7-fhir-r4-core/CodeableConcept"; +import type { Extension } from "../../hl7-fhir-r4-core/Extension"; + +export type USCoreTribalAffiliationExtension_Extension_TribalAffiliationSliceFlat = Omit & CodeableConcept; +export type USCoreTribalAffiliationExtension_Extension_IsEnrolledSliceFlat = Omit; + +import { + buildResource, + isRawExtensionInput, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + wrapSliceChoice, + unwrapSliceChoice, + isExtension, + getExtensionValue, + pushExtension, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreTribalAffiliationExtensionProfileRaw = { + extension?: Extension[]; +} + +export type USCoreTribalAffiliationExtensionProfileFlat = { + tribalAffiliation: CodeableConcept; + isEnrolled?: boolean; +} + +// CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation (pkg: hl7.fhir.us.core#8.0.1) +export class USCoreTribalAffiliationExtensionProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation"; + + private static readonly tribalAffiliationSliceMatch: Record = {"url":"tribalAffiliation"}; + private static readonly isEnrolledSliceMatch: Record = {"url":"isEnrolled"}; + + private resource: Extension; + + constructor (resource: Extension) { + this.resource = resource; + } + + static from (resource: Extension) : USCoreTribalAffiliationExtensionProfile { + const profile = new USCoreTribalAffiliationExtensionProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Extension) : USCoreTribalAffiliationExtensionProfile { + return new USCoreTribalAffiliationExtensionProfile(resource); + } + + private static resolveInput (args: USCoreTribalAffiliationExtensionProfileRaw | USCoreTribalAffiliationExtensionProfileFlat) : Extension[] { + if (isRawExtensionInput(args)) { + return args.extension ?? []; + } else { + const result: Extension[] = []; + if (args.tribalAffiliation !== undefined) { + result.push({ url: "tribalAffiliation", valueCodeableConcept: args.tribalAffiliation } as Extension); + } + if (args.isEnrolled !== undefined) { + result.push({ url: "isEnrolled", valueBoolean: args.isEnrolled } as Extension); + } + return result; + } + } + + 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, + }) + return resource; + } + + static create (args: USCoreTribalAffiliationExtensionProfileRaw | USCoreTribalAffiliationExtensionProfileFlat) : USCoreTribalAffiliationExtensionProfile { + return USCoreTribalAffiliationExtensionProfile.apply(USCoreTribalAffiliationExtensionProfile.createResource(args)); + } + + toResource () : Extension { + return this.resource; + } + + // Field accessors + getExtension () : Extension[] | undefined { + return this.resource.extension as Extension[] | undefined; + } + + setExtension (value: Extension[]) : this { + Object.assign(this.resource, { extension: value }); + return this; + } + + getUrl () : string | undefined { + 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) + } else { + pushExtension(this.resource, { url: "tribalAffiliation", ...value } as Extension) + } + return this + } + + public getTribalAffiliation (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "tribalAffiliation") + } + + 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) + } else { + pushExtension(this.resource, { url: "isEnrolled", ...value } as Extension) + } + return this + } + + public getIsEnrolled (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "isEnrolled") + } + + // Slices + public setExtensionTribalAffiliation (input?: USCoreTribalAffiliationExtension_Extension_TribalAffiliationSliceFlat | Extension): this { + const match = USCoreTribalAffiliationExtensionProfile.tribalAffiliationSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const wrapped = wrapSliceChoice(input ?? {}, "valueCodeableConcept") + const value = applySliceMatch(wrapped, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public setExtensionIsEnrolled (input?: USCoreTribalAffiliationExtension_Extension_IsEnrolledSliceFlat | Extension): this { + const match = USCoreTribalAffiliationExtensionProfile.isEnrolledSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const value = applySliceMatch(input ?? {}, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public getExtensionTribalAffiliation(mode: 'flat'): USCoreTribalAffiliationExtension_Extension_TribalAffiliationSliceFlat | undefined; + public getExtensionTribalAffiliation(mode: 'raw'): Extension | undefined; + public getExtensionTribalAffiliation(): USCoreTribalAffiliationExtension_Extension_TribalAffiliationSliceFlat | undefined; + public getExtensionTribalAffiliation (mode: 'flat' | 'raw' = 'flat'): USCoreTribalAffiliationExtension_Extension_TribalAffiliationSliceFlat | Extension | undefined { + const match = USCoreTribalAffiliationExtensionProfile.tribalAffiliationSliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["url"], "valueCodeableConcept") + } + + public getExtensionIsEnrolled(mode: 'flat'): USCoreTribalAffiliationExtension_Extension_IsEnrolledSliceFlat | undefined; + public getExtensionIsEnrolled(mode: 'raw'): Extension | undefined; + public getExtensionIsEnrolled(): USCoreTribalAffiliationExtension_Extension_IsEnrolledSliceFlat | undefined; + public getExtensionIsEnrolled (mode: 'flat' | 'raw' = 'flat'): USCoreTribalAffiliationExtension_Extension_IsEnrolledSliceFlat | Extension | undefined { + const match = USCoreTribalAffiliationExtensionProfile.isEnrolledSliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return stripMatchKeys(item, ["url"]) + } + + // Validation + validate(): { errors: string[]; warnings: string[] } { + const profileName = "USCoreTribalAffiliationExtension" + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "extension"), + ...validateSliceCardinality(res, profileName, "extension", {"url":"tribalAffiliation"}, "tribalAffiliation", 1, 1), + ...validateSliceCardinality(res, profileName, "extension", {"url":"isEnrolled"}, "isEnrolled", 0, 1), + ...validateRequired(res, profileName, "url"), + ...validateFixedValue(res, profileName, "url", USCoreTribalAffiliationExtensionProfile.canonicalUrl), + ], + warnings: [], + } + } + +} + 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 c9ab6c05f..99ad22151 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 @@ -3,8 +3,7 @@ // Any manual changes made to this file may be overwritten. import type { CodeableConcept } from "../../hl7-fhir-r4-core/CodeableConcept"; -import type { Observation } from "../../hl7-fhir-r4-core/Observation"; -import type { ObservationComponent } from "../../hl7-fhir-r4-core/Observation"; +import type { Observation, ObservationComponent } from "../../hl7-fhir-r4-core/Observation"; import type { Period } from "../../hl7-fhir-r4-core/Period"; import type { Quantity } from "../../hl7-fhir-r4-core/Quantity"; import type { Range } from "../../hl7-fhir-r4-core/Range"; @@ -17,13 +16,32 @@ export interface USCoreBloodPressureProfile extends Observation { subject: Reference<"Patient">; } -export type USCoreBloodPressureProfile_Category_VSCatSliceInput = Omit; -export type USCoreBloodPressureProfile_Component_SystolicSliceInput = Omit & Quantity; -export type USCoreBloodPressureProfile_Component_DiastolicSliceInput = Omit & Quantity; - -import { ensureProfile, applySliceMatch, matchesValue, setArraySlice, getArraySlice, ensureSliceDefaults, stripMatchKeys, wrapSliceChoice, unwrapSliceChoice, validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type USCoreBloodPressureProfileProfileParams = { +export type USCoreBloodPressureProfile_Category_VSCatSliceFlat = Omit; +export type USCoreBloodPressureProfile_Component_SystolicSliceFlat = Omit & Quantity; +export type USCoreBloodPressureProfile_Component_DiastolicSliceFlat = Omit & Quantity; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + wrapSliceChoice, + unwrapSliceChoice, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreBloodPressureProfileRaw = { status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); subject: Reference<"Patient">; category?: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]; @@ -31,308 +49,327 @@ export type USCoreBloodPressureProfileProfileParams = { } // CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure (pkg: hl7.fhir.us.core#8.0.1) -export class USCoreBloodPressureProfileProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure" +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 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"}]}} + 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"}]}}; - private resource: Observation + private resource: Observation; constructor (resource: Observation) { - this.resource = resource - ensureProfile(resource, "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure") + this.resource = resource; } - static from (resource: Observation) : USCoreBloodPressureProfileProfile { - return new USCoreBloodPressureProfileProfile(resource) + static from (resource: Observation) : USCoreBloodPressureProfile { + if (!resource.meta?.profile?.includes(USCoreBloodPressureProfile.canonicalUrl)) { + throw new Error(`USCoreBloodPressureProfile: meta.profile must include ${USCoreBloodPressureProfile.canonicalUrl}`) + } + const profile = new USCoreBloodPressureProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; } - static createResource (args: USCoreBloodPressureProfileProfileParams) : Observation { + static apply (resource: Observation) : USCoreBloodPressureProfile { + ensureProfile(resource, USCoreBloodPressureProfile.canonicalUrl); + return new USCoreBloodPressureProfile(resource); + } + + static createResource (args: USCoreBloodPressureProfileRaw) : Observation { const categoryWithDefaults = ensureSliceDefaults( [...(args.category ?? [])], - USCoreBloodPressureProfileProfile.VSCatSliceMatch, - ) + USCoreBloodPressureProfile.VSCatSliceMatch, + ); const componentWithDefaults = ensureSliceDefaults( [...(args.component ?? [])], - USCoreBloodPressureProfileProfile.systolicSliceMatch, - USCoreBloodPressureProfileProfile.diastolicSliceMatch, - ) + USCoreBloodPressureProfile.systolicSliceMatch, + USCoreBloodPressureProfile.diastolicSliceMatch, + ); - const resource = { + const resource = buildResource( { resourceType: "Observation", code: {"coding":[{"system":"http://loinc.org","code":"85354-9"}]}, category: categoryWithDefaults, component: componentWithDefaults, status: args.status, subject: args.subject, - meta: { profile: [USCoreBloodPressureProfileProfile.canonicalUrl] }, - } as unknown as Observation - return resource + meta: { profile: [USCoreBloodPressureProfile.canonicalUrl] }, + }) + return resource; } - static create (args: USCoreBloodPressureProfileProfileParams) : USCoreBloodPressureProfileProfile { - return USCoreBloodPressureProfileProfile.from(USCoreBloodPressureProfileProfile.createResource(args)) + static create (args: USCoreBloodPressureProfileRaw) : USCoreBloodPressureProfile { + return USCoreBloodPressureProfile.apply(USCoreBloodPressureProfile.createResource(args)); } toResource () : Observation { - return this.resource + return this.resource; } // Field accessors - getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { - return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; } setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { - Object.assign(this.resource, { status: value }) - return this + Object.assign(this.resource, { status: value }); + return this; } getSubject () : Reference<"Patient"> | undefined { - return this.resource.subject as Reference<"Patient"> | undefined + return this.resource.subject as Reference<"Patient"> | undefined; } setSubject (value: Reference<"Patient">) : this { - Object.assign(this.resource, { subject: value }) - return this + Object.assign(this.resource, { subject: value }); + return this; } getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { - return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; } setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { - Object.assign(this.resource, { category: value }) - return this + Object.assign(this.resource, { category: value }); + return this; } getCode () : 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 { - 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 + 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 + Object.assign(this.resource, { code: value }); + return this; } getComponent () : ObservationComponent[] | undefined { - return this.resource.component as ObservationComponent[] | undefined + return this.resource.component as ObservationComponent[] | undefined; } setComponent (value: ObservationComponent[]) : this { - Object.assign(this.resource, { component: value }) - return this + Object.assign(this.resource, { component: value }); + return this; } getEffectiveDateTime () : string | undefined { - return this.resource.effectiveDateTime as string | undefined + return this.resource.effectiveDateTime as string | undefined; } setEffectiveDateTime (value: string) : this { - Object.assign(this.resource, { effectiveDateTime: value }) - return this + Object.assign(this.resource, { effectiveDateTime: value }); + return this; } getEffectivePeriod () : Period | undefined { - return this.resource.effectivePeriod as Period | undefined + return this.resource.effectivePeriod as Period | undefined; } setEffectivePeriod (value: Period) : this { - Object.assign(this.resource, { effectivePeriod: value }) - return this + Object.assign(this.resource, { effectivePeriod: value }); + return this; } getValueQuantity () : Quantity | undefined { - return this.resource.valueQuantity as Quantity | undefined + return this.resource.valueQuantity as Quantity | undefined; } setValueQuantity (value: Quantity) : this { - Object.assign(this.resource, { valueQuantity: value }) - return this + Object.assign(this.resource, { valueQuantity: value }); + return this; } getValueCodeableConcept () : CodeableConcept | undefined { - return this.resource.valueCodeableConcept as CodeableConcept | undefined + return this.resource.valueCodeableConcept as CodeableConcept | undefined; } setValueCodeableConcept (value: CodeableConcept) : this { - Object.assign(this.resource, { valueCodeableConcept: value }) - return this + Object.assign(this.resource, { valueCodeableConcept: value }); + return this; } getValueString () : string | undefined { - return this.resource.valueString as string | undefined + return this.resource.valueString as string | undefined; } setValueString (value: string) : this { - Object.assign(this.resource, { valueString: value }) - return this + Object.assign(this.resource, { valueString: value }); + return this; } getValueBoolean () : boolean | undefined { - return this.resource.valueBoolean as boolean | undefined + return this.resource.valueBoolean as boolean | undefined; } setValueBoolean (value: boolean) : this { - Object.assign(this.resource, { valueBoolean: value }) - return this + Object.assign(this.resource, { valueBoolean: value }); + return this; } getValueInteger () : number | undefined { - return this.resource.valueInteger as number | undefined + return this.resource.valueInteger as number | undefined; } setValueInteger (value: number) : this { - Object.assign(this.resource, { valueInteger: value }) - return this + Object.assign(this.resource, { valueInteger: value }); + return this; } getValueRange () : Range | undefined { - return this.resource.valueRange as Range | undefined + return this.resource.valueRange as Range | undefined; } setValueRange (value: Range) : this { - Object.assign(this.resource, { valueRange: value }) - return this + Object.assign(this.resource, { valueRange: value }); + return this; } getValueRatio () : Ratio | undefined { - return this.resource.valueRatio as Ratio | undefined + return this.resource.valueRatio as Ratio | undefined; } setValueRatio (value: Ratio) : this { - Object.assign(this.resource, { valueRatio: value }) - return this + Object.assign(this.resource, { valueRatio: value }); + return this; } getValueSampledData () : SampledData | undefined { - return this.resource.valueSampledData as SampledData | undefined + return this.resource.valueSampledData as SampledData | undefined; } setValueSampledData (value: SampledData) : this { - Object.assign(this.resource, { valueSampledData: value }) - return this + Object.assign(this.resource, { valueSampledData: value }); + return this; } getValueTime () : string | undefined { - return this.resource.valueTime as string | undefined + return this.resource.valueTime as string | undefined; } setValueTime (value: string) : this { - Object.assign(this.resource, { valueTime: value }) - return this + Object.assign(this.resource, { valueTime: value }); + return this; } getValueDateTime () : string | undefined { - return this.resource.valueDateTime as string | undefined + return this.resource.valueDateTime as string | undefined; } setValueDateTime (value: string) : this { - Object.assign(this.resource, { valueDateTime: value }) - return this + Object.assign(this.resource, { valueDateTime: value }); + return this; } getValuePeriod () : Period | undefined { - return this.resource.valuePeriod as Period | undefined + return this.resource.valuePeriod as Period | undefined; } setValuePeriod (value: Period) : this { - Object.assign(this.resource, { valuePeriod: value }) - return this - } - - toProfile () : USCoreBloodPressureProfile { - return this.resource as USCoreBloodPressureProfile - } - - // Slices and extensions - - public setVSCat (input?: USCoreBloodPressureProfile_Category_VSCatSliceInput): this { - const match = USCoreBloodPressureProfileProfile.VSCatSliceMatch + Object.assign(this.resource, { valuePeriod: value }); + return this; + } + + // Extensions + // Slices + public setVSCat (input?: USCoreBloodPressureProfile_Category_VSCatSliceFlat | CodeableConcept): this { + const match = USCoreBloodPressureProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.category ??= [], match, value) return this } - public setSystolic (input?: USCoreBloodPressureProfile_Component_SystolicSliceInput): this { - const match = USCoreBloodPressureProfileProfile.systolicSliceMatch + public setSystolic (input?: USCoreBloodPressureProfile_Component_SystolicSliceFlat | ObservationComponent): this { + const match = USCoreBloodPressureProfile.systolicSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) + return this + } const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") const value = applySliceMatch(wrapped, match) setArraySlice(this.resource.component ??= [], match, value) return this } - public setDiastolic (input?: USCoreBloodPressureProfile_Component_DiastolicSliceInput): this { - const match = USCoreBloodPressureProfileProfile.diastolicSliceMatch + public setDiastolic (input?: USCoreBloodPressureProfile_Component_DiastolicSliceFlat | ObservationComponent): this { + const match = USCoreBloodPressureProfile.diastolicSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) + return this + } const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") const value = applySliceMatch(wrapped, match) setArraySlice(this.resource.component ??= [], match, value) return this } - public getVSCat (): USCoreBloodPressureProfile_Category_VSCatSliceInput | undefined { - const match = USCoreBloodPressureProfileProfile.VSCatSliceMatch + public getVSCat(mode: 'flat'): USCoreBloodPressureProfile_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): USCoreBloodPressureProfile_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): USCoreBloodPressureProfile_Category_VSCatSliceFlat | CodeableConcept | undefined { + const match = USCoreBloodPressureProfile.VSCatSliceMatch const item = getArraySlice(this.resource.category, match) if (!item) return undefined - return stripMatchKeys(item, ["coding"]) + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) } - public getVSCatRaw (): CodeableConcept | undefined { - const match = USCoreBloodPressureProfileProfile.VSCatSliceMatch - const item = getArraySlice(this.resource.category, match) - return item - } - - public getSystolic (): USCoreBloodPressureProfile_Component_SystolicSliceInput | undefined { - const match = USCoreBloodPressureProfileProfile.systolicSliceMatch + public getSystolic(mode: 'flat'): USCoreBloodPressureProfile_Component_SystolicSliceFlat | undefined; + public getSystolic(mode: 'raw'): ObservationComponent | undefined; + public getSystolic(): USCoreBloodPressureProfile_Component_SystolicSliceFlat | undefined; + public getSystolic (mode: 'flat' | 'raw' = 'flat'): USCoreBloodPressureProfile_Component_SystolicSliceFlat | ObservationComponent | undefined { + const match = USCoreBloodPressureProfile.systolicSliceMatch const item = getArraySlice(this.resource.component, match) if (!item) return undefined - return unwrapSliceChoice(item, ["code"], "valueQuantity") - } - - public getSystolicRaw (): ObservationComponent | undefined { - const match = USCoreBloodPressureProfileProfile.systolicSliceMatch - const item = getArraySlice(this.resource.component, match) - return item + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["code"], "valueQuantity") } - public getDiastolic (): USCoreBloodPressureProfile_Component_DiastolicSliceInput | undefined { - const match = USCoreBloodPressureProfileProfile.diastolicSliceMatch + public getDiastolic(mode: 'flat'): USCoreBloodPressureProfile_Component_DiastolicSliceFlat | undefined; + public getDiastolic(mode: 'raw'): ObservationComponent | undefined; + public getDiastolic(): USCoreBloodPressureProfile_Component_DiastolicSliceFlat | undefined; + public getDiastolic (mode: 'flat' | 'raw' = 'flat'): USCoreBloodPressureProfile_Component_DiastolicSliceFlat | ObservationComponent | undefined { + const match = USCoreBloodPressureProfile.diastolicSliceMatch const item = getArraySlice(this.resource.component, match) if (!item) return undefined - return unwrapSliceChoice(item, ["code"], "valueQuantity") - } - - public getDiastolicRaw (): ObservationComponent | undefined { - const match = USCoreBloodPressureProfileProfile.diastolicSliceMatch - const item = getArraySlice(this.resource.component, match) - return item + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["code"], "valueQuantity") } // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "USCoreBloodPressureProfile" - const res = this.resource as unknown as Record - return [ - ...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), - ...validateRequired(res, profileName, "code"), - ...validateFixedValue(res, profileName, "code", {"coding":[{"system":"http://loinc.org","code":"85354-9"}]}), - ...validateRequired(res, profileName, "subject"), - ...validateReference(res, profileName, "subject", ["Patient"]), - ...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":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", 1, 1), - ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", 1, 1), - ...validateReference(res, profileName, "performer", ["PractitionerRole","USCoreCareTeam","USCoreOrganizationProfile","Patient","USCorePractitionerProfile","USCoreRelatedPersonProfile"]), - ] + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateFixedValue(res, profileName, "code", {"coding":[{"system":"http://loinc.org","code":"85354-9"}]}), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...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":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", 1, 1), + ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", 1, 1), + ...validateReference(res, profileName, "performer", ["PractitionerRole","USCoreCareTeam","USCoreOrganizationProfile","Patient","USCorePractitionerProfile","USCoreRelatedPersonProfile"]), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ...validateMustSupport(res, profileName, "performer"), + ], + } } } 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 55d337013..a144624fe 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 @@ -16,258 +16,290 @@ export interface USCoreBodyWeightProfile extends Observation { subject: Reference<"Patient">; } -export type USCoreBodyWeightProfile_Category_VSCatSliceInput = Omit; - -import { ensureProfile, applySliceMatch, matchesValue, setArraySlice, getArraySlice, ensureSliceDefaults, stripMatchKeys, validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type USCoreBodyWeightProfileProfileParams = { +export type USCoreBodyWeightProfile_Category_VSCatSliceFlat = Omit; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreBodyWeightProfileRaw = { status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); subject: Reference<"Patient">; category?: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]; } // CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight (pkg: hl7.fhir.us.core#8.0.1) -export class USCoreBodyWeightProfileProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight" +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 + private resource: Observation; constructor (resource: Observation) { - this.resource = resource - ensureProfile(resource, "http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight") + this.resource = resource; + } + + static from (resource: Observation) : USCoreBodyWeightProfile { + if (!resource.meta?.profile?.includes(USCoreBodyWeightProfile.canonicalUrl)) { + throw new Error(`USCoreBodyWeightProfile: meta.profile must include ${USCoreBodyWeightProfile.canonicalUrl}`) + } + const profile = new USCoreBodyWeightProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; } - static from (resource: Observation) : USCoreBodyWeightProfileProfile { - return new USCoreBodyWeightProfileProfile(resource) + static apply (resource: Observation) : USCoreBodyWeightProfile { + ensureProfile(resource, USCoreBodyWeightProfile.canonicalUrl); + return new USCoreBodyWeightProfile(resource); } - static createResource (args: USCoreBodyWeightProfileProfileParams) : Observation { + static createResource (args: USCoreBodyWeightProfileRaw) : Observation { const categoryWithDefaults = ensureSliceDefaults( [...(args.category ?? [])], - USCoreBodyWeightProfileProfile.VSCatSliceMatch, - ) + USCoreBodyWeightProfile.VSCatSliceMatch, + ); - const resource = { + const resource = buildResource( { resourceType: "Observation", code: {"coding":[{"system":"http://loinc.org","code":"29463-7"}]}, category: categoryWithDefaults, status: args.status, subject: args.subject, - meta: { profile: [USCoreBodyWeightProfileProfile.canonicalUrl] }, - } as unknown as Observation - return resource + meta: { profile: [USCoreBodyWeightProfile.canonicalUrl] }, + }) + return resource; } - static create (args: USCoreBodyWeightProfileProfileParams) : USCoreBodyWeightProfileProfile { - return USCoreBodyWeightProfileProfile.from(USCoreBodyWeightProfileProfile.createResource(args)) + static create (args: USCoreBodyWeightProfileRaw) : USCoreBodyWeightProfile { + return USCoreBodyWeightProfile.apply(USCoreBodyWeightProfile.createResource(args)); } toResource () : Observation { - return this.resource + return this.resource; } // Field accessors - getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { - return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; } setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { - Object.assign(this.resource, { status: value }) - return this + Object.assign(this.resource, { status: value }); + return this; } getSubject () : Reference<"Patient"> | undefined { - return this.resource.subject as Reference<"Patient"> | undefined + return this.resource.subject as Reference<"Patient"> | undefined; } setSubject (value: Reference<"Patient">) : this { - Object.assign(this.resource, { subject: value }) - return this + Object.assign(this.resource, { subject: value }); + return this; } getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { - return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; } setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { - Object.assign(this.resource, { category: value }) - return this + Object.assign(this.resource, { category: value }); + return this; } getCode () : 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 { - 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 + 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 + Object.assign(this.resource, { code: value }); + return this; } getEffectiveDateTime () : string | undefined { - return this.resource.effectiveDateTime as string | undefined + return this.resource.effectiveDateTime as string | undefined; } setEffectiveDateTime (value: string) : this { - Object.assign(this.resource, { effectiveDateTime: value }) - return this + Object.assign(this.resource, { effectiveDateTime: value }); + return this; } getEffectivePeriod () : Period | undefined { - return this.resource.effectivePeriod as Period | undefined + return this.resource.effectivePeriod as Period | undefined; } setEffectivePeriod (value: Period) : this { - Object.assign(this.resource, { effectivePeriod: value }) - return this + Object.assign(this.resource, { effectivePeriod: value }); + return this; } getValueQuantity () : Quantity | undefined { - return this.resource.valueQuantity as Quantity | undefined + return this.resource.valueQuantity as Quantity | undefined; } setValueQuantity (value: Quantity) : this { - Object.assign(this.resource, { valueQuantity: value }) - return this + Object.assign(this.resource, { valueQuantity: value }); + return this; } getValueCodeableConcept () : CodeableConcept | undefined { - return this.resource.valueCodeableConcept as CodeableConcept | undefined + return this.resource.valueCodeableConcept as CodeableConcept | undefined; } setValueCodeableConcept (value: CodeableConcept) : this { - Object.assign(this.resource, { valueCodeableConcept: value }) - return this + Object.assign(this.resource, { valueCodeableConcept: value }); + return this; } getValueString () : string | undefined { - return this.resource.valueString as string | undefined + return this.resource.valueString as string | undefined; } setValueString (value: string) : this { - Object.assign(this.resource, { valueString: value }) - return this + Object.assign(this.resource, { valueString: value }); + return this; } getValueBoolean () : boolean | undefined { - return this.resource.valueBoolean as boolean | undefined + return this.resource.valueBoolean as boolean | undefined; } setValueBoolean (value: boolean) : this { - Object.assign(this.resource, { valueBoolean: value }) - return this + Object.assign(this.resource, { valueBoolean: value }); + return this; } getValueInteger () : number | undefined { - return this.resource.valueInteger as number | undefined + return this.resource.valueInteger as number | undefined; } setValueInteger (value: number) : this { - Object.assign(this.resource, { valueInteger: value }) - return this + Object.assign(this.resource, { valueInteger: value }); + return this; } getValueRange () : Range | undefined { - return this.resource.valueRange as Range | undefined + return this.resource.valueRange as Range | undefined; } setValueRange (value: Range) : this { - Object.assign(this.resource, { valueRange: value }) - return this + Object.assign(this.resource, { valueRange: value }); + return this; } getValueRatio () : Ratio | undefined { - return this.resource.valueRatio as Ratio | undefined + return this.resource.valueRatio as Ratio | undefined; } setValueRatio (value: Ratio) : this { - Object.assign(this.resource, { valueRatio: value }) - return this + Object.assign(this.resource, { valueRatio: value }); + return this; } getValueSampledData () : SampledData | undefined { - return this.resource.valueSampledData as SampledData | undefined + return this.resource.valueSampledData as SampledData | undefined; } setValueSampledData (value: SampledData) : this { - Object.assign(this.resource, { valueSampledData: value }) - return this + Object.assign(this.resource, { valueSampledData: value }); + return this; } getValueTime () : string | undefined { - return this.resource.valueTime as string | undefined + return this.resource.valueTime as string | undefined; } setValueTime (value: string) : this { - Object.assign(this.resource, { valueTime: value }) - return this + Object.assign(this.resource, { valueTime: value }); + return this; } getValueDateTime () : string | undefined { - return this.resource.valueDateTime as string | undefined + return this.resource.valueDateTime as string | undefined; } setValueDateTime (value: string) : this { - Object.assign(this.resource, { valueDateTime: value }) - return this + Object.assign(this.resource, { valueDateTime: value }); + return this; } getValuePeriod () : Period | undefined { - return this.resource.valuePeriod as Period | undefined + return this.resource.valuePeriod as Period | undefined; } setValuePeriod (value: Period) : this { - Object.assign(this.resource, { valuePeriod: value }) - return this - } - - toProfile () : USCoreBodyWeightProfile { - return this.resource as USCoreBodyWeightProfile - } - - // Slices and extensions - - public setVSCat (input?: USCoreBodyWeightProfile_Category_VSCatSliceInput): this { - const match = USCoreBodyWeightProfileProfile.VSCatSliceMatch + Object.assign(this.resource, { valuePeriod: value }); + return this; + } + + // Extensions + // Slices + public setVSCat (input?: USCoreBodyWeightProfile_Category_VSCatSliceFlat | CodeableConcept): this { + const match = USCoreBodyWeightProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.category ??= [], match, value) return this } - public getVSCat (): USCoreBodyWeightProfile_Category_VSCatSliceInput | undefined { - const match = USCoreBodyWeightProfileProfile.VSCatSliceMatch + public getVSCat(mode: 'flat'): USCoreBodyWeightProfile_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): USCoreBodyWeightProfile_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): USCoreBodyWeightProfile_Category_VSCatSliceFlat | CodeableConcept | undefined { + const match = USCoreBodyWeightProfile.VSCatSliceMatch const item = getArraySlice(this.resource.category, match) if (!item) return undefined - return stripMatchKeys(item, ["coding"]) - } - - public getVSCatRaw (): CodeableConcept | undefined { - const match = USCoreBodyWeightProfileProfile.VSCatSliceMatch - const item = getArraySlice(this.resource.category, match) - return item + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) } // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "USCoreBodyWeightProfile" - const res = this.resource as unknown as Record - return [ - ...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), - ...validateRequired(res, profileName, "code"), - ...validateFixedValue(res, profileName, "code", {"coding":[{"system":"http://loinc.org","code":"29463-7"}]}), - ...validateRequired(res, profileName, "subject"), - ...validateReference(res, profileName, "subject", ["Patient"]), - ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), - ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), - ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), - ...validateReference(res, profileName, "performer", ["PractitionerRole","USCoreCareTeam","USCoreOrganizationProfile","Patient","USCorePractitionerProfile","USCoreRelatedPersonProfile"]), - ] + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateFixedValue(res, profileName, "code", {"coding":[{"system":"http://loinc.org","code":"29463-7"}]}), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), + ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), + ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), + ...validateReference(res, profileName, "performer", ["PractitionerRole","USCoreCareTeam","USCoreOrganizationProfile","Patient","USCorePractitionerProfile","USCoreRelatedPersonProfile"]), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ...validateMustSupport(res, profileName, "performer"), + ], + } } } 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 af9c2dffe..7d1d2bf89 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 @@ -16,11 +16,28 @@ export interface USCoreVitalSignsProfile extends Observation { subject: Reference<"Patient">; } -export type USCoreVitalSignsProfile_Category_VSCatSliceInput = Omit; - -import { ensureProfile, applySliceMatch, matchesValue, setArraySlice, getArraySlice, ensureSliceDefaults, stripMatchKeys, validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type USCoreVitalSignsProfileProfileParams = { +export type USCoreVitalSignsProfile_Category_VSCatSliceFlat = Omit; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreVitalSignsProfileRaw = { status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); code: 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)>; subject: Reference<"Patient">; @@ -28,246 +45,261 @@ export type USCoreVitalSignsProfileProfileParams = { } // CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs (pkg: hl7.fhir.us.core#8.0.1) -export class USCoreVitalSignsProfileProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs" +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 + private resource: Observation; constructor (resource: Observation) { - this.resource = resource - ensureProfile(resource, "http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs") + this.resource = resource; + } + + static from (resource: Observation) : USCoreVitalSignsProfile { + if (!resource.meta?.profile?.includes(USCoreVitalSignsProfile.canonicalUrl)) { + throw new Error(`USCoreVitalSignsProfile: meta.profile must include ${USCoreVitalSignsProfile.canonicalUrl}`) + } + const profile = new USCoreVitalSignsProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; } - static from (resource: Observation) : USCoreVitalSignsProfileProfile { - return new USCoreVitalSignsProfileProfile(resource) + static apply (resource: Observation) : USCoreVitalSignsProfile { + ensureProfile(resource, USCoreVitalSignsProfile.canonicalUrl); + return new USCoreVitalSignsProfile(resource); } - static createResource (args: USCoreVitalSignsProfileProfileParams) : Observation { + static createResource (args: USCoreVitalSignsProfileRaw) : Observation { const categoryWithDefaults = ensureSliceDefaults( [...(args.category ?? [])], - USCoreVitalSignsProfileProfile.VSCatSliceMatch, - ) + USCoreVitalSignsProfile.VSCatSliceMatch, + ); - const resource = { + const resource = buildResource( { resourceType: "Observation", category: categoryWithDefaults, status: args.status, code: args.code, subject: args.subject, - meta: { profile: [USCoreVitalSignsProfileProfile.canonicalUrl] }, - } as unknown as Observation - return resource + meta: { profile: [USCoreVitalSignsProfile.canonicalUrl] }, + }) + return resource; } - static create (args: USCoreVitalSignsProfileProfileParams) : USCoreVitalSignsProfileProfile { - return USCoreVitalSignsProfileProfile.from(USCoreVitalSignsProfileProfile.createResource(args)) + static create (args: USCoreVitalSignsProfileRaw) : USCoreVitalSignsProfile { + return USCoreVitalSignsProfile.apply(USCoreVitalSignsProfile.createResource(args)); } toResource () : Observation { - return this.resource + return this.resource; } // Field accessors - getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { - return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; } setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { - Object.assign(this.resource, { status: value }) - return this + Object.assign(this.resource, { status: value }); + return this; } getCode () : 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 { - 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 + 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 + Object.assign(this.resource, { code: value }); + return this; } getSubject () : Reference<"Patient"> | undefined { - return this.resource.subject as Reference<"Patient"> | undefined + return this.resource.subject as Reference<"Patient"> | undefined; } setSubject (value: Reference<"Patient">) : this { - Object.assign(this.resource, { subject: value }) - return this + Object.assign(this.resource, { subject: value }); + return this; } getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { - return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; } setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { - Object.assign(this.resource, { category: value }) - return this + Object.assign(this.resource, { category: value }); + return this; } getEffectiveDateTime () : string | undefined { - return this.resource.effectiveDateTime as string | undefined + return this.resource.effectiveDateTime as string | undefined; } setEffectiveDateTime (value: string) : this { - Object.assign(this.resource, { effectiveDateTime: value }) - return this + Object.assign(this.resource, { effectiveDateTime: value }); + return this; } getEffectivePeriod () : Period | undefined { - return this.resource.effectivePeriod as Period | undefined + return this.resource.effectivePeriod as Period | undefined; } setEffectivePeriod (value: Period) : this { - Object.assign(this.resource, { effectivePeriod: value }) - return this + Object.assign(this.resource, { effectivePeriod: value }); + return this; } getValueQuantity () : Quantity | undefined { - return this.resource.valueQuantity as Quantity | undefined + return this.resource.valueQuantity as Quantity | undefined; } setValueQuantity (value: Quantity) : this { - Object.assign(this.resource, { valueQuantity: value }) - return this + Object.assign(this.resource, { valueQuantity: value }); + return this; } getValueCodeableConcept () : CodeableConcept | undefined { - return this.resource.valueCodeableConcept as CodeableConcept | undefined + return this.resource.valueCodeableConcept as CodeableConcept | undefined; } setValueCodeableConcept (value: CodeableConcept) : this { - Object.assign(this.resource, { valueCodeableConcept: value }) - return this + Object.assign(this.resource, { valueCodeableConcept: value }); + return this; } getValueString () : string | undefined { - return this.resource.valueString as string | undefined + return this.resource.valueString as string | undefined; } setValueString (value: string) : this { - Object.assign(this.resource, { valueString: value }) - return this + Object.assign(this.resource, { valueString: value }); + return this; } getValueBoolean () : boolean | undefined { - return this.resource.valueBoolean as boolean | undefined + return this.resource.valueBoolean as boolean | undefined; } setValueBoolean (value: boolean) : this { - Object.assign(this.resource, { valueBoolean: value }) - return this + Object.assign(this.resource, { valueBoolean: value }); + return this; } getValueInteger () : number | undefined { - return this.resource.valueInteger as number | undefined + return this.resource.valueInteger as number | undefined; } setValueInteger (value: number) : this { - Object.assign(this.resource, { valueInteger: value }) - return this + Object.assign(this.resource, { valueInteger: value }); + return this; } getValueRange () : Range | undefined { - return this.resource.valueRange as Range | undefined + return this.resource.valueRange as Range | undefined; } setValueRange (value: Range) : this { - Object.assign(this.resource, { valueRange: value }) - return this + Object.assign(this.resource, { valueRange: value }); + return this; } getValueRatio () : Ratio | undefined { - return this.resource.valueRatio as Ratio | undefined + return this.resource.valueRatio as Ratio | undefined; } setValueRatio (value: Ratio) : this { - Object.assign(this.resource, { valueRatio: value }) - return this + Object.assign(this.resource, { valueRatio: value }); + return this; } getValueSampledData () : SampledData | undefined { - return this.resource.valueSampledData as SampledData | undefined + return this.resource.valueSampledData as SampledData | undefined; } setValueSampledData (value: SampledData) : this { - Object.assign(this.resource, { valueSampledData: value }) - return this + Object.assign(this.resource, { valueSampledData: value }); + return this; } getValueTime () : string | undefined { - return this.resource.valueTime as string | undefined + return this.resource.valueTime as string | undefined; } setValueTime (value: string) : this { - Object.assign(this.resource, { valueTime: value }) - return this + Object.assign(this.resource, { valueTime: value }); + return this; } getValueDateTime () : string | undefined { - return this.resource.valueDateTime as string | undefined + return this.resource.valueDateTime as string | undefined; } setValueDateTime (value: string) : this { - Object.assign(this.resource, { valueDateTime: value }) - return this + Object.assign(this.resource, { valueDateTime: value }); + return this; } getValuePeriod () : Period | undefined { - return this.resource.valuePeriod as Period | undefined + return this.resource.valuePeriod as Period | undefined; } setValuePeriod (value: Period) : this { - Object.assign(this.resource, { valuePeriod: value }) - return this - } - - toProfile () : USCoreVitalSignsProfile { - return this.resource as USCoreVitalSignsProfile - } - - // Slices and extensions - - public setVSCat (input?: USCoreVitalSignsProfile_Category_VSCatSliceInput): this { - const match = USCoreVitalSignsProfileProfile.VSCatSliceMatch + Object.assign(this.resource, { valuePeriod: value }); + return this; + } + + // Extensions + // Slices + public setVSCat (input?: USCoreVitalSignsProfile_Category_VSCatSliceFlat | CodeableConcept): this { + const match = USCoreVitalSignsProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.category ??= [], match, value) return this } - public getVSCat (): USCoreVitalSignsProfile_Category_VSCatSliceInput | undefined { - const match = USCoreVitalSignsProfileProfile.VSCatSliceMatch + public getVSCat(mode: 'flat'): USCoreVitalSignsProfile_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): USCoreVitalSignsProfile_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): USCoreVitalSignsProfile_Category_VSCatSliceFlat | CodeableConcept | undefined { + const match = USCoreVitalSignsProfile.VSCatSliceMatch const item = getArraySlice(this.resource.category, match) if (!item) return undefined - return stripMatchKeys(item, ["coding"]) - } - - public getVSCatRaw (): CodeableConcept | undefined { - const match = USCoreVitalSignsProfileProfile.VSCatSliceMatch - const item = getArraySlice(this.resource.category, match) - return item + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) } // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "USCoreVitalSignsProfile" - const res = this.resource as unknown as Record - return [ - ...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), - ...validateRequired(res, profileName, "code"), - ...validateRequired(res, profileName, "subject"), - ...validateReference(res, profileName, "subject", ["Patient"]), - ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), - ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), - ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), - ...validateReference(res, profileName, "performer", ["PractitionerRole","USCoreCareTeam","USCoreOrganizationProfile","Patient","USCorePractitionerProfile","USCoreRelatedPersonProfile"]), - ] + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), + ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), + ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), + ...validateReference(res, profileName, "performer", ["PractitionerRole","USCoreCareTeam","USCoreOrganizationProfile","Patient","USCorePractitionerProfile","USCoreRelatedPersonProfile"]), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ...validateMustSupport(res, profileName, "performer"), + ], + } } } 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 6a505f26f..c50d98195 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 @@ -9,220 +9,246 @@ import type { HumanName } from "../../hl7-fhir-r4-core/HumanName"; import type { Identifier } from "../../hl7-fhir-r4-core/Identifier"; import type { Patient } from "../../hl7-fhir-r4-core/Patient"; +import { + USCoreEthnicityExtensionProfile, + type USCoreEthnicityExtensionProfileFlat, +} from "./Extension_USCoreEthnicityExtension"; +import { USCoreIndividualSexExtensionProfile } from "./Extension_USCoreIndividualSexExtension"; +import { USCoreInterpreterNeededExtensionProfile } from "./Extension_USCoreInterpreterNeededExtension"; +import { USCoreRaceExtensionProfile, type USCoreRaceExtensionProfileFlat } from "./Extension_USCoreRaceExtension"; +import { + USCoreTribalAffiliationExtensionProfile, + type USCoreTribalAffiliationExtensionProfileFlat, +} from "./Extension_USCoreTribalAffiliationExtension"; + export interface USCorePatientProfile extends Patient { identifier: Identifier[]; name: HumanName[]; } -export type USCorePatientProfile_RaceInput = { - ombCategory?: Coding; - detailed?: Coding[]; - text: string; -} - -export type USCorePatientProfile_EthnicityInput = { - ombCategory?: Coding; - detailed?: Coding[]; - text: string; -} - -export type USCorePatientProfile_TribalAffiliationInput = { - tribalAffiliation: CodeableConcept; - isEnrolled?: boolean; -} - -import { ensureProfile, extractComplexExtension, validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type USCorePatientProfileProfileParams = { +import { + buildResource, + ensureProfile, + extractComplexExtension, + isExtension, + getExtensionValue, + pushExtension, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCorePatientProfileRaw = { identifier: Identifier[]; name: HumanName[]; } // CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient (pkg: hl7.fhir.us.core#8.0.1) -export class USCorePatientProfileProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" +export class USCorePatientProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"; - private resource: Patient + private resource: Patient; constructor (resource: Patient) { - this.resource = resource - ensureProfile(resource, "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient") + this.resource = resource; } - static from (resource: Patient) : USCorePatientProfileProfile { - return new USCorePatientProfileProfile(resource) + static from (resource: Patient) : USCorePatientProfile { + if (!resource.meta?.profile?.includes(USCorePatientProfile.canonicalUrl)) { + throw new Error(`USCorePatientProfile: meta.profile must include ${USCorePatientProfile.canonicalUrl}`) + } + const profile = new USCorePatientProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Patient) : USCorePatientProfile { + ensureProfile(resource, USCorePatientProfile.canonicalUrl); + return new USCorePatientProfile(resource); } - static createResource (args: USCorePatientProfileProfileParams) : Patient { - const resource = { + static createResource (args: USCorePatientProfileRaw) : Patient { + const resource = buildResource( { resourceType: "Patient", identifier: args.identifier, name: args.name, - meta: { profile: [USCorePatientProfileProfile.canonicalUrl] }, - } as unknown as Patient - return resource + meta: { profile: [USCorePatientProfile.canonicalUrl] }, + }) + return resource; } - static create (args: USCorePatientProfileProfileParams) : USCorePatientProfileProfile { - return USCorePatientProfileProfile.from(USCorePatientProfileProfile.createResource(args)) + static create (args: USCorePatientProfileRaw) : USCorePatientProfile { + return USCorePatientProfile.apply(USCorePatientProfile.createResource(args)); } toResource () : Patient { - return this.resource + return this.resource; } // Field accessors - getIdentifier () : Identifier[] | undefined { - return this.resource.identifier as Identifier[] | undefined + return this.resource.identifier as Identifier[] | undefined; } setIdentifier (value: Identifier[]) : this { - Object.assign(this.resource, { identifier: value }) - return this + Object.assign(this.resource, { identifier: value }); + return this; } getName () : HumanName[] | undefined { - return this.resource.name as HumanName[] | undefined + return this.resource.name as HumanName[] | undefined; } setName (value: HumanName[]) : this { - Object.assign(this.resource, { name: value }) - return this - } - - toProfile () : USCorePatientProfile { - return this.resource as USCorePatientProfile - } - - // Slices and extensions - - public setRace (input: USCorePatientProfile_RaceInput): this { - const subExtensions: Extension[] = [] - if (input.ombCategory !== undefined) { - subExtensions.push({ url: "ombCategory", valueCoding: input.ombCategory }) + Object.assign(this.resource, { name: value }); + return this; + } + + // Extensions + public setRace (input: USCoreRaceExtensionProfileFlat | USCoreRaceExtensionProfile | Extension): this { + if (input instanceof USCoreRaceExtensionProfile) { + pushExtension(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) + } else { + pushExtension(this.resource, USCoreRaceExtensionProfile.createResource(input)) } - if (input.detailed) { - for (const item of input.detailed) { - subExtensions.push({ url: "detailed", valueCoding: item }) - } - } - if (input.text !== undefined) { - subExtensions.push({ url: "text", valueString: input.text }) - } - const list = (this.resource.extension ??= []) - list.push({ url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", extension: subExtensions }) return this } - public setEthnicity (input: USCorePatientProfile_EthnicityInput): this { - const subExtensions: Extension[] = [] - if (input.ombCategory !== undefined) { - subExtensions.push({ url: "ombCategory", valueCoding: input.ombCategory }) - } - if (input.detailed) { - for (const item of input.detailed) { - subExtensions.push({ url: "detailed", valueCoding: item }) - } - } - if (input.text !== undefined) { - subExtensions.push({ url: "text", valueString: input.text }) - } - const list = (this.resource.extension ??= []) - list.push({ url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", extension: subExtensions }) - return this - } - - public setTribalAffiliation (input: USCorePatientProfile_TribalAffiliationInput): this { - const subExtensions: Extension[] = [] - if (input.tribalAffiliation !== undefined) { - subExtensions.push({ url: "tribalAffiliation", valueCodeableConcept: input.tribalAffiliation }) - } - if (input.isEnrolled !== undefined) { - subExtensions.push({ url: "isEnrolled", valueBoolean: input.isEnrolled }) - } - const list = (this.resource.extension ??= []) - list.push({ url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation", extension: subExtensions }) - return this - } - - public setSex (value: Coding): this { - const list = (this.resource.extension ??= []) - list.push({ url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex", valueCoding: value } as Extension) - return this - } - - public setInterpreterRequired (value: Coding): this { - const list = (this.resource.extension ??= []) - list.push({ url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed", valueCoding: value } as Extension) - return this - } - - public getRace (): USCorePatientProfile_RaceInput | undefined { + public getRace(mode: 'flat'): USCoreRaceExtensionProfileFlat | undefined; + public getRace(mode: 'profile'): USCoreRaceExtensionProfile | undefined; + public getRace(mode: 'raw'): Extension | undefined; + public getRace(): USCoreRaceExtensionProfileFlat | undefined; + public getRace (mode: 'flat' | 'profile' | 'raw' = 'flat'): USCoreRaceExtensionProfileFlat | USCoreRaceExtensionProfile | Extension | undefined { const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race") if (!ext) return undefined + if (mode === 'raw') return ext + if (mode === 'profile') return USCoreRaceExtensionProfile.apply(ext) const config = [{ name: "ombCategory", valueField: "valueCoding", isArray: false }, { name: "detailed", valueField: "valueCoding", isArray: true }, { name: "text", valueField: "valueString", isArray: false }] - return extractComplexExtension(ext as unknown as { extension?: Array<{ url?: string; [key: string]: unknown }> }, config) as USCorePatientProfile_RaceInput + return extractComplexExtension(ext, config) } - public getRaceExtension (): Extension | undefined { - const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race") - return ext + public setEthnicity (input: USCoreEthnicityExtensionProfileFlat | USCoreEthnicityExtensionProfile | Extension): this { + if (input instanceof USCoreEthnicityExtensionProfile) { + pushExtension(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) + } else { + pushExtension(this.resource, USCoreEthnicityExtensionProfile.createResource(input)) + } + return this } - public getEthnicity (): USCorePatientProfile_EthnicityInput | undefined { + public getEthnicity(mode: 'flat'): USCoreEthnicityExtensionProfileFlat | undefined; + public getEthnicity(mode: 'profile'): USCoreEthnicityExtensionProfile | undefined; + public getEthnicity(mode: 'raw'): Extension | undefined; + public getEthnicity(): USCoreEthnicityExtensionProfileFlat | undefined; + public getEthnicity (mode: 'flat' | 'profile' | 'raw' = 'flat'): USCoreEthnicityExtensionProfileFlat | USCoreEthnicityExtensionProfile | Extension | undefined { const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity") if (!ext) return undefined + if (mode === 'raw') return ext + if (mode === 'profile') return USCoreEthnicityExtensionProfile.apply(ext) const config = [{ name: "ombCategory", valueField: "valueCoding", isArray: false }, { name: "detailed", valueField: "valueCoding", isArray: true }, { name: "text", valueField: "valueString", isArray: false }] - return extractComplexExtension(ext as unknown as { extension?: Array<{ url?: string; [key: string]: unknown }> }, config) as USCorePatientProfile_EthnicityInput + return extractComplexExtension(ext, config) } - public getEthnicityExtension (): Extension | undefined { - const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity") - return ext + public setTribalAffiliation (input: USCoreTribalAffiliationExtensionProfileFlat | USCoreTribalAffiliationExtensionProfile | Extension): this { + if (input instanceof USCoreTribalAffiliationExtensionProfile) { + pushExtension(this.resource, input.toResource()) + } else if (isExtension(input)) { + if (input.url !== "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation") throw new Error(`Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation', got '${input.url}'`) + pushExtension(this.resource, input) + } else { + pushExtension(this.resource, USCoreTribalAffiliationExtensionProfile.createResource(input)) + } + return this } - public getTribalAffiliation (): USCorePatientProfile_TribalAffiliationInput | undefined { + public getTribalAffiliation(mode: 'flat'): USCoreTribalAffiliationExtensionProfileFlat | undefined; + public getTribalAffiliation(mode: 'profile'): USCoreTribalAffiliationExtensionProfile | undefined; + public getTribalAffiliation(mode: 'raw'): Extension | undefined; + public getTribalAffiliation(): USCoreTribalAffiliationExtensionProfileFlat | undefined; + public getTribalAffiliation (mode: 'flat' | 'profile' | 'raw' = 'flat'): USCoreTribalAffiliationExtensionProfileFlat | USCoreTribalAffiliationExtensionProfile | Extension | undefined { const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation") if (!ext) return undefined + if (mode === 'raw') return ext + if (mode === 'profile') return USCoreTribalAffiliationExtensionProfile.apply(ext) const config = [{ name: "tribalAffiliation", valueField: "valueCodeableConcept", isArray: false }, { name: "isEnrolled", valueField: "valueBoolean", isArray: false }] - return extractComplexExtension(ext as unknown as { extension?: Array<{ url?: string; [key: string]: unknown }> }, config) as USCorePatientProfile_TribalAffiliationInput + return extractComplexExtension(ext, config) } - public getTribalAffiliationExtension (): Extension | undefined { - const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation") - return ext - } - - public getSex (): Coding | undefined { - const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex") - return (ext as Record | undefined)?.valueCoding as Coding | undefined + public setSex (value: USCoreIndividualSexExtensionProfile | Extension | Coding): this { + if (value instanceof USCoreIndividualSexExtensionProfile) { + pushExtension(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) + } else { + pushExtension(this.resource, USCoreIndividualSexExtensionProfile.createResource({ valueCoding: value as Coding })) + } + return this } - public getSexExtension (): Extension | undefined { + public getSex(mode: 'flat'): Coding | undefined; + public getSex(mode: 'profile'): USCoreIndividualSexExtensionProfile | undefined; + public getSex(mode: 'raw'): Extension | undefined; + public getSex(): Coding | undefined; + public getSex (mode: 'flat' | 'profile' | 'raw' = 'flat'): Coding | USCoreIndividualSexExtensionProfile | Extension | undefined { const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex") - return ext - } - - public getInterpreterRequired (): Coding | undefined { - const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed") - return (ext as Record | undefined)?.valueCoding as Coding | undefined + if (!ext) return undefined + if (mode === 'raw') return ext + if (mode === 'profile') return USCoreIndividualSexExtensionProfile.apply(ext) + return getExtensionValue(ext, "valueCoding") + } + + public setInterpreterRequired (value: USCoreInterpreterNeededExtensionProfile | Extension | Coding): this { + if (value instanceof USCoreInterpreterNeededExtensionProfile) { + pushExtension(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) + } else { + pushExtension(this.resource, USCoreInterpreterNeededExtensionProfile.createResource({ valueCoding: value as Coding })) + } + return this } - public getInterpreterRequiredExtension (): Extension | undefined { + public getInterpreterRequired(mode: 'flat'): Coding | undefined; + public getInterpreterRequired(mode: 'profile'): USCoreInterpreterNeededExtensionProfile | undefined; + public getInterpreterRequired(mode: 'raw'): Extension | undefined; + public getInterpreterRequired(): Coding | undefined; + public getInterpreterRequired (mode: 'flat' | 'profile' | 'raw' = 'flat'): Coding | USCoreInterpreterNeededExtensionProfile | Extension | undefined { const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed") - return ext + if (!ext) return undefined + if (mode === 'raw') return ext + if (mode === 'profile') return USCoreInterpreterNeededExtensionProfile.apply(ext) + return getExtensionValue(ext, "valueCoding") } + // Slices // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "USCorePatientProfile" - const res = this.resource as unknown as Record - return [ - ...validateRequired(res, profileName, "identifier"), - ...validateRequired(res, profileName, "name"), - ] + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "identifier"), + ...validateRequired(res, profileName, "name"), + ], + warnings: [ + ...validateMustSupport(res, profileName, "birthDate"), + ...validateMustSupport(res, profileName, "address"), + ], + } } } diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/index.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/index.ts index cfacf70dc..03d18461a 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/index.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-us-core/profiles/index.ts @@ -1,6 +1,9 @@ -export type { USCorePatientProfile } from "./Patient_USCorePatientProfile"; -export type { USCoreVitalSignsProfile } from "./Observation_USCoreVitalSignsProfile"; -export { USCoreBloodPressureProfileProfile } from "./Observation_USCoreBloodPressureProfile"; -export { USCoreBodyWeightProfileProfile } from "./Observation_USCoreBodyWeightProfile"; -export { USCorePatientProfileProfile } from "./Patient_USCorePatientProfile"; -export { USCoreVitalSignsProfileProfile } from "./Observation_USCoreVitalSignsProfile"; +export { USCoreBloodPressureProfile } from "./Observation_USCoreBloodPressureProfile"; +export { USCoreBodyWeightProfile } from "./Observation_USCoreBodyWeightProfile"; +export { USCoreEthnicityExtensionProfile } from "./Extension_USCoreEthnicityExtension"; +export { USCoreIndividualSexExtensionProfile } from "./Extension_USCoreIndividualSexExtension"; +export { USCoreInterpreterNeededExtensionProfile } from "./Extension_USCoreInterpreterNeededExtension"; +export { USCorePatientProfile } from "./Patient_USCorePatientProfile"; +export { USCoreRaceExtensionProfile } from "./Extension_USCoreRaceExtension"; +export { USCoreTribalAffiliationExtensionProfile } from "./Extension_USCoreTribalAffiliationExtension"; +export { USCoreVitalSignsProfile } from "./Observation_USCoreVitalSignsProfile"; diff --git a/examples/typescript-us-core/fhir-types/profile-helpers.ts b/examples/typescript-us-core/fhir-types/profile-helpers.ts index 4f45db45b..9191c8ff9 100644 --- a/examples/typescript-us-core/fhir-types/profile-helpers.ts +++ b/examples/typescript-us-core/fhir-types/profile-helpers.ts @@ -121,6 +121,42 @@ export const matchesValue = (value: unknown, match: unknown): boolean => { return value === match; }; +/** + * Type guard that discriminates a raw extension input (with an `extension` + * array) from a flat-API input object. Using a custom type guard instead of + * a bare `"extension" in args` lets TypeScript narrow *both* branches of the + * union — the plain `in` check cannot eliminate a type whose `extension` + * property is optional. + */ +export const isRawExtensionInput = (input: object): input is TRaw => "extension" in input; + +/** + * Type guard that tests whether an unknown setter input is a raw Extension + * (i.e. an object with a `url` property). When `url` is provided, also + * checks that the extension's URL matches the expected value. + */ +export const isExtension = (input: unknown, url?: string): input is E => + typeof input === "object" && input !== null && "url" in input && (url === undefined || input.url === url); + +/** + * Read a single typed value field from an Extension, returning `undefined` + * when the extension itself is absent or the field is not set. + * + * This avoids the double-cast `(ext as Record<…>)?.field as T` that would + * otherwise be needed for value fields not declared on the base Extension type. + */ +export const getExtensionValue = (ext: { url?: string } | undefined, field: string): T | undefined => { + if (!ext) return undefined; + return (ext as Record)[field] as T | undefined; +}; + +/** + * Push an extension onto `target.extension`, creating the array if absent. + */ +export const pushExtension = (target: { extension?: E[] }, ext: E): void => { + (target.extension ??= []).push(ext); +}; + // --------------------------------------------------------------------------- // Extension helpers // --------------------------------------------------------------------------- @@ -134,10 +170,10 @@ export const matchesValue = (value: unknown, match: unknown): boolean => { * @returns A record keyed by sub-extension URL, or `undefined` if the * extension has no nested children. */ -export const extractComplexExtension = ( - extension: { extension?: Array<{ url?: string; [key: string]: unknown }> } | undefined, +export const extractComplexExtension = >( + extension: { extension?: Array<{ url?: string }> } | undefined, config: Array<{ name: string; valueField: string; isArray: boolean }>, -): Record | undefined => { +): T | undefined => { if (!extension?.extension) return undefined; const result: Record = {}; for (const { name, valueField, isArray } of config) { @@ -148,7 +184,7 @@ export const extractComplexExtension = ( result[name] = (subExts[0] as Record)[valueField]; } } - return result; + return result as T; }; // --------------------------------------------------------------------------- @@ -219,6 +255,13 @@ export const ensureSliceDefaults = (items: T[], ...matches: Record(obj: object): T => obj as unknown as T; + /** * Add `canonicalUrl` to `resource.meta.profile` if not already present. * Creates `meta` and `profile` when missing. @@ -256,25 +299,31 @@ export const getArraySlice = (list: readonly T[] | undefined, match: Record, profileName: string, field: string): string[] => { - return res[field] === undefined || res[field] === null +export const validateRequired = (res: object, profileName: string, field: string): string[] => { + const rec = res as Record; + return rec[field] === undefined || rec[field] === null ? [`${profileName}: required field '${field}' is missing`] : []; }; +/** Checks that a must-support field is populated (warning, not error). */ +export const validateMustSupport = (res: object, profileName: string, field: string): string[] => { + const rec = res as Record; + return rec[field] === undefined || rec[field] === null + ? [`${profileName}: must-support field '${field}' is not populated`] + : []; +}; + /** Checks that `field` is absent (profiles may exclude base fields). */ -export const validateExcluded = (res: Record, profileName: string, field: string): string[] => { - return res[field] !== undefined ? [`${profileName}: field '${field}' must not be present`] : []; +export const validateExcluded = (res: object, profileName: string, field: string): string[] => { + return (res as Record)[field] !== undefined + ? [`${profileName}: field '${field}' must not be present`] + : []; }; /** Checks that `field` structurally contains the expected fixed value. */ -export const validateFixedValue = ( - res: Record, - profileName: string, - field: string, - expected: unknown, -): string[] => { - return matchesValue(res[field], expected) +export const validateFixedValue = (res: object, profileName: string, field: string, expected: unknown): string[] => { + return matchesValue((res as Record)[field], expected) ? [] : [`${profileName}: field '${field}' does not match expected fixed value`]; }; @@ -284,7 +333,7 @@ export const validateFixedValue = ( * discriminator) falls within [`min`, `max`]. Pass `max = 0` for unbounded. */ export const validateSliceCardinality = ( - res: Record, + res: object, profileName: string, field: string, match: Record, @@ -292,7 +341,7 @@ export const validateSliceCardinality = ( min: number, max: number, ): string[] => { - const items = res[field] as unknown[] | undefined; + const items = (res as Record)[field] as unknown[] | undefined; const count = (items ?? []).filter((item) => matchesValue(item, match)).length; const errors: string[] = []; if (count < min) { @@ -308,12 +357,9 @@ export const validateSliceCardinality = ( * Checks that at least one of the listed choice-type variants is present. * E.g. `["effectiveDateTime", "effectivePeriod"]`. */ -export const validateChoiceRequired = ( - res: Record, - profileName: string, - choices: string[], -): string[] => { - return choices.some((c) => res[c] !== undefined) +export const validateChoiceRequired = (res: object, profileName: string, choices: string[]): string[] => { + const rec = res as Record; + return choices.some((c) => rec[c] !== undefined) ? [] : [`${profileName}: at least one of ${choices.join(", ")} is required`]; }; @@ -323,13 +369,8 @@ export const validateChoiceRequired = ( * Handles plain strings, Coding objects, and CodeableConcept objects. * Skips validation when the field is absent. */ -export const validateEnum = ( - res: Record, - profileName: string, - field: string, - allowed: string[], -): string[] => { - const value = res[field]; +export const validateEnum = (res: object, profileName: string, field: string, allowed: string[]): string[] => { + const value = (res as Record)[field]; if (value === undefined || value === null) return []; if (typeof value === "string") { return allowed.includes(value) @@ -357,13 +398,8 @@ export const validateEnum = ( * types. Extracts the type from the `reference` string (the part before * the first `/`). Skips validation when the field or reference is absent. */ -export const validateReference = ( - res: Record, - profileName: string, - field: string, - allowed: string[], -): string[] => { - const value = res[field]; +export const validateReference = (res: object, profileName: string, field: string, allowed: string[]): string[] => { + const value = (res as Record)[field]; if (value === undefined || value === null) return []; const ref = (value as Record).reference as string | undefined; if (!ref) return []; diff --git a/examples/typescript-us-core/fhir-types/examples/typescript-us-core/type-tree.yaml b/examples/typescript-us-core/fhir-types/type-tree.yaml similarity index 94% rename from examples/typescript-us-core/fhir-types/examples/typescript-us-core/type-tree.yaml rename to examples/typescript-us-core/fhir-types/type-tree.yaml index 5398bcb4c..e720815bd 100644 --- a/examples/typescript-us-core/fhir-types/examples/typescript-us-core/type-tree.yaml +++ b/examples/typescript-us-core/fhir-types/type-tree.yaml @@ -13,7 +13,12 @@ hl7.fhir.us.core: profile: http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure: {} http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight: {} + http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity: {} + http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex: {} + http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed: {} http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient: {} + http://hl7.org/fhir/us/core/StructureDefinition/us-core-race: {} + http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation: {} http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs: {} logical: {} hl7.fhir.r4.core: diff --git a/examples/typescript-us-core/generate.ts b/examples/typescript-us-core/generate.ts index 230b42c71..b6aa82d0f 100644 --- a/examples/typescript-us-core/generate.ts +++ b/examples/typescript-us-core/generate.ts @@ -15,6 +15,11 @@ if (require.main === module) { "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient": {}, "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure": {}, "http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed": {}, }, }, }) @@ -24,12 +29,15 @@ if (require.main === module) { openResourceTypeSet: false, }) .outputTo("./examples/typescript-us-core/fhir-types") - .introspection({ typeTree: "./examples/typescript-us-core/type-tree.yaml" }) + .introspection({ + typeTree: "./type-tree.yaml", + typeSchemas: "./ts", + }) .cleanOutput(true); const report = await builder.generate(); - console.log(report); + // console.log(report); if (report.success) { console.log("✅ FHIR US Core types generated successfully!"); diff --git a/examples/typescript-us-core/multi-profile-demo.ts b/examples/typescript-us-core/multi-profile-demo.ts deleted file mode 100644 index bafc2efd0..000000000 --- a/examples/typescript-us-core/multi-profile-demo.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Demonstrates using profile classes with the generated getter/setter methods. - * - * This example shows: - * 1. Creating observations with Body Weight profile - * 2. Using setters to apply profile-defined slices (no input needed for constant slices) - * 3. Using getters to read values: - * - getVSCat() - returns flat API (simplified, without discriminator) - * - getVSCatRaw() - returns full FHIR type (with discriminator) - * 4. The override interface for type-safe cardinality constraints - */ - -import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation"; -import { USCoreBodyWeightProfileProfile as usWeightProfile } from "./fhir-types/hl7-fhir-us-core/profiles"; - -// Helper to create a base Observation resource -const createBaseObservation = (): Observation => ({ - resourceType: "Observation", - status: "final", - code: {}, -}); - -// Example 1: Create a Body Weight observation -function createBodyWeightObservation(): Observation { - const resource = createBaseObservation(); - const profile = new usWeightProfile(resource); - - // Set the vital-signs category slice (auto-applies discriminator) - // No input needed when all fields are part of the discriminator - profile.setVSCat(); - - // Set additional required fields - resource.code = { - coding: [{ system: "http://loinc.org", code: "29463-7", display: "Body Weight" }], - }; - resource.valueQuantity = { - value: 70, - unit: "kg", - system: "http://unitsofmeasure.org", - code: "kg", - }; - resource.subject = { reference: "Patient/example" }; - resource.effectiveDateTime = new Date().toISOString(); - - return profile.toResource(); -} - -// Example 2: Using getters to read values -function demonstrateGetters() { - const resource = createBaseObservation(); - const profile = new usWeightProfile(resource); - profile.setVSCat(); - - // Get simplified value (without discriminator) - flat API - const simplified = profile.getVSCat(); - console.log("Simplified slice:", simplified); - - // Get raw value (with discriminator) - full FHIR type - const raw = profile.getVSCatRaw(); - console.log("Raw slice:", raw); - - // The raw value includes the coding discriminator - console.log("Raw coding:", raw?.coding); -} - -// Example 3: Using override interface types -function demonstrateTypeNarrowing() { - // The USCoreBodyWeightProfile interface extends Observation with: - // - subject: Reference<"Patient"> (narrowed from broader union) - // - category: CodeableConcept[] (made required) - - const resource = createBaseObservation(); - const profile = new usWeightProfile(resource); - profile.setVSCat(); - - // TypeScript knows this is an Observation - // The override interface ensures type safety for constrained fields - console.log("Resource type:", profile.toResource().resourceType); -} - -// Run examples -if (require.main === module) { - console.log("=== Body Weight Observation ==="); - console.log(JSON.stringify(createBodyWeightObservation(), null, 2)); - - console.log("\n=== Getter Demonstration ==="); - demonstrateGetters(); - - console.log("\n=== Type Narrowing ==="); - demonstrateTypeNarrowing(); -} diff --git a/examples/typescript-us-core/multi-profile.test.ts b/examples/typescript-us-core/multi-profile.test.ts deleted file mode 100644 index cc1345244..000000000 --- a/examples/typescript-us-core/multi-profile.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation"; -import { USCoreBodyWeightProfileProfile as usWeightProfile } from "./fhir-types/hl7-fhir-us-core/profiles"; - -const createObservation = (): Observation => ({ resourceType: "Observation", status: "final", code: {} }); - -describe("Multi-Profile Pattern", () => { - describe("Body Weight Profile", () => { - it("creates valid body weight observation", () => { - const profile = new usWeightProfile(createObservation()); - profile.setVSCat({}); - - const resource = profile.toResource(); - expect(resource.resourceType).toBe("Observation"); - expect(resource.category).toBeDefined(); - expect(resource.category?.length).toBeGreaterThan(0); - }); - - it("has getters for slices", () => { - const profile = new usWeightProfile(createObservation()); - profile.setVSCat({}); - - const vscat = profile.getVSCat(); - expect(vscat).toBeDefined(); - }); - }); - - describe("Profile composition patterns", () => { - it("wraps existing resource with profile", () => { - // Create a base observation - const baseObservation: Observation = { - resourceType: "Observation", - status: "final", - code: { coding: [{ system: "http://loinc.org", code: "29463-7", display: "Body Weight" }] }, - }; - - // Wrap with body weight profile - const weightProfile = new usWeightProfile(baseObservation); - weightProfile.setVSCat({}); - - const resource = weightProfile.toResource(); - expect(resource.status).toBe("final"); - expect(resource.code?.coding?.[0]?.code).toBe("29463-7"); - expect(resource.category).toBeDefined(); - }); - - it("creates weight observation with value", () => { - const weightProfile = new usWeightProfile(createObservation()); - weightProfile.setVSCat({}); - const weightObs = weightProfile.toResource(); - weightObs.code = { coding: [{ system: "http://loinc.org", code: "29463-7", display: "Body Weight" }] }; - weightObs.valueQuantity = { value: 70, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }; - - expect(weightObs.category?.length).toBeGreaterThan(0); - expect(weightObs.code?.coding?.[0]?.code).toBe("29463-7"); - expect(weightObs.valueQuantity?.value).toBe(70); - }); - }); - - describe("Override interface type safety", () => { - it("profile class uses narrowed types from override interface", () => { - const profile = new usWeightProfile(createObservation()); - const resource = profile.toResource(); - expect(resource.resourceType).toBe("Observation"); - }); - }); -}); diff --git a/examples/typescript-us-core/profile-bodyweight.test.ts b/examples/typescript-us-core/profile-bodyweight.test.ts new file mode 100644 index 000000000..c1c88e201 --- /dev/null +++ b/examples/typescript-us-core/profile-bodyweight.test.ts @@ -0,0 +1,90 @@ +/** + * 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"] }, + 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); + + 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"); + }); + + test("apply profile to a bare Observation and populate it", () => { + const bareObservation: Observation = { resourceType: "Observation", status: "preliminary", code: {} }; + const profile = USCoreBodyWeightProfile.apply(bareObservation); + + 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({ + status: "final", + subject: { reference: "Patient/example" }, + }); + + profile.setValueQuantity({ value: 70, unit: "kg", system: "http://unitsofmeasure.org", code: "kg" }); + profile.setEffectiveDateTime("2024-01-15"); + + 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([]); + }); + + 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 new file mode 100644 index 000000000..1a2f75be8 --- /dev/null +++ b/examples/typescript-us-core/profile-bp.test.ts @@ -0,0 +1,249 @@ +/** + * US Core Blood Pressure Profile Class API Tests + */ + +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", () => { + test("import a profiled Observation from an API and read components", () => { + const apiResponse: 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", + }, + }, + ] as any, + code: { coding: [{ code: "85354-9", system: "http://loinc.org", display: "Blood pressure panel" }] }, + subject: { reference: "Patient/pt-1" }, + effectiveDateTime: "2024-06-15", + component: [ + { + code: { coding: [{ code: "8480-6", system: "http://loinc.org" }] }, + valueQuantity: { value: 120, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }, + }, + { + code: { coding: [{ code: "8462-4", system: "http://loinc.org" }] }, + valueQuantity: { value: 80, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }, + }, + ], + }; + + const profile = USCoreBloodPressureProfile.from(apiResponse); + + 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]", + }); + 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); + + 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", + }); + }); + + 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(); + }); + + test("create() with custom category preserves user values and adds required VSCat", () => { + const custom = USCoreBloodPressureProfile.create({ + status: "final", + subject: { reference: "Patient/pt-1" }, + category: [{ text: "My Category" }], + }); + const obs = custom.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", + }); + }); + + test("create() with category already containing VSCat does not duplicate it", () => { + const custom = 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", + }, + }, + ] as any, + }); + const obs = custom.toResource(); + expect(obs.category).toHaveLength(1); + }); +}); diff --git a/examples/typescript-us-core/profile-demo.ts b/examples/typescript-us-core/profile-demo.ts deleted file mode 100644 index 64e54619f..000000000 --- a/examples/typescript-us-core/profile-demo.ts +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Demo file showing how to use generated FHIR profile classes. - * - * Profile classes provide a fluent API for working with FHIR profiles, - * making it easy to set extensions and slices with proper discriminator values. - * - * Run this demo with: bun run examples/typescript-us-core/profile-demo.ts - */ - -import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation"; -import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient"; -import { - USCoreBloodPressureProfileProfile as usBpProfile, - USCorePatientProfileProfile as usPatientProfile, -} from "./fhir-types/hl7-fhir-us-core/profiles"; - -// ============================================================================= -// Example 1: Creating a US Core Patient with extensions (flat API) -// ============================================================================= -function createPatientExample(): Patient { - console.log("=== Example 1: US Core Patient Profile ===\n"); - - // Create a new patient profile builder with a base Patient resource - const USCorePatientProfileProfile = new usPatientProfile({ resourceType: "Patient" }); - - // Use fluent API with FLAT object structure - no nested extensions! - USCorePatientProfileProfile.setRace({ - // Flat API - just provide the values directly - ombCategory: { - system: "urn:oid:2.16.840.1.113883.6.238", - code: "2106-3", - display: "White", - }, - text: "White", - }) - .setEthnicity({ - // Flat API - no need to deal with extension arrays - ombCategory: { - system: "urn:oid:2.16.840.1.113883.6.238", - code: "2186-5", - display: "Not Hispanic or Latino", - }, - text: "Not Hispanic or Latino", - }) - .setSex({ - system: "http://hl7.org/fhir/us/core/CodeSystem/birthsex", - code: "M", - display: "Male", - }); - - // Get the underlying FHIR resource - const patient = USCorePatientProfileProfile.toResource(); - - // Add additional fields directly to the resource - patient.id = "example-patient-1"; - patient.identifier = [ - { - system: "http://hospital.example.org/patients", - value: "12345", - }, - ]; - patient.name = [ - { - family: "Smith", - given: ["John", "William"], - }, - ]; - patient.gender = "male"; - patient.birthDate = "1970-01-15"; - - console.log("Created Patient resource:"); - console.log(JSON.stringify(patient, null, 2)); - console.log("\n"); - - return patient; -} - -// ============================================================================= -// Example 2: Creating a Blood Pressure Observation with slices -// ============================================================================= -function createBloodPressureExample(): Observation { - console.log("=== Example 2: US Core Blood Pressure Profile ===\n"); - - // Create a blood pressure profile builder with a base Observation resource - const USCoreBloodPressureProfileProfile = new usBpProfile({ resourceType: "Observation" } as Observation); - - // Use fluent API to set slices - discriminator values are auto-applied - USCoreBloodPressureProfileProfile.setVSCat() - .setSystolic({ - // The code for systolic (8480-6) is auto-applied by the profile - // Quantity fields are flat (valueQuantity is unwrapped) - value: 120, - unit: "mmHg", - system: "http://unitsofmeasure.org", - code: "mm[Hg]", - }) - .setDiastolic({ - // The code for diastolic (8462-4) is auto-applied by the profile - value: 80, - unit: "mmHg", - system: "http://unitsofmeasure.org", - code: "mm[Hg]", - }); - - // Get the underlying FHIR resource - const observation = USCoreBloodPressureProfileProfile.toResource(); - - // Add additional fields - observation.id = "blood-pressure-1"; - observation.status = "final"; - observation.code = { - coding: [ - { - system: "http://loinc.org", - code: "85354-9", - display: "Blood pressure panel with all children optional", - }, - ], - text: "Blood Pressure", - }; - observation.effectiveDateTime = "2024-01-15T10:30:00Z"; - observation.subject = { - reference: "Patient/example-patient-1", - }; - - console.log("Created Blood Pressure Observation:"); - console.log(JSON.stringify(observation, null, 2)); - console.log("\n"); - - return observation; -} - -// ============================================================================= -// Example 3: Wrapping an existing resource -// ============================================================================= -function wrapExistingResource(): Patient { - console.log("=== Example 3: Wrapping an Existing Resource ===\n"); - - // Existing patient resource (e.g., from API) - const existingPatient: Patient = { - resourceType: "Patient", - id: "existing-patient", - name: [{ family: "Doe", given: ["Jane"] }], - gender: "female", - }; - - // Wrap it with the profile class to add US Core extensions - const USCorePatientProfileProfile = new usPatientProfile(existingPatient); - - // Add extensions using the fluent API - USCorePatientProfileProfile.setSex({ - system: "http://hl7.org/fhir/us/core/CodeSystem/birthsex", - code: "F", - display: "Female", - }); - - const updatedPatient = USCorePatientProfileProfile.toResource(); - - console.log("Updated Patient with extensions:"); - console.log(JSON.stringify(updatedPatient, null, 2)); - console.log("\n"); - - return updatedPatient; -} - -// ============================================================================= -// Example 4: Using reset methods -// ============================================================================= -function resetExtensionExample(): Patient { - console.log("=== Example 4: Resetting Extensions ===\n"); - - // Create patient with extensions - const USCorePatientProfileProfile = new usPatientProfile({ resourceType: "Patient" }); - USCorePatientProfileProfile.setSex({ - system: "http://hl7.org/fhir/us/core/CodeSystem/birthsex", - code: "M", - display: "Male", - }).setInterpreterRequired({ - system: "http://terminology.hl7.org/CodeSystem/v2-0136", - code: "Y", - display: "Yes", - }); - - console.log("Before reset - extensions count:", USCorePatientProfileProfile.toResource().extension?.length); - - console.log( - "After resetInterpreterRequired - extensions count:", - USCorePatientProfileProfile.toResource().extension?.length, - ); - - const patient = USCorePatientProfileProfile.toResource(); - console.log("Patient after reset:"); - console.log(JSON.stringify(patient, null, 2)); - console.log("\n"); - - return patient; -} - -// ============================================================================= -// Run all examples -// ============================================================================= -console.log("FHIR Profile Usage Demo\n"); -console.log("This demo shows how to use generated profile classes to work with"); -console.log("FHIR profiles in a type-safe manner.\n"); -console.log(`${"=".repeat(70)}\n`); - -createPatientExample(); -createBloodPressureExample(); -wrapExistingResource(); -resetExtensionExample(); - -console.log("=".repeat(70)); -console.log("\nDemo completed successfully!"); diff --git a/examples/typescript-us-core/profile-getters.test.ts b/examples/typescript-us-core/profile-getters.test.ts deleted file mode 100644 index a255eb9d2..000000000 --- a/examples/typescript-us-core/profile-getters.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation"; -import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient"; -import { - USCoreBloodPressureProfileProfile as usBpProfile, - USCorePatientProfileProfile as usPatientProfile, -} from "./fhir-types/hl7-fhir-us-core/profiles"; - -const createPatient = (): Patient => ({ resourceType: "Patient" }); -const createObservation = (): Observation => ({ resourceType: "Observation", status: "final", code: {} }); - -describe("Profile Getter Methods", () => { - describe("Extension getters", () => { - it("returns simplified object via getRace()", () => { - const profile = new usPatientProfile(createPatient()); - profile.setRace({ - ombCategory: { system: "urn:oid:2.16.840.1.113883.6.238", code: "2106-3", display: "White" }, - text: "White", - }); - - const result = profile.getRace(); - expect(result).toBeDefined(); - expect(result?.ombCategory?.code).toBe("2106-3"); - expect(result?.text).toBe("White"); - }); - - it("returns raw Extension via getRaceExtension()", () => { - const profile = new usPatientProfile(createPatient()); - profile.setRace({ - ombCategory: { system: "urn:oid:2.16.840.1.113883.6.238", code: "2106-3", display: "White" }, - text: "White", - }); - - const raw = profile.getRaceExtension(); - expect(raw).toBeDefined(); - expect(raw?.url).toBe("http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"); - expect(raw?.extension).toBeArray(); - }); - - it("returns undefined when extension not set", () => { - const profile = new usPatientProfile(createPatient()); - expect(profile.getRace()).toBeUndefined(); - }); - - it("simple extension getter returns value directly", () => { - const profile = new usPatientProfile(createPatient()); - profile.setSex({ system: "http://hl7.org/fhir/administrative-gender", code: "male" }); - - const result = profile.getSex(); - expect(result).toBeDefined(); - expect(result?.code).toBe("male"); - }); - - it("simple extension getSexExtension() returns raw Extension", () => { - const profile = new usPatientProfile(createPatient()); - profile.setSex({ system: "http://hl7.org/fhir/administrative-gender", code: "male" }); - - const raw = profile.getSexExtension(); - expect(raw).toBeDefined(); - expect(raw?.url).toBe("http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex"); - expect(raw?.valueCoding?.code).toBe("male"); - }); - }); - - describe("Slice getters", () => { - it("returns simplified slice without discriminator via getSystolic()", () => { - const profile = new usBpProfile(createObservation()); - profile.setSystolic({ value: 120, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }); - - const result = profile.getSystolic(); - expect(result).toBeDefined(); - expect(result?.value).toBe(120); - }); - - it("returns full slice with discriminator via getSystolicRaw()", () => { - const profile = new usBpProfile(createObservation()); - profile.setSystolic({ value: 120, unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" }); - - const raw = profile.getSystolicRaw(); - expect(raw).toBeDefined(); - expect(raw?.valueQuantity?.value).toBe(120); - // Raw should include the code discriminator - expect(raw?.code?.coding?.[0]?.code).toBe("8480-6"); - }); - - it("returns undefined when slice not set", () => { - const profile = new usBpProfile(createObservation()); - expect(profile.getSystolic()).toBeUndefined(); - expect(profile.getDiastolic()).toBeUndefined(); - }); - - it("can get multiple slices independently", () => { - const profile = new usBpProfile(createObservation()); - profile.setSystolic({ value: 120, unit: "mmHg" }); - profile.setDiastolic({ value: 80, unit: "mmHg" }); - - const systolic = profile.getSystolic(); - const diastolic = profile.getDiastolic(); - - expect(systolic?.value).toBe(120); - expect(diastolic?.value).toBe(80); - }); - }); - - describe("Round-trip: set and get", () => { - it("can set and get extension values", () => { - const profile = new usPatientProfile(createPatient()); - - // Set values - profile.setRace({ - ombCategory: { code: "2106-3", display: "White" }, - detailed: [{ code: "2108-9", display: "European" }], - text: "White European", - }); - - // Get and verify - const race = profile.getRace(); - expect(race?.text).toBe("White European"); - - // Verify the resource has the extension - const resource = profile.toResource(); - expect(resource.extension).toBeArray(); - expect( - resource.extension?.some( - (e) => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", - ), - ).toBe(true); - }); - }); -}); diff --git a/examples/typescript-us-core/profile-patient.test.ts b/examples/typescript-us-core/profile-patient.test.ts new file mode 100644 index 000000000..419503756 --- /dev/null +++ b/examples/typescript-us-core/profile-patient.test.ts @@ -0,0 +1,477 @@ +/** + * US Core Patient Profile Class API Tests + * + * Demonstrates all profile features: + * - Creation methods: create(), createResource(), from() (validating), apply() (non-validating) + * - Field accessors with fluent chaining + * - Complex extension setters (flat input, profile instance, raw Extension) + * - Simple extension setters (flat input, profile instance, raw Extension) + * - Extension getters: simplified and raw Extension forms + * - Validation of required fields + */ + +import { describe, expect, test } from "bun:test"; +import type { Extension } from "./fhir-types/hl7-fhir-r4-core/Extension"; +import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient"; +import { + USCoreEthnicityExtensionProfile, + USCoreIndividualSexExtensionProfile, + USCorePatientProfile, + USCoreRaceExtensionProfile, +} from "./fhir-types/hl7-fhir-us-core/profiles"; +import type { USCoreRaceExtensionProfileFlat } from "./fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreRaceExtension"; + +describe("demo", () => { + test("three ways to set an extension: flat input, profile instance, raw Extension", () => { + const patient: USCorePatientProfile = USCorePatientProfile.create({ + identifier: [{ system: "http://hospital.example.org/mrn", value: "MRN-12345" }], + name: [{ family: "Garcia", given: ["Maria", "Elena"] }], + }); + + const raceInput: USCoreRaceExtensionProfileFlat = { + ombCategory: { system: "urn:oid:2.16.840.1.113883.6.238", code: "2106-3", display: "White" }, + text: "White", + }; + patient.setRace(raceInput); + + const ethnicityProfile: USCoreEthnicityExtensionProfile = USCoreEthnicityExtensionProfile.create({ + ombCategory: { code: "2135-2", display: "Hispanic or Latino" }, + detailed: [{ code: "2148-5", display: "Mexican" }], + text: "Mexican", + }); + patient.setEthnicity(ethnicityProfile); + + const sexExtension: Extension = { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex", + valueCoding: { code: "female", display: "Female" }, + }; + patient.setSex(sexExtension); + + expect(patient.validate().errors).toEqual([]); + expect(patient.toResource()).toEqual({ + resourceType: "Patient", + identifier: [{ system: "http://hospital.example.org/mrn", value: "MRN-12345" }], + name: [{ family: "Garcia", given: ["Maria", "Elena"] }], + meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"] }, + extension: [ + { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + extension: [ + { + url: "ombCategory", + valueCoding: { + system: "urn:oid:2.16.840.1.113883.6.238", + code: "2106-3", + display: "White", + }, + }, + { url: "text", valueString: "White" }, + ], + }, + { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + 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-individual-sex", + valueCoding: { code: "female", display: "Female" }, + }, + ], + }); + }); + + test("import a profiled resource from an API and access data via typed getters", () => { + const apiResponse: Patient = { + resourceType: "Patient", + meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"] }, + identifier: [{ system: "http://hospital.example.org/mrn", value: "MRN-99999" }], + name: [{ family: "Smith", given: ["John"] }], + extension: [ + { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + extension: [ + { url: "ombCategory", valueCoding: { code: "2054-5", display: "Black or African American" } }, + { url: "text", valueString: "Black or African American" }, + ], + }, + { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex", + valueCoding: { code: "male" }, + }, + ], + }; + + const patient = USCorePatientProfile.from(apiResponse); + + expect(apiResponse.meta?.profile).toContain("http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"); + expect(patient.getName()).toEqual([{ family: "Smith", given: ["John"] }]); + expect(patient.getRace()).toEqual({ + ombCategory: { code: "2054-5", display: "Black or African American" }, + detailed: [], + text: "Black or African American", + }); + expect(patient.getSex()).toEqual({ code: "male" }); + expect(patient.getEthnicity()).toBeUndefined(); + }); + + test("apply profile to a bare resource and populate it", () => { + const patient = USCorePatientProfile.apply({ resourceType: "Patient" }); + + patient.setIdentifier([{ system: "http://hospital.example.org/mrn", value: "MRN-00001" }]); + patient.setName([{ family: "Chen", given: ["Wei"] }]); + patient.setRace({ ombCategory: { code: "2028-9", display: "Asian" }, text: "Chinese" }); + patient.setEthnicity({ text: "Not Hispanic or Latino" }); + + expect(patient.validate().errors).toEqual([]); + expect(patient.toResource()).toEqual({ + resourceType: "Patient", + identifier: [{ system: "http://hospital.example.org/mrn", value: "MRN-00001" }], + name: [{ family: "Chen", given: ["Wei"] }], + meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"] }, + extension: [ + { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + extension: [ + { url: "ombCategory", valueCoding: { code: "2028-9", display: "Asian" } }, + { url: "text", valueString: "Chinese" }, + ], + }, + { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + extension: [{ url: "text", valueString: "Not Hispanic or Latino" }], + }, + ], + }); + }); +}); + +describe("US Core Patient profile creation", () => { + let fromCreate: Patient; + let fromCreateResource: Patient; + let fromFrom: Patient; + + test("create() returns a profile wrapping the resource", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ system: "http://hospital.example.org", value: "12345" }], + name: [{ family: "Smith", given: ["John"] }], + }); + fromCreate = profile.toResource(); + + expect(fromCreate.resourceType).toBe("Patient"); + expect(fromCreate.identifier![0]!.value).toBe("12345"); + expect(fromCreate.name![0]!.family).toBe("Smith"); + }); + + test("createResource() returns a plain Patient", () => { + fromCreateResource = USCorePatientProfile.createResource({ + identifier: [{ system: "http://hospital.example.org", value: "12345" }], + name: [{ family: "Smith", given: ["John"] }], + }); + + expect(fromCreateResource.resourceType).toBe("Patient"); + expect(fromCreateResource.identifier![0]!.value).toBe("12345"); + }); + + test("apply() wraps an existing Patient", () => { + const patient: Patient = { resourceType: "Patient" }; + const profile = USCorePatientProfile.apply(patient); + + profile + .setIdentifier([{ system: "http://hospital.example.org", value: "12345" }]) + .setName([{ family: "Smith", given: ["John"] }]); + + fromFrom = profile.toResource(); + + expect(fromFrom).toBe(patient); // same reference + expect(profile.getIdentifier()![0]!.value).toBe("12345"); + expect(profile.getName()![0]!.family).toBe("Smith"); + }); + + test("all three methods produce equal resources", () => { + expect(fromCreate).toEqual(fromCreateResource); + expect(fromCreate).toEqual(fromFrom); + }); + + test("all three methods set meta.profile", () => { + const expected = ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"]; + expect(fromCreate.meta?.profile).toEqual(expected); + expect(fromCreateResource.meta?.profile).toEqual(expected); + expect(fromFrom.meta?.profile).toEqual(expected); + }); +}); + +describe("US Core Patient profile field accessors", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ system: "http://hospital.example.org", value: "12345" }], + name: [{ family: "Smith", given: ["John"] }], + }); + + test("getIdentifier / setIdentifier", () => { + expect(profile.getIdentifier()![0]!.value).toBe("12345"); + profile.setIdentifier([{ system: "http://hospital.example.org", value: "67890" }]); + expect(profile.getIdentifier()![0]!.value).toBe("67890"); + }); + + test("getName / setName", () => { + expect(profile.getName()![0]!.family).toBe("Smith"); + profile.setName([{ family: "Doe", given: ["Jane"] }]); + expect(profile.getName()![0]!.family).toBe("Doe"); + }); + + test("fluent chaining across field accessors", () => { + const result = profile + .setIdentifier([{ system: "http://hospital.example.org", value: "AAA" }]) + .setName([{ family: "Lee" }]); + + expect(result).toBe(profile); + expect(profile.getIdentifier()![0]!.value).toBe("AAA"); + expect(profile.getName()![0]!.family).toBe("Lee"); + }); +}); + +describe("US Core Patient profile extensions", () => { + test("canonicalUrl is exposed", () => { + expect(USCorePatientProfile.canonicalUrl).toBe( + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient", + ); + }); + + test("setRace / getRace round-trip with detailed categories", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + profile.setRace({ + ombCategory: { system: "urn:oid:2.16.840.1.113883.6.238", code: "2106-3", display: "White" }, + detailed: [{ code: "2108-9", display: "European" }], + text: "White European", + }); + + const race = profile.getRace(); + expect(race?.ombCategory?.code).toBe("2106-3"); + expect(race?.text).toBe("White European"); + }); + + test("getRace('raw') returns raw Extension", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + profile.setRace({ + ombCategory: { code: "2106-3", display: "White" }, + text: "White", + }); + + const raw = profile.getRace("raw"); + expect(raw).toBeDefined(); + expect(raw?.url).toBe("http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"); + expect(raw?.extension).toBeArray(); + }); + + test("setSex / getSex round-trip", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + profile.setSex({ system: "http://hl7.org/fhir/administrative-gender", code: "male" }); + + expect(profile.getSex()?.code).toBe("male"); + }); + + test("getSex('raw') returns raw Extension", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + profile.setSex({ system: "http://hl7.org/fhir/administrative-gender", code: "female" }); + + const raw = profile.getSex("raw"); + expect(raw?.url).toBe("http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex"); + expect(raw?.valueCoding?.code).toBe("female"); + }); + + test("extension getters return undefined when not set", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + expect(profile.getRace()).toBeUndefined(); + expect(profile.getEthnicity()).toBeUndefined(); + expect(profile.getSex()).toBeUndefined(); + expect(profile.getTribalAffiliation()).toBeUndefined(); + expect(profile.getInterpreterRequired()).toBeUndefined(); + + expect(profile.getRace("raw")).toBeUndefined(); + expect(profile.getEthnicity("raw")).toBeUndefined(); + expect(profile.getSex("raw")).toBeUndefined(); + expect(profile.getTribalAffiliation("raw")).toBeUndefined(); + expect(profile.getInterpreterRequired("raw")).toBeUndefined(); + }); + + test("fluent chaining across extensions", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + const result = profile + .setRace({ text: "White" }) + .setEthnicity({ text: "Not Hispanic or Latino" }) + .setSex({ code: "male" }) + .setTribalAffiliation({ tribalAffiliation: { text: "Navajo" } }) + .setInterpreterRequired({ code: "no" }); + + expect(result).toBe(profile); + expect(profile.getRace()?.text).toBe("White"); + expect(profile.getEthnicity()?.text).toBe("Not Hispanic or Latino"); + expect(profile.getSex()?.code).toBe("male"); + expect(profile.getTribalAffiliation()?.tribalAffiliation?.text).toBe("Navajo"); + expect(profile.getInterpreterRequired()?.code).toBe("no"); + }); + + test("extensions are added to the resource", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + profile.setRace({ text: "White" }).setSex({ code: "male" }); + + const resource = profile.toResource(); + expect(resource.extension).toBeArray(); + expect(resource.extension!.length).toBe(2); + expect(resource.extension!.some((e) => e.url?.includes("us-core-race"))).toBe(true); + expect(resource.extension!.some((e) => e.url?.includes("us-core-individual-sex"))).toBe(true); + }); +}); + +describe("US Core Patient multi-form extension setters", () => { + // -- Race: complex extension (representative for ethnicity, tribal) -- + + test("setRace accepts extension profile instance", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + const raceProfile = USCoreRaceExtensionProfile.create({ extension: [] }); + raceProfile.setExtensionOmbCategory({ code: "2106-3", display: "White" }); + raceProfile.setExtensionText({ valueString: "White" }); + + profile.setRace(raceProfile); + + const raw = profile.getRace("raw"); + expect(raw?.url).toBe("http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"); + expect(raw?.extension).toBeArray(); + }); + + test("setRace accepts raw Extension", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + const rawExtension: Extension = { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + extension: [ + { url: "ombCategory", valueCoding: { code: "2106-3", display: "White" } }, + { url: "text", valueString: "White" }, + ], + }; + + profile.setRace(rawExtension); + + const raw = profile.getRace("raw"); + expect(raw?.url).toBe("http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"); + expect(raw).toBe(rawExtension); + }); + + test("setRace throws on wrong Extension url", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + const wrongExtension: Extension = { + url: "http://example.com/wrong-url", + extension: [], + }; + + expect(() => profile.setRace(wrongExtension)).toThrow("Expected extension url"); + }); + + // -- Sex: simple extension (representative for interpreterRequired) -- + + test("setSex accepts extension profile instance", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + const sexProfile = USCoreIndividualSexExtensionProfile.create({ valueCoding: { code: "male" } }); + profile.setSex(sexProfile); + + const raw = profile.getSex("raw"); + expect(raw?.url).toBe("http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex"); + expect(raw?.valueCoding?.code).toBe("male"); + }); + + test("setSex accepts raw Extension", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + const rawExtension: Extension = { + url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex", + valueCoding: { code: "female" }, + }; + + profile.setSex(rawExtension); + + const raw = profile.getSex("raw"); + expect(raw?.url).toBe("http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex"); + expect(raw).toBe(rawExtension); + }); +}); + +describe("US Core Patient profile mutability", () => { + test("profile mutates the underlying resource", () => { + const patient: Patient = { resourceType: "Patient" }; + const profile = USCorePatientProfile.apply(patient); + + profile.setIdentifier([{ value: "123" }]); + expect(patient.identifier![0]!.value).toBe("123"); + + profile.setName([{ family: "Doe" }]); + expect(patient.name![0]!.family).toBe("Doe"); + }); +}); + +describe("US Core Patient profile validation", () => { + test("freshly created profile with required fields is valid", () => { + const profile = USCorePatientProfile.create({ + identifier: [{ value: "1" }], + name: [{ family: "Test" }], + }); + + expect(profile.validate().errors).toEqual([]); + }); + + test("profile from empty resource reports missing required fields", () => { + const profile = USCorePatientProfile.apply({ resourceType: "Patient" }); + + const { errors } = profile.validate(); + expect(errors).toContain("USCorePatientProfile: required field 'identifier' is missing"); + expect(errors).toContain("USCorePatientProfile: required field 'name' is missing"); + }); +}); diff --git a/src/api/writer-generator/typescript/name.ts b/src/api/writer-generator/typescript/name.ts index aad27f8c9..49e821e4c 100644 --- a/src/api/writer-generator/typescript/name.ts +++ b/src/api/writer-generator/typescript/name.ts @@ -88,41 +88,53 @@ export const tsProfileModuleFileName = (tsIndex: TypeSchemaIndex, schema: Profil }; export const tsProfileClassName = (schema: ProfileTypeSchema): string => { - return `${normalizeTsName(schema.identifier.name)}Profile`; + const name = normalizeTsName(schema.identifier.name); + return name.endsWith("Profile") ? name : `${name}Profile`; }; -export const tsSliceInputTypeName = (profileName: string, fieldName: string, sliceName: string): string => { - return `${uppercaseFirstLetter(profileName)}_${uppercaseFirstLetter(normalizeTsName(fieldName))}_${uppercaseFirstLetter(normalizeTsName(sliceName))}SliceInput`; +export const tsSliceFlatTypeName = (profileName: string, fieldName: string, sliceName: string): string => { + return `${uppercaseFirstLetter(profileName)}_${uppercaseFirstLetter(normalizeTsName(fieldName))}_${uppercaseFirstLetter(normalizeTsName(sliceName))}SliceFlat`; }; -export const tsExtensionInputTypeName = (profileName: string, extensionName: string): string => { - return `${uppercaseFirstLetter(profileName)}_${uppercaseFirstLetter(normalizeTsName(extensionName))}Input`; +export const tsExtensionFlatTypeName = (profileName: string, extensionName: string): string => { + return `${uppercaseFirstLetter(profileName)}_${uppercaseFirstLetter(normalizeTsName(extensionName))}Flat`; }; export const tsSliceStaticName = (name: string): string => name.replace(/\[x\]/g, "").replace(/[^a-zA-Z0-9_$]/g, "_"); -export const tsSliceMethodName = (sliceName: string): string => { - return `set${uppercaseFirstLetter(normalizeTsName(sliceName) || "Slice")}`; -}; +export const tsSliceMethodBaseName = (sliceName: string): string => + uppercaseFirstLetter(normalizeTsName(sliceName) || "Slice"); -export const tsExtensionMethodName = (name: string): string => { - const normalized = tsCamelCase(name); - return `set${uppercaseFirstLetter(normalized || "Extension")}`; -}; +export const tsExtensionMethodBaseName = (name: string): string => + uppercaseFirstLetter(tsCamelCase(name) || "Extension"); -export const tsQualifiedExtensionMethodName = (name: string, path?: string): string => { +export const tsQualifiedExtensionMethodBaseName = (name: string, path?: string): string => { const rawPath = path ?.split(".") .filter((p) => p && p !== "extension") .join("_") ?? ""; const pathPart = rawPath ? uppercaseFirstLetter(tsCamelCase(rawPath)) : ""; - const normalized = tsCamelCase(name); - return `setExtension${pathPart}${uppercaseFirstLetter(normalized || "Extension")}`; + return `${pathPart}${uppercaseFirstLetter(tsCamelCase(name) || "Extension")}`; }; -export const tsQualifiedSliceMethodName = (fieldName: string, sliceName: string): string => { +export const tsQualifiedSliceMethodBaseName = (fieldName: string, sliceName: string): string => { const fieldPart = uppercaseFirstLetter(tsCamelCase(fieldName) || "Field"); const slicePart = uppercaseFirstLetter(normalizeTsName(sliceName) || "Slice"); - return `setSlice${fieldPart}${slicePart}`; + return `${fieldPart}${slicePart}`; }; + +export const tsResolvedExtensionBaseName = ( + extensionBaseNames: Record, + url: string, + path: string, + fallbackName: string, +): string => extensionBaseNames[`${url}:${path}`] ?? fallbackName; + +export const tsResolvedSliceBaseName = ( + sliceBaseNames: Record, + fieldName: string, + sliceName: string, +): string => sliceBaseNames[`${fieldName}:${sliceName}`] ?? sliceName; + +export const tsValueFieldName = (id: Identifier): string => `value${uppercaseFirstLetter(id.name)}`; diff --git a/src/api/writer-generator/typescript/profile-extensions.ts b/src/api/writer-generator/typescript/profile-extensions.ts new file mode 100644 index 000000000..510c34db4 --- /dev/null +++ b/src/api/writer-generator/typescript/profile-extensions.ts @@ -0,0 +1,471 @@ +import { + type CanonicalUrl, + type Identifier, + isChoiceDeclarationField, + isProfileTypeSchema, + type ProfileExtension, + type ProfileTypeSchema, +} from "@root/typeschema/types"; +import type { TypeSchemaIndex } from "@root/typeschema/utils"; +import { + tsCamelCase, + tsExtensionFlatTypeName, + tsProfileClassName, + tsProfileModuleName, + tsResolvedExtensionBaseName, + tsResourceName, + tsValueFieldName, +} from "./name"; +import { collectProfileFactoryInfo } from "./profile"; +import { tsTypeFromIdentifier } from "./utils"; +import type { TypeScript } from "./writer"; + +export type SubExtensionSliceInfo = { + name: string; + url: string; + valueField: string; + tsType: string; + isArray: boolean; + isRequired: boolean; +}; + +export type ExtensionProfileInfo = { + className: string; + modulePath: string; + flatProfile: ProfileTypeSchema; +}; + +/** + * Extract value field name from a slice's `elements` list. + * E.g. `["url", "value", "valueCoding"]` → `"valueCoding"` + */ +export const extractValueField = (elements: string[] | undefined): string | undefined => { + if (!elements) return undefined; + return elements.find((e) => e.startsWith("value") && e !== "value"); +}; + +/** + * Map a FHIR value field name (e.g. "valueCoding") to its TypeScript type. + */ +export const valueFieldToTsType = (valueField: string): string => { + const fhirName = valueField.replace(/^value/, ""); + // Primitive types that map to TS primitives + const primitives: Record = { + String: "string", + Boolean: "boolean", + Integer: "number", + Decimal: "number", + Date: "string", + DateTime: "string", + Time: "string", + Instant: "string", + Uri: "string", + Url: "string", + Canonical: "string", + Code: "string", + Oid: "string", + Id: "string", + Markdown: "string", + UnsignedInt: "number", + PositiveInt: "number", + Uuid: "string", + Base64Binary: "string", + }; + return primitives[fhirName] ?? fhirName; +}; + +/** + * Collect sub-extension "flat input" info from an extension profile's own + * slice definitions on its `extension` field. + */ +export const collectSubExtensionSlices = (extProfile: ProfileTypeSchema): SubExtensionSliceInfo[] => { + const extensionField = extProfile.fields?.extension; + if (!extensionField || isChoiceDeclarationField(extensionField) || !extensionField.slicing?.slices) return []; + const result: SubExtensionSliceInfo[] = []; + for (const [sliceName, slice] of Object.entries(extensionField.slicing.slices)) { + const valueField = extractValueField(slice.elements); + if (!valueField) continue; + const tsType = valueFieldToTsType(valueField); + const isArray = slice.max === undefined; + const isRequired = slice.min !== undefined && slice.min >= 1; + result.push({ + name: tsCamelCase(sliceName) || sliceName, + url: sliceName, + valueField, + tsType, + isArray, + isRequired, + }); + } + return result; +}; + +/** + * Resolve extension URL → extension profile class info (if the extension has + * its own generated profile class in the index). + */ +export const resolveExtensionProfile = ( + tsIndex: TypeSchemaIndex, + pkgName: string, + url: string, +): ExtensionProfileInfo | undefined => { + const schema = tsIndex.resolveByUrl(pkgName, url as CanonicalUrl); + if (!schema || !isProfileTypeSchema(schema)) return undefined; + // Only resolve extension profiles from the same package to avoid cross-package imports + if (schema.identifier.package !== pkgName) return undefined; + const className = tsProfileClassName(schema); + const modulePath = `./${tsProfileModuleName(tsIndex, schema)}`; + const flatProfile = tsIndex.flatProfile(schema); + 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") => { + w.line( + `if (${paramName}.url !== ${JSON.stringify(ext.url)}) throw new Error(\`Expected extension url '${ext.url}', got '\${${paramName}.url}'\`)`, + ); + generateExtensionPush(w, targetPath, paramName); +}; + +/** Generate the code that pushes an extension onto the target (root or nested path). */ +export const generateExtensionPush = (w: TypeScript, targetPath: string[], extExpr: string) => { + if (targetPath.length === 0) { + w.line(`pushExtension(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})`); + } +}; + +/** Generate the extension lookup code for getters. */ +const generateExtLookup = (w: TypeScript, ext: ProfileExtension, targetPath: string[]) => { + if (targetPath.length === 0) { + w.line(`const ext = this.resource.extension?.find(e => e.url === "${ext.url}")`); + } else { + w.line( + `const target = ensurePath(this.resource as unknown as Record, ${JSON.stringify(targetPath)})`, + ); + w.line(`const ext = (target.extension as Extension[] | undefined)?.find(e => e.url === "${ext.url}")`); + } +}; + +const effectiveGetterDefault = (w: TypeScript, hasProfile: boolean): "flat" | "profile" | "raw" => { + const configured = w.opts.extensionGetterDefault ?? "flat"; + if (configured === "profile" && !hasProfile) return "flat"; + return configured; +}; + +const returnTypeForMode = (mode: "flat" | "profile" | "raw", inputType: string, profileClassName?: string): string => { + if (mode === "profile" && profileClassName) return profileClassName; + if (mode === "raw") return "Extension"; + return inputType; +}; + +const generateExtensionGetterOverloads = ( + w: TypeScript, + ext: ProfileExtension, + targetPath: string[], + methodName: string, + inputType: string, + extProfileInfo: ExtensionProfileInfo | undefined, + generateInputBody: () => void, +) => { + const hasProfile = !!extProfileInfo; + const defaultMode = effectiveGetterDefault(w, hasProfile); + const modes: ("flat" | "profile" | "raw")[] = hasProfile ? ["flat", "profile", "raw"] : ["flat", "raw"]; + + for (const mode of modes) { + const rt = returnTypeForMode(mode, inputType, extProfileInfo?.className); + w.lineSM(`public ${methodName}(mode: '${mode}'): ${rt} | undefined`); + } + const defaultReturn = returnTypeForMode(defaultMode, inputType, extProfileInfo?.className); + w.lineSM(`public ${methodName}(): ${defaultReturn} | undefined`); + + const allReturns = [...new Set(modes.map((m) => returnTypeForMode(m, inputType, extProfileInfo?.className)))]; + const modesUnion = modes.map((m) => `'${m}'`).join(" | "); + w.curlyBlock( + ["public", methodName, `(mode: ${modesUnion} = '${defaultMode}'): ${allReturns.join(" | ")} | undefined`], + () => { + generateExtLookup(w, ext, targetPath); + w.line("if (!ext) return undefined"); + w.line("if (mode === 'raw') return ext"); + if (hasProfile) { + w.line(`if (mode === 'profile') return ${extProfileInfo?.className}.apply(ext)`); + } + generateInputBody(); + }, + ); +}; + +type ExtensionMethodInfo = { + ext: ProfileExtension; + flatProfile: ProfileTypeSchema; + setMethodName: string; + getMethodName: string; + targetPath: string[]; + extProfileInfo: ExtensionProfileInfo | undefined; +}; + +// Complex extension — has sub-extensions (e.g., Race with ombCategory, detailed, text) + +const generateComplexExtensionSetter = (w: TypeScript, info: ExtensionMethodInfo) => { + const { ext, flatProfile, setMethodName, targetPath, extProfileInfo } = info; + const tsProfileName = tsResourceName(flatProfile.identifier); + const inputTypeName = tsExtensionFlatTypeName(tsProfileName, ext.name); + const extProfileHasFlatInput = extProfileInfo + ? collectSubExtensionSlices(extProfileInfo.flatProfile).length > 0 + : false; + + if (extProfileInfo && extProfileHasFlatInput) { + const paramType = `${extProfileInfo.className}Flat | ${extProfileInfo.className} | Extension`; + w.curlyBlock(["public", setMethodName, `(input: ${paramType}): this`], () => { + w.ifElseChain( + [ + { + cond: `input instanceof ${extProfileInfo.className}`, + body: () => generateExtensionPush(w, targetPath, "input.toResource()"), + }, + { + cond: "isExtension(input)", + body: () => generateRawExtensionBody(w, ext, targetPath), + }, + ], + () => generateExtensionPush(w, targetPath, `${extProfileInfo.className}.createResource(input)`), + ); + w.line("return this"); + }); + } else { + w.curlyBlock(["public", setMethodName, `(input: ${inputTypeName}): this`], () => { + w.line("const subExtensions: Extension[] = []"); + for (const sub of ext.subExtensions ?? []) { + const valueField = sub.valueType ? tsValueFieldName(sub.valueType) : "value"; + if (sub.max === "*") { + w.curlyBlock(["if", `(input.${sub.name})`], () => { + w.curlyBlock(["for", `(const item of input.${sub.name})`], () => { + w.line(`subExtensions.push({ url: "${sub.url}", ${valueField}: item } as Extension)`); + }); + }); + } else { + w.curlyBlock(["if", `(input.${sub.name} !== undefined)`], () => { + w.line( + `subExtensions.push({ url: "${sub.url}", ${valueField}: input.${sub.name} } as Extension)`, + ); + }); + } + } + if (targetPath.length === 0) { + w.line("const list = (this.resource.extension ??= [])"); + w.line(`list.push({ url: "${ext.url}", extension: subExtensions })`); + } 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("return this"); + }); + } +}; + +const generateComplexExtensionGetter = (w: TypeScript, info: ExtensionMethodInfo) => { + const { ext, flatProfile, getMethodName, targetPath, extProfileInfo } = info; + const tsProfileName = tsResourceName(flatProfile.identifier); + const inputTypeName = tsExtensionFlatTypeName(tsProfileName, ext.name); + const extProfileHasFlatInput = extProfileInfo + ? collectSubExtensionSlices(extProfileInfo.flatProfile).length > 0 + : false; + const inputType = extProfileHasFlatInput && extProfileInfo ? `${extProfileInfo.className}Flat` : inputTypeName; + + generateExtensionGetterOverloads(w, ext, targetPath, getMethodName, inputType, extProfileInfo, () => { + const configItems = (ext.subExtensions ?? []).map((sub) => { + const valueField = sub.valueType ? tsValueFieldName(sub.valueType) : "value"; + const isArray = sub.max === "*"; + return `{ name: "${sub.url}", valueField: "${valueField}", isArray: ${isArray} }`; + }); + w.line(`const config = [${configItems.join(", ")}]`); + w.line(`return extractComplexExtension<${inputType}>(ext, config)`); + }); +}; + +// Single-value extension — one known value type (e.g., birthSex with valueCode) + +const generateSingleValueExtensionSetter = (w: TypeScript, tsIndex: TypeSchemaIndex, info: ExtensionMethodInfo) => { + const { ext, setMethodName, targetPath, extProfileInfo } = info; + const firstValueType = ext.valueTypes?.[0]; + if (!firstValueType) return; + const valueType = tsTypeFromIdentifier(firstValueType); + const valueField = tsValueFieldName(firstValueType); + + if (extProfileInfo) { + const paramType = `${extProfileInfo.className} | Extension | ${valueType}`; + const extHasValueParam = collectProfileFactoryInfo(tsIndex, extProfileInfo.flatProfile).params.some( + (p) => p.name === valueField, + ); + const elseExpr = extHasValueParam + ? `${extProfileInfo.className}.createResource({ ${valueField}: value as ${valueType} })` + : `{ url: "${ext.url}", ${valueField}: value as ${valueType} } as Extension`; + w.curlyBlock(["public", setMethodName, `(value: ${paramType}): this`], () => { + w.ifElseChain( + [ + { + cond: `value instanceof ${extProfileInfo.className}`, + body: () => generateExtensionPush(w, targetPath, "value.toResource()"), + }, + { + cond: "isExtension(value)", + body: () => generateRawExtensionBody(w, ext, targetPath, "value"), + }, + ], + () => generateExtensionPush(w, targetPath, elseExpr), + ); + 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); + w.line("return this"); + }); + } +}; + +const generateSingleValueExtensionGetter = (w: TypeScript, info: ExtensionMethodInfo) => { + const { ext, getMethodName, targetPath, extProfileInfo } = info; + const firstValueType = ext.valueTypes?.[0]; + if (!firstValueType) return; + const valueType = tsTypeFromIdentifier(firstValueType); + const valueField = tsValueFieldName(firstValueType); + + generateExtensionGetterOverloads(w, ext, targetPath, getMethodName, valueType, extProfileInfo, () => { + w.line(`return getExtensionValue<${valueType}>(ext, "${valueField}")`); + }); +}; + +// Generic extension — no known value type + +const generateGenericExtensionSetter = (w: TypeScript, info: ExtensionMethodInfo) => { + const { ext, setMethodName, targetPath } = info; + + w.curlyBlock(["public", setMethodName, `(value: Omit | Extension): this`], () => { + w.ifElseChain( + [ + { + cond: "isExtension(value)", + body: () => generateRawExtensionBody(w, ext, targetPath, "value"), + }, + ], + () => generateExtensionPush(w, targetPath, `{ url: "${ext.url}", ...value } as Extension`), + ); + w.line("return this"); + }); +}; + +const generateGenericExtensionGetter = (w: TypeScript, info: ExtensionMethodInfo) => { + const { ext, getMethodName, targetPath } = info; + + w.curlyBlock(["public", getMethodName, "(): Extension | undefined"], () => { + if (targetPath.length === 0) { + w.line(`return this.resource.extension?.find(e => e.url === "${ext.url}")`); + } else { + w.line( + `const target = ensurePath(this.resource as unknown as Record, ${JSON.stringify(targetPath)})`, + ); + w.line(`return (target.extension as Extension[] | undefined)?.find(e => e.url === "${ext.url}")`); + } + }); +}; + +export const generateExtensionMethods = ( + w: TypeScript, + tsIndex: TypeSchemaIndex, + flatProfile: ProfileTypeSchema, + extensionBaseNames: Record, +) => { + for (const ext of flatProfile.extensions ?? []) { + if (!ext.url) continue; + const baseName = tsResolvedExtensionBaseName(extensionBaseNames, ext.url, ext.path, ext.name); + const targetPath = ext.path.split(".").filter((segment) => segment !== "extension"); + const extProfileInfo = resolveExtensionProfile(tsIndex, flatProfile.identifier.package, ext.url); + const info: ExtensionMethodInfo = { + ext, + flatProfile, + setMethodName: `set${baseName}`, + getMethodName: `get${baseName}`, + targetPath, + extProfileInfo, + }; + + if (ext.isComplex && ext.subExtensions) { + generateComplexExtensionSetter(w, info); + w.line(); + generateComplexExtensionGetter(w, info); + } else if (ext.valueTypes?.length === 1 && ext.valueTypes[0]) { + generateSingleValueExtensionSetter(w, tsIndex, info); + w.line(); + generateSingleValueExtensionGetter(w, info); + } else { + generateGenericExtensionSetter(w, info); + w.line(); + generateGenericExtensionGetter(w, info); + } + w.line(); + } +}; + +export const collectTypesFromExtensions = ( + tsIndex: TypeSchemaIndex, + flatProfile: ProfileTypeSchema, + addType: (typeId: Identifier) => void, +): boolean => { + let needsExtensionType = false; + + for (const ext of flatProfile.extensions ?? []) { + if (ext.isComplex && ext.subExtensions) { + needsExtensionType = true; + for (const sub of ext.subExtensions) { + if (!sub.valueType) continue; + const resolvedType = tsIndex.resolveByUrl( + flatProfile.identifier.package, + sub.valueType.url as CanonicalUrl, + ); + addType(resolvedType?.identifier ?? sub.valueType); + } + } else if (ext.valueTypes && ext.valueTypes.length === 1) { + needsExtensionType = true; + if (ext.valueTypes[0]) { + const resolvedType = tsIndex.resolveByUrl( + flatProfile.identifier.package, + ext.valueTypes[0].url as CanonicalUrl, + ); + addType(resolvedType?.identifier ?? ext.valueTypes[0]); + } + } else { + needsExtensionType = true; + } + } + + return needsExtensionType; +}; + +/** Collect types used in the FlatInput of extension profiles. */ +export const collectTypesFromFlatInput = ( + tsIndex: TypeSchemaIndex, + flatProfile: ProfileTypeSchema, + addType: (typeId: Identifier) => void, +) => { + if (flatProfile.base.name !== "Extension") return; + const subSlices = collectSubExtensionSlices(flatProfile); + for (const sub of subSlices) { + const tsType = sub.tsType; + // Primitive types (string, boolean, number) don't need imports + if (["string", "boolean", "number"].includes(tsType)) continue; + // Resolve complex FHIR type by name + const fhirUrl = `http://hl7.org/fhir/StructureDefinition/${tsType}` as CanonicalUrl; + const schema = tsIndex.resolveByUrl(flatProfile.identifier.package, fhirUrl); + if (schema) addType(schema.identifier); + } +}; diff --git a/src/api/writer-generator/typescript/profile-slices.ts b/src/api/writer-generator/typescript/profile-slices.ts new file mode 100644 index 000000000..cda3488c7 --- /dev/null +++ b/src/api/writer-generator/typescript/profile-slices.ts @@ -0,0 +1,206 @@ +import { + type ConstrainedChoiceInfo, + type Identifier, + isChoiceDeclarationField, + isNotChoiceDeclarationField, + isPrimitiveIdentifier, + type ProfileTypeSchema, + type RegularField, +} from "@root/typeschema/types"; +import type { TypeSchemaIndex } from "@root/typeschema/utils"; +import { + tsFieldName, + tsProfileClassName, + tsResolvedSliceBaseName, + tsResourceName, + tsSliceFlatTypeName, + tsSliceStaticName, +} from "./name"; +import { tsGet, tsTypeFromIdentifier } from "./utils"; +import type { TypeScript } from "./writer"; + +/** Collect choice declaration field names from a base type schema */ +const collectChoiceBaseNames = (tsIndex: TypeSchemaIndex, typeId: Identifier): Set => { + const names = new Set(); + const schema = tsIndex.resolve(typeId); + if (schema && "fields" in schema && schema.fields) { + for (const [name, f] of Object.entries(schema.fields)) { + if (isChoiceDeclarationField(f)) names.add(name); + } + } + return names; +}; + +export const collectTypesFromSlices = ( + tsIndex: TypeSchemaIndex, + flatProfile: ProfileTypeSchema, + addType: (typeId: Identifier) => void, +) => { + const pkgName = flatProfile.identifier.package; + for (const field of Object.values(flatProfile.fields ?? {})) { + if (!isNotChoiceDeclarationField(field) || !field.slicing?.slices || !field.type) continue; + for (const slice of Object.values(field.slicing.slices)) { + if (Object.keys(slice.match ?? {}).length > 0) { + addType(field.type); + const cc = slice.elements ? tsIndex.constrainedChoice(pkgName, field.type, slice.elements) : undefined; + if (cc) addType(cc.variantType); + } + } + } +}; + +export const collectRequiredSliceNames = (field: RegularField): string[] | undefined => { + if (!field.array || !field.slicing?.slices) 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) + .map(([name]) => name); + return names.length > 0 ? names : undefined; +}; + +export type SliceDef = { + fieldName: string; + baseType: string; + sliceName: string; + match: Record; + /** Required fields, already filtered (match keys and polymorphic base names removed) */ + required: string[]; + excluded: string[]; + array: boolean; + constrainedChoice: ConstrainedChoiceInfo | undefined; +}; + +export const collectSliceDefs = (tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema): SliceDef[] => + Object.entries(flatProfile.fields ?? {}) + .filter(([_, field]) => isNotChoiceDeclarationField(field) && field.slicing?.slices) + .flatMap(([fieldName, field]) => { + if (!isNotChoiceDeclarationField(field) || !field.slicing?.slices || !field.type) return []; + const baseType = tsTypeFromIdentifier(field.type); + const pkgName = flatProfile.identifier.package; + const choiceBaseNames = collectChoiceBaseNames(tsIndex, field.type); + return Object.entries(field.slicing.slices) + .filter(([_, slice]) => Object.keys(slice.match ?? {}).length > 0) + .map(([sliceName, slice]) => { + const matchFields = Object.keys(slice.match ?? {}); + const required = (slice.required ?? []).filter( + (name) => !matchFields.includes(name) && !choiceBaseNames.has(name), + ); + const cc = slice.elements + ? tsIndex.constrainedChoice(pkgName, field.type, slice.elements) + : undefined; + // Skip flattening for primitive types — can't intersect object with boolean/string/etc. + const constrainedChoice = cc && !isPrimitiveIdentifier(cc.variantType) ? cc : undefined; + return { + fieldName, + baseType, + sliceName, + match: slice.match ?? {}, + required, + excluded: slice.excluded ?? [], + array: Boolean(field.array), + constrainedChoice, + }; + }); + }); + +export const generateSliceSetters = ( + w: TypeScript, + sliceDefs: SliceDef[], + flatProfile: ProfileTypeSchema, + sliceBaseNames: Record, +) => { + const profileClassName = tsProfileClassName(flatProfile); + const tsProfileName = tsResourceName(flatProfile.identifier); + for (const sliceDef of sliceDefs) { + const baseName = tsResolvedSliceBaseName(sliceBaseNames, sliceDef.fieldName, sliceDef.sliceName); + const methodName = `set${baseName}`; + const typeName = tsSliceFlatTypeName(tsProfileName, sliceDef.fieldName, sliceDef.sliceName); + const matchRef = `${profileClassName}.${tsSliceStaticName(sliceDef.sliceName)}SliceMatch`; + const tsField = tsFieldName(sliceDef.fieldName); + const fieldAccess = tsGet("this.resource", tsField); + const baseType = sliceDef.baseType; + // Make input optional when there are no required fields (input can be empty object) + const inputOptional = sliceDef.required.length === 0; + const unionType = `${typeName} | ${baseType}`; + const paramSignature = inputOptional ? `(input?: ${unionType}): this` : `(input: ${unionType}): this`; + w.curlyBlock(["public", methodName, paramSignature], () => { + w.line(`const match = ${matchRef}`); + w.curlyBlock(["if", "(input && matchesValue(input, match))"], () => { + if (sliceDef.array) { + w.line(`setArraySlice(${fieldAccess} ??= [], match, input as ${baseType})`); + } else { + w.line(`${fieldAccess} = input as ${baseType}`); + } + w.line("return this"); + }); + const inputExpr = inputOptional ? "input ?? {}" : "input"; + if (sliceDef.constrainedChoice) { + const cc = sliceDef.constrainedChoice; + w.line(`const wrapped = wrapSliceChoice<${baseType}>(${inputExpr}, ${JSON.stringify(cc.variant)})`); + w.line(`const value = applySliceMatch<${baseType}>(wrapped, match)`); + } else { + w.line(`const value = applySliceMatch<${baseType}>(${inputExpr}, match)`); + } + if (sliceDef.array) { + w.line(`setArraySlice(${fieldAccess} ??= [], match, value)`); + } else { + w.line(`${fieldAccess} = value`); + } + w.line("return this"); + }); + w.line(); + } +}; + +export const generateSliceGetters = ( + w: TypeScript, + sliceDefs: SliceDef[], + flatProfile: ProfileTypeSchema, + sliceBaseNames: Record, +) => { + const profileClassName = tsProfileClassName(flatProfile); + const tsProfileName = tsResourceName(flatProfile.identifier); + const defaultMode = w.opts.sliceGetterDefault ?? "flat"; + for (const sliceDef of sliceDefs) { + const baseName = tsResolvedSliceBaseName(sliceBaseNames, sliceDef.fieldName, sliceDef.sliceName); + const getMethodName = `get${baseName}`; + const typeName = tsSliceFlatTypeName(tsProfileName, sliceDef.fieldName, sliceDef.sliceName); + const matchRef = `${profileClassName}.${tsSliceStaticName(sliceDef.sliceName)}SliceMatch`; + const matchKeys = JSON.stringify(Object.keys(sliceDef.match)); + const tsField = tsFieldName(sliceDef.fieldName); + const fieldAccess = tsGet("this.resource", tsField); + const baseType = sliceDef.baseType; + const defaultReturn = defaultMode === "raw" ? baseType : typeName; + + // Overload signatures + w.lineSM(`public ${getMethodName}(mode: 'flat'): ${typeName} | undefined`); + w.lineSM(`public ${getMethodName}(mode: 'raw'): ${baseType} | undefined`); + w.lineSM(`public ${getMethodName}(): ${defaultReturn} | undefined`); + + // Implementation + w.curlyBlock( + [ + "public", + getMethodName, + `(mode: 'flat' | 'raw' = '${defaultMode}'): ${typeName} | ${baseType} | undefined`, + ], + () => { + w.line(`const match = ${matchRef}`); + if (sliceDef.array) { + w.line(`const item = getArraySlice(${fieldAccess}, match)`); + w.line("if (!item) return undefined"); + } else { + w.line(`const item = ${fieldAccess}`); + w.line("if (!item || !matchesValue(item, match)) return undefined"); + } + w.line("if (mode === 'raw') return item"); + if (sliceDef.constrainedChoice) { + const cc = sliceDef.constrainedChoice; + w.line(`return unwrapSliceChoice<${typeName}>(item, ${matchKeys}, ${JSON.stringify(cc.variant)})`); + } else { + w.line(`return stripMatchKeys<${typeName}>(item, ${matchKeys})`); + } + }, + ); + w.line(); + } +}; diff --git a/src/api/writer-generator/typescript/profile-validation.ts b/src/api/writer-generator/typescript/profile-validation.ts new file mode 100644 index 000000000..b03b91e10 --- /dev/null +++ b/src/api/writer-generator/typescript/profile-validation.ts @@ -0,0 +1,110 @@ +import { + type ChoiceFieldInstance, + type Identifier, + isChoiceDeclarationField, + isChoiceInstanceField, + type ProfileTypeSchema, + type RegularField, +} from "@root/typeschema/types"; +import type { TypeSchemaIndex } from "@root/typeschema/utils"; +import { tsProfileClassName } from "./name"; +import type { TypeScript } from "./writer"; + +export const collectRegularFieldValidation = ( + errors: string[], + warnings: string[], + name: string, + field: RegularField | ChoiceFieldInstance, + resolveRef: (ref: Identifier) => Identifier, + canonicalUrlExpr?: { url: string; expr: string }, +) => { + if (field.excluded) { + errors.push(`...validateExcluded(res, profileName, ${JSON.stringify(name)})`); + return; + } + + if (field.required) errors.push(`...validateRequired(res, profileName, ${JSON.stringify(name)})`); + + if (field.valueConstraint) { + const valueExpr = + canonicalUrlExpr && name === "url" && field.valueConstraint.value === canonicalUrlExpr.url + ? canonicalUrlExpr.expr + : JSON.stringify(field.valueConstraint.value); + errors.push(`...validateFixedValue(res, profileName, ${JSON.stringify(name)}, ${valueExpr})`); + } + + if (field.enum) { + const target = field.enum.isOpen ? warnings : errors; + target.push(`...validateEnum(res, profileName, ${JSON.stringify(name)}, ${JSON.stringify(field.enum.values)})`); + } + + if (field.mustSupport && !field.required) + warnings.push(`...validateMustSupport(res, profileName, ${JSON.stringify(name)})`); + + if (field.reference && field.reference.length > 0) + errors.push( + `...validateReference(res, profileName, ${JSON.stringify(name)}, ${JSON.stringify(field.reference.map((ref) => resolveRef(ref).name))})`, + ); + + if (field.slicing?.slices) { + for (const [sliceName, slice] of Object.entries(field.slicing.slices)) { + if (slice.min === undefined && slice.max === undefined) continue; + const match = slice.match ?? {}; + if (Object.keys(match).length === 0) continue; + const min = slice.min ?? 0; + const max = slice.max ?? 0; + errors.push( + `...validateSliceCardinality(res, profileName, ${JSON.stringify(name)}, ${JSON.stringify(match)}, ${JSON.stringify(sliceName)}, ${min}, ${max})`, + ); + } + } +}; + +export const generateValidateMethod = (w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema) => { + const fields = flatProfile.fields ?? {}; + const profileName = flatProfile.identifier.name; + const canonicalUrl = flatProfile.identifier.url; + const canonicalUrlExpr = canonicalUrl + ? { url: canonicalUrl, expr: `${tsProfileClassName(flatProfile)}.canonicalUrl` } + : undefined; + w.curlyBlock(["validate(): { errors: string[]; warnings: string[] }"], () => { + w.line(`const profileName = "${profileName}"`); + w.line("const res = this.resource"); + + const errors: string[] = []; + const warnings: string[] = []; + for (const [name, field] of Object.entries(fields)) { + if (isChoiceInstanceField(field)) continue; + + if (isChoiceDeclarationField(field)) { + if (field.required) + errors.push(`...validateChoiceRequired(res, profileName, ${JSON.stringify(field.choices)})`); + continue; + } + + collectRegularFieldValidation( + errors, + warnings, + name, + field, + tsIndex.findLastSpecializationByIdentifier, + canonicalUrlExpr, + ); + } + + const emitArray = (label: string, exprs: string[]) => { + if (exprs.length === 0) { + w.line(`${label}: [],`); + } else { + w.squareBlock([`${label}:`], () => { + for (const expr of exprs) w.line(`${expr},`); + }, [","]); + } + }; + w.curlyBlock(["return"], () => { + emitArray("errors", errors); + emitArray("warnings", warnings); + }); + }); + w.line(); +}; diff --git a/src/api/writer-generator/typescript/profile.ts b/src/api/writer-generator/typescript/profile.ts index 3911da5bc..ab915b3df 100644 --- a/src/api/writer-generator/typescript/profile.ts +++ b/src/api/writer-generator/typescript/profile.ts @@ -1,7 +1,6 @@ import { pascalCase, typeSchemaInfo, uppercaseFirstLetter } from "@root/api/writer-generator/utils"; import { type CanonicalUrl, - type ChoiceFieldInstance, type Identifier, isChoiceDeclarationField, isChoiceInstanceField, @@ -14,29 +13,42 @@ import { type ProfileTypeSchema, packageMeta, packageMetaToFhir, - type RegularField, - type TypeSchema, } from "@root/typeschema/types"; import type { TypeSchemaIndex } from "@root/typeschema/utils"; import { - normalizeTsName, tsCamelCase, - tsExtensionInputTypeName, - tsExtensionMethodName, + tsExtensionFlatTypeName, + tsExtensionMethodBaseName, tsFieldName, tsModulePath, tsNameFromCanonical, tsPackageDir, tsProfileClassName, tsProfileModuleName, - tsQualifiedExtensionMethodName, - tsQualifiedSliceMethodName, + tsQualifiedExtensionMethodBaseName, + tsQualifiedSliceMethodBaseName, tsResourceName, - tsSliceInputTypeName, - tsSliceMethodName, + tsSliceFlatTypeName, + tsSliceMethodBaseName, tsSliceStaticName, } from "./name"; -import { resolveFieldTsType, resolvePrimitiveType, tsEnumType, tsGet, tsTypeFromIdentifier } from "./utils"; +import { + collectSubExtensionSlices, + collectTypesFromExtensions, + collectTypesFromFlatInput, + generateExtensionMethods, + resolveExtensionProfile, +} from "./profile-extensions"; +import { + collectRequiredSliceNames, + collectSliceDefs, + collectTypesFromSlices, + generateSliceGetters, + generateSliceSetters, + type SliceDef, +} from "./profile-slices"; +import { generateValidateMethod } from "./profile-validation"; +import { fieldTsType, resolvePrimitiveType, tsEnumType, tsGet, tsTypeFromIdentifier } from "./utils"; import type { TypeScript } from "./writer"; type ProfileFactoryInfo = { @@ -79,15 +91,10 @@ const tryPromoteChoice = ( promotedChoices.add(choiceName); }; -const collectRequiredSliceNames = (field: RegularField): string[] | undefined => { - if (!field.array || !field.slicing?.slices) 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) - .map(([name]) => name); - return names.length > 0 ? names : undefined; -}; - -const collectProfileFactoryInfo = (tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema): ProfileFactoryInfo => { +export const collectProfileFactoryInfo = ( + tsIndex: TypeSchemaIndex, + flatProfile: ProfileTypeSchema, +): ProfileFactoryInfo => { const autoFields: ProfileFactoryInfo["autoFields"] = []; const sliceAutoFields: ProfileFactoryInfo["sliceAutoFields"] = []; const params: ProfileFactoryInfo["params"] = []; @@ -113,7 +120,7 @@ const collectProfileFactoryInfo = (tsIndex: TypeSchemaIndex, flatProfile: Profil const value = JSON.stringify(field.valueConstraint.value); autoFields.push({ name, value: field.array ? `[${value}]` : value }); if (isNotChoiceDeclarationField(field) && field.type) { - const tsType = resolveFieldTsType("", "", field, resolveRef) + (field.array ? "[]" : ""); + const tsType = fieldTsType(field, resolveRef); autoAccessors.push({ name, tsType, typeId: field.type }); } continue; @@ -123,7 +130,7 @@ const collectProfileFactoryInfo = (tsIndex: TypeSchemaIndex, flatProfile: Profil const sliceNames = collectRequiredSliceNames(field); if (sliceNames) { if (field.type) { - const tsType = resolveFieldTsType("", "", field, resolveRef) + (field.array ? "[]" : ""); + const tsType = fieldTsType(field, resolveRef); sliceAutoFields.push({ name, tsType, @@ -137,36 +144,45 @@ const collectProfileFactoryInfo = (tsIndex: TypeSchemaIndex, flatProfile: Profil } if (field.required) { - const tsType = resolveFieldTsType("", "", field, resolveRef) + (field.array ? "[]" : ""); + const tsType = fieldTsType(field, resolveRef); params.push({ name, tsType, typeId: field.type }); } } - // Include base-type required fields not already covered by profile constraints - const coveredFields = new Set([ + collectBaseRequiredParams(tsIndex, flatProfile, resolveRef, params, [ ...autoFields.map((f) => f.name), ...sliceAutoFields.map((f) => f.name), ...params.map((f) => f.name), ...promotedChoices, ]); - const baseSchema = tsIndex.resolve(flatProfile.base); - if (baseSchema && "fields" in baseSchema && baseSchema.fields) { - for (const [name, field] of Object.entries(baseSchema.fields)) { - if (coveredFields.has(name)) continue; - if (!field.required) continue; - if (isChoiceInstanceField(field)) continue; - if (isChoiceDeclarationField(field)) continue; - if (isNotChoiceDeclarationField(field) && field.type) { - const tsType = resolveFieldTsType("", "", field, resolveRef) + (field.array ? "[]" : ""); - params.push({ name, tsType, typeId: field.type }); - } - } - } const accessors = [...autoAccessors, ...collectChoiceAccessors(flatProfile, promotedChoices)]; return { autoFields, sliceAutoFields, params, accessors }; }; +/** Include base-type required fields not already covered by profile constraints */ +const collectBaseRequiredParams = ( + tsIndex: TypeSchemaIndex, + flatProfile: ProfileTypeSchema, + resolveRef: TypeSchemaIndex["findLastSpecializationByIdentifier"], + params: ProfileFactoryInfo["params"], + coveredNames: string[], +) => { + const covered = new Set(coveredNames); + const baseSchema = tsIndex.resolve(flatProfile.base); + if (!baseSchema || !("fields" in baseSchema) || !baseSchema.fields) return; + for (const [name, field] of Object.entries(baseSchema.fields)) { + if (covered.has(name)) continue; + if (!field.required) continue; + if (isChoiceInstanceField(field)) continue; + if (isChoiceDeclarationField(field)) continue; + if (isNotChoiceDeclarationField(field) && field.type) { + const tsType = fieldTsType(field, resolveRef); + params.push({ name, tsType, typeId: field.type }); + } + } +}; + export const generateProfileIndexFile = ( w: TypeScript, tsIndex: TypeSchemaIndex, @@ -178,7 +194,7 @@ export const generateProfileIndexFile = ( const profiles: [ProfileTypeSchema, string, string | undefined][] = initialProfiles.map((profile) => { const className = tsProfileClassName(profile); const resourceName = tsResourceName(profile.identifier); - const overrides = detectFieldOverrides(w, tsIndex, profile); + const overrides = detectFieldOverrides(tsIndex, profile); let typeExport; if (overrides.size > 0) typeExport = resourceName; return [profile, className, typeExport]; @@ -191,7 +207,7 @@ export const generateProfileIndexFile = ( if (!classExports.has(className)) { classExports.set(className, `export { ${className} } from "./${moduleName}"`); } - if (typeName && !typeExports.has(typeName)) { + if (typeName && !typeExports.has(typeName) && !classExports.has(typeName)) { typeExports.set(typeName, `export type { ${typeName} } from "./${moduleName}"`); } } @@ -204,7 +220,6 @@ export const generateProfileIndexFile = ( }; const tsTypeForProfileField = ( - _w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema, fieldName: string, @@ -265,25 +280,28 @@ const tsTypeForProfileField = ( const generateProfileHelpersImport = ( w: TypeScript, - options: { - needsGetOrCreateObjectAtPath: boolean; - needsSliceHelpers: boolean; - needsExtensionExtraction: boolean; - needsSliceExtraction: boolean; - needsSliceChoiceHelpers: boolean; - needsValidation: boolean; - needsRegisterProfile: boolean; - }, + tsIndex: TypeSchemaIndex, + flatProfile: ProfileTypeSchema, + sliceDefs: SliceDef[], + factoryInfo: ProfileFactoryInfo, ) => { + const extensions = flatProfile.extensions ?? []; + const hasMeta = tsIndex.isWithMetaField(flatProfile); + const canonicalUrl = flatProfile.identifier.url; + const imports: string[] = []; - if (options.needsRegisterProfile) imports.push("ensureProfile"); - if (options.needsSliceHelpers) + if (!isPrimitiveIdentifier(flatProfile.base)) imports.push("buildResource"); + if (flatProfile.base.name === "Extension" && !!canonicalUrl && collectSubExtensionSlices(flatProfile).length > 0) + imports.push("isRawExtensionInput"); + if (canonicalUrl && hasMeta) imports.push("ensureProfile"); + if (sliceDefs.length > 0 || factoryInfo.sliceAutoFields.length > 0) imports.push("applySliceMatch", "matchesValue", "setArraySlice", "getArraySlice", "ensureSliceDefaults"); - if (options.needsGetOrCreateObjectAtPath) imports.push("ensurePath"); - if (options.needsExtensionExtraction) imports.push("extractComplexExtension"); - if (options.needsSliceExtraction) imports.push("stripMatchKeys"); - if (options.needsSliceChoiceHelpers) imports.push("wrapSliceChoice", "unwrapSliceChoice"); - if (options.needsValidation) + if (extensions.some((ext) => ext.path.split(".").some((s) => s !== "extension"))) imports.push("ensurePath"); + if (extensions.some((ext) => ext.isComplex && ext.subExtensions)) imports.push("extractComplexExtension"); + if (sliceDefs.length > 0) 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 (Object.keys(flatProfile.fields ?? {}).length > 0) imports.push( "validateRequired", "validateExcluded", @@ -292,88 +310,20 @@ const generateProfileHelpersImport = ( "validateEnum", "validateReference", "validateChoiceRequired", + "validateMustSupport", ); - if (imports.length > 0) w.lineSM(`import { ${imports.join(", ")} } from "../../profile-helpers"`); -}; - -const collectTypesFromSlices = ( - tsIndex: TypeSchemaIndex, - flatProfile: ProfileTypeSchema, - addType: (typeId: Identifier) => void, -) => { - for (const field of Object.values(flatProfile.fields ?? {})) { - if (!isNotChoiceDeclarationField(field) || !field.slicing?.slices || !field.type) continue; - for (const slice of Object.values(field.slicing.slices)) { - if (Object.keys(slice.match ?? {}).length > 0) { - addType(field.type); - // Also add constrained choice variant types for flattened API - const cc = detectConstrainedChoice(tsIndex, flatProfile, field.type, slice.elements); - if (cc) addType(cc.variantTypeId); - } - } - } -}; - -const collectTypesFromExtensions = ( - tsIndex: TypeSchemaIndex, - flatProfile: ProfileTypeSchema, - addType: (typeId: Identifier) => void, -): boolean => { - let needsExtensionType = false; - - for (const ext of flatProfile.extensions ?? []) { - if (ext.isComplex && ext.subExtensions) { - needsExtensionType = true; - for (const sub of ext.subExtensions) { - if (!sub.valueType) continue; - const resolvedType = tsIndex.resolveByUrl( - flatProfile.identifier.package, - sub.valueType.url as CanonicalUrl, - ); - addType(resolvedType?.identifier ?? sub.valueType); - } - } else if (ext.valueTypes && ext.valueTypes.length === 1) { - needsExtensionType = true; - if (ext.valueTypes[0]) { - const resolvedType = tsIndex.resolveByUrl( - flatProfile.identifier.package, - ext.valueTypes[0].url as CanonicalUrl, - ); - addType(resolvedType?.identifier ?? ext.valueTypes[0]); - } - } else { - needsExtensionType = true; - } + if (imports.length > 0) { + w.tsImport("../../profile-helpers", ...imports); + w.line(); } - - return needsExtensionType; }; -const collectTypesFromFieldOverrides = ( +export const generateProfileImports = ( + w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema, - addType: (typeId: Identifier) => void, + overrides: FieldOverrides, ) => { - const referenceUrl = "http://hl7.org/fhir/StructureDefinition/Reference" as CanonicalUrl; - const referenceSchema = tsIndex.resolveByUrl(flatProfile.identifier.package, referenceUrl); - const specialization = tsIndex.findLastSpecialization(flatProfile); - - if (!isSpecializationTypeSchema(specialization)) return; - - for (const [fieldName, pField] of Object.entries(flatProfile.fields ?? {})) { - if (!isNotChoiceDeclarationField(pField)) continue; - const sField = specialization.fields?.[fieldName]; - if (!sField || isChoiceDeclarationField(sField)) continue; - - if (pField.reference && sField.reference && pField.reference.length < sField.reference.length) { - if (referenceSchema) addType(referenceSchema.identifier); - } else if (pField.required && !sField.required && pField.type) { - addType(pField.type); - } - } -}; - -export const generateProfileImports = (w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema) => { const usedTypes = new Map(); const getModulePath = (typeId: Identifier): string => { @@ -395,7 +345,8 @@ export const generateProfileImports = (w: TypeScript, tsIndex: TypeSchemaIndex, addType(flatProfile.base); collectTypesFromSlices(tsIndex, flatProfile, addType); const needsExtensionType = collectTypesFromExtensions(tsIndex, flatProfile, addType); - collectTypesFromFieldOverrides(tsIndex, flatProfile, addType); + for (const { typeId } of overrides.values()) addType(typeId); + collectTypesFromFlatInput(tsIndex, flatProfile, addType); const factoryInfo = collectProfileFactoryInfo(tsIndex, flatProfile); for (const param of factoryInfo.params) addType(param.typeId); @@ -408,291 +359,184 @@ export const generateProfileImports = (w: TypeScript, tsIndex: TypeSchemaIndex, if (extensionSchema) addType(extensionSchema.identifier); } - const sortedImports = Array.from(usedTypes.values()).sort((a, b) => a.tsName.localeCompare(b.tsName)); - for (const { importPath, tsName } of sortedImports) { - w.tsImportType(importPath, tsName); + const grouped = new Map(); + for (const { importPath, tsName } of usedTypes.values()) { + let names = grouped.get(importPath); + if (!names) { + names = []; + grouped.set(importPath, names); + } + names.push(tsName); } - if (sortedImports.length > 0) w.line(); -}; + const sortedModules = [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)); + for (const [importPath, names] of sortedModules) { + w.tsImport(importPath, ...names.sort(), { typeOnly: true }); + } + if (sortedModules.length > 0) w.line(); -/** Collect method suffixes that extension/slice accessors will generate, to avoid duplicates */ -const collectExtSliceMethodSuffixes = ( - extensions: ProfileExtension[], - extensionMethodNames: Map, -): Set => { - const suffixes = new Set(); - for (const name of extensionMethodNames.values()) { - suffixes.add(name.replace(/^set/, "")); + // Import extension profile classes for delegation in setters + const extProfileImports = new Map(); + for (const ext of flatProfile.extensions ?? []) { + if (!ext.url) continue; + const info = resolveExtensionProfile(tsIndex, flatProfile.identifier.package, ext.url); + if (!info) continue; + if (!extProfileImports.has(info.className)) { + const hasFlatInput = collectSubExtensionSlices(info.flatProfile).length > 0; + extProfileImports.set(info.className, { modulePath: info.modulePath, hasFlatInput }); + } } - for (const ext of extensions) { - if (ext.url) suffixes.add(uppercaseFirstLetter(tsCamelCase(ext.name))); + for (const [className, { modulePath, hasFlatInput }] of [...extProfileImports.entries()].sort(([a], [b]) => + a.localeCompare(b), + )) { + const imports = [className, ...(hasFlatInput ? [`type ${className}Flat`] : [])]; + w.tsImport(modulePath, ...imports); } - return suffixes; + if (extProfileImports.size > 0) w.line(); }; -type ConstrainedChoice = { - choiceBase: string; - variant: string; - variantType: string; - variantTypeId: Identifier; - allChoiceNames: string[]; -}; - -/** - * Detect if a slice constrains a polymorphic field to exactly one variant. - * E.g., BP systolic slice constrains value[x] (11 variants) to only valueQuantity. - */ -const detectConstrainedChoice = ( - tsIndex: TypeSchemaIndex, - flatProfile: ProfileTypeSchema, - sliceBaseTypeId: Identifier, - sliceElements: string[] | undefined, -): ConstrainedChoice | undefined => { - if (!sliceElements) return undefined; - - // Resolve the base type's TypeSchema to find choice declarations - const baseSchema = tsIndex.resolveByUrl(flatProfile.identifier.package, sliceBaseTypeId.url as CanonicalUrl); - if (!baseSchema || !("fields" in baseSchema) || !baseSchema.fields) return undefined; - - for (const [fieldName, field] of Object.entries(baseSchema.fields)) { - if (!isChoiceDeclarationField(field)) continue; - - // Find which choice instances are in the slice elements - const matchingVariants = field.choices.filter((c) => sliceElements.includes(c)); - if (matchingVariants.length !== 1) continue; - - const variantName = matchingVariants[0] as string; - const variantField = baseSchema.fields[variantName]; - if (!variantField || !isChoiceInstanceField(variantField)) continue; - - // Skip flattening for primitive types — can't intersect object with boolean/string/etc. - if (isPrimitiveIdentifier(variantField.type)) continue; - - return { - choiceBase: fieldName, - variant: variantName, - variantType: tsTypeFromIdentifier(variantField.type), - variantTypeId: variantField.type, - allChoiceNames: field.choices, - }; +const generateStaticSliceFields = (w: TypeScript, sliceDefs: SliceDef[]) => { + for (const sliceDef of sliceDefs) { + const staticName = `${tsSliceStaticName(sliceDef.sliceName)}SliceMatch`; + w.lineSM(`private static readonly ${staticName}: Record = ${JSON.stringify(sliceDef.match)}`); } - return undefined; + if (sliceDefs.length > 0) w.line(); }; -export const generateProfileClass = ( +const generateFactoryMethods = ( w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema, - schema?: TypeSchema, + factoryInfo: ProfileFactoryInfo, ) => { - const tsBaseResourceName = tsTypeFromIdentifier(flatProfile.base); - const tsProfileName = tsResourceName(flatProfile.identifier); const profileClassName = tsProfileClassName(flatProfile); - - // Known polymorphic field base names in FHIR (value[x], effective[x], etc.) - // These don't exist as direct properties on TypeScript types - const polymorphicBaseNames = new Set([ - "value", - "effective", - "onset", - "abatement", - "occurrence", - "timing", - "deceased", - "born", - "age", - "medication", - "performed", - "serviced", - "collected", - "item", - "subject", - "bounds", - "amount", - "content", - "product", - "rate", - "dose", - "asNeeded", - ]); - - const sliceDefs = Object.entries(flatProfile.fields ?? {}) - .filter(([_fieldName, field]) => isNotChoiceDeclarationField(field) && field.slicing?.slices) - .flatMap(([fieldName, field]) => { - if (!isNotChoiceDeclarationField(field) || !field.slicing?.slices || !field.type) return []; - const baseType = tsTypeFromIdentifier(field.type); - return Object.entries(field.slicing.slices) - .filter(([_sliceName, slice]) => { - const match = slice.match ?? {}; - return Object.keys(match).length > 0; - }) - .map(([sliceName, slice]) => { - const matchFields = Object.keys(slice.match ?? {}); - const required = slice.required ?? []; - // Filter out fields that are in match or polymorphic base names - const filteredRequired = required.filter( - (name) => !matchFields.includes(name) && !polymorphicBaseNames.has(name), - ); - const constrainedChoice = detectConstrainedChoice(tsIndex, flatProfile, field.type, slice.elements); - return { - fieldName, - baseType, - sliceName, - match: slice.match ?? {}, - required, - excluded: slice.excluded ?? [], - array: Boolean(field.array), - // Input is optional when there are no required fields after filtering - inputOptional: filteredRequired.length === 0, - constrainedChoice, - }; - }); - }); - - const extensions = flatProfile.extensions ?? []; - const complexExtensions = extensions.filter((ext) => ext.isComplex && ext.subExtensions); - - for (const ext of complexExtensions) { - const typeName = tsExtensionInputTypeName(tsProfileName, ext.name); - w.curlyBlock(["export", "type", typeName, "="], () => { - for (const sub of ext.subExtensions ?? []) { - const tsType = sub.valueType ? tsTypeFromIdentifier(sub.valueType) : "unknown"; - const isArray = sub.max === "*"; - const isRequired = sub.min !== undefined && sub.min > 0; - w.lineSM(`${sub.name}${isRequired ? "" : "?"}: ${tsType}${isArray ? "[]" : ""}`); - } - }); - w.line(); - } - - if (sliceDefs.length > 0) { - for (const sliceDef of sliceDefs) { - const typeName = tsSliceInputTypeName(tsProfileName, sliceDef.fieldName, sliceDef.sliceName); - const matchFields = Object.keys(sliceDef.match); - const allExcluded = [...new Set([...sliceDef.excluded, ...matchFields])]; - // When a choice is constrained to a single variant, also omit the choice base + all instances - if (sliceDef.constrainedChoice) { - const cc = sliceDef.constrainedChoice; - allExcluded.push(cc.choiceBase); - for (const name of cc.allChoiceNames) { - if (!allExcluded.includes(name)) allExcluded.push(name); - } - } - const excludedNames = allExcluded.map((name) => JSON.stringify(name)); - // Filter out polymorphic base names that don't exist as direct TS properties - const filteredRequired = sliceDef.required.filter( - (name) => !matchFields.includes(name) && !polymorphicBaseNames.has(name), - ); - const requiredNames = filteredRequired.map((name) => JSON.stringify(name)); - let typeExpr = sliceDef.baseType; - if (excludedNames.length > 0) { - typeExpr = `Omit<${typeExpr}, ${excludedNames.join(" | ")}>`; - } - if (requiredNames.length > 0) { - typeExpr = `${typeExpr} & Required>`; - } - // Intersect with the single variant's type for flattened API - if (sliceDef.constrainedChoice) { - typeExpr = `${typeExpr} & ${sliceDef.constrainedChoice.variantType}`; - } - w.lineSM(`export type ${typeName} = ${typeExpr}`); - } - w.line(); - } - - // Check if we have an override interface (narrowed types) - const hasOverrideInterface = detectFieldOverrides(w, tsIndex, flatProfile).size > 0; - const factoryInfo = collectProfileFactoryInfo(tsIndex, flatProfile); - - // Determine which helpers are actually needed - const needsSliceHelpers = sliceDefs.length > 0 || factoryInfo.sliceAutoFields.length > 0; - const extensionsWithNestedPath = extensions.filter((ext) => { - const targetPath = ext.path.split(".").filter((segment) => segment !== "extension"); - return targetPath.length > 0; - }); - const needsGetOrCreateObjectAtPath = extensionsWithNestedPath.length > 0; - const needsExtensionExtraction = complexExtensions.length > 0; - const needsSliceExtraction = sliceDefs.length > 0; - const needsSliceChoiceHelpers = sliceDefs.some((s) => s.constrainedChoice); - - const needsValidation = Object.keys(flatProfile.fields ?? {}).length > 0; + const tsBaseResourceName = tsTypeFromIdentifier(flatProfile.base); const hasMeta = tsIndex.isWithMetaField(flatProfile); - const needsRegisterProfile = !!schema?.identifier.url && hasMeta; - - if ( - needsSliceHelpers || - needsGetOrCreateObjectAtPath || - needsExtensionExtraction || - needsSliceExtraction || - needsSliceChoiceHelpers || - needsValidation || - needsRegisterProfile - ) { - generateProfileHelpersImport(w, { - needsGetOrCreateObjectAtPath, - needsSliceHelpers, - needsExtensionExtraction, - needsSliceExtraction, - needsSliceChoiceHelpers, - needsValidation, - needsRegisterProfile, - }); - w.line(); - } - const hasParams = factoryInfo.params.length > 0 || factoryInfo.sliceAutoFields.length > 0; - const createArgsTypeName = `${profileClassName}Params`; + const createArgsTypeName = `${profileClassName}Raw`; const paramSignature = hasParams ? `args: ${createArgsTypeName}` : ""; const allFields = [ ...factoryInfo.autoFields.map((f) => ({ name: f.name, value: f.value })), ...factoryInfo.sliceAutoFields.map((f) => ({ name: f.name, value: `${f.name}WithDefaults` })), ...factoryInfo.params.map((p) => ({ name: p.name, value: `args.${p.name}` })), ]; - - if (hasParams) { - w.curlyBlock(["export", "type", createArgsTypeName, "="], () => { - for (const p of factoryInfo.params) { - w.lineSM(`${p.name}: ${p.tsType}`); - } - for (const f of factoryInfo.sliceAutoFields) { - w.lineSM(`${f.name}?: ${f.tsType}`); - } - }); - w.line(); - } - - const canonicalUrl = schema?.identifier.url; - - if (schema) { - w.comment("CanonicalURL:", schema.identifier.url, `(pkg: ${packageMetaToFhir(packageMeta(schema))})`); - } - w.curlyBlock(["export", "class", profileClassName], () => { - if (canonicalUrl) { - w.line(`static readonly canonicalUrl = ${JSON.stringify(canonicalUrl)}`); - w.line(); + w.curlyBlock(["constructor", `(resource: ${tsBaseResourceName})`], () => { + w.lineSM("this.resource = resource"); + }); + w.line(); + w.curlyBlock(["static", "from", `(resource: ${tsBaseResourceName})`, `: ${profileClassName}`], () => { + if (hasMeta) { + w.curlyBlock(["if", `(!resource.meta?.profile?.includes(${profileClassName}.canonicalUrl))`], () => { + w.line( + `throw new Error(\`${profileClassName}: meta.profile must include \${${profileClassName}.canonicalUrl}\`)`, + ); + }); } - for (const sliceDef of sliceDefs) { - const staticName = `${tsSliceStaticName(sliceDef.sliceName)}SliceMatch`; - w.line( - `private static readonly ${staticName}: Record = ${JSON.stringify(sliceDef.match)}`, - ); + w.lineSM(`const profile = new ${profileClassName}(resource)`); + w.lineSM("const { errors } = profile.validate()"); + w.line(`if (errors.length > 0) throw new Error(errors.join("; "))`); + w.lineSM("return profile"); + }); + w.line(); + w.curlyBlock(["static", "apply", `(resource: ${tsBaseResourceName})`, `: ${profileClassName}`], () => { + if (hasMeta) { + w.lineSM(`ensureProfile(resource, ${profileClassName}.canonicalUrl)`); } - if (sliceDefs.length > 0) w.line(); - w.line(`private resource: ${tsBaseResourceName}`); + w.lineSM(`return new ${profileClassName}(resource)`); + }); + w.line(); + // For extension profiles with sub-extension slices: generate resolveInput helper, + // widen createResource and create to accept Input | Raw + const subSlicesForInput = flatProfile.base.name === "Extension" ? collectSubExtensionSlices(flatProfile) : []; + const hasInputHelper = subSlicesForInput.length > 0; + + if (hasInputHelper) { + const rawInputTypeName = `${profileClassName}Raw`; + const inputTypeName = `${profileClassName}Flat`; + + // Private helper: converts Input to Extension[], passes through Raw.extension + w.curlyBlock( + ["private static", "resolveInput", `(args: ${rawInputTypeName} | ${inputTypeName})`, ": Extension[]"], + () => { + w.ifElseChain( + [ + { + cond: `isRawExtensionInput<${rawInputTypeName}>(args)`, + body: () => w.lineSM("return args.extension ?? []"), + }, + ], + () => { + w.lineSM("const result: Extension[] = []"); + for (const sub of subSlicesForInput) { + if (sub.isArray) { + w.curlyBlock(["if", `(args.${sub.name})`], () => { + w.curlyBlock(["for", `(const item of args.${sub.name})`], () => { + w.lineSM( + `result.push({ url: "${sub.url}", ${sub.valueField}: item } as Extension)`, + ); + }); + }); + } else { + w.curlyBlock(["if", `(args.${sub.name} !== undefined)`], () => { + w.lineSM( + `result.push({ url: "${sub.url}", ${sub.valueField}: args.${sub.name} } as Extension)`, + ); + }); + } + } + w.lineSM("return result"); + }, + ); + }, + ); w.line(); - w.curlyBlock(["constructor", `(resource: ${tsBaseResourceName})`], () => { - w.line("this.resource = resource"); - if (canonicalUrl && hasMeta) { - w.line(`ensureProfile(resource, ${JSON.stringify(canonicalUrl)})`); + + // createResource — accepts Input | Raw + const createResourceSig = hasParams + ? `args: ${rawInputTypeName} | ${inputTypeName}` + : `args?: ${rawInputTypeName} | ${inputTypeName}`; + w.curlyBlock(["static", "createResource", `(${createResourceSig})`, `: ${tsBaseResourceName}`], () => { + w.lineSM(`const resolvedExtensions = ${profileClassName}.resolveInput(args ?? {})`); + const extSliceField = factoryInfo.sliceAutoFields.find((f) => f.name === "extension"); + if (extSliceField) { + const matchRefs = extSliceField.sliceNames.map( + (s) => `${profileClassName}.${tsSliceStaticName(s)}SliceMatch`, + ); + w.line("const extensionWithDefaults = ensureSliceDefaults("); + w.indentBlock(() => { + w.line("resolvedExtensions,"); + for (const ref of matchRefs) { + w.line(`${ref},`); + } + }); + w.lineSM(")"); } + w.line(); + const extensionVar = extSliceField ? "extensionWithDefaults" : "resolvedExtensions"; + w.curlyBlock([`const resource = buildResource<${tsBaseResourceName}>(`], () => { + for (const f of allFields) { + if (f.name === "extension") continue; + w.line(`${f.name}: ${f.value},`); + } + w.line(`extension: ${extensionVar},`); + if (hasMeta) { + w.line(`meta: { profile: [${profileClassName}.canonicalUrl] },`); + } + }, [")"]); + + w.lineSM("return resource"); }); w.line(); - w.curlyBlock(["static", "from", `(resource: ${tsBaseResourceName})`, `: ${profileClassName}`], () => { - w.line(`return new ${profileClassName}(resource)`); + + // create — accepts Input | Raw, delegates to createResource + const createSig = hasParams + ? `args: ${rawInputTypeName} | ${inputTypeName}` + : `args?: ${rawInputTypeName} | ${inputTypeName}`; + w.curlyBlock(["static", "create", `(${createSig})`, `: ${profileClassName}`], () => { + w.lineSM(`return ${profileClassName}.apply(${profileClassName}.createResource(args))`); }); - w.line(); + } else { + // Standard createResource / create (no Input helper) w.curlyBlock(["static", "createResource", `(${paramSignature})`, `: ${tsBaseResourceName}`], () => { - // Generate merge logic for slice auto-fields for (const f of factoryInfo.sliceAutoFields) { const matchRefs = f.sliceNames.map((s) => `${profileClassName}.${tsSliceStaticName(s)}SliceMatch`); w.line(`const ${f.name}WithDefaults = ensureSliceDefaults(`); @@ -702,464 +546,293 @@ export const generateProfileClass = ( w.line(`${ref},`); } }); - w.line(")"); + w.lineSM(")"); } if (factoryInfo.sliceAutoFields.length > 0) { w.line(); } if (isPrimitiveIdentifier(flatProfile.base)) { - w.line(`const resource = undefined as unknown as ${tsBaseResourceName}`); + w.lineSM(`const resource = undefined as unknown as ${tsBaseResourceName}`); } else { - w.curlyBlock(["const resource ="], () => { + w.curlyBlock([`const resource = buildResource<${tsBaseResourceName}>(`], () => { for (const f of allFields) { w.line(`${f.name}: ${f.value},`); } - if (canonicalUrl && hasMeta) { + if (hasMeta) { w.line(`meta: { profile: [${profileClassName}.canonicalUrl] },`); } - }, [` as unknown as ${tsBaseResourceName}`]); + }, [")"]); } - w.line("return resource"); + w.lineSM("return resource"); }); w.line(); w.curlyBlock(["static", "create", `(${paramSignature})`, `: ${profileClassName}`], () => { - w.line(`return ${profileClassName}.from(${profileClassName}.createResource(${hasParams ? "args" : ""}))`); + w.lineSM( + `return ${profileClassName}.apply(${profileClassName}.createResource(${hasParams ? "args" : ""}))`, + ); + }); + } + w.line(); + // toResource() returns base type (e.g., Patient) + w.curlyBlock(["toResource", "()", `: ${tsBaseResourceName}`], () => { + w.lineSM("return this.resource"); + }); + w.line(); +}; + +const generateFieldAccessors = ( + w: TypeScript, + factoryInfo: ProfileFactoryInfo, + extSliceMethodBaseNames: Set, +) => { + w.line("// Field accessors"); + for (const p of factoryInfo.params) { + const methodBaseName = uppercaseFirstLetter(p.name); + w.curlyBlock([`get${methodBaseName}`, "()", `: ${p.tsType} | undefined`], () => { + w.lineSM(`return this.resource.${p.name} as ${p.tsType} | undefined`); }); w.line(); - // toResource() returns base type (e.g., Patient) - w.curlyBlock(["toResource", "()", `: ${tsBaseResourceName}`], () => { - w.line("return this.resource"); + w.curlyBlock([`set${methodBaseName}`, `(value: ${p.tsType})`, ": this"], () => { + w.lineSM(`Object.assign(this.resource, { ${p.name}: value })`); + w.lineSM("return this"); }); w.line(); - // -- Field accessors section -- - const hasFieldAccessors = factoryInfo.params.length > 0 || factoryInfo.accessors.length > 0; - if (hasFieldAccessors) { - w.line("// Field accessors"); - w.line(); + } + + // Getter and setter methods for choice instance fields (skip if extension/slice has same name) + for (const a of factoryInfo.accessors) { + const methodBaseName = uppercaseFirstLetter(tsCamelCase(a.name)); + if (extSliceMethodBaseNames.has(methodBaseName)) continue; + const fieldAccess = tsFieldName(a.name); + w.curlyBlock([`get${methodBaseName}`, "()", `: ${a.tsType} | undefined`], () => { + 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(); + } +}; + +/** Generate inline extension input types only for complex extensions without a resolved FlatInput profile */ +const generateInlineExtensionInputTypes = (w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema) => { + const tsProfileName = tsResourceName(flatProfile.identifier); + const complexExtensions = (flatProfile.extensions ?? []).filter((ext) => ext.isComplex && ext.subExtensions); + for (const ext of complexExtensions) { + if (!ext.url) continue; + const extProfileInfo = resolveExtensionProfile(tsIndex, flatProfile.identifier.package, ext.url); + const hasFlatInput = extProfileInfo ? collectSubExtensionSlices(extProfileInfo.flatProfile).length > 0 : false; + if (hasFlatInput) continue; + const typeName = tsExtensionFlatTypeName(tsProfileName, ext.name); + w.curlyBlock(["export", "type", typeName, "="], () => { + for (const sub of ext.subExtensions ?? []) { + const tsType = sub.valueType ? tsTypeFromIdentifier(sub.valueType) : "unknown"; + const isArray = sub.max === "*"; + const isRequired = sub.min !== undefined && sub.min > 0; + w.lineSM(`${sub.name}${isRequired ? "" : "?"}: ${tsType}${isArray ? "[]" : ""}`); + } + }); + w.line(); + } +}; + +const generateSliceInputTypes = (w: TypeScript, flatProfile: ProfileTypeSchema, sliceDefs: SliceDef[]) => { + if (sliceDefs.length === 0) return; + const tsProfileName = tsResourceName(flatProfile.identifier); + for (const sliceDef of sliceDefs) { + const typeName = tsSliceFlatTypeName(tsProfileName, sliceDef.fieldName, sliceDef.sliceName); + const matchFields = Object.keys(sliceDef.match); + const allExcluded = [...new Set([...sliceDef.excluded, ...matchFields])]; + if (sliceDef.constrainedChoice) { + const cc = sliceDef.constrainedChoice; + allExcluded.push(cc.choiceBase); + for (const name of cc.allChoiceNames) { + if (!allExcluded.includes(name)) allExcluded.push(name); + } } - for (const p of factoryInfo.params) { - const methodSuffix = uppercaseFirstLetter(p.name); - w.curlyBlock([`get${methodSuffix}`, "()", `: ${p.tsType} | undefined`], () => { - w.line(`return this.resource.${p.name} as ${p.tsType} | undefined`); - }); - w.line(); - w.curlyBlock([`set${methodSuffix}`, `(value: ${p.tsType})`, ": this"], () => { - w.line(`Object.assign(this.resource, { ${p.name}: value })`); - w.line("return this"); - }); - w.line(); + const excludedNames = allExcluded.map((name) => JSON.stringify(name)); + const requiredNames = sliceDef.required.map((name) => JSON.stringify(name)); + let typeExpr = sliceDef.baseType; + if (excludedNames.length > 0) { + typeExpr = `Omit<${typeExpr}, ${excludedNames.join(" | ")}>`; } - // Compute extension and slice method names first to detect collisions with accessors - const extensionMethods = extensions - .filter((ext) => ext.url) - .map((ext) => ({ - ext, - baseName: tsExtensionMethodName(ext.name), - fallbackName: tsQualifiedExtensionMethodName(ext.name, ext.path), - })); - const sliceMethodBases = sliceDefs.map((slice) => tsSliceMethodName(slice.sliceName)); - const methodCounts = new Map(); - for (const name of [...sliceMethodBases, ...extensionMethods.map((m) => m.baseName)]) { - methodCounts.set(name, (methodCounts.get(name) ?? 0) + 1); + if (requiredNames.length > 0) { + typeExpr = `${typeExpr} & Required>`; } - const extensionMethodNames = new Map( - extensionMethods.map((entry) => [ - entry.ext, - (methodCounts.get(entry.baseName) ?? 0) > 1 ? entry.fallbackName : entry.baseName, - ]), - ); - const sliceMethodNames = new Map( - sliceDefs.map((slice) => { - const baseName = tsSliceMethodName(slice.sliceName); - const needsFallback = (methodCounts.get(baseName) ?? 0) > 1; - const fallback = tsQualifiedSliceMethodName(slice.fieldName, slice.sliceName); - return [slice, needsFallback ? fallback : baseName]; - }), - ); + if (sliceDef.constrainedChoice) { + typeExpr = `${typeExpr} & ${tsTypeFromIdentifier(sliceDef.constrainedChoice.variantType)}`; + } + w.lineSM(`export type ${typeName} = ${typeExpr}`); + } + w.line(); +}; - const extSliceMethodSuffixes = collectExtSliceMethodSuffixes(extensions, extensionMethodNames); +const generateRawType = (w: TypeScript, flatProfile: ProfileTypeSchema, factoryInfo: ProfileFactoryInfo) => { + const hasParams = factoryInfo.params.length > 0 || factoryInfo.sliceAutoFields.length > 0; + const subSlices = flatProfile.base.name === "Extension" ? collectSubExtensionSlices(flatProfile) : []; + if (!hasParams && subSlices.length === 0) return; - // Getter and setter methods for choice instance fields (skip if extension/slice has same name) - for (const a of factoryInfo.accessors) { - const methodSuffix = uppercaseFirstLetter(tsCamelCase(a.name)); - if (extSliceMethodSuffixes.has(methodSuffix)) continue; - const fieldAccess = tsFieldName(a.name); - w.curlyBlock([`get${methodSuffix}`, "()", `: ${a.tsType} | undefined`], () => { - w.line(`return ${tsGet("this.resource", fieldAccess)} as ${a.tsType} | undefined`); - }); - w.line(); - w.curlyBlock([`set${methodSuffix}`, `(value: ${a.tsType})`, ": this"], () => { - w.line(`Object.assign(this.resource, { ${fieldAccess}: value })`); - w.line("return this"); - }); - w.line(); + const createArgsTypeName = `${tsProfileClassName(flatProfile)}Raw`; + w.curlyBlock(["export", "type", createArgsTypeName, "="], () => { + for (const p of factoryInfo.params) { + w.lineSM(`${p.name}: ${p.tsType}`); } - // toProfile() returns casted profile type if override interface exists - if (hasOverrideInterface) { - w.curlyBlock(["toProfile", "()", `: ${tsProfileName}`], () => { - w.line(`return this.resource as ${tsProfileName}`); - }); - w.line(); + for (const f of factoryInfo.sliceAutoFields) { + w.lineSM(`${f.name}?: ${f.tsType}`); } - - // -- Slices and extensions section -- - const hasSlicesOrExtensions = extensions.length > 0 || sliceDefs.length > 0; - if (hasSlicesOrExtensions) { - w.line("// Slices and extensions"); - w.line(); + const extensionCovered = + factoryInfo.params.some((p) => p.name === "extension") || + factoryInfo.sliceAutoFields.some((f) => f.name === "extension"); + if (subSlices.length > 0 && !extensionCovered) { + w.lineSM("extension?: Extension[]"); } + }); + w.line(); +}; - generateExtensionSetterMethods(w, extensions, extensionMethodNames, tsProfileName); - - for (const sliceDef of sliceDefs) { - const methodName = - sliceMethodNames.get(sliceDef) ?? tsQualifiedSliceMethodName(sliceDef.fieldName, sliceDef.sliceName); - const typeName = tsSliceInputTypeName(tsProfileName, sliceDef.fieldName, sliceDef.sliceName); - const matchRef = `${profileClassName}.${tsSliceStaticName(sliceDef.sliceName)}SliceMatch`; - const tsField = tsFieldName(sliceDef.fieldName); - const fieldAccess = tsGet("this.resource", tsField); - // Make input optional when there are no required fields (input can be empty object) - const paramSignature = sliceDef.inputOptional - ? `(input?: ${typeName}): this` - : `(input: ${typeName}): this`; - w.curlyBlock(["public", methodName, paramSignature], () => { - w.line(`const match = ${matchRef}`); - const inputExpr = sliceDef.inputOptional ? "input ?? {}" : "input"; - if (sliceDef.constrainedChoice) { - const cc = sliceDef.constrainedChoice; - w.line( - `const wrapped = wrapSliceChoice<${sliceDef.baseType}>(${inputExpr}, ${JSON.stringify(cc.variant)})`, - ); - w.line(`const value = applySliceMatch<${sliceDef.baseType}>(wrapped, match)`); - } else { - w.line(`const value = applySliceMatch<${sliceDef.baseType}>(${inputExpr}, match)`); - } - if (sliceDef.array) { - w.line(`setArraySlice(${fieldAccess} ??= [], match, value)`); - } else { - w.line(`${fieldAccess} = value`); - } - w.line("return this"); - }); - w.line(); - } +const generateFlatInputType = (w: TypeScript, flatProfile: ProfileTypeSchema) => { + const subSlices = flatProfile.base.name === "Extension" ? collectSubExtensionSlices(flatProfile) : []; + if (subSlices.length === 0) return; - // Generate extension getters - two methods per extension: - // 1. get{Name}() - returns flat API (simplified) - // 2. get{Name}Extension() - returns raw FHIR Extension - const generatedGetMethods = new Set(); - - for (const ext of extensions) { - if (!ext.url) continue; - const baseName = uppercaseFirstLetter(tsCamelCase(ext.name)); - const getMethodName = `get${baseName}`; - const getExtensionMethodName = `get${baseName}Extension`; - if (generatedGetMethods.has(getMethodName)) continue; - generatedGetMethods.add(getMethodName); - const valueTypes = ext.valueTypes ?? []; - const targetPath = ext.path.split(".").filter((segment) => segment !== "extension"); - - // Helper to generate the extension lookup code - const generateExtLookup = () => { - if (targetPath.length === 0) { - w.line(`const ext = this.resource.extension?.find(e => e.url === "${ext.url}")`); - } else { - w.line( - `const target = ensurePath(this.resource as unknown as Record, ${JSON.stringify(targetPath)})`, - ); - w.line( - `const ext = (target.extension as Extension[] | undefined)?.find(e => e.url === "${ext.url}")`, - ); - } - }; - - if (ext.isComplex && ext.subExtensions) { - const inputTypeName = tsExtensionInputTypeName(tsProfileName, ext.name); - // Flat API getter - w.curlyBlock(["public", getMethodName, `(): ${inputTypeName} | undefined`], () => { - generateExtLookup(); - w.line("if (!ext) return undefined"); - // Build extraction config - const configItems = (ext.subExtensions ?? []).map((sub) => { - const valueField = sub.valueType ? `value${uppercaseFirstLetter(sub.valueType.name)}` : "value"; - const isArray = sub.max === "*"; - return `{ name: "${sub.url}", valueField: "${valueField}", isArray: ${isArray} }`; - }); - w.line(`const config = [${configItems.join(", ")}]`); - w.line( - `return extractComplexExtension(ext as unknown as { extension?: Array<{ url?: string; [key: string]: unknown }> }, config) as ${inputTypeName}`, - ); - }); - w.line(); - // Raw Extension getter - w.curlyBlock(["public", getExtensionMethodName, "(): Extension | undefined"], () => { - generateExtLookup(); - w.line("return ext"); - }); - } else if (valueTypes.length === 1 && valueTypes[0]) { - const firstValueType = valueTypes[0]; - const valueType = tsTypeFromIdentifier(firstValueType); - const valueField = `value${uppercaseFirstLetter(firstValueType.name)}`; - // Flat API getter (cast needed: value field may not exist on Extension in this FHIR version) - w.curlyBlock(["public", getMethodName, `(): ${valueType} | undefined`], () => { - generateExtLookup(); - w.line( - `return (ext as Record | undefined)?.${valueField} as ${valueType} | undefined`, - ); - }); - w.line(); - // Raw Extension getter - w.curlyBlock(["public", getExtensionMethodName, "(): Extension | undefined"], () => { - generateExtLookup(); - w.line("return ext"); - }); - } else { - // Generic extension - only raw getter makes sense - w.curlyBlock(["public", getMethodName, "(): Extension | undefined"], () => { - if (targetPath.length === 0) { - w.line(`return this.resource.extension?.find(e => e.url === "${ext.url}")`); - } else { - w.line( - `const target = ensurePath(this.resource as unknown as Record, ${JSON.stringify(targetPath)})`, - ); - w.line( - `return (target.extension as Extension[] | undefined)?.find(e => e.url === "${ext.url}")`, - ); - } - }); - } - w.line(); + const flatInputTypeName = `${tsProfileClassName(flatProfile)}Flat`; + w.curlyBlock(["export", "type", flatInputTypeName, "="], () => { + for (const sub of subSlices) { + const opt = sub.isRequired ? "" : "?"; + const arr = sub.isArray ? "[]" : ""; + w.lineSM(`${sub.name}${opt}: ${sub.tsType}${arr}`); } + }); + w.line(); +}; - // Generate slice getters - two methods per slice: - // 1. get{SliceName}() - returns simplified (without discriminator fields) - // 2. get{SliceName}Raw() - returns full FHIR type with all fields - for (const sliceDef of sliceDefs) { - const baseName = uppercaseFirstLetter(normalizeTsName(sliceDef.sliceName)); - const getMethodName = `get${baseName}`; - const getRawMethodName = `get${baseName}Raw`; - if (generatedGetMethods.has(getMethodName)) continue; - generatedGetMethods.add(getMethodName); - const typeName = tsSliceInputTypeName(tsProfileName, sliceDef.fieldName, sliceDef.sliceName); - const matchRef = `${profileClassName}.${tsSliceStaticName(sliceDef.sliceName)}SliceMatch`; - const matchKeys = JSON.stringify(Object.keys(sliceDef.match)); - const tsField = tsFieldName(sliceDef.fieldName); - const fieldAccess = tsGet("this.resource", tsField); - const baseType = sliceDef.baseType; - - // Helper to find the slice item - const generateSliceLookup = () => { - w.line(`const match = ${matchRef}`); - if (sliceDef.array) { - w.line(`const item = getArraySlice(${fieldAccess}, match)`); - } else { - w.line(`const item = ${fieldAccess}`); - w.line("if (!item || !matchesValue(item, match)) return undefined"); - } - }; +type ResolvedProfileMethods = { + /** "url:path" → method base name (e.g., "Race" or "PathRace") */ + extensions: Record; + /** "fieldName:sliceName" → method base name */ + slices: Record; + /** All resolved base names (extensions + slices) for field accessor dedup */ + allBaseNames: Set; +}; - // Flat API getter (simplified) - w.curlyBlock(["public", getMethodName, `(): ${typeName} | undefined`], () => { - generateSliceLookup(); - if (sliceDef.array) { - w.line("if (!item) return undefined"); - } - if (sliceDef.constrainedChoice) { - const cc = sliceDef.constrainedChoice; - w.line(`return unwrapSliceChoice<${typeName}>(item, ${matchKeys}, ${JSON.stringify(cc.variant)})`); - } else { - w.line(`return stripMatchKeys<${typeName}>(item, ${matchKeys})`); - } - }); - w.line(); +type NameEntry = { key: string; candidates: string[] }; + +const countBy = (entries: NameEntry[], level: number): Record => + entries.reduce( + (counts, e) => { + const name = e.candidates[level] ?? ""; + counts[name] = (counts[name] ?? 0) + 1; + return counts; + }, + {} as Record, + ); + +/** Resolve naming collisions across multiple levels of candidates. + * Each entry provides candidate names in priority order (e.g. base → qualified → discriminated). */ +const resolveNameCollisions = (entries: NameEntry[]): Record => { + const levels = entries[0]?.candidates.length ?? 0; + + const resolve = (unresolved: NameEntry[], level: number): Record => { + if (unresolved.length === 0 || level >= levels) return {}; + const counts = countBy(unresolved, level); + const isLastLevel = level >= levels - 1; + const [resolved, colliding] = unresolved.reduce( + ([res, col], e) => { + const name = e.candidates[level] ?? ""; + return (counts[name] ?? 0) > 1 && !isLastLevel ? [res, [...col, e]] : [{ ...res, [e.key]: name }, col]; + }, + [{} as Record, [] as NameEntry[]], + ); + return { ...resolved, ...resolve(colliding, level + 1) }; + }; - // Raw getter (full FHIR type) - w.curlyBlock(["public", getRawMethodName, `(): ${baseType} | undefined`], () => { - generateSliceLookup(); - w.line("return item"); - }); - w.line(); - } + return resolve(entries, 0); +}; - // -- Validation section -- - if (needsValidation) { - w.line("// Validation"); - w.line(); - } - generateValidateMethod(w, tsIndex, flatProfile); +const toRecord = (entries: NameEntry[], resolved: Record): Record => + Object.fromEntries(entries.map((e) => [e.key, resolved[e.key] ?? e.candidates[0] ?? ""])); + +const resolveProfileMethodBaseNames = ( + extensions: ProfileExtension[], + sliceDefs: SliceDef[], +): ResolvedProfileMethods => { + const extensionEntries: NameEntry[] = extensions + .filter((ext) => ext.url) + .map((ext) => { + const base = tsExtensionMethodBaseName(ext.name); + const qualified = tsQualifiedExtensionMethodBaseName(ext.name, ext.path); + return { key: `${ext.url}:${ext.path}`, candidates: [base, qualified, `${qualified}Extension`] }; + }); + + const sliceEntries: NameEntry[] = sliceDefs.map((slice) => { + const base = tsSliceMethodBaseName(slice.sliceName); + const qualified = tsQualifiedSliceMethodBaseName(slice.fieldName, slice.sliceName); + return { key: `${slice.fieldName}:${slice.sliceName}`, candidates: [base, qualified, `${qualified}Slice`] }; }); - w.line(); + + const resolved = resolveNameCollisions([...extensionEntries, ...sliceEntries]); + const extensionsRecords = toRecord(extensionEntries, resolved); + const slicesRecords = toRecord(sliceEntries, resolved); + const allBaseNames = new Set([...Object.values(extensionsRecords), ...Object.values(slicesRecords)]); + + return { extensions: extensionsRecords, slices: slicesRecords, allBaseNames }; }; -const collectRegularFieldValidation = ( - exprs: string[], - name: string, - field: RegularField | ChoiceFieldInstance, - resolveRef: (ref: Identifier) => Identifier, -) => { - if (field.excluded) { - exprs.push(`...validateExcluded(res, profileName, ${JSON.stringify(name)})`); - return; - } +export const generateProfileClass = (w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema) => { + const tsBaseResourceName = tsTypeFromIdentifier(flatProfile.base); + const profileClassName = tsProfileClassName(flatProfile); + const sliceDefs = collectSliceDefs(tsIndex, flatProfile); + const factoryInfo = collectProfileFactoryInfo(tsIndex, flatProfile); - if (field.required) exprs.push(`...validateRequired(res, profileName, ${JSON.stringify(name)})`); + generateInlineExtensionInputTypes(w, tsIndex, flatProfile); + generateSliceInputTypes(w, flatProfile, sliceDefs); - if (field.valueConstraint) - exprs.push( - `...validateFixedValue(res, profileName, ${JSON.stringify(name)}, ${JSON.stringify(field.valueConstraint.value)})`, - ); + generateProfileHelpersImport(w, tsIndex, flatProfile, sliceDefs, factoryInfo); - if (field.enum && !field.enum.isOpen) - exprs.push(`...validateEnum(res, profileName, ${JSON.stringify(name)}, ${JSON.stringify(field.enum.values)})`); + generateRawType(w, flatProfile, factoryInfo); + generateFlatInputType(w, flatProfile); - if (field.reference && field.reference.length > 0) - exprs.push( - `...validateReference(res, profileName, ${JSON.stringify(name)}, ${JSON.stringify(field.reference.map((ref) => resolveRef(ref).name))})`, - ); + const canonicalUrl = flatProfile.identifier.url; + w.comment("CanonicalURL:", canonicalUrl, `(pkg: ${packageMetaToFhir(packageMeta(flatProfile))})`); - if (field.slicing?.slices) { - for (const [sliceName, slice] of Object.entries(field.slicing.slices)) { - if (slice.min === undefined && slice.max === undefined) continue; - const match = slice.match ?? {}; - if (Object.keys(match).length === 0) continue; - const min = slice.min ?? 0; - const max = slice.max ?? 0; - exprs.push( - `...validateSliceCardinality(res, profileName, ${JSON.stringify(name)}, ${JSON.stringify(match)}, ${JSON.stringify(sliceName)}, ${min}, ${max})`, - ); - } - } -}; + const resolvedMethodNames = resolveProfileMethodBaseNames(flatProfile.extensions ?? [], sliceDefs); -const generateValidateMethod = (w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema) => { - const fields = flatProfile.fields ?? {}; - const profileName = flatProfile.identifier.name; - w.curlyBlock(["validate(): string[]"], () => { - w.line(`const profileName = "${profileName}"`); - w.line("const res = this.resource as unknown as Record"); - - const exprs: string[] = []; - for (const [name, field] of Object.entries(fields)) { - if (isChoiceInstanceField(field)) continue; - - if (isChoiceDeclarationField(field)) { - if (field.required) - exprs.push(`...validateChoiceRequired(res, profileName, ${JSON.stringify(field.choices)})`); - continue; - } + w.curlyBlock(["export", "class", profileClassName], () => { + w.lineSM(`static readonly canonicalUrl = ${JSON.stringify(canonicalUrl)}`); + w.line(); + generateStaticSliceFields(w, sliceDefs); + w.lineSM(`private resource: ${tsBaseResourceName}`); + w.line(); + generateFactoryMethods(w, tsIndex, flatProfile, factoryInfo); + generateFieldAccessors(w, factoryInfo, resolvedMethodNames.allBaseNames); - collectRegularFieldValidation(exprs, name, field, tsIndex.findLastSpecializationByIdentifier); - } + w.line("// Extensions"); + generateExtensionMethods(w, tsIndex, flatProfile, resolvedMethodNames.extensions); - if (exprs.length === 0) { - w.line("return []"); - } else { - w.squareBlock(["return"], () => { - for (const expr of exprs) w.line(`${expr},`); - }); - } + w.line("// Slices"); + generateSliceSetters(w, sliceDefs, flatProfile, resolvedMethodNames.slices); + generateSliceGetters(w, sliceDefs, flatProfile, resolvedMethodNames.slices); + + w.line("// Validation"); + generateValidateMethod(w, tsIndex, flatProfile); }); w.line(); }; -const generateExtensionSetterMethods = ( - w: TypeScript, - extensions: ProfileExtension[], - extensionMethodNames: Map, - tsProfileName: string, -) => { - for (const ext of extensions) { - if (!ext.url) continue; - const methodName = extensionMethodNames.get(ext) ?? tsQualifiedExtensionMethodName(ext.name, ext.path); - const valueTypes = ext.valueTypes ?? []; - const targetPath = ext.path.split(".").filter((segment) => segment !== "extension"); - - if (ext.isComplex && ext.subExtensions) { - const inputTypeName = tsExtensionInputTypeName(tsProfileName, ext.name); - w.curlyBlock(["public", methodName, `(input: ${inputTypeName}): this`], () => { - w.line("const subExtensions: Extension[] = []"); - for (const sub of ext.subExtensions ?? []) { - const valueField = sub.valueType ? `value${uppercaseFirstLetter(sub.valueType.name)}` : "value"; - // When value type is unknown, cast to Extension to avoid TS error - const needsCast = !sub.valueType; - const pushSuffix = needsCast ? " as Extension" : ""; - if (sub.max === "*") { - w.curlyBlock(["if", `(input.${sub.name})`], () => { - w.curlyBlock(["for", `(const item of input.${sub.name})`], () => { - w.line(`subExtensions.push({ url: "${sub.url}", ${valueField}: item }${pushSuffix})`); - }); - }); - } else { - w.curlyBlock(["if", `(input.${sub.name} !== undefined)`], () => { - w.line( - `subExtensions.push({ url: "${sub.url}", ${valueField}: input.${sub.name} }${pushSuffix})`, - ); - }); - } - } - if (targetPath.length === 0) { - w.line("const list = (this.resource.extension ??= [])"); - w.line(`list.push({ url: "${ext.url}", extension: subExtensions })`); - } 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("return this"); - }); - } else if (valueTypes.length === 1 && valueTypes[0]) { - const firstValueType = valueTypes[0]; - const valueType = tsTypeFromIdentifier(firstValueType); - const valueField = `value${uppercaseFirstLetter(firstValueType.name)}`; - w.curlyBlock(["public", methodName, `(value: ${valueType}): this`], () => { - // Cast needed: value field may not exist on Extension in this FHIR version - const extLiteral = `{ url: "${ext.url}", ${valueField}: value } as Extension`; - if (targetPath.length === 0) { - w.line("const list = (this.resource.extension ??= [])"); - w.line(`list.push(${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(${extLiteral})`); - } - w.line("return this"); - }); - } else { - w.curlyBlock(["public", methodName, `(value: Omit): this`], () => { - if (targetPath.length === 0) { - w.line("const list = (this.resource.extension ??= [])"); - w.line(`list.push({ url: "${ext.url}", ...value })`); - } 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}", ...value })`); - } - w.line("return this"); - }); - } - w.line(); - } -}; +export type FieldOverrides = Map< + string, + { profileType: string; required: boolean; array: boolean; typeId: Identifier } +>; -const detectFieldOverrides = ( - w: TypeScript, - tsIndex: TypeSchemaIndex, - flatProfile: ProfileTypeSchema, -): Map => { - const overrides = new Map(); +export const detectFieldOverrides = (tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema): FieldOverrides => { + const overrides: FieldOverrides = new Map(); const specialization = tsIndex.findLastSpecialization(flatProfile); if (!isSpecializationTypeSchema(specialization)) return overrides; + const referenceUrl = "http://hl7.org/fhir/StructureDefinition/Reference" as CanonicalUrl; + const referenceSchema = tsIndex.resolveByUrl(flatProfile.identifier.package, referenceUrl); + for (const [fieldName, pField] of Object.entries(flatProfile.fields ?? {})) { if (!isNotChoiceDeclarationField(pField)) continue; const sField = specialization.fields?.[fieldName]; @@ -1167,6 +840,7 @@ const detectFieldOverrides = ( // Check for Reference narrowing if (pField.reference && sField.reference && pField.reference.length < sField.reference.length) { + if (!referenceSchema) continue; const references = pField.reference .map((ref) => { const resRef = tsIndex.findLastSpecializationByIdentifier(ref); @@ -1180,15 +854,17 @@ const detectFieldOverrides = ( profileType: `Reference<${references}>`, required: pField.required ?? false, array: pField.array ?? false, + typeId: referenceSchema.identifier, }); } // Check for cardinality change (optional -> required) else if (pField.required && !sField.required) { - const tsType = tsTypeForProfileField(w, tsIndex, flatProfile, fieldName, pField); + const tsType = tsTypeForProfileField(tsIndex, flatProfile, fieldName, pField); overrides.set(fieldName, { profileType: tsType, required: true, array: pField.array ?? false, + typeId: pField.type, }); } } @@ -1197,10 +873,9 @@ const detectFieldOverrides = ( export const generateProfileOverrideInterface = ( w: TypeScript, - tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema, + overrides: FieldOverrides, ) => { - const overrides = detectFieldOverrides(w, tsIndex, flatProfile); if (overrides.size === 0) return; const tsProfileName = tsResourceName(flatProfile.identifier); diff --git a/src/api/writer-generator/typescript/utils.ts b/src/api/writer-generator/typescript/utils.ts index aec1de7f5..7475ccb5a 100644 --- a/src/api/writer-generator/typescript/utils.ts +++ b/src/api/writer-generator/typescript/utils.ts @@ -84,6 +84,11 @@ export const resolveFieldTsType = ( return field.type.name as string; }; +export const fieldTsType = ( + field: RegularField | ChoiceFieldInstance, + resolveRef?: (ref: Identifier) => Identifier, +): string => resolveFieldTsType("", "", field, resolveRef) + (field.array ? "[]" : ""); + export const tsTypeFromIdentifier = (id: Identifier): string => { if (isNestedIdentifier(id)) return tsResourceName(id); if (isPrimitiveIdentifier(id)) return resolvePrimitiveType(id.name); diff --git a/src/api/writer-generator/typescript/writer.ts b/src/api/writer-generator/typescript/writer.ts index 70c01914e..190e6a36e 100644 --- a/src/api/writer-generator/typescript/writer.ts +++ b/src/api/writer-generator/typescript/writer.ts @@ -29,6 +29,7 @@ import { tsResourceName, } from "./name"; import { + detectFieldOverrides, generateProfileClass, generateProfileImports, generateProfileIndexFile, @@ -46,6 +47,7 @@ export const resolveTsAssets = (fn: string) => { }; export type TypeScriptOptions = { + lineWidth?: number; /** openResourceTypeSet -- for resource families (Resource, DomainResource) use open set for resourceType field. * * - when openResourceTypeSet is false: `type Resource = { resourceType: "Resource" | "DomainResource" | "Patient" }` @@ -53,15 +55,49 @@ export type TypeScriptOptions = { */ openResourceTypeSet: boolean; primitiveTypeExtension: boolean; + extensionGetterDefault?: "flat" | "profile" | "raw"; + sliceGetterDefault?: "flat" | "raw"; } & WriterOptions; export class TypeScript extends Writer { constructor(options: TypeScriptOptions) { - super({ ...options, resolveAssets: options.resolveAssets ?? resolveTsAssets }); + super({ lineWidth: 120, ...options, resolveAssets: options.resolveAssets ?? resolveTsAssets }); } - tsImportType(tsPackageName: string, ...entities: string[]) { - this.lineSM(`import type { ${entities.join(", ")} } from "${tsPackageName}"`); + ifElseChain(branches: { cond: string; body: () => void }[], elseBody?: () => void) { + branches.forEach((branch, i) => { + const prefix = i === 0 ? "if" : "} else if"; + this.line(`${prefix} (${branch.cond}) {`); + this.indent(); + branch.body(); + this.deindent(); + }); + if (elseBody) { + this.line("} else {"); + this.indent(); + elseBody(); + this.deindent(); + } + this.line("}"); + } + + tsImport(tsPackageName: string, ...entities: string[]): void; + tsImport(tsPackageName: string, ...args: [...string[], { typeOnly: boolean }]): void; + tsImport(tsPackageName: string, ...rest: (string | { typeOnly: boolean })[]) { + const last = rest[rest.length - 1]; + const typeOnly = typeof last === "object" ? last.typeOnly : false; + const entities = (typeof last === "object" ? rest.slice(0, -1) : rest) as string[]; + const keyword = typeOnly ? "import type" : "import"; + const singleLine = `${keyword} { ${entities.join(", ")} } from "${tsPackageName}"`; + if (singleLine.length <= (this.opts.lineWidth ?? 120)) { + this.lineSM(singleLine); + } else { + this.curlyBlock([keyword], () => { + for (const entity of entities) { + this.line(`${entity},`); + } + }, [` from "${tsPackageName}";`]); + } } generateFhirPackageIndexFile(schemas: TypeSchema[]) { @@ -140,7 +176,7 @@ export class TypeScript extends Writer { imports.sort((a, b) => a.name.localeCompare(b.name)); for (const dep of imports) { this.debugComment(dep.dep); - this.tsImportType(dep.tsPackage, dep.name); + this.tsImport(dep.tsPackage, dep.name, { typeOnly: true }); } for (const dep of skipped) { this.debugComment("skip:", dep); @@ -155,7 +191,7 @@ export class TypeScript extends Writer { const element = tsIndex.resolveByUrl(schema.identifier.package, elementUrl); if (!element) throw new Error(`'${elementUrl}' not found for ${schema.identifier.package}.`); - this.tsImportType(`${importPrefix}${tsModulePath(element.identifier)}`, "Element"); + this.tsImport(`${importPrefix}${tsModulePath(element.identifier)}`, "Element", { typeOnly: true }); } } } @@ -272,9 +308,10 @@ export class TypeScript extends Writer { this.cat(`${tsProfileModuleFileName(tsIndex, schema)}`, () => { this.generateDisclaimer(); const flatProfile = tsIndex.flatProfile(schema); - generateProfileImports(this, tsIndex, flatProfile); - generateProfileOverrideInterface(this, tsIndex, flatProfile); - generateProfileClass(this, tsIndex, flatProfile, schema); + const overrides = detectFieldOverrides(tsIndex, flatProfile); + generateProfileImports(this, tsIndex, flatProfile, overrides); + generateProfileOverrideInterface(this, flatProfile, overrides); + generateProfileClass(this, tsIndex, flatProfile); }); }); } else if (["complex-type", "resource", "logical"].includes(schema.identifier.kind)) { diff --git a/src/api/writer-generator/writer.ts b/src/api/writer-generator/writer.ts index 81c946d1b..dbad9b762 100644 --- a/src/api/writer-generator/writer.ts +++ b/src/api/writer-generator/writer.ts @@ -177,11 +177,11 @@ export abstract class FileSystemWriter extends FileSystemWriter { currentIndent: number = 0; - private indent() { + protected indent() { this.currentIndent += this.opts.tabSize; } - private deindent() { + protected deindent() { this.currentIndent -= this.opts.tabSize; } diff --git a/src/typeschema/core/field-builder.ts b/src/typeschema/core/field-builder.ts index 3b8acbf4e..60e189ea2 100644 --- a/src/typeschema/core/field-builder.ts +++ b/src/typeschema/core/field-builder.ts @@ -333,6 +333,7 @@ export const mkField = ( binding: binding, enum: enumResult, valueConstraint, + mustSupport: element.mustSupport, }; }; diff --git a/src/typeschema/core/profile-extensions.ts b/src/typeschema/core/profile-extensions.ts index 484effae2..78b3428f9 100644 --- a/src/typeschema/core/profile-extensions.ts +++ b/src/typeschema/core/profile-extensions.ts @@ -191,5 +191,13 @@ export const extractProfileExtensions = ( walkElement([], fhirSchema); - return extensions.length === 0 ? undefined : extensions; + const seen = new Set(); + const deduped = extensions.filter((ext) => { + const key = `${ext.url}:${ext.path}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + return deduped.length === 0 ? undefined : deduped; }; diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index b452817b7..6772f64a2 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -218,6 +218,13 @@ export interface FieldSlicing { slices?: Record; } +export type ConstrainedChoiceInfo = { + choiceBase: string; + variant: string; + variantType: Identifier; + allChoiceNames: string[]; +}; + export interface FieldSlice { min?: number; max?: number; @@ -269,6 +276,7 @@ export interface RegularField { max?: number; slicing?: FieldSlicing; valueConstraint?: ValueConstraint; + mustSupport?: boolean; } export interface ChoiceFieldDeclaration { @@ -293,6 +301,7 @@ export interface ChoiceFieldInstance { max?: number; slicing?: FieldSlicing; valueConstraint?: ValueConstraint; + mustSupport?: boolean; } export type Concept = { diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index f2dda9007..0a8935a1a 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -6,8 +6,11 @@ import type { IrReport } from "./ir/types"; import type { Register } from "./register"; import { type CanonicalUrl, + type ConstrainedChoiceInfo, type Field, type Identifier, + isChoiceDeclarationField, + isChoiceInstanceField, isComplexTypeTypeSchema, isLogicalTypeSchema, isProfileTypeSchema, @@ -182,6 +185,11 @@ export type TypeSchemaIndex = { findLastSpecialization: (schema: TypeSchema) => TypeSchema; findLastSpecializationByIdentifier: (id: Identifier) => Identifier; flatProfile: (schema: ProfileTypeSchema) => ProfileTypeSchema; + constrainedChoice: ( + pkgName: PkgName, + baseTypeId: Identifier, + sliceElements: string[], + ) => ConstrainedChoiceInfo | undefined; isWithMetaField: (profile: ProfileTypeSchema) => boolean; entityTree: () => EntityTree; exportTree: (filename: string) => Promise; @@ -360,6 +368,30 @@ export const mkTypeSchemaIndex = ( }; }; + const constrainedChoice = ( + pkgName: PkgName, + baseTypeId: Identifier, + sliceElements: string[], + ): ConstrainedChoiceInfo | undefined => { + const baseSchema = resolveByUrl(pkgName, baseTypeId.url as CanonicalUrl); + if (!baseSchema || !("fields" in baseSchema) || !baseSchema.fields) return undefined; + for (const [fieldName, field] of Object.entries(baseSchema.fields)) { + if (!isChoiceDeclarationField(field)) continue; + const matchingVariants = field.choices.filter((c) => sliceElements.includes(c)); + if (matchingVariants.length !== 1) continue; + const variantName = matchingVariants[0] as string; + const variantField = baseSchema.fields[variantName]; + if (!variantField || !isChoiceInstanceField(variantField)) continue; + return { + choiceBase: fieldName, + variant: variantName, + variantType: variantField.type, + allChoiceNames: field.choices, + }; + } + return undefined; + }; + const isWithMetaField = (profile: ProfileTypeSchema): boolean => { const genealogy = tryHierarchy(profile); if (!genealogy) return false; @@ -413,6 +445,7 @@ export const mkTypeSchemaIndex = ( findLastSpecialization, findLastSpecializationByIdentifier, flatProfile, + constrainedChoice, isWithMetaField, entityTree, exportTree, diff --git a/test/api/write-generator/__snapshots__/typescript.test.ts.snap b/test/api/write-generator/__snapshots__/typescript.test.ts.snap index 15cb76e8c..4ed722537 100644 --- a/test/api/write-generator/__snapshots__/typescript.test.ts.snap +++ b/test/api/write-generator/__snapshots__/typescript.test.ts.snap @@ -327,11 +327,28 @@ export interface observation_bodyweight extends Observation { subject: Reference<"Patient">; } -export type Observation_bodyweight_Category_VSCatSliceInput = Omit; - -import { ensureProfile, applySliceMatch, matchesValue, setArraySlice, getArraySlice, ensureSliceDefaults, stripMatchKeys, validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type observation_bodyweightProfileParams = { +export type Observation_bodyweight_Category_VSCatSliceFlat = Omit; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type observation_bodyweightProfileRaw = { status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); subject: Reference<"Patient">; category?: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]; @@ -339,155 +356,169 @@ export type observation_bodyweightProfileParams = { // CanonicalURL: http://hl7.org/fhir/StructureDefinition/bodyweight (pkg: hl7.fhir.r4.core#4.0.1) export class observation_bodyweightProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/bodyweight" + 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 + private resource: Observation; constructor (resource: Observation) { - this.resource = resource - ensureProfile(resource, "http://hl7.org/fhir/StructureDefinition/bodyweight") + this.resource = resource; } static from (resource: Observation) : observation_bodyweightProfile { - return new observation_bodyweightProfile(resource) + if (!resource.meta?.profile?.includes(observation_bodyweightProfile.canonicalUrl)) { + throw new Error(\`observation_bodyweightProfile: meta.profile must include \${observation_bodyweightProfile.canonicalUrl}\`) + } + const profile = new observation_bodyweightProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Observation) : observation_bodyweightProfile { + ensureProfile(resource, observation_bodyweightProfile.canonicalUrl); + return new observation_bodyweightProfile(resource); } - static createResource (args: observation_bodyweightProfileParams) : Observation { + static createResource (args: observation_bodyweightProfileRaw) : Observation { const categoryWithDefaults = ensureSliceDefaults( [...(args.category ?? [])], observation_bodyweightProfile.VSCatSliceMatch, - ) + ); - const resource = { + const resource = buildResource( { resourceType: "Observation", code: {"coding":[{"code":"29463-7","system":"http://loinc.org"}]}, category: categoryWithDefaults, status: args.status, subject: args.subject, meta: { profile: [observation_bodyweightProfile.canonicalUrl] }, - } as unknown as Observation - return resource + }) + return resource; } - static create (args: observation_bodyweightProfileParams) : observation_bodyweightProfile { - return observation_bodyweightProfile.from(observation_bodyweightProfile.createResource(args)) + static create (args: observation_bodyweightProfileRaw) : observation_bodyweightProfile { + return observation_bodyweightProfile.apply(observation_bodyweightProfile.createResource(args)); } toResource () : Observation { - return this.resource + return this.resource; } // Field accessors - getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { - return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; } setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { - Object.assign(this.resource, { status: value }) - return this + Object.assign(this.resource, { status: value }); + return this; } getSubject () : Reference<"Patient"> | undefined { - return this.resource.subject as Reference<"Patient"> | undefined + return this.resource.subject as Reference<"Patient"> | undefined; } setSubject (value: Reference<"Patient">) : this { - Object.assign(this.resource, { subject: value }) - return this + Object.assign(this.resource, { subject: value }); + return this; } getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { - return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; } setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { - Object.assign(this.resource, { category: value }) - return this + Object.assign(this.resource, { category: value }); + return this; } getCode () : 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 { - 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 + 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 + Object.assign(this.resource, { code: value }); + return this; } getEffectiveDateTime () : string | undefined { - return this.resource.effectiveDateTime as string | undefined + return this.resource.effectiveDateTime as string | undefined; } setEffectiveDateTime (value: string) : this { - Object.assign(this.resource, { effectiveDateTime: value }) - return this + Object.assign(this.resource, { effectiveDateTime: value }); + return this; } getEffectivePeriod () : Period | undefined { - return this.resource.effectivePeriod as Period | undefined + return this.resource.effectivePeriod as Period | undefined; } setEffectivePeriod (value: Period) : this { - Object.assign(this.resource, { effectivePeriod: value }) - return this + Object.assign(this.resource, { effectivePeriod: value }); + return this; } getValueQuantity () : Quantity | undefined { - return this.resource.valueQuantity as Quantity | undefined + return this.resource.valueQuantity as Quantity | undefined; } setValueQuantity (value: Quantity) : this { - Object.assign(this.resource, { valueQuantity: value }) - return this + Object.assign(this.resource, { valueQuantity: value }); + return this; } - toProfile () : observation_bodyweight { - return this.resource as observation_bodyweight - } - - // Slices and extensions - - public setVSCat (input?: Observation_bodyweight_Category_VSCatSliceInput): this { + // Extensions + // Slices + public setVSCat (input?: Observation_bodyweight_Category_VSCatSliceFlat | CodeableConcept): this { const match = observation_bodyweightProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.category ??= [], match, value) return this } - public getVSCat (): Observation_bodyweight_Category_VSCatSliceInput | undefined { + public getVSCat(mode: 'flat'): Observation_bodyweight_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): Observation_bodyweight_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): Observation_bodyweight_Category_VSCatSliceFlat | CodeableConcept | undefined { const match = observation_bodyweightProfile.VSCatSliceMatch const item = getArraySlice(this.resource.category, match) if (!item) return undefined - return stripMatchKeys(item, ["coding"]) - } - - public getVSCatRaw (): CodeableConcept | undefined { - const match = observation_bodyweightProfile.VSCatSliceMatch - const item = getArraySlice(this.resource.category, match) - return item + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) } // Validation - - validate(): string[] { + validate(): { errors: string[]; warnings: string[] } { const profileName = "observation-bodyweight" - const res = this.resource as unknown as Record - return [ - ...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), - ...validateRequired(res, profileName, "code"), - ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"29463-7","system":"http://loinc.org"}]}), - ...validateRequired(res, profileName, "subject"), - ...validateReference(res, profileName, "subject", ["Patient"]), - ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), - ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), - ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), - ] + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"29463-7","system":"http://loinc.org"}]}), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), + ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), + ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ], + } } } @@ -501,8 +532,7 @@ exports[`TypeScript R4 Example (with generateProfile) generates bp profile with // Any manual changes made to this file may be overwritten. import type { CodeableConcept } from "../../hl7-fhir-r4-core/CodeableConcept"; -import type { Observation } from "../../hl7-fhir-r4-core/Observation"; -import type { ObservationComponent } from "../../hl7-fhir-r4-core/Observation"; +import type { Observation, ObservationComponent } from "../../hl7-fhir-r4-core/Observation"; import type { Period } from "../../hl7-fhir-r4-core/Period"; import type { Quantity } from "../../hl7-fhir-r4-core/Quantity"; import type { Reference } from "../../hl7-fhir-r4-core/Reference"; @@ -512,13 +542,30 @@ export interface observation_bp extends Observation { subject: Reference<"Patient">; } -export type Observation_bp_Category_VSCatSliceInput = Omit; -export type Observation_bp_Component_SystolicBPSliceInput = Omit; -export type Observation_bp_Component_DiastolicBPSliceInput = Omit; - -import { ensureProfile, applySliceMatch, matchesValue, setArraySlice, getArraySlice, ensureSliceDefaults, stripMatchKeys, validateRequired, validateExcluded, validateFixedValue, validateSliceCardinality, validateEnum, validateReference, validateChoiceRequired } from "../../profile-helpers"; - -export type observation_bpProfileParams = { +export type Observation_bp_Category_VSCatSliceFlat = Omit; +export type Observation_bp_Component_SystolicBPSliceFlat = Omit; +export type Observation_bp_Component_DiastolicBPSliceFlat = Omit; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type observation_bpProfileRaw = { status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); subject: Reference<"Patient">; category?: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]; @@ -527,35 +574,45 @@ export type observation_bpProfileParams = { // CanonicalURL: http://hl7.org/fhir/StructureDefinition/bp (pkg: hl7.fhir.r4.core#4.0.1) export class observation_bpProfile { - static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/bp" + 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 + private resource: Observation; constructor (resource: Observation) { - this.resource = resource - ensureProfile(resource, "http://hl7.org/fhir/StructureDefinition/bp") + this.resource = resource; } static from (resource: Observation) : observation_bpProfile { - return new observation_bpProfile(resource) + if (!resource.meta?.profile?.includes(observation_bpProfile.canonicalUrl)) { + throw new Error(\`observation_bpProfile: meta.profile must include \${observation_bpProfile.canonicalUrl}\`) + } + const profile = new observation_bpProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; } - static createResource (args: observation_bpProfileParams) : Observation { + static apply (resource: Observation) : observation_bpProfile { + ensureProfile(resource, observation_bpProfile.canonicalUrl); + return new observation_bpProfile(resource); + } + + static createResource (args: observation_bpProfileRaw) : Observation { const categoryWithDefaults = ensureSliceDefaults( [...(args.category ?? [])], observation_bpProfile.VSCatSliceMatch, - ) + ); const componentWithDefaults = ensureSliceDefaults( [...(args.component ?? [])], observation_bpProfile.SystolicBPSliceMatch, observation_bpProfile.DiastolicBPSliceMatch, - ) + ); - const resource = { + const resource = buildResource( { resourceType: "Observation", code: {"coding":[{"code":"85354-9","system":"http://loinc.org"}]}, category: categoryWithDefaults, @@ -563,181 +620,1416 @@ export class observation_bpProfile { status: args.status, subject: args.subject, meta: { profile: [observation_bpProfile.canonicalUrl] }, - } as unknown as Observation - return resource + }) + return resource; } - static create (args: observation_bpProfileParams) : observation_bpProfile { - return observation_bpProfile.from(observation_bpProfile.createResource(args)) + static create (args: observation_bpProfileRaw) : observation_bpProfile { + return observation_bpProfile.apply(observation_bpProfile.createResource(args)); } toResource () : Observation { - return this.resource + return this.resource; } // Field accessors - getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { - return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; } setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { - Object.assign(this.resource, { status: value }) - return this + Object.assign(this.resource, { status: value }); + return this; } getSubject () : Reference<"Patient"> | undefined { - return this.resource.subject as Reference<"Patient"> | undefined + return this.resource.subject as Reference<"Patient"> | undefined; } setSubject (value: Reference<"Patient">) : this { - Object.assign(this.resource, { subject: value }) - return this + Object.assign(this.resource, { subject: value }); + return this; } getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { - return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; } setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { - Object.assign(this.resource, { category: value }) - return this + Object.assign(this.resource, { category: value }); + return this; } getCode () : 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 { - 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 + 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 + Object.assign(this.resource, { code: value }); + return this; } getComponent () : ObservationComponent[] | undefined { - return this.resource.component as ObservationComponent[] | undefined + return this.resource.component as ObservationComponent[] | undefined; } setComponent (value: ObservationComponent[]) : this { - Object.assign(this.resource, { component: value }) - return this + Object.assign(this.resource, { component: value }); + return this; } getEffectiveDateTime () : string | undefined { - return this.resource.effectiveDateTime as string | undefined + return this.resource.effectiveDateTime as string | undefined; } setEffectiveDateTime (value: string) : this { - Object.assign(this.resource, { effectiveDateTime: value }) - return this + Object.assign(this.resource, { effectiveDateTime: value }); + return this; } getEffectivePeriod () : Period | undefined { - return this.resource.effectivePeriod as Period | undefined + return this.resource.effectivePeriod as Period | undefined; } setEffectivePeriod (value: Period) : this { - Object.assign(this.resource, { effectivePeriod: value }) - return this + Object.assign(this.resource, { effectivePeriod: value }); + return this; } getValueQuantity () : Quantity | undefined { - return this.resource.valueQuantity as Quantity | undefined + return this.resource.valueQuantity as Quantity | undefined; } setValueQuantity (value: Quantity) : this { - Object.assign(this.resource, { valueQuantity: value }) - return this - } - - toProfile () : observation_bp { - return this.resource as observation_bp + Object.assign(this.resource, { valueQuantity: value }); + return this; } - // Slices and extensions - - public setVSCat (input?: Observation_bp_Category_VSCatSliceInput): this { + // Extensions + // Slices + public setVSCat (input?: Observation_bp_Category_VSCatSliceFlat | CodeableConcept): this { const match = observation_bpProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.category ??= [], match, value) return this } - public setSystolicBP (input?: Observation_bp_Component_SystolicBPSliceInput): this { + public setSystolicBP (input?: Observation_bp_Component_SystolicBPSliceFlat | ObservationComponent): this { const match = observation_bpProfile.SystolicBPSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.component ??= [], match, value) return this } - public setDiastolicBP (input?: Observation_bp_Component_DiastolicBPSliceInput): this { + public setDiastolicBP (input?: Observation_bp_Component_DiastolicBPSliceFlat | ObservationComponent): this { const match = observation_bpProfile.DiastolicBPSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) + return this + } const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.component ??= [], match, value) return this } - public getVSCat (): Observation_bp_Category_VSCatSliceInput | undefined { + public getVSCat(mode: 'flat'): Observation_bp_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): Observation_bp_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): Observation_bp_Category_VSCatSliceFlat | CodeableConcept | undefined { const match = observation_bpProfile.VSCatSliceMatch const item = getArraySlice(this.resource.category, match) if (!item) return undefined - return stripMatchKeys(item, ["coding"]) - } - - public getVSCatRaw (): CodeableConcept | undefined { - const match = observation_bpProfile.VSCatSliceMatch - const item = getArraySlice(this.resource.category, match) - return item + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) } - public getSystolicBP (): Observation_bp_Component_SystolicBPSliceInput | undefined { + public getSystolicBP(mode: 'flat'): Observation_bp_Component_SystolicBPSliceFlat | undefined; + public getSystolicBP(mode: 'raw'): ObservationComponent | undefined; + public getSystolicBP(): Observation_bp_Component_SystolicBPSliceFlat | undefined; + public getSystolicBP (mode: 'flat' | 'raw' = 'flat'): Observation_bp_Component_SystolicBPSliceFlat | ObservationComponent | undefined { const match = observation_bpProfile.SystolicBPSliceMatch const item = getArraySlice(this.resource.component, match) if (!item) return undefined - return stripMatchKeys(item, ["code"]) + if (mode === 'raw') return item + return stripMatchKeys(item, ["code"]) } - public getSystolicBPRaw (): ObservationComponent | undefined { - const match = observation_bpProfile.SystolicBPSliceMatch + public getDiastolicBP(mode: 'flat'): Observation_bp_Component_DiastolicBPSliceFlat | undefined; + public getDiastolicBP(mode: 'raw'): ObservationComponent | undefined; + public getDiastolicBP(): Observation_bp_Component_DiastolicBPSliceFlat | undefined; + public getDiastolicBP (mode: 'flat' | 'raw' = 'flat'): Observation_bp_Component_DiastolicBPSliceFlat | ObservationComponent | undefined { + const match = observation_bpProfile.DiastolicBPSliceMatch const item = getArraySlice(this.resource.component, match) - return item + if (!item) return undefined + if (mode === 'raw') return item + return stripMatchKeys(item, ["code"]) } - public getDiastolicBP (): Observation_bp_Component_DiastolicBPSliceInput | undefined { - const match = observation_bpProfile.DiastolicBPSliceMatch + // Validation + validate(): { errors: string[]; warnings: string[] } { + const profileName = "observation-bp" + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"85354-9","system":"http://loinc.org"}]}), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...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), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["85353-1","9279-1","8867-4","2708-6","8310-5","8302-2","9843-4","29463-7","39156-5","85354-9","8480-6","8462-4","8478-0"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ], + } + } + +} + +" +`; + +exports[`TypeScript US Core Example generates US Core Patient profile 1`] = ` +"// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { CodeableConcept } from "../../hl7-fhir-r4-core/CodeableConcept"; +import type { Coding } from "../../hl7-fhir-r4-core/Coding"; +import type { Extension } from "../../hl7-fhir-r4-core/Extension"; +import type { HumanName } from "../../hl7-fhir-r4-core/HumanName"; +import type { Identifier } from "../../hl7-fhir-r4-core/Identifier"; +import type { Patient } from "../../hl7-fhir-r4-core/Patient"; + +import { + USCoreEthnicityExtensionProfile, + type USCoreEthnicityExtensionProfileFlat, +} from "./Extension_USCoreEthnicityExtension"; +import { USCoreIndividualSexExtensionProfile } from "./Extension_USCoreIndividualSexExtension"; +import { USCoreInterpreterNeededExtensionProfile } from "./Extension_USCoreInterpreterNeededExtension"; +import { USCoreRaceExtensionProfile, type USCoreRaceExtensionProfileFlat } from "./Extension_USCoreRaceExtension"; +import { + USCoreTribalAffiliationExtensionProfile, + type USCoreTribalAffiliationExtensionProfileFlat, +} from "./Extension_USCoreTribalAffiliationExtension"; + +export interface USCorePatientProfile extends Patient { + identifier: Identifier[]; + name: HumanName[]; +} + +import { + buildResource, + ensureProfile, + extractComplexExtension, + isExtension, + getExtensionValue, + pushExtension, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCorePatientProfileRaw = { + identifier: Identifier[]; + name: HumanName[]; +} + +// CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient (pkg: hl7.fhir.us.core#8.0.1) +export class USCorePatientProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"; + + private resource: Patient; + + constructor (resource: Patient) { + this.resource = resource; + } + + static from (resource: Patient) : USCorePatientProfile { + if (!resource.meta?.profile?.includes(USCorePatientProfile.canonicalUrl)) { + throw new Error(\`USCorePatientProfile: meta.profile must include \${USCorePatientProfile.canonicalUrl}\`) + } + const profile = new USCorePatientProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Patient) : USCorePatientProfile { + ensureProfile(resource, USCorePatientProfile.canonicalUrl); + return new USCorePatientProfile(resource); + } + + static createResource (args: USCorePatientProfileRaw) : Patient { + const resource = buildResource( { + resourceType: "Patient", + identifier: args.identifier, + name: args.name, + meta: { profile: [USCorePatientProfile.canonicalUrl] }, + }) + return resource; + } + + static create (args: USCorePatientProfileRaw) : USCorePatientProfile { + return USCorePatientProfile.apply(USCorePatientProfile.createResource(args)); + } + + toResource () : Patient { + return this.resource; + } + + // Field accessors + getIdentifier () : Identifier[] | undefined { + return this.resource.identifier as Identifier[] | undefined; + } + + setIdentifier (value: Identifier[]) : this { + Object.assign(this.resource, { identifier: value }); + return this; + } + + getName () : HumanName[] | undefined { + return this.resource.name as HumanName[] | undefined; + } + + setName (value: HumanName[]) : this { + Object.assign(this.resource, { name: value }); + return this; + } + + // Extensions + public setRace (input: USCoreRaceExtensionProfileFlat | USCoreRaceExtensionProfile | Extension): this { + if (input instanceof USCoreRaceExtensionProfile) { + pushExtension(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) + } else { + pushExtension(this.resource, USCoreRaceExtensionProfile.createResource(input)) + } + return this + } + + public getRace(mode: 'flat'): USCoreRaceExtensionProfileFlat | undefined; + public getRace(mode: 'profile'): USCoreRaceExtensionProfile | undefined; + public getRace(mode: 'raw'): Extension | undefined; + public getRace(): USCoreRaceExtensionProfileFlat | undefined; + public getRace (mode: 'flat' | 'profile' | 'raw' = 'flat'): USCoreRaceExtensionProfileFlat | USCoreRaceExtensionProfile | Extension | undefined { + const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race") + if (!ext) return undefined + if (mode === 'raw') return ext + if (mode === 'profile') return USCoreRaceExtensionProfile.apply(ext) + const config = [{ name: "ombCategory", valueField: "valueCoding", isArray: false }, { name: "detailed", valueField: "valueCoding", isArray: true }, { name: "text", valueField: "valueString", isArray: false }] + return extractComplexExtension(ext, config) + } + + public setEthnicity (input: USCoreEthnicityExtensionProfileFlat | USCoreEthnicityExtensionProfile | Extension): this { + if (input instanceof USCoreEthnicityExtensionProfile) { + pushExtension(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) + } else { + pushExtension(this.resource, USCoreEthnicityExtensionProfile.createResource(input)) + } + return this + } + + public getEthnicity(mode: 'flat'): USCoreEthnicityExtensionProfileFlat | undefined; + public getEthnicity(mode: 'profile'): USCoreEthnicityExtensionProfile | undefined; + public getEthnicity(mode: 'raw'): Extension | undefined; + public getEthnicity(): USCoreEthnicityExtensionProfileFlat | undefined; + public getEthnicity (mode: 'flat' | 'profile' | 'raw' = 'flat'): USCoreEthnicityExtensionProfileFlat | USCoreEthnicityExtensionProfile | Extension | undefined { + const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity") + if (!ext) return undefined + if (mode === 'raw') return ext + if (mode === 'profile') return USCoreEthnicityExtensionProfile.apply(ext) + const config = [{ name: "ombCategory", valueField: "valueCoding", isArray: false }, { name: "detailed", valueField: "valueCoding", isArray: true }, { name: "text", valueField: "valueString", isArray: false }] + return extractComplexExtension(ext, config) + } + + public setTribalAffiliation (input: USCoreTribalAffiliationExtensionProfileFlat | USCoreTribalAffiliationExtensionProfile | Extension): this { + if (input instanceof USCoreTribalAffiliationExtensionProfile) { + pushExtension(this.resource, input.toResource()) + } else if (isExtension(input)) { + if (input.url !== "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation") throw new Error(\`Expected extension url 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation', got '\${input.url}'\`) + pushExtension(this.resource, input) + } else { + pushExtension(this.resource, USCoreTribalAffiliationExtensionProfile.createResource(input)) + } + return this + } + + public getTribalAffiliation(mode: 'flat'): USCoreTribalAffiliationExtensionProfileFlat | undefined; + public getTribalAffiliation(mode: 'profile'): USCoreTribalAffiliationExtensionProfile | undefined; + public getTribalAffiliation(mode: 'raw'): Extension | undefined; + public getTribalAffiliation(): USCoreTribalAffiliationExtensionProfileFlat | undefined; + public getTribalAffiliation (mode: 'flat' | 'profile' | 'raw' = 'flat'): USCoreTribalAffiliationExtensionProfileFlat | USCoreTribalAffiliationExtensionProfile | Extension | undefined { + const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation") + if (!ext) return undefined + if (mode === 'raw') return ext + if (mode === 'profile') return USCoreTribalAffiliationExtensionProfile.apply(ext) + const config = [{ name: "tribalAffiliation", valueField: "valueCodeableConcept", isArray: false }, { name: "isEnrolled", valueField: "valueBoolean", isArray: false }] + return extractComplexExtension(ext, config) + } + + public setSex (value: USCoreIndividualSexExtensionProfile | Extension | Coding): this { + if (value instanceof USCoreIndividualSexExtensionProfile) { + pushExtension(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) + } else { + pushExtension(this.resource, USCoreIndividualSexExtensionProfile.createResource({ valueCoding: value as Coding })) + } + return this + } + + public getSex(mode: 'flat'): Coding | undefined; + public getSex(mode: 'profile'): USCoreIndividualSexExtensionProfile | undefined; + public getSex(mode: 'raw'): Extension | undefined; + public getSex(): Coding | undefined; + public getSex (mode: 'flat' | 'profile' | 'raw' = 'flat'): Coding | USCoreIndividualSexExtensionProfile | Extension | undefined { + const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex") + if (!ext) return undefined + if (mode === 'raw') return ext + if (mode === 'profile') return USCoreIndividualSexExtensionProfile.apply(ext) + return getExtensionValue(ext, "valueCoding") + } + + public setInterpreterRequired (value: USCoreInterpreterNeededExtensionProfile | Extension | Coding): this { + if (value instanceof USCoreInterpreterNeededExtensionProfile) { + pushExtension(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) + } else { + pushExtension(this.resource, USCoreInterpreterNeededExtensionProfile.createResource({ valueCoding: value as Coding })) + } + return this + } + + public getInterpreterRequired(mode: 'flat'): Coding | undefined; + public getInterpreterRequired(mode: 'profile'): USCoreInterpreterNeededExtensionProfile | undefined; + public getInterpreterRequired(mode: 'raw'): Extension | undefined; + public getInterpreterRequired(): Coding | undefined; + public getInterpreterRequired (mode: 'flat' | 'profile' | 'raw' = 'flat'): Coding | USCoreInterpreterNeededExtensionProfile | Extension | undefined { + const ext = this.resource.extension?.find(e => e.url === "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed") + if (!ext) return undefined + if (mode === 'raw') return ext + if (mode === 'profile') return USCoreInterpreterNeededExtensionProfile.apply(ext) + return getExtensionValue(ext, "valueCoding") + } + + // Slices + // Validation + validate(): { errors: string[]; warnings: string[] } { + const profileName = "USCorePatientProfile" + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "identifier"), + ...validateRequired(res, profileName, "name"), + ], + warnings: [ + ...validateMustSupport(res, profileName, "birthDate"), + ...validateMustSupport(res, profileName, "address"), + ], + } + } + +} + +" +`; + +exports[`TypeScript US Core Example generates US Core Blood Pressure profile 1`] = ` +"// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { CodeableConcept } from "../../hl7-fhir-r4-core/CodeableConcept"; +import type { Observation, ObservationComponent } from "../../hl7-fhir-r4-core/Observation"; +import type { Period } from "../../hl7-fhir-r4-core/Period"; +import type { Quantity } from "../../hl7-fhir-r4-core/Quantity"; +import type { Range } from "../../hl7-fhir-r4-core/Range"; +import type { Ratio } from "../../hl7-fhir-r4-core/Ratio"; +import type { Reference } from "../../hl7-fhir-r4-core/Reference"; +import type { SampledData } from "../../hl7-fhir-r4-core/SampledData"; + +export interface USCoreBloodPressureProfile extends Observation { + category: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]; + subject: Reference<"Patient">; +} + +export type USCoreBloodPressureProfile_Category_VSCatSliceFlat = Omit; +export type USCoreBloodPressureProfile_Component_SystolicSliceFlat = Omit & Quantity; +export type USCoreBloodPressureProfile_Component_DiastolicSliceFlat = Omit & Quantity; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + wrapSliceChoice, + unwrapSliceChoice, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreBloodPressureProfileRaw = { + status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); + subject: Reference<"Patient">; + category?: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]; + component?: ObservationComponent[]; +} + +// CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure (pkg: hl7.fhir.us.core#8.0.1) +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 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"}]}}; + + private resource: Observation; + + constructor (resource: Observation) { + this.resource = resource; + } + + static from (resource: Observation) : USCoreBloodPressureProfile { + if (!resource.meta?.profile?.includes(USCoreBloodPressureProfile.canonicalUrl)) { + throw new Error(\`USCoreBloodPressureProfile: meta.profile must include \${USCoreBloodPressureProfile.canonicalUrl}\`) + } + const profile = new USCoreBloodPressureProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Observation) : USCoreBloodPressureProfile { + ensureProfile(resource, USCoreBloodPressureProfile.canonicalUrl); + return new USCoreBloodPressureProfile(resource); + } + + static createResource (args: USCoreBloodPressureProfileRaw) : Observation { + const categoryWithDefaults = ensureSliceDefaults( + [...(args.category ?? [])], + USCoreBloodPressureProfile.VSCatSliceMatch, + ); + const componentWithDefaults = ensureSliceDefaults( + [...(args.component ?? [])], + USCoreBloodPressureProfile.systolicSliceMatch, + USCoreBloodPressureProfile.diastolicSliceMatch, + ); + + const resource = buildResource( { + resourceType: "Observation", + code: {"coding":[{"system":"http://loinc.org","code":"85354-9"}]}, + category: categoryWithDefaults, + component: componentWithDefaults, + status: args.status, + subject: args.subject, + meta: { profile: [USCoreBloodPressureProfile.canonicalUrl] }, + }) + return resource; + } + + static create (args: USCoreBloodPressureProfileRaw) : USCoreBloodPressureProfile { + return USCoreBloodPressureProfile.apply(USCoreBloodPressureProfile.createResource(args)); + } + + toResource () : Observation { + return this.resource; + } + + // Field accessors + getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; + } + + setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { + Object.assign(this.resource, { status: value }); + return this; + } + + getSubject () : Reference<"Patient"> | undefined { + return this.resource.subject as Reference<"Patient"> | undefined; + } + + setSubject (value: Reference<"Patient">) : this { + Object.assign(this.resource, { subject: value }); + return this; + } + + getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; + } + + setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { + Object.assign(this.resource, { category: value }); + return this; + } + + getCode () : 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 { + 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; + } + + setComponent (value: ObservationComponent[]) : this { + Object.assign(this.resource, { component: value }); + return this; + } + + getEffectiveDateTime () : string | undefined { + return this.resource.effectiveDateTime as string | undefined; + } + + setEffectiveDateTime (value: string) : this { + Object.assign(this.resource, { effectiveDateTime: value }); + return this; + } + + getEffectivePeriod () : Period | undefined { + return this.resource.effectivePeriod as Period | undefined; + } + + setEffectivePeriod (value: Period) : this { + Object.assign(this.resource, { effectivePeriod: value }); + return this; + } + + getValueQuantity () : Quantity | undefined { + return this.resource.valueQuantity as Quantity | undefined; + } + + setValueQuantity (value: Quantity) : this { + Object.assign(this.resource, { valueQuantity: value }); + return this; + } + + getValueCodeableConcept () : CodeableConcept | undefined { + return this.resource.valueCodeableConcept as CodeableConcept | undefined; + } + + setValueCodeableConcept (value: CodeableConcept) : this { + Object.assign(this.resource, { valueCodeableConcept: value }); + return this; + } + + getValueString () : string | undefined { + return this.resource.valueString as string | undefined; + } + + setValueString (value: string) : this { + Object.assign(this.resource, { valueString: value }); + return this; + } + + getValueBoolean () : boolean | undefined { + return this.resource.valueBoolean as boolean | undefined; + } + + setValueBoolean (value: boolean) : this { + Object.assign(this.resource, { valueBoolean: value }); + return this; + } + + getValueInteger () : number | undefined { + return this.resource.valueInteger as number | undefined; + } + + setValueInteger (value: number) : this { + Object.assign(this.resource, { valueInteger: value }); + return this; + } + + getValueRange () : Range | undefined { + return this.resource.valueRange as Range | undefined; + } + + setValueRange (value: Range) : this { + Object.assign(this.resource, { valueRange: value }); + return this; + } + + getValueRatio () : Ratio | undefined { + return this.resource.valueRatio as Ratio | undefined; + } + + setValueRatio (value: Ratio) : this { + Object.assign(this.resource, { valueRatio: value }); + return this; + } + + getValueSampledData () : SampledData | undefined { + return this.resource.valueSampledData as SampledData | undefined; + } + + setValueSampledData (value: SampledData) : this { + Object.assign(this.resource, { valueSampledData: value }); + return this; + } + + getValueTime () : string | undefined { + return this.resource.valueTime as string | undefined; + } + + setValueTime (value: string) : this { + Object.assign(this.resource, { valueTime: value }); + return this; + } + + getValueDateTime () : string | undefined { + return this.resource.valueDateTime as string | undefined; + } + + setValueDateTime (value: string) : this { + Object.assign(this.resource, { valueDateTime: value }); + return this; + } + + getValuePeriod () : Period | undefined { + return this.resource.valuePeriod as Period | undefined; + } + + setValuePeriod (value: Period) : this { + Object.assign(this.resource, { valuePeriod: value }); + return this; + } + + // Extensions + // Slices + public setVSCat (input?: USCoreBloodPressureProfile_Category_VSCatSliceFlat | CodeableConcept): this { + const match = USCoreBloodPressureProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } + const value = applySliceMatch(input ?? {}, match) + setArraySlice(this.resource.category ??= [], match, value) + return this + } + + public setSystolic (input?: USCoreBloodPressureProfile_Component_SystolicSliceFlat | ObservationComponent): this { + const match = USCoreBloodPressureProfile.systolicSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) + return this + } + const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") + const value = applySliceMatch(wrapped, match) + setArraySlice(this.resource.component ??= [], match, value) + return this + } + + public setDiastolic (input?: USCoreBloodPressureProfile_Component_DiastolicSliceFlat | ObservationComponent): this { + const match = USCoreBloodPressureProfile.diastolicSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) + return this + } + const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") + const value = applySliceMatch(wrapped, match) + setArraySlice(this.resource.component ??= [], match, value) + return this + } + + public getVSCat(mode: 'flat'): USCoreBloodPressureProfile_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): USCoreBloodPressureProfile_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): USCoreBloodPressureProfile_Category_VSCatSliceFlat | CodeableConcept | undefined { + const match = USCoreBloodPressureProfile.VSCatSliceMatch + const item = getArraySlice(this.resource.category, match) + if (!item) return undefined + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) + } + + public getSystolic(mode: 'flat'): USCoreBloodPressureProfile_Component_SystolicSliceFlat | undefined; + public getSystolic(mode: 'raw'): ObservationComponent | undefined; + public getSystolic(): USCoreBloodPressureProfile_Component_SystolicSliceFlat | undefined; + public getSystolic (mode: 'flat' | 'raw' = 'flat'): USCoreBloodPressureProfile_Component_SystolicSliceFlat | ObservationComponent | undefined { + const match = USCoreBloodPressureProfile.systolicSliceMatch const item = getArraySlice(this.resource.component, match) if (!item) return undefined - return stripMatchKeys(item, ["code"]) + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["code"], "valueQuantity") } - public getDiastolicBPRaw (): ObservationComponent | undefined { - const match = observation_bpProfile.DiastolicBPSliceMatch + public getDiastolic(mode: 'flat'): USCoreBloodPressureProfile_Component_DiastolicSliceFlat | undefined; + public getDiastolic(mode: 'raw'): ObservationComponent | undefined; + public getDiastolic(): USCoreBloodPressureProfile_Component_DiastolicSliceFlat | undefined; + public getDiastolic (mode: 'flat' | 'raw' = 'flat'): USCoreBloodPressureProfile_Component_DiastolicSliceFlat | ObservationComponent | undefined { + const match = USCoreBloodPressureProfile.diastolicSliceMatch const item = getArraySlice(this.resource.component, match) - return item + if (!item) return undefined + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["code"], "valueQuantity") } // Validation + validate(): { errors: string[]; warnings: string[] } { + const profileName = "USCoreBloodPressureProfile" + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateFixedValue(res, profileName, "code", {"coding":[{"system":"http://loinc.org","code":"85354-9"}]}), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...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":[{"system":"http://loinc.org","code":"8480-6"}]}}, "systolic", 1, 1), + ...validateSliceCardinality(res, profileName, "component", {"code":{"coding":[{"system":"http://loinc.org","code":"8462-4"}]}}, "diastolic", 1, 1), + ...validateReference(res, profileName, "performer", ["PractitionerRole","USCoreCareTeam","USCoreOrganizationProfile","Patient","USCorePractitionerProfile","USCoreRelatedPersonProfile"]), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ...validateMustSupport(res, profileName, "performer"), + ], + } + } - validate(): string[] { - const profileName = "observation-bp" - const res = this.resource as unknown as Record - return [ - ...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), - ...validateRequired(res, profileName, "code"), - ...validateFixedValue(res, profileName, "code", {"coding":[{"code":"85354-9","system":"http://loinc.org"}]}), - ...validateRequired(res, profileName, "subject"), - ...validateReference(res, profileName, "subject", ["Patient"]), - ...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), - ] +} + +" +`; + +exports[`TypeScript US Core Example generates US Core Body Weight profile 1`] = ` +"// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { CodeableConcept } from "../../hl7-fhir-r4-core/CodeableConcept"; +import type { Observation } from "../../hl7-fhir-r4-core/Observation"; +import type { Period } from "../../hl7-fhir-r4-core/Period"; +import type { Quantity } from "../../hl7-fhir-r4-core/Quantity"; +import type { Range } from "../../hl7-fhir-r4-core/Range"; +import type { Ratio } from "../../hl7-fhir-r4-core/Ratio"; +import type { Reference } from "../../hl7-fhir-r4-core/Reference"; +import type { SampledData } from "../../hl7-fhir-r4-core/SampledData"; + +export interface USCoreBodyWeightProfile extends Observation { + category: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]; + subject: Reference<"Patient">; +} + +export type USCoreBodyWeightProfile_Category_VSCatSliceFlat = Omit; + +import { + buildResource, + ensureProfile, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreBodyWeightProfileRaw = { + status: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown"); + subject: Reference<"Patient">; + category?: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]; +} + +// CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight (pkg: hl7.fhir.us.core#8.0.1) +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 resource: Observation; + + constructor (resource: Observation) { + this.resource = resource; + } + + static from (resource: Observation) : USCoreBodyWeightProfile { + if (!resource.meta?.profile?.includes(USCoreBodyWeightProfile.canonicalUrl)) { + throw new Error(\`USCoreBodyWeightProfile: meta.profile must include \${USCoreBodyWeightProfile.canonicalUrl}\`) + } + const profile = new USCoreBodyWeightProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Observation) : USCoreBodyWeightProfile { + ensureProfile(resource, USCoreBodyWeightProfile.canonicalUrl); + return new USCoreBodyWeightProfile(resource); + } + + static createResource (args: USCoreBodyWeightProfileRaw) : Observation { + const categoryWithDefaults = ensureSliceDefaults( + [...(args.category ?? [])], + USCoreBodyWeightProfile.VSCatSliceMatch, + ); + + const resource = buildResource( { + resourceType: "Observation", + code: {"coding":[{"system":"http://loinc.org","code":"29463-7"}]}, + category: categoryWithDefaults, + status: args.status, + subject: args.subject, + meta: { profile: [USCoreBodyWeightProfile.canonicalUrl] }, + }) + return resource; + } + + static create (args: USCoreBodyWeightProfileRaw) : USCoreBodyWeightProfile { + return USCoreBodyWeightProfile.apply(USCoreBodyWeightProfile.createResource(args)); + } + + toResource () : Observation { + return this.resource; + } + + // Field accessors + getStatus () : ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined { + return this.resource.status as ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown") | undefined; + } + + setStatus (value: ("registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown")) : this { + Object.assign(this.resource, { status: value }); + return this; + } + + getSubject () : Reference<"Patient"> | undefined { + return this.resource.subject as Reference<"Patient"> | undefined; + } + + setSubject (value: Reference<"Patient">) : this { + Object.assign(this.resource, { subject: value }); + return this; + } + + getCategory () : CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined { + return this.resource.category as CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[] | undefined; + } + + setCategory (value: CodeableConcept<("social-history" | "vital-signs" | "imaging" | "laboratory" | "procedure" | "survey" | "exam" | "therapy" | "activity" | string)>[]) : this { + Object.assign(this.resource, { category: value }); + return this; + } + + getCode () : 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 { + 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; + } + + setEffectiveDateTime (value: string) : this { + Object.assign(this.resource, { effectiveDateTime: value }); + return this; + } + + getEffectivePeriod () : Period | undefined { + return this.resource.effectivePeriod as Period | undefined; + } + + setEffectivePeriod (value: Period) : this { + Object.assign(this.resource, { effectivePeriod: value }); + return this; + } + + getValueQuantity () : Quantity | undefined { + return this.resource.valueQuantity as Quantity | undefined; + } + + setValueQuantity (value: Quantity) : this { + Object.assign(this.resource, { valueQuantity: value }); + return this; + } + + getValueCodeableConcept () : CodeableConcept | undefined { + return this.resource.valueCodeableConcept as CodeableConcept | undefined; + } + + setValueCodeableConcept (value: CodeableConcept) : this { + Object.assign(this.resource, { valueCodeableConcept: value }); + return this; + } + + getValueString () : string | undefined { + return this.resource.valueString as string | undefined; + } + + setValueString (value: string) : this { + Object.assign(this.resource, { valueString: value }); + return this; + } + + getValueBoolean () : boolean | undefined { + return this.resource.valueBoolean as boolean | undefined; + } + + setValueBoolean (value: boolean) : this { + Object.assign(this.resource, { valueBoolean: value }); + return this; + } + + getValueInteger () : number | undefined { + return this.resource.valueInteger as number | undefined; + } + + setValueInteger (value: number) : this { + Object.assign(this.resource, { valueInteger: value }); + return this; + } + + getValueRange () : Range | undefined { + return this.resource.valueRange as Range | undefined; + } + + setValueRange (value: Range) : this { + Object.assign(this.resource, { valueRange: value }); + return this; + } + + getValueRatio () : Ratio | undefined { + return this.resource.valueRatio as Ratio | undefined; + } + + setValueRatio (value: Ratio) : this { + Object.assign(this.resource, { valueRatio: value }); + return this; + } + + getValueSampledData () : SampledData | undefined { + return this.resource.valueSampledData as SampledData | undefined; + } + + setValueSampledData (value: SampledData) : this { + Object.assign(this.resource, { valueSampledData: value }); + return this; + } + + getValueTime () : string | undefined { + return this.resource.valueTime as string | undefined; + } + + setValueTime (value: string) : this { + Object.assign(this.resource, { valueTime: value }); + return this; + } + + getValueDateTime () : string | undefined { + return this.resource.valueDateTime as string | undefined; + } + + setValueDateTime (value: string) : this { + Object.assign(this.resource, { valueDateTime: value }); + return this; + } + + getValuePeriod () : Period | undefined { + return this.resource.valuePeriod as Period | undefined; + } + + setValuePeriod (value: Period) : this { + Object.assign(this.resource, { valuePeriod: value }); + return this; + } + + // Extensions + // Slices + public setVSCat (input?: USCoreBodyWeightProfile_Category_VSCatSliceFlat | CodeableConcept): this { + const match = USCoreBodyWeightProfile.VSCatSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.category ??= [], match, input as CodeableConcept) + return this + } + const value = applySliceMatch(input ?? {}, match) + setArraySlice(this.resource.category ??= [], match, value) + return this + } + + public getVSCat(mode: 'flat'): USCoreBodyWeightProfile_Category_VSCatSliceFlat | undefined; + public getVSCat(mode: 'raw'): CodeableConcept | undefined; + public getVSCat(): USCoreBodyWeightProfile_Category_VSCatSliceFlat | undefined; + public getVSCat (mode: 'flat' | 'raw' = 'flat'): USCoreBodyWeightProfile_Category_VSCatSliceFlat | CodeableConcept | undefined { + const match = USCoreBodyWeightProfile.VSCatSliceMatch + const item = getArraySlice(this.resource.category, match) + if (!item) return undefined + if (mode === 'raw') return item + return stripMatchKeys(item, ["coding"]) + } + + // Validation + validate(): { errors: string[]; warnings: string[] } { + const profileName = "USCoreBodyWeightProfile" + const res = this.resource + return { + errors: [ + ...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), + ...validateRequired(res, profileName, "code"), + ...validateFixedValue(res, profileName, "code", {"coding":[{"system":"http://loinc.org","code":"29463-7"}]}), + ...validateRequired(res, profileName, "subject"), + ...validateReference(res, profileName, "subject", ["Patient"]), + ...validateChoiceRequired(res, profileName, ["effectiveDateTime","effectivePeriod"]), + ...validateReference(res, profileName, "hasMember", ["MolecularSequence","QuestionnaireResponse","Observation"]), + ...validateReference(res, profileName, "derivedFrom", ["DocumentReference","ImagingStudy","Media","MolecularSequence","QuestionnaireResponse","Observation"]), + ...validateReference(res, profileName, "performer", ["PractitionerRole","USCoreCareTeam","USCoreOrganizationProfile","Patient","USCorePractitionerProfile","USCoreRelatedPersonProfile"]), + ], + warnings: [ + ...validateEnum(res, profileName, "category", ["social-history","vital-signs","imaging","laboratory","procedure","survey","exam","therapy","activity"]), + ...validateEnum(res, profileName, "code", ["2708-6","29463-7","3140-1","3150-0","3151-8","39156-5","59408-5","59575-1","59576-9","77606-2","8287-5","8289-1","8302-2","8306-3","8310-5","8462-4","8478-0","8480-6","8867-4","9279-1","9843-4"]), + ...validateEnum(res, profileName, "dataAbsentReason", ["unknown","asked-unknown","temp-unknown","not-asked","asked-declined","masked","not-applicable","unsupported","as-text","error","not-a-number","negative-infinity","positive-infinity","not-performed","not-permitted"]), + ...validateMustSupport(res, profileName, "dataAbsentReason"), + ...validateMustSupport(res, profileName, "performer"), + ], + } + } + +} + +" +`; + +exports[`TypeScript US Core Example generates US Core Race extension profile 1`] = ` +"// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { Coding } from "../../hl7-fhir-r4-core/Coding"; +import type { Extension } from "../../hl7-fhir-r4-core/Extension"; + +export type USCoreRaceExtension_Extension_OmbCategorySliceFlat = Omit & Coding; +export type USCoreRaceExtension_Extension_DetailedSliceFlat = Omit & Coding; +export type USCoreRaceExtension_Extension_TextSliceFlat = Omit; + +import { + buildResource, + isRawExtensionInput, + applySliceMatch, + matchesValue, + setArraySlice, + getArraySlice, + ensureSliceDefaults, + stripMatchKeys, + wrapSliceChoice, + unwrapSliceChoice, + isExtension, + getExtensionValue, + pushExtension, + validateRequired, + validateExcluded, + validateFixedValue, + validateSliceCardinality, + validateEnum, + validateReference, + validateChoiceRequired, + validateMustSupport, +} from "../../profile-helpers"; + +export type USCoreRaceExtensionProfileRaw = { + extension?: Extension[]; +} + +export type USCoreRaceExtensionProfileFlat = { + ombCategory?: Coding; + detailed?: Coding[]; + text: string; +} + +// CanonicalURL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-race (pkg: hl7.fhir.us.core#8.0.1) +export class USCoreRaceExtensionProfile { + static readonly canonicalUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"; + + private static readonly ombCategorySliceMatch: Record = {"url":"ombCategory"}; + private static readonly detailedSliceMatch: Record = {"url":"detailed"}; + private static readonly textSliceMatch: Record = {"url":"text"}; + + private resource: Extension; + + constructor (resource: Extension) { + this.resource = resource; + } + + static from (resource: Extension) : USCoreRaceExtensionProfile { + const profile = new USCoreRaceExtensionProfile(resource); + const { errors } = profile.validate(); + if (errors.length > 0) throw new Error(errors.join("; ")) + return profile; + } + + static apply (resource: Extension) : USCoreRaceExtensionProfile { + return new USCoreRaceExtensionProfile(resource); + } + + private static resolveInput (args: USCoreRaceExtensionProfileRaw | USCoreRaceExtensionProfileFlat) : Extension[] { + if (isRawExtensionInput(args)) { + return args.extension ?? []; + } else { + const result: Extension[] = []; + if (args.ombCategory !== undefined) { + result.push({ url: "ombCategory", valueCoding: args.ombCategory } as Extension); + } + if (args.detailed) { + for (const item of args.detailed) { + result.push({ url: "detailed", valueCoding: item } as Extension); + } + } + if (args.text !== undefined) { + result.push({ url: "text", valueString: args.text } as Extension); + } + return result; + } + } + + 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, + }) + return resource; + } + + static create (args: USCoreRaceExtensionProfileRaw | USCoreRaceExtensionProfileFlat) : USCoreRaceExtensionProfile { + return USCoreRaceExtensionProfile.apply(USCoreRaceExtensionProfile.createResource(args)); + } + + toResource () : Extension { + return this.resource; + } + + // Field accessors + getExtension () : Extension[] | undefined { + return this.resource.extension as Extension[] | undefined; + } + + setExtension (value: Extension[]) : this { + Object.assign(this.resource, { extension: value }); + return this; + } + + getUrl () : string | undefined { + 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) + } else { + pushExtension(this.resource, { url: "ombCategory", ...value } as Extension) + } + return this + } + + public getOmbCategory (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "ombCategory") + } + + public setDetailed (value: Omit | Extension): this { + if (isExtension(value)) { + if (value.url !== "detailed") throw new Error(\`Expected extension url 'detailed', got '\${value.url}'\`) + pushExtension(this.resource, value) + } else { + pushExtension(this.resource, { url: "detailed", ...value } as Extension) + } + return this + } + + public getDetailed (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "detailed") + } + + 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) + } else { + pushExtension(this.resource, { url: "text", ...value } as Extension) + } + return this + } + + public getText (): Extension | undefined { + return this.resource.extension?.find(e => e.url === "text") + } + + // Slices + public setExtensionOmbCategory (input?: USCoreRaceExtension_Extension_OmbCategorySliceFlat | Extension): this { + const match = USCoreRaceExtensionProfile.ombCategorySliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const wrapped = wrapSliceChoice(input ?? {}, "valueCoding") + const value = applySliceMatch(wrapped, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public setExtensionDetailed (input?: USCoreRaceExtension_Extension_DetailedSliceFlat | Extension): this { + const match = USCoreRaceExtensionProfile.detailedSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const wrapped = wrapSliceChoice(input ?? {}, "valueCoding") + const value = applySliceMatch(wrapped, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public setExtensionText (input?: USCoreRaceExtension_Extension_TextSliceFlat | Extension): this { + const match = USCoreRaceExtensionProfile.textSliceMatch + if (input && matchesValue(input, match)) { + setArraySlice(this.resource.extension ??= [], match, input as Extension) + return this + } + const value = applySliceMatch(input ?? {}, match) + setArraySlice(this.resource.extension ??= [], match, value) + return this + } + + public getExtensionOmbCategory(mode: 'flat'): USCoreRaceExtension_Extension_OmbCategorySliceFlat | undefined; + public getExtensionOmbCategory(mode: 'raw'): Extension | undefined; + public getExtensionOmbCategory(): USCoreRaceExtension_Extension_OmbCategorySliceFlat | undefined; + public getExtensionOmbCategory (mode: 'flat' | 'raw' = 'flat'): USCoreRaceExtension_Extension_OmbCategorySliceFlat | Extension | undefined { + const match = USCoreRaceExtensionProfile.ombCategorySliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["url"], "valueCoding") + } + + public getExtensionDetailed(mode: 'flat'): USCoreRaceExtension_Extension_DetailedSliceFlat | undefined; + public getExtensionDetailed(mode: 'raw'): Extension | undefined; + public getExtensionDetailed(): USCoreRaceExtension_Extension_DetailedSliceFlat | undefined; + public getExtensionDetailed (mode: 'flat' | 'raw' = 'flat'): USCoreRaceExtension_Extension_DetailedSliceFlat | Extension | undefined { + const match = USCoreRaceExtensionProfile.detailedSliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return unwrapSliceChoice(item, ["url"], "valueCoding") + } + + public getExtensionText(mode: 'flat'): USCoreRaceExtension_Extension_TextSliceFlat | undefined; + public getExtensionText(mode: 'raw'): Extension | undefined; + public getExtensionText(): USCoreRaceExtension_Extension_TextSliceFlat | undefined; + public getExtensionText (mode: 'flat' | 'raw' = 'flat'): USCoreRaceExtension_Extension_TextSliceFlat | Extension | undefined { + const match = USCoreRaceExtensionProfile.textSliceMatch + const item = getArraySlice(this.resource.extension, match) + if (!item) return undefined + if (mode === 'raw') return item + return stripMatchKeys(item, ["url"]) + } + + // Validation + validate(): { errors: string[]; warnings: string[] } { + const profileName = "USCoreRaceExtension" + const res = this.resource + return { + errors: [ + ...validateRequired(res, profileName, "extension"), + ...validateSliceCardinality(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", 0, 6), + ...validateSliceCardinality(res, profileName, "extension", {"url":"text"}, "text", 1, 1), + ...validateRequired(res, profileName, "url"), + ...validateFixedValue(res, profileName, "url", USCoreRaceExtensionProfile.canonicalUrl), + ], + warnings: [], + } } } " -`; \ No newline at end of file +`; + +exports[`TypeScript US Core Example generates US Core profiles index 1`] = ` +"export { USCoreBloodPressureProfile } from "./Observation_USCoreBloodPressureProfile"; +export { USCoreBodyWeightProfile } from "./Observation_USCoreBodyWeightProfile"; +export { USCoreEthnicityExtensionProfile } from "./Extension_USCoreEthnicityExtension"; +export { USCoreIndividualSexExtensionProfile } from "./Extension_USCoreIndividualSexExtension"; +export { USCoreInterpreterNeededExtensionProfile } from "./Extension_USCoreInterpreterNeededExtension"; +export { USCorePatientProfile } from "./Patient_USCorePatientProfile"; +export { USCoreRaceExtensionProfile } from "./Extension_USCoreRaceExtension"; +export { USCoreTribalAffiliationExtensionProfile } from "./Extension_USCoreTribalAffiliationExtension"; +export { USCoreVitalSignsProfile } from "./Observation_USCoreVitalSignsProfile"; +" +`; diff --git a/test/api/write-generator/typescript.test.ts b/test/api/write-generator/typescript.test.ts index 1975d23b8..51871de30 100644 --- a/test/api/write-generator/typescript.test.ts +++ b/test/api/write-generator/typescript.test.ts @@ -83,3 +83,62 @@ describe("TypeScript R4 Example (with generateProfile)", async () => { ).toMatchSnapshot(); }); }); + +describe("TypeScript US Core Example", async () => { + const logger = mkErrorLogger(); + const result = await new APIBuilder({ logger }) + .fromPackage("hl7.fhir.us.core", "8.0.1") + .typeSchema({ + treeShake: { + "hl7.fhir.us.core": { + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-individual-sex": {}, + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-interpreter-needed": {}, + }, + }, + }) + .typescript({ + inMemoryOnly: true, + withDebugComment: false, + generateProfile: true, + openResourceTypeSet: false, + }) + .generate(); + + it("generates successfully", () => { + expect(result.success).toBeTrue(); + }); + + it("generates US Core Patient profile", () => { + expect( + result.filesGenerated["generated/types/hl7-fhir-us-core/profiles/Patient_USCorePatientProfile.ts"], + ).toMatchSnapshot(); + }); + + it("generates US Core Blood Pressure profile", () => { + expect( + result.filesGenerated[ + "generated/types/hl7-fhir-us-core/profiles/Observation_USCoreBloodPressureProfile.ts" + ], + ).toMatchSnapshot(); + }); + + it("generates US Core Body Weight profile", () => { + const key = "generated/types/hl7-fhir-us-core/profiles/Observation_USCoreBodyWeightProfile.ts"; + expect(result.filesGenerated[key]).toMatchSnapshot(); + }); + + it("generates US Core Race extension profile", () => { + const key = "generated/types/hl7-fhir-us-core/profiles/Extension_USCoreRaceExtension.ts"; + expect(result.filesGenerated[key]).toMatchSnapshot(); + }); + + it("generates US Core profiles index", () => { + expect(result.filesGenerated["generated/types/hl7-fhir-us-core/profiles/index.ts"]).toMatchSnapshot(); + }); +});