diff --git a/src/api/writer-generator/typescript/name.ts b/src/api/writer-generator/typescript/name.ts index f6bcedca..27daf8e0 100644 --- a/src/api/writer-generator/typescript/name.ts +++ b/src/api/writer-generator/typescript/name.ts @@ -106,39 +106,4 @@ export const tsExtensionFlatTypeName = (profileName: string, extensionName: stri export const tsSliceStaticName = (name: string): string => name.replace(/\[x\]/g, "").replace(/[^a-zA-Z0-9_$]/g, "_"); -export const tsSliceMethodBaseName = (sliceName: string): string => - uppercaseFirstLetter(normalizeTsName(sliceName) || "Slice"); - -export const tsExtensionMethodBaseName = (name: string): string => - uppercaseFirstLetter(tsCamelCase(name) || "Extension"); - -export const tsQualifiedExtensionMethodBaseName = (name: string, path?: string): string => { - const rawPath = - path - ?.split(".") - .filter((p) => p && p !== "extension") - .join("_") ?? ""; - const pathPart = rawPath ? uppercaseFirstLetter(tsCamelCase(rawPath)) : ""; - return `${pathPart}${uppercaseFirstLetter(tsCamelCase(name) || "Extension")}`; -}; - -export const tsQualifiedSliceMethodBaseName = (fieldName: string, sliceName: string): string => { - const fieldPart = uppercaseFirstLetter(tsCamelCase(fieldName) || "Field"); - const slicePart = uppercaseFirstLetter(normalizeTsName(sliceName) || "Slice"); - 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: TypeIdentifier): 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 index 6a0c3f68..de23c910 100644 --- a/src/api/writer-generator/typescript/profile-extensions.ts +++ b/src/api/writer-generator/typescript/profile-extensions.ts @@ -12,7 +12,6 @@ import { tsExtensionFlatTypeName, tsProfileClassName, tsProfileModuleName, - tsResolvedExtensionBaseName, tsResourceName, tsValueFieldName, } from "./name"; @@ -396,15 +395,10 @@ const generateGenericExtensionGetter = (w: TypeScript, info: ExtensionMethodInfo }); }; -export const generateExtensionMethods = ( - w: TypeScript, - tsIndex: TypeSchemaIndex, - flatProfile: ProfileTypeSchema, - extensionBaseNames: Record, -) => { +export const generateExtensionMethods = (w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema) => { for (const ext of flatProfile.extensions ?? []) { if (!ext.url) continue; - const baseName = tsResolvedExtensionBaseName(extensionBaseNames, ext.url, ext.path, ext.name); + const baseName = ext.nameCandidates.recommended; const targetPath = ext.path.split(".").filter((segment) => segment !== "extension"); const extProfileInfo = resolveExtensionProfile(tsIndex, flatProfile.identifier.package, ext.url); const info: ExtensionMethodInfo = { diff --git a/src/api/writer-generator/typescript/profile-slices.ts b/src/api/writer-generator/typescript/profile-slices.ts index 655c1c4b..db79bb7a 100644 --- a/src/api/writer-generator/typescript/profile-slices.ts +++ b/src/api/writer-generator/typescript/profile-slices.ts @@ -11,7 +11,6 @@ import type { TypeSchemaIndex } from "@root/typeschema/utils"; import { tsFieldName, tsProfileClassName, - tsResolvedSliceBaseName, tsResourceName, tsSliceFlatAllTypeName, tsSliceFlatTypeName, @@ -101,6 +100,8 @@ export type SliceDef = { /** Base type parameterized with the matched resource type (e.g. "BundleEntry") */ typedBaseType: string; sliceName: string; + /** Collision-free base name from nameCandidates.recommended (e.g. "VSCat", "SystolicBP") */ + baseName: string; match: Record; /** Required fields, already filtered (match keys and polymorphic base names removed) */ required: string[]; @@ -141,6 +142,7 @@ export const collectSliceDefs = (tsIndex: TypeSchemaIndex, flatProfile: ProfileT baseType, typedBaseType, sliceName, + baseName: slice.nameCandidates.recommended, match: slice.match ?? {}, required, excluded: slice.excluded ?? [], @@ -152,16 +154,11 @@ export const collectSliceDefs = (tsIndex: TypeSchemaIndex, flatProfile: ProfileT }); }); -export const generateSliceSetters = ( - w: TypeScript, - sliceDefs: SliceDef[], - flatProfile: ProfileTypeSchema, - sliceBaseNames: Record, -) => { +export const generateSliceSetters = (w: TypeScript, sliceDefs: SliceDef[], flatProfile: ProfileTypeSchema) => { const profileClassName = tsProfileClassName(flatProfile); const tsProfileName = tsResourceName(flatProfile.identifier); for (const sliceDef of sliceDefs) { - const baseName = tsResolvedSliceBaseName(sliceBaseNames, sliceDef.fieldName, sliceDef.sliceName); + const baseName = sliceDef.baseName; const methodName = `set${baseName}`; const inputTypeName = tsSliceFlatTypeName(tsProfileName, sliceDef.fieldName, sliceDef.sliceName); const matchRef = `${profileClassName}.${tsSliceStaticName(sliceDef.sliceName)}SliceMatch`; @@ -225,17 +222,12 @@ export const generateSliceSetters = ( } }; -export const generateSliceGetters = ( - w: TypeScript, - sliceDefs: SliceDef[], - flatProfile: ProfileTypeSchema, - sliceBaseNames: Record, -) => { +export const generateSliceGetters = (w: TypeScript, sliceDefs: SliceDef[], flatProfile: ProfileTypeSchema) => { 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 baseName = sliceDef.baseName; const getMethodName = `get${baseName}`; const flatTypeName = tsSliceFlatAllTypeName(tsProfileName, sliceDef.fieldName, sliceDef.sliceName); const matchRef = `${profileClassName}.${tsSliceStaticName(sliceDef.sliceName)}SliceMatch`; diff --git a/src/api/writer-generator/typescript/profile.ts b/src/api/writer-generator/typescript/profile.ts index 1bc77361..dc730143 100644 --- a/src/api/writer-generator/typescript/profile.ts +++ b/src/api/writer-generator/typescript/profile.ts @@ -7,7 +7,6 @@ import { isNotChoiceDeclarationField, isPrimitiveIdentifier, isResourceIdentifier, - type ProfileExtension, type ProfileTypeSchema, packageMeta, packageMetaToFhir, @@ -17,19 +16,15 @@ import type { TypeSchemaIndex } from "@root/typeschema/utils"; import { tsCamelCase, tsExtensionFlatTypeName, - tsExtensionMethodBaseName, tsFieldName, tsModulePath, tsNameFromCanonical, tsPackageDir, tsProfileClassName, tsProfileModuleName, - tsQualifiedExtensionMethodBaseName, - tsQualifiedSliceMethodBaseName, tsResourceName, tsSliceFlatAllTypeName, tsSliceFlatTypeName, - tsSliceMethodBaseName, tsSliceStaticName, } from "./name"; import { @@ -560,11 +555,7 @@ const generateFactoryMethods = ( w.line(); }; -const generateFieldAccessors = ( - w: TypeScript, - factoryInfo: ProfileFactoryInfo, - extSliceMethodBaseNames: Set, -) => { +const generateFieldAccessors = (w: TypeScript, factoryInfo: ProfileFactoryInfo) => { w.line("// Field accessors"); for (const p of factoryInfo.params) { const methodBaseName = uppercaseFirstLetter(p.name); @@ -579,10 +570,8 @@ const generateFieldAccessors = ( 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`); @@ -726,78 +715,6 @@ const generateFlatInputType = (w: TypeScript, flatProfile: ProfileTypeSchema) => w.line(); }; -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; -}; - -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) }; - }; - - return resolve(entries, 0); -}; - -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`] }; - }); - - 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 }; -}; - export const generateProfileClass = (w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema) => { const tsBaseResourceName = tsTypeFromIdentifier(flatProfile.base); const profileClassName = tsProfileClassName(flatProfile); @@ -815,8 +732,6 @@ export const generateProfileClass = (w: TypeScript, tsIndex: TypeSchemaIndex, fl const canonicalUrl = flatProfile.identifier.url; w.comment("CanonicalURL:", canonicalUrl, `(pkg: ${packageMetaToFhir(packageMeta(flatProfile))})`); - const resolvedMethodNames = resolveProfileMethodBaseNames(flatProfile.extensions ?? [], sliceDefs); - w.curlyBlock(["export", "class", profileClassName], () => { w.lineSM(`static readonly canonicalUrl = ${JSON.stringify(canonicalUrl)}`); w.line(); @@ -824,14 +739,14 @@ export const generateProfileClass = (w: TypeScript, tsIndex: TypeSchemaIndex, fl w.lineSM(`private resource: ${tsBaseResourceName}`); w.line(); generateFactoryMethods(w, tsIndex, flatProfile, factoryInfo); - generateFieldAccessors(w, factoryInfo, resolvedMethodNames.allBaseNames); + generateFieldAccessors(w, factoryInfo); w.line("// Extensions"); - generateExtensionMethods(w, tsIndex, flatProfile, resolvedMethodNames.extensions); + generateExtensionMethods(w, tsIndex, flatProfile); w.line("// Slices"); - generateSliceSetters(w, sliceDefs, flatProfile, resolvedMethodNames.slices); - generateSliceGetters(w, sliceDefs, flatProfile, resolvedMethodNames.slices); + generateSliceSetters(w, sliceDefs, flatProfile); + generateSliceGetters(w, sliceDefs, flatProfile); w.line("// Validation"); generateValidateMethod(w, tsIndex, flatProfile); diff --git a/src/typeschema/core/field-builder.ts b/src/typeschema/core/field-builder.ts index 7c6de590..07483533 100644 --- a/src/typeschema/core/field-builder.ts +++ b/src/typeschema/core/field-builder.ts @@ -22,6 +22,7 @@ import type { } from "../types"; import { BINDABLE_TYPES, buildEnum } from "./binding"; import { mkBindingIdentifier, mkIdentifier } from "./identifier"; +import { mkSliceNameCandidates } from "./name-candidates"; import { mkNestedIdentifier } from "./nested-types"; function isRequired(register: Register, fhirSchema: RichFHIRSchema, path: string[]): boolean { @@ -238,7 +239,7 @@ const computeMatchFromSchema = ( return result; }; -const buildSlicing = (element: FHIRSchemaElement): FieldSlicing | undefined => { +const buildSlicing = (fieldName: string, element: FHIRSchemaElement): FieldSlicing | undefined => { const slicing = element.slicing; if (!slicing) return undefined; @@ -255,6 +256,7 @@ const buildSlicing = (element: FHIRSchemaElement): FieldSlicing | undefined => { required, excluded, elements, + nameCandidates: mkSliceNameCandidates(fieldName, name), }; } @@ -372,7 +374,7 @@ export const mkField = ( array: element.array || false, min: element.min, max: element.max, - slicing: buildSlicing(element), + slicing: buildSlicing(path[path.length - 1] ?? "", element), choices: element.choices, choiceOf: element.choiceOf, @@ -396,6 +398,6 @@ export function mkNestedField( array: element.array || false, required: isRequired(register, fhirSchema, path), excluded: isExcluded(register, fhirSchema, path), - slicing: buildSlicing(element), + slicing: buildSlicing(path[path.length - 1] ?? "", element), }; } diff --git a/src/typeschema/core/name-candidates.ts b/src/typeschema/core/name-candidates.ts new file mode 100644 index 00000000..2245c0a7 --- /dev/null +++ b/src/typeschema/core/name-candidates.ts @@ -0,0 +1,131 @@ +import { camelCase, uppercaseFirstLetter } from "@root/api/writer-generator/utils"; +import type { NameCandidates, ProfileTypeSchema } from "@root/typeschema/types"; + +// ── Language-neutral normalization ────────────────────────────────────── + +/** Normalize a FHIR name for use in identifiers. + * Strips special chars, preserves original casing, uppercases first letter. */ +const normalizeName = (s: string): string => { + const cleaned = s.replace(/\[x\]/g, "").replace(/[- :.]/g, "_"); + if (!cleaned) return ""; + return uppercaseFirstLetter(cleaned); +}; + +/** Normalize via camelCase + uppercaseFirstLetter (for extension names that may be kebab/snake). */ +const normalizeCamelName = (s: string): string => { + const cleaned = s.replace(/\[x\]/g, "").replace(/:/g, "_"); + if (!cleaned) return ""; + return uppercaseFirstLetter(camelCase(cleaned)); +}; + +// ── Candidate generators ──────────────────────────────────────────────── + +const extensionCandidates = (name: string, path: string): string[] => { + const base = normalizeCamelName(name) || "Extension"; + const pathParts = path + .split(".") + .filter((p) => p && p !== "extension") + .join("_"); + const pathPart = pathParts ? normalizeCamelName(pathParts) : ""; + const qualified = `${pathPart}${base}`; + return [base, qualified, `${qualified}Extension`]; +}; + +const sliceCandidates = (fieldName: string, sliceName: string): string[] => { + const base = normalizeName(sliceName) || "Slice"; + const fieldPart = normalizeCamelName(fieldName) || "Field"; + const qualified = `${fieldPart}${base}`; + return [base, qualified, `${qualified}Slice`]; +}; + +// ── Collision resolution ──────────────────────────────────────────────── + +type NameEntry = { key: string; candidates: string[] }; + +const countBy = (entries: NameEntry[], level: number, reserved: Set): Record => + entries.reduce( + (counts, e) => { + const name = e.candidates[level] ?? ""; + counts[name] = (counts[name] ?? 0) + 1; + if (reserved.has(name)) 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). + * Names in `reserved` are treated as taken — entries colliding with them are bumped to the next level. */ +const resolveNameCollisions = (entries: NameEntry[], reserved: Set): 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, reserved); + 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) }; + }; + + return resolve(entries, 0); +}; + +// ── Public API ────────────────────────────────────────────────────────── + +/** Compute nameCandidates for a ProfileExtension (recommended is set later by assignRecommendedBaseNames). */ +export const mkExtensionNameCandidates = (ext: { name: string; path: string }): NameCandidates => { + return { candidates: extensionCandidates(ext.name, ext.path), recommended: "" }; +}; + +/** Compute nameCandidates for a FieldSlice (recommended is set later by assignRecommendedBaseNames). */ +export const mkSliceNameCandidates = (fieldName: string, sliceName: string): NameCandidates => { + return { candidates: sliceCandidates(fieldName, sliceName), recommended: "" }; +}; + +/** Resolve collisions across all extensions and slices within a profile. + * Field accessor names are reserved — slices/extensions are bumped to avoid them. + * Mutates `nameCandidates.recommended` on each extension/slice in place. */ +export const assignRecommendedBaseNames = (profile: ProfileTypeSchema): void => { + const extensionEntries: NameEntry[] = (profile.extensions ?? []) + .filter((ext) => ext.url) + .map((ext) => ({ + key: `ext:${ext.url}:${ext.path}`, + candidates: ext.nameCandidates.candidates, + })); + + const sliceEntries: NameEntry[] = Object.entries(profile.fields ?? {}).flatMap(([fieldName, field]) => { + if (!("slicing" in field) || !field.slicing?.slices) return []; + return Object.entries(field.slicing.slices).map(([sliceName, slice]) => ({ + key: `slice:${fieldName}:${sliceName}`, + candidates: slice.nameCandidates.candidates, + })); + }); + + // Field names are reserved so slices/extensions avoid colliding with field accessors + const reservedNames = new Set(Object.keys(profile.fields ?? {}).map(normalizeCamelName)); + + const allEntries = [...extensionEntries, ...sliceEntries]; + if (allEntries.length === 0) return; + + const resolved = resolveNameCollisions(allEntries, reservedNames); + + for (const ext of profile.extensions ?? []) { + if (!ext.url) continue; + const key = `ext:${ext.url}:${ext.path}`; + if (resolved[key]) ext.nameCandidates.recommended = resolved[key]; + } + + for (const [fieldName, field] of Object.entries(profile.fields ?? {})) { + if (!("slicing" in field) || !field.slicing?.slices) continue; + for (const [sliceName, slice] of Object.entries(field.slicing.slices)) { + const key = `slice:${fieldName}:${sliceName}`; + if (resolved[key]) slice.nameCandidates.recommended = resolved[key]; + } + } +}; diff --git a/src/typeschema/core/profile-extensions.ts b/src/typeschema/core/profile-extensions.ts index 42c51837..068c5e42 100644 --- a/src/typeschema/core/profile-extensions.ts +++ b/src/typeschema/core/profile-extensions.ts @@ -20,6 +20,7 @@ import { import { buildFieldType } from "./field-builder"; import { mkIdentifier } from "./identifier"; +import { mkExtensionNameCandidates } from "./name-candidates"; const extractExtensionValueFieldTypes = ( register: Register, @@ -158,9 +159,10 @@ export const extractProfileExtensions = ( const extFs = url ? register.resolveFs(fhirSchema.package_meta, url) : undefined; const profile = extFs ? (mkIdentifier(extFs) as ProfileIdentifier) : undefined; + const extPath = [...path, "extension"].join("."); extensions.push({ name, - path: [...path, "extension"].join("."), + path: extPath, url, profile, min: schema.min, @@ -169,6 +171,7 @@ export const extractProfileExtensions = ( valueFieldTypes, subExtensions, isComplex, + nameCandidates: mkExtensionNameCandidates({ name, path: extPath }), }); }; diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 5ae8f038..391bce33 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -17,6 +17,7 @@ import { isNestedIdentifier, type NestedTypeSchema, type ProfileIdentifier, + type ProfileTypeSchema, packageMetaToFhir, type RichFHIRSchema, type RichValueSet, @@ -29,6 +30,7 @@ import { import { collectBindingSchemas, extractValueSetConceptsByUrl } from "./binding"; import { mkField, mkNestedField } from "./field-builder"; import { mkIdentifier, mkValueSetIdentifierByUrl } from "./identifier"; +import { assignRecommendedBaseNames } from "./name-candidates"; import { extractNestedDependencies, isNestedElement, mkNestedTypes } from "./nested-types"; import { extractProfileExtensions } from "./profile-extensions"; @@ -161,18 +163,17 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche const extensions = extractProfileExtensions(register, fhirSchema, logger); const extensionDeps = extensions?.flatMap(extractExtensionDeps); const rawDeps = extractProfileDependencies(identifier, base, fields, nested); - return [ - { - identifier, - base, - fields, - nested, - description: fhirSchema.description, - dependencies: concatIdentifiers(rawDeps, extensionDeps), - extensions, - }, - ...bindingSchemas, - ]; + const profileSchema: ProfileTypeSchema = { + identifier, + base, + fields, + nested, + description: fhirSchema.description, + dependencies: concatIdentifiers(rawDeps, extensionDeps), + extensions, + }; + assignRecommendedBaseNames(profileSchema); + return [profileSchema, ...bindingSchemas]; } if (fhirSchema.kind === "primitive-type") { diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index 7b8edfb7..cd0ccb3e 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -248,6 +248,11 @@ export type ConstrainedChoiceInfo = { allChoiceNames: string[]; }; +export type NameCandidates = { + candidates: string[]; + recommended: string; +}; + export interface FieldSlice { min?: number; max?: number; @@ -255,6 +260,7 @@ export interface FieldSlice { required?: string[]; excluded?: string[]; elements?: string[]; + nameCandidates: NameCandidates; } export interface ExtensionSubField { @@ -276,6 +282,7 @@ export interface ProfileExtension { valueFieldTypes?: TypeIdentifier[]; subExtensions?: ExtensionSubField[]; isComplex?: boolean; + nameCandidates: NameCandidates; } export const extractExtensionDeps = (ext: ProfileExtension): TypeIdentifier[] => [ diff --git a/test/unit/typeschema/ir/tree-shake.test.ts b/test/unit/typeschema/ir/tree-shake.test.ts index 77bd2881..ab73a6d7 100644 --- a/test/unit/typeschema/ir/tree-shake.test.ts +++ b/test/unit/typeschema/ir/tree-shake.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "bun:test"; import assert from "node:assert"; +import { mkExtensionNameCandidates } from "@root/typeschema/core/name-candidates"; import { packageTreeShakeReadme, rootTreeShakeReadme, @@ -301,6 +302,7 @@ describe("ignoreExtensions", () => { url: "http://example.com/ext/race", profile: mkProfileId("http://example.com/ext/race"), valueFieldTypes: [mkDep("http://hl7.org/fhir/StructureDefinition/Coding")], + nameCandidates: mkExtensionNameCandidates({ name: "race", path: "Patient.extension" }), }, { name: "ethnicity", @@ -308,12 +310,14 @@ describe("ignoreExtensions", () => { url: "http://example.com/ext/ethnicity", profile: mkProfileId("http://example.com/ext/ethnicity"), valueFieldTypes: [mkDep("http://hl7.org/fhir/StructureDefinition/CodeableConcept")], + nameCandidates: mkExtensionNameCandidates({ name: "ethnicity", path: "Patient.extension" }), }, { name: "birthsex", path: "Patient.extension", url: "http://example.com/ext/birthsex", profile: mkProfileId("http://example.com/ext/birthsex"), + nameCandidates: mkExtensionNameCandidates({ name: "birthsex", path: "Patient.extension" }), }, ], });