From 9525bfcd0a0d06a644119a2437a5989312c02b0c Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 23 Mar 2026 15:35:54 +0100 Subject: [PATCH] Remove NestedIdentifier from Identifier union MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Identifier is now the union of 7 non-nested kinds (primitive, complex-type, resource, binding, value-set, profile, logical) - TypeIdentifier = Identifier | NestedIdentifier serves as the wider alias - Export LogicalIdentifier - extractDependencies filters out all nested identifiers - Split transformFhirSchema into profile/specialization branches - Add nestedIndex fallback in resolveByUrl for constrainedChoice - Remove dead nested-handling branches in TS writer - Add casts in Python, C#, tree-shake for NestedTypeSchema ↔ TypeSchema --- src/api/writer-generator/csharp/csharp.ts | 2 +- src/api/writer-generator/python.ts | 9 ++- src/api/writer-generator/typescript/writer.ts | 25 +++----- src/typeschema/core/transformer.ts | 63 +++++++++++-------- src/typeschema/ir/logic-promotion.ts | 2 +- src/typeschema/ir/tree-shake.ts | 8 +-- src/typeschema/types.ts | 3 +- src/typeschema/utils.ts | 3 +- .../__snapshots__/typescript.test.ts.snap | 32 +++++----- .../typeschema/transformer/constraint.test.ts | 16 +---- 10 files changed, 76 insertions(+), 87 deletions(-) diff --git a/src/api/writer-generator/csharp/csharp.ts b/src/api/writer-generator/csharp/csharp.ts index 29bdf546f..b59383814 100644 --- a/src/api/writer-generator/csharp/csharp.ts +++ b/src/api/writer-generator/csharp/csharp.ts @@ -166,7 +166,7 @@ export class CSharp extends Writer { this.line(); for (const subtype of schema.nested) { - this.generateType(subtype, packageName); + this.generateType(subtype as unknown as SpecializationTypeSchema, packageName); } } diff --git a/src/api/writer-generator/python.ts b/src/api/writer-generator/python.ts index 6d33412eb..626c8d159 100644 --- a/src/api/writer-generator/python.ts +++ b/src/api/writer-generator/python.ts @@ -167,8 +167,11 @@ export class Python extends Writer { override async generate(tsIndex: TypeSchemaIndex): Promise { this.tsIndex = tsIndex; const groups: TypeSchemaPackageGroups = { - groupedComplexTypes: groupByPackages(tsIndex.collectComplexTypes()), - groupedResources: groupByPackages(tsIndex.collectResources()), + groupedComplexTypes: groupByPackages(tsIndex.collectComplexTypes()) as Record< + string, + SpecializationTypeSchema[] + >, + groupedResources: groupByPackages(tsIndex.collectResources()) as Record, }; this.generateRootPackages(groups); this.generateSDKPackages(groups); @@ -561,7 +564,7 @@ export class Python extends Writer { this.line(); for (const subtype of schema.nested) { - this.generateType(subtype); + this.generateType(subtype as unknown as SpecializationTypeSchema); } } diff --git a/src/api/writer-generator/typescript/writer.ts b/src/api/writer-generator/typescript/writer.ts index 7cc31cb7b..a7ee057c4 100644 --- a/src/api/writer-generator/typescript/writer.ts +++ b/src/api/writer-generator/typescript/writer.ts @@ -7,12 +7,10 @@ import { isChoiceDeclarationField, isComplexTypeIdentifier, isLogicalTypeSchema, - isNestedIdentifier, isPrimitiveIdentifier, isProfileTypeSchema, isResourceTypeSchema, isSpecializationTypeSchema, - type Name, packageMeta, packageMetaToFhir, type SpecializationTypeSchema, @@ -156,14 +154,6 @@ export class TypeScript extends Writer { name: tsResourceName(dep), dep: dep, }); - } else if (isNestedIdentifier(dep)) { - const ndep = { ...dep }; - ndep.name = tsNameFromCanonical(dep.url) as Name; - imports.push({ - tsPackage: `${importPrefix}${tsModulePath(ndep)}`, - name: tsResourceName(dep), - dep: dep, - }); } else { skipped.push(dep); } @@ -214,8 +204,6 @@ export class TypeScript extends Writer { const genericTypes = ["Reference", "Coding", "CodeableConcept"]; if (genericTypes.includes(schema.identifier.name)) { name = `${schema.identifier.name}`; - } else if (schema.identifier.kind === "nested") { - name = tsResourceName(schema.identifier); } else { name = tsResourceName(schema.identifier); } @@ -320,7 +308,7 @@ export class TypeScript extends Writer { generateNestedTypes(tsIndex: TypeSchemaIndex, schema: SpecializationTypeSchema) { if (schema.nested) { for (const subtype of schema.nested) { - this.generateType(tsIndex, subtype); + this.generateType(tsIndex, subtype as unknown as SpecializationTypeSchema); this.line(); } } @@ -337,18 +325,19 @@ export class TypeScript extends Writer { }); }); } else if (["complex-type", "resource", "logical"].includes(schema.identifier.kind)) { + const resourceSchema = schema as SpecializationTypeSchema; this.cat(`${tsModuleFileName(schema.identifier)}`, () => { this.generateDisclaimer(); - this.generateDependenciesImports(tsIndex, schema); - this.generateComplexTypeReexports(schema); - this.generateNestedTypes(tsIndex, schema); + this.generateDependenciesImports(tsIndex, resourceSchema); + this.generateComplexTypeReexports(resourceSchema); + this.generateNestedTypes(tsIndex, resourceSchema); this.comment( "CanonicalURL:", schema.identifier.url, `(pkg: ${packageMetaToFhir(packageMeta(schema))})`, ); - this.generateType(tsIndex, schema); - this.generateResourceTypePredicate(schema); + this.generateType(tsIndex, resourceSchema); + this.generateResourceTypePredicate(resourceSchema); }); } else { throw new Error(`Profile generation not implemented for kind: ${schema.identifier.kind}`); diff --git a/src/typeschema/core/transformer.ts b/src/typeschema/core/transformer.ts index 1a6a7fc7f..110c55136 100644 --- a/src/typeschema/core/transformer.ts +++ b/src/typeschema/core/transformer.ts @@ -14,11 +14,12 @@ import { type Field, type Identifier, isNestedIdentifier, - isProfileIdentifier, type NestedTypeSchema, + type ProfileIdentifier, packageMetaToFhir, type RichFHIRSchema, type RichValueSet, + type TypeIdentifier, type TypeSchema, type ValueSetTypeSchema, } from "@typeschema/types"; @@ -60,8 +61,8 @@ export function mkFields( return fields; } -function extractFieldDependencies(fields: Record): Identifier[] { - const deps: Identifier[] = []; +function extractFieldDependencies(fields: Record): TypeIdentifier[] { + const deps: TypeIdentifier[] = []; for (const field of Object.values(fields)) { if ("type" in field && field.type) { @@ -98,21 +99,18 @@ export function extractDependencies( fields: Record | undefined, nestedTypes: NestedTypeSchema[] | undefined, ): Identifier[] | undefined { - const deps = []; + const deps: TypeIdentifier[] = []; if (base) deps.push(base); if (fields) deps.push(...extractFieldDependencies(fields)); if (nestedTypes) deps.push(...extractNestedDependencies(nestedTypes)); - const localNestedTypeUrls = new Set(nestedTypes?.map((nt) => nt.identifier.url)); - - const filtered = deps.filter((dep) => { + const filtered = deps.filter((dep): dep is Identifier => { if (dep.url === identifier.url) return false; - if (isProfileIdentifier(identifier)) return true; - if (!isNestedIdentifier(dep)) return true; - return !localNestedTypeUrls.has(dep.url); + if (isNestedIdentifier(dep)) return false; + return true; }); - return concatIdentifiers(filtered); + return concatIdentifiers(filtered) as Identifier[] | undefined; } export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSchema, logger?: CodegenLog): TypeSchema[] { @@ -133,22 +131,33 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche const fields = mkFields(register, fhirSchema, [], fhirSchema.elements, logger); const nested = mkNestedTypes(register, fhirSchema, logger); - - const extensions = - fhirSchema.derivation === "constraint" ? extractProfileExtensions(register, fhirSchema, logger) : undefined; - const extensionDeps = extensions?.flatMap(extractExtensionDeps); - const dependencies = concatIdentifiers(extractDependencies(identifier, base, fields, nested), extensionDeps); - - const typeSchema: TypeSchema = { - identifier, - base, - fields, - nested, - description: fhirSchema.description, - dependencies, - extensions, - typeFamily: undefined, // NOTE: should be populateTypeFamily later. - }; + const rawDeps = extractDependencies(identifier, base, fields, nested); + + let typeSchema: TypeSchema; + if (fhirSchema.derivation === "constraint") { + if (!base) throw new Error(`Profile ${fhirSchema.url} must have a base type`); + const extensions = extractProfileExtensions(register, fhirSchema, logger); + const extensionDeps = extensions?.flatMap(extractExtensionDeps); + typeSchema = { + identifier: identifier as ProfileIdentifier, + base, + fields, + nested, + description: fhirSchema.description, + dependencies: concatIdentifiers(rawDeps, extensionDeps), + extensions, + }; + } else { + typeSchema = { + identifier, + base, + fields, + nested, + description: fhirSchema.description, + dependencies: rawDeps, + typeFamily: undefined, // NOTE: should be populateTypeFamily later. + } as TypeSchema; + } const bindingSchemas = collectBindingSchemas(register, fhirSchema, logger); return [typeSchema, ...bindingSchemas]; diff --git a/src/typeschema/ir/logic-promotion.ts b/src/typeschema/ir/logic-promotion.ts index 98be9b569..e1328e109 100644 --- a/src/typeschema/ir/logic-promotion.ts +++ b/src/typeschema/ir/logic-promotion.ts @@ -36,7 +36,7 @@ export const promoteLogical = (tsIndex: TypeSchemaIndex, promotes: LogicalPromot return Object.fromEntries( Object.entries(fields).map(([k, f]) => { if (isChoiceDeclarationField(f)) return [k, f]; - return [k, { ...f, type: f.type ? replace(f.type) : undefined }]; + return [k, { ...f, type: f.type ? replace(f.type as Identifier) : undefined }]; }), ); }; diff --git a/src/typeschema/ir/tree-shake.ts b/src/typeschema/ir/tree-shake.ts index 540a4e8ac..3e34ec66c 100644 --- a/src/typeschema/ir/tree-shake.ts +++ b/src/typeschema/ir/tree-shake.ts @@ -193,12 +193,12 @@ export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _lo if (rule.selectFields) { if (rule.ignoreFields) throw new Error("Cannot use both ignoreFields and selectFields in the same rule"); - mutableSelectFields(schema, rule.selectFields); + mutableSelectFields(schema as SpecializationTypeSchema, rule.selectFields); } if (rule.ignoreFields) { if (rule.selectFields) throw new Error("Cannot use both ignoreFields and selectFields in the same rule"); - mutableIgnoreFields(schema, rule.ignoreFields); + mutableIgnoreFields(schema as SpecializationTypeSchema, rule.ignoreFields); } if (isProfileTypeSchema(schema) && rule.ignoreExtensions) { @@ -221,7 +221,7 @@ export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _lo } }); }; - collectUsedNestedTypes(schema); + collectUsedNestedTypes(schema as SpecializationTypeSchema); schema.nested = schema.nested.filter((n) => usedTypes.has(n.identifier.url)); } @@ -267,7 +267,7 @@ export const treeShake = (tsIndex: TypeSchemaIndex, treeShake: TreeShakeConf): T for (const nest of schema.nested) { if (isNestedIdentifier(nest.identifier)) continue; const id = JSON.stringify(nest.identifier); - if (!acc[id]) newSchemas.push(nest); + if (!acc[id]) newSchemas.push(nest as unknown as TypeSchema); } } } diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index 86bb61532..a70ff277a 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -100,13 +100,12 @@ export type ValueSetIdentifier = { kind: "value-set" } & IdentifierBase; export type NestedIdentifier = { kind: "nested" } & IdentifierBase; export type BindingIdentifier = { kind: "binding" } & IdentifierBase; export type ProfileIdentifier = { kind: "profile" } & IdentifierBase; -type LogicalIdentifier = { kind: "logical" } & IdentifierBase; +export type LogicalIdentifier = { kind: "logical" } & IdentifierBase; export type Identifier = | PrimitiveIdentifier | ComplexTypeIdentifier | ResourceIdentifier - | NestedIdentifier | BindingIdentifier | ValueSetIdentifier | ProfileIdentifier diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index d8bf09c76..a30907b72 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -209,7 +209,7 @@ export const mkTypeSchemaIndex = ( const nurl = nschema.identifier.url; const npkg = nschema.identifier.package; nestedIndex[nurl] ??= {}; - nestedIndex[nurl][npkg] = nschema; + nestedIndex[nurl][npkg] = nschema as unknown as TypeSchema; }); } } @@ -232,6 +232,7 @@ export const mkTypeSchemaIndex = ( } } if (index[url]?.[pkgName]) return index[url]?.[pkgName]; + if (nestedIndex[url]?.[pkgName]) return nestedIndex[url]?.[pkgName]; logger?.dryWarn(`Type '${url}' not found in '${pkgName}'`); // Fallback: search across all packages when type exists elsewhere diff --git a/test/api/write-generator/__snapshots__/typescript.test.ts.snap b/test/api/write-generator/__snapshots__/typescript.test.ts.snap index 76015e356..6e844f61f 100644 --- a/test/api/write-generator/__snapshots__/typescript.test.ts.snap +++ b/test/api/write-generator/__snapshots__/typescript.test.ts.snap @@ -533,8 +533,8 @@ import type { Quantity } from "../../hl7-fhir-r4-core/Quantity"; import type { Reference } from "../../hl7-fhir-r4-core/Reference"; export type Observation_bp_Category_VSCatSliceFlat = Omit; -export type Observation_bp_Component_SystolicBPSliceFlat = Omit; -export type Observation_bp_Component_DiastolicBPSliceFlat = Omit; +export type Observation_bp_Component_SystolicBPSliceFlat = Omit & Quantity; +export type Observation_bp_Component_DiastolicBPSliceFlat = Omit & Quantity; import { buildResource, @@ -545,6 +545,8 @@ import { getArraySlice, ensureSliceDefaults, stripMatchKeys, + wrapSliceChoice, + unwrapSliceChoice, validateRequired, validateExcluded, validateFixedValue, @@ -714,7 +716,8 @@ export class observation_bpProfile { setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) return this } - const value = applySliceMatch(input ?? {}, match) + const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") + const value = applySliceMatch(wrapped, match) setArraySlice(this.resource.component ??= [], match, value) return this } @@ -725,7 +728,8 @@ export class observation_bpProfile { setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) return this } - const value = applySliceMatch(input ?? {}, match) + const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") + const value = applySliceMatch(wrapped, match) setArraySlice(this.resource.component ??= [], match, value) return this } @@ -749,7 +753,7 @@ export class observation_bpProfile { const item = getArraySlice(this.resource.component, match) if (!item) return undefined if (mode === 'raw') return item - return stripMatchKeys(item, ["code"]) + return unwrapSliceChoice(item, ["code"], "valueQuantity") } public getDiastolicBP(mode: 'flat'): Observation_bp_Component_DiastolicBPSliceFlat | undefined; @@ -760,7 +764,7 @@ export class observation_bpProfile { const item = getArraySlice(this.resource.component, match) if (!item) return undefined if (mode === 'raw') return item - return stripMatchKeys(item, ["code"]) + return unwrapSliceChoice(item, ["code"], "valueQuantity") } // Validation @@ -1066,8 +1070,8 @@ import type { Reference } from "../../hl7-fhir-r4-core/Reference"; import type { SampledData } from "../../hl7-fhir-r4-core/SampledData"; export type USCoreBloodPressureProfile_Category_VSCatSliceFlat = Omit; -export type USCoreBloodPressureProfile_Component_SystolicSliceFlat = Omit & Quantity; -export type USCoreBloodPressureProfile_Component_DiastolicSliceFlat = Omit & Quantity; +export type USCoreBloodPressureProfile_Component_SystolicSliceFlat = Omit; +export type USCoreBloodPressureProfile_Component_DiastolicSliceFlat = Omit; import { buildResource, @@ -1078,8 +1082,6 @@ import { getArraySlice, ensureSliceDefaults, stripMatchKeys, - wrapSliceChoice, - unwrapSliceChoice, validateRequired, validateExcluded, validateFixedValue, @@ -1339,8 +1341,7 @@ export class USCoreBloodPressureProfile { setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) return this } - const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") - const value = applySliceMatch(wrapped, match) + const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.component ??= [], match, value) return this } @@ -1351,8 +1352,7 @@ export class USCoreBloodPressureProfile { setArraySlice(this.resource.component ??= [], match, input as ObservationComponent) return this } - const wrapped = wrapSliceChoice(input ?? {}, "valueQuantity") - const value = applySliceMatch(wrapped, match) + const value = applySliceMatch(input ?? {}, match) setArraySlice(this.resource.component ??= [], match, value) return this } @@ -1376,7 +1376,7 @@ export class USCoreBloodPressureProfile { const item = getArraySlice(this.resource.component, match) if (!item) return undefined if (mode === 'raw') return item - return unwrapSliceChoice(item, ["code"], "valueQuantity") + return stripMatchKeys(item, ["code"]) } public getDiastolic(mode: 'flat'): USCoreBloodPressureProfile_Component_DiastolicSliceFlat | undefined; @@ -1387,7 +1387,7 @@ export class USCoreBloodPressureProfile { const item = getArraySlice(this.resource.component, match) if (!item) return undefined if (mode === 'raw') return item - return unwrapSliceChoice(item, ["code"], "valueQuantity") + return stripMatchKeys(item, ["code"]) } // Validation diff --git a/test/unit/typeschema/transformer/constraint.test.ts b/test/unit/typeschema/transformer/constraint.test.ts index 72bb6f37a..2a1b4c695 100644 --- a/test/unit/typeschema/transformer/constraint.test.ts +++ b/test/unit/typeschema/transformer/constraint.test.ts @@ -56,10 +56,7 @@ describe("TypeSchema Processing constraint generation", async () => { foo: { type: { kind: "nested", name: "foo", url: "uri::A#foo" } }, }, nested: undefined, - dependencies: [ - { kind: "resource", name: "a", url: "uri::A" }, - { kind: "nested", name: "foo", url: "uri::A#foo" }, - ], + dependencies: [{ kind: "resource", name: "a", url: "uri::A" }], }, ]); }); @@ -81,10 +78,7 @@ describe("TypeSchema Processing constraint generation", async () => { foo: { type: { kind: "nested", name: "foo", url: "uri::A#foo" } }, }, nested: undefined, - dependencies: [ - { kind: "nested", name: "foo", url: "uri::A#foo" }, - { kind: "profile", name: "b", url: "uri::B" }, - ], + dependencies: [{ kind: "profile", name: "b", url: "uri::B" }], }, ]); }); @@ -166,12 +160,6 @@ describe("TypeSchema Processing constraint generation", async () => { { kind: "primitive-type", url: "http://hl7.org/fhir/StructureDefinition/boolean" }, { kind: "primitive-type", url: "http://hl7.org/fhir/StructureDefinition/code" }, { kind: "resource", url: "http://hl7.org/fhir/StructureDefinition/CodeSystem" }, - { kind: "nested", url: "http://hl7.org/fhir/StructureDefinition/CodeSystem#concept" }, - { - kind: "nested", - url: "http://hl7.org/fhir/StructureDefinition/CodeSystem#concept.designation", - }, - { kind: "nested", url: "http://hl7.org/fhir/StructureDefinition/CodeSystem#concept.property" }, { kind: "primitive-type", url: "http://hl7.org/fhir/StructureDefinition/markdown" }, { kind: "primitive-type", url: "http://hl7.org/fhir/StructureDefinition/string" }, { kind: "primitive-type", url: "http://hl7.org/fhir/StructureDefinition/uri" },