Skip to content

Commit dab78af

Browse files
authored
Merge pull request #118 from atomic-ehr/split-dependency-types
TypeSchema: Split dependency types for specializations and profiles
2 parents 4bd9724 + 245139d commit dab78af

8 files changed

Lines changed: 112 additions & 68 deletions

File tree

src/api/writer-generator/python.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import { fileURLToPath } from "node:url";
55
import { camelCase, pascalCase, snakeCase, uppercaseFirstLetterOfEach } from "@root/api/writer-generator/utils";
66
import { Writer, type WriterOptions } from "@root/api/writer-generator/writer.ts";
77
import { groupByPackages, sortAsDeclarationSequence, type TypeSchemaIndex } from "@root/typeschema/utils";
8-
import type { EnumDefinition, Field, SpecializationTypeSchema, TypeIdentifier } from "@typeschema/types.ts";
8+
import {
9+
type EnumDefinition,
10+
type Field,
11+
isResourceTypeSchema,
12+
type SpecializationTypeSchema,
13+
type TypeIdentifier,
14+
} from "@typeschema/types.ts";
915

1016
const PRIMITIVE_TYPE_MAP: Record<string, string> = {
1117
boolean: "bool",
@@ -430,13 +436,13 @@ export class Python extends Writer<PythonGeneratorOptions> {
430436
return;
431437
}
432438

433-
if (schema.identifier.kind === "resource") {
439+
if (isResourceTypeSchema(schema)) {
434440
this.generateResourceTypeField(schema);
435441
}
436442

437443
this.generateFields(schema, schema.identifier.name);
438444

439-
if (schema.identifier.kind === "resource") {
445+
if (isResourceTypeSchema(schema)) {
440446
this.generateResourceMethods(schema);
441447
}
442448
}

src/api/writer-generator/typescript/writer.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ import {
77
isChoiceDeclarationField,
88
isComplexTypeIdentifier,
99
isLogicalTypeSchema,
10-
isNestedIdentifier,
1110
isPrimitiveIdentifier,
1211
isProfileTypeSchema,
1312
isResourceTypeSchema,
1413
isSpecializationTypeSchema,
15-
type Name,
1614
packageMeta,
1715
packageMetaToFhir,
1816
type SpecializationTypeSchema,
@@ -156,14 +154,6 @@ export class TypeScript extends Writer<TypeScriptOptions> {
156154
name: tsResourceName(dep),
157155
dep: dep,
158156
});
159-
} else if (isNestedIdentifier(dep)) {
160-
const ndep = { ...dep };
161-
ndep.name = tsNameFromCanonical(dep.url) as Name;
162-
imports.push({
163-
tsPackage: `${importPrefix}${tsModulePath(ndep)}`,
164-
name: tsResourceName(dep),
165-
dep: dep,
166-
});
167157
} else {
168158
skipped.push(dep);
169159
}
@@ -214,8 +204,6 @@ export class TypeScript extends Writer<TypeScriptOptions> {
214204
const genericTypes = ["Reference", "Coding", "CodeableConcept"];
215205
if (genericTypes.includes(schema.identifier.name)) {
216206
name = `${schema.identifier.name}<T extends string = string>`;
217-
} else if (schema.identifier.kind === "nested") {
218-
name = tsResourceName(schema.identifier);
219207
} else {
220208
name = tsResourceName(schema.identifier);
221209
}
@@ -336,7 +324,7 @@ export class TypeScript extends Writer<TypeScriptOptions> {
336324
generateProfileClass(this, tsIndex, flatProfile);
337325
});
338326
});
339-
} else if (["complex-type", "resource", "logical"].includes(schema.identifier.kind)) {
327+
} else if (isSpecializationTypeSchema(schema)) {
340328
this.cat(`${tsModuleFileName(schema.identifier)}`, () => {
341329
this.generateDisclaimer();
342330
this.generateDependenciesImports(tsIndex, schema);

src/typeschema/core/transformer.ts

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Core transformation logic for converting FHIRSchema to TypeSchema format
55
*/
66

7+
import assert from "node:assert";
78
import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema";
89
import { shouldSkipCanonical } from "@root/typeschema/skip-hack";
910
import type { CodegenLog } from "@root/utils/log";
@@ -12,12 +13,15 @@ import {
1213
concatIdentifiers,
1314
extractExtensionDeps,
1415
type Field,
16+
type Identifier,
1517
isNestedIdentifier,
1618
isProfileIdentifier,
1719
type NestedTypeSchema,
20+
type ProfileIdentifier,
1821
packageMetaToFhir,
1922
type RichFHIRSchema,
2023
type RichValueSet,
24+
type SpecializationTypeSchema,
2125
type TypeIdentifier,
2226
type TypeSchema,
2327
type ValueSetTypeSchema,
@@ -92,33 +96,50 @@ export async function transformValueSet(
9296
};
9397
}
9498

95-
export function extractDependencies(
96-
identifier: TypeIdentifier,
99+
const collectRawDeps = (
97100
base: TypeIdentifier | undefined,
98101
fields: Record<string, Field> | undefined,
99102
nestedTypes: NestedTypeSchema[] | undefined,
100-
): TypeIdentifier[] | undefined {
101-
const deps = [];
103+
): TypeIdentifier[] => {
104+
const deps: TypeIdentifier[] = [];
102105
if (base) deps.push(base);
103106
if (fields) deps.push(...extractFieldDependencies(fields));
104107
if (nestedTypes) deps.push(...extractNestedDependencies(nestedTypes));
108+
return deps;
109+
};
105110

106-
const localNestedTypeUrls = new Set(nestedTypes?.map((nt) => nt.identifier.url));
111+
export const extractDependencies = (
112+
identifier: Identifier,
113+
base: TypeIdentifier | undefined,
114+
fields: Record<string, Field> | undefined,
115+
nestedTypes: NestedTypeSchema[] | undefined,
116+
): Identifier[] | undefined => {
117+
const deps = collectRawDeps(base, fields, nestedTypes);
107118

108-
const filtered = deps.filter((dep) => {
119+
const filtered = deps.filter((dep): dep is Identifier => {
109120
if (dep.url === identifier.url) return false;
110-
if (isProfileIdentifier(identifier)) return true;
111-
if (!isNestedIdentifier(dep)) return true;
112-
return !localNestedTypeUrls.has(dep.url);
121+
if (isNestedIdentifier(dep)) return false;
122+
return true;
113123
});
114124

115125
return concatIdentifiers(filtered);
116-
}
126+
};
127+
128+
export const extractProfileDependencies = (
129+
identifier: ProfileIdentifier,
130+
base: TypeIdentifier | undefined,
131+
fields: Record<string, Field> | undefined,
132+
nestedTypes: NestedTypeSchema[] | undefined,
133+
): TypeIdentifier[] | undefined => {
134+
const deps = collectRawDeps(base, fields, nestedTypes);
135+
const filtered = deps.filter((dep) => dep.url !== identifier.url);
136+
return concatIdentifiers(filtered);
137+
};
117138

118139
export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSchema, logger?: CodegenLog): TypeSchema[] {
119140
const identifier = mkIdentifier(fhirSchema);
120141

121-
let base: TypeIdentifier | undefined;
142+
let base: Identifier | undefined;
122143
if (fhirSchema.base) {
123144
const baseFs = register.resolveFs(
124145
fhirSchema.package_meta,
@@ -128,27 +149,44 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche
128149
throw new Error(
129150
`Base resource not found '${fhirSchema.base}' for <${fhirSchema.url}> from ${packageMetaToFhir(fhirSchema.package_meta)}`,
130151
);
131-
base = mkIdentifier(baseFs);
152+
const baseId = mkIdentifier(baseFs);
153+
assert(!isNestedIdentifier(baseId), `Unexpected nested base for ${fhirSchema.url}`);
154+
base = baseId;
132155
}
133156

134157
const fields = mkFields(register, fhirSchema, [], fhirSchema.elements, logger);
135158
const nested = mkNestedTypes(register, fhirSchema, logger);
136159

137-
const extensions =
138-
fhirSchema.derivation === "constraint" ? extractProfileExtensions(register, fhirSchema, logger) : undefined;
139-
const extensionDeps = extensions?.flatMap(extractExtensionDeps);
140-
const dependencies = concatIdentifiers(extractDependencies(identifier, base, fields, nested), extensionDeps);
141-
142-
const typeSchema: TypeSchema = {
143-
identifier,
144-
base,
145-
fields,
146-
nested,
147-
description: fhirSchema.description,
148-
dependencies,
149-
extensions,
150-
typeFamily: undefined, // NOTE: should be populateTypeFamily later.
151-
};
160+
let typeSchema: TypeSchema;
161+
if (fhirSchema.derivation === "constraint") {
162+
if (!base) throw new Error(`Profile ${fhirSchema.url} must have a base type`);
163+
assert(isProfileIdentifier(identifier));
164+
const extensions = extractProfileExtensions(register, fhirSchema, logger);
165+
const extensionDeps = extensions?.flatMap(extractExtensionDeps);
166+
const rawDeps = extractProfileDependencies(identifier, base, fields, nested);
167+
typeSchema = {
168+
identifier,
169+
base,
170+
fields,
171+
nested,
172+
description: fhirSchema.description,
173+
dependencies: concatIdentifiers(rawDeps, extensionDeps),
174+
extensions,
175+
};
176+
} else {
177+
assert(!isNestedIdentifier(identifier), `Unexpected nested identifier for ${fhirSchema.url}`);
178+
const rawDeps = extractDependencies(identifier, base, fields, nested);
179+
const specialization: SpecializationTypeSchema = {
180+
identifier,
181+
base,
182+
fields,
183+
nested,
184+
description: fhirSchema.description,
185+
dependencies: rawDeps,
186+
typeFamily: undefined, // NOTE: should be populateTypeFamily later.
187+
};
188+
typeSchema = specialization;
189+
}
152190

153191
const bindingSchemas = collectBindingSchemas(register, fhirSchema, logger);
154192
return [typeSchema, ...bindingSchemas];

src/typeschema/ir/logic-promotion.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type Field,
44
type Identifier,
55
isChoiceDeclarationField,
6+
isLogicalTypeSchema,
67
isPrimitiveTypeSchema,
78
isProfileTypeSchema,
89
isSpecializationTypeSchema,
@@ -25,7 +26,7 @@ export const promoteLogical = (tsIndex: TypeSchemaIndex, promotes: LogicalPromot
2526
.map((schema) => {
2627
const promo = promoteSets[schema.identifier.package]?.has(schema.identifier.url);
2728
if (!promo) return undefined;
28-
if (schema.identifier.kind !== "logical")
29+
if (!isLogicalTypeSchema(schema))
2930
throw new Error(`Unexpected schema kind: ${JSON.stringify(schema.identifier)}`);
3031
return [identifierToString(schema.identifier), { ...schema.identifier, kind: "resource" }] as const;
3132
})

src/typeschema/ir/tree-shake.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from "node:assert";
22
import type { CodegenLog } from "@root/utils/log";
3-
import { extractDependencies } from "../core/transformer";
3+
import { extractDependencies, extractProfileDependencies } from "../core/transformer";
44
import {
55
type CanonicalUrl,
66
concatIdentifiers,
@@ -15,7 +15,6 @@ import {
1515
isProfileTypeSchema,
1616
isSpecializationTypeSchema,
1717
isValueSetTypeSchema,
18-
type NestedTypeSchema,
1918
type PkgName,
2019
type ProfileTypeSchema,
2120
type SpecializationTypeSchema,
@@ -78,7 +77,7 @@ export const packageTreeShakeReadme = (report: TypeSchemaIndex | IrReport, pkgNa
7877
return lines.join("\n");
7978
};
8079

81-
const mutableSelectFields = (schema: SpecializationTypeSchema, selectFields: string[]) => {
80+
const mutableSelectFields = (schema: SpecializationTypeSchema | ProfileTypeSchema, selectFields: string[]) => {
8281
const selectedFields: Record<string, Field> = {};
8382

8483
const selectPolimorphic: Record<string, { declaration?: string[]; instances?: string[] }> = {};
@@ -113,7 +112,7 @@ const mutableSelectFields = (schema: SpecializationTypeSchema, selectFields: str
113112
schema.fields = selectedFields;
114113
};
115114

116-
const mutableIgnoreFields = (schema: SpecializationTypeSchema, ignoreFields: string[]) => {
115+
const mutableIgnoreFields = (schema: SpecializationTypeSchema | ProfileTypeSchema, ignoreFields: string[]) => {
117116
for (const fieldName of ignoreFields) {
118117
const field = schema.fields?.[fieldName];
119118
if (!schema.fields || !field) throw new Error(`Field ${fieldName} not found`);
@@ -207,7 +206,7 @@ export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _lo
207206

208207
if (schema.nested) {
209208
const usedTypes = new Set<CanonicalUrl>();
210-
const collectUsedNestedTypes = (s: SpecializationTypeSchema | NestedTypeSchema) => {
209+
const collectUsedNestedTypes = (s: { fields?: Record<string, Field> }) => {
211210
Object.values(s.fields ?? {})
212211
.filter(isNotChoiceDeclarationField)
213212
.filter((f) => isNestedIdentifier(f.type))
@@ -225,11 +224,16 @@ export const treeShakeTypeSchema = (schema: TypeSchema, rule: TreeShakeRule, _lo
225224
schema.nested = schema.nested.filter((n) => usedTypes.has(n.identifier.url));
226225
}
227226

228-
const extDeps = isProfileTypeSchema(schema) ? schema.extensions?.flatMap(extractExtensionDeps) : undefined;
229-
schema.dependencies = concatIdentifiers(
230-
extractDependencies(schema.identifier, schema.base, schema.fields, schema.nested),
231-
extDeps,
232-
);
227+
if (isProfileTypeSchema(schema)) {
228+
const extDeps = schema.extensions?.flatMap(extractExtensionDeps);
229+
schema.dependencies = concatIdentifiers(
230+
extractProfileDependencies(schema.identifier, schema.base, schema.fields, schema.nested),
231+
extDeps,
232+
);
233+
} else {
234+
assert(!isNestedIdentifier(schema.identifier));
235+
schema.dependencies = extractDependencies(schema.identifier, schema.base, schema.fields, schema.nested);
236+
}
233237
return schema;
234238
};
235239

src/typeschema/types.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,14 @@ export const isProfileIdentifier = (id: TypeIdentifier | undefined): id is Profi
137137
return id?.kind === "profile";
138138
};
139139

140-
export const concatIdentifiers = (...sources: (TypeIdentifier[] | undefined)[]): TypeIdentifier[] | undefined => {
140+
export const concatIdentifiers = <T extends TypeIdentifier = TypeIdentifier>(
141+
...sources: (T[] | undefined)[]
142+
): T[] | undefined => {
141143
const entries = sources
142-
.filter((s): s is TypeIdentifier[] => s !== undefined)
143-
.flatMap((s) => s.map((id): [string, TypeIdentifier] => [id.url, id]));
144+
.filter((s): s is T[] => s !== undefined)
145+
.flatMap((s) => s.map((id): [string, T] => [id.url, id]));
144146
if (entries.length === 0) return undefined;
145-
const deduped = Object.values(Object.fromEntries(entries) as Record<string, TypeIdentifier>);
147+
const deduped = Object.values(Object.fromEntries(entries) as Record<string, T>);
146148
return deduped.sort((a, b) => a.url.localeCompare(b.url));
147149
};
148150

@@ -271,7 +273,7 @@ export interface SpecializationTypeSchema {
271273
description?: string;
272274
fields?: { [k: string]: Field };
273275
nested?: NestedTypeSchema[];
274-
dependencies?: TypeIdentifier[];
276+
dependencies?: Identifier[];
275277
/** Transitive children grouped by kind (e.g. Resource → { resources: [DomainResource, Patient, …] }) */
276278
typeFamily?: {
277279
resources?: ResourceIdentifier[];

src/typeschema/utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ import {
2929
///////////////////////////////////////////////////////////
3030
// TypeSchema processing
3131

32-
export const groupByPackages = (typeSchemas: TypeSchema[]): Record<PkgName, TypeSchema[]> => {
33-
const grouped = {} as Record<PkgName, TypeSchema[]>;
32+
export const groupByPackages = <T extends { identifier: TypeIdentifier }>(typeSchemas: T[]): Record<PkgName, T[]> => {
33+
const grouped = {} as Record<PkgName, T[]>;
3434
for (const ts of typeSchemas) {
3535
const pkgName = ts.identifier.package;
3636
if (!grouped[pkgName]) grouped[pkgName] = [];
3737
grouped[pkgName].push(ts);
3838
}
3939
for (const [packageName, typeSchemas] of Object.entries(grouped)) {
40-
const dict: Record<string, TypeSchema> = {};
40+
const dict: Record<string, T> = {};
4141
for (const ts of typeSchemas) {
4242
dict[JSON.stringify(ts.identifier)] = ts;
4343
}
@@ -231,6 +231,7 @@ export const mkTypeSchemaIndex = (
231231
}
232232
}
233233
if (index[url]?.[pkgName]) return index[url]?.[pkgName];
234+
if (nestedIndex[url]?.[pkgName]) return nestedIndex[url]?.[pkgName];
234235
logger?.dryWarn(`Type '${url}' not found in '${pkgName}'`);
235236

236237
// Fallback: search across all packages when type exists elsewhere

0 commit comments

Comments
 (0)