Skip to content
Merged
12 changes: 9 additions & 3 deletions src/api/writer-generator/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import { fileURLToPath } from "node:url";
import { camelCase, pascalCase, snakeCase, uppercaseFirstLetterOfEach } from "@root/api/writer-generator/utils";
import { Writer, type WriterOptions } from "@root/api/writer-generator/writer.ts";
import { groupByPackages, sortAsDeclarationSequence, type TypeSchemaIndex } from "@root/typeschema/utils";
import type { EnumDefinition, Field, SpecializationTypeSchema, TypeIdentifier } from "@typeschema/types.ts";
import {
type EnumDefinition,
type Field,
isResourceTypeSchema,
type SpecializationTypeSchema,
type TypeIdentifier,
} from "@typeschema/types.ts";

const PRIMITIVE_TYPE_MAP: Record<string, string> = {
boolean: "bool",
Expand Down Expand Up @@ -430,13 +436,13 @@ export class Python extends Writer<PythonGeneratorOptions> {
return;
}

if (schema.identifier.kind === "resource") {
if (isResourceTypeSchema(schema)) {
this.generateResourceTypeField(schema);
}

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

if (schema.identifier.kind === "resource") {
if (isResourceTypeSchema(schema)) {
this.generateResourceMethods(schema);
}
}
Expand Down
14 changes: 1 addition & 13 deletions src/api/writer-generator/typescript/writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ import {
isChoiceDeclarationField,
isComplexTypeIdentifier,
isLogicalTypeSchema,
isNestedIdentifier,
isPrimitiveIdentifier,
isProfileTypeSchema,
isResourceTypeSchema,
isSpecializationTypeSchema,
type Name,
packageMeta,
packageMetaToFhir,
type SpecializationTypeSchema,
Expand Down Expand Up @@ -156,14 +154,6 @@ export class TypeScript extends Writer<TypeScriptOptions> {
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);
}
Expand Down Expand Up @@ -214,8 +204,6 @@ export class TypeScript extends Writer<TypeScriptOptions> {
const genericTypes = ["Reference", "Coding", "CodeableConcept"];
if (genericTypes.includes(schema.identifier.name)) {
name = `${schema.identifier.name}<T extends string = string>`;
} else if (schema.identifier.kind === "nested") {
name = tsResourceName(schema.identifier);
} else {
name = tsResourceName(schema.identifier);
}
Expand Down Expand Up @@ -336,7 +324,7 @@ export class TypeScript extends Writer<TypeScriptOptions> {
generateProfileClass(this, tsIndex, flatProfile);
});
});
} else if (["complex-type", "resource", "logical"].includes(schema.identifier.kind)) {
} else if (isSpecializationTypeSchema(schema)) {
this.cat(`${tsModuleFileName(schema.identifier)}`, () => {
this.generateDisclaimer();
this.generateDependenciesImports(tsIndex, schema);
Expand Down
92 changes: 65 additions & 27 deletions src/typeschema/core/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Core transformation logic for converting FHIRSchema to TypeSchema format
*/

import assert from "node:assert";
import type { FHIRSchemaElement } from "@atomic-ehr/fhirschema";
import { shouldSkipCanonical } from "@root/typeschema/skip-hack";
import type { CodegenLog } from "@root/utils/log";
Expand All @@ -12,12 +13,15 @@ import {
concatIdentifiers,
extractExtensionDeps,
type Field,
type Identifier,
isNestedIdentifier,
isProfileIdentifier,
type NestedTypeSchema,
type ProfileIdentifier,
packageMetaToFhir,
type RichFHIRSchema,
type RichValueSet,
type SpecializationTypeSchema,
type TypeIdentifier,
type TypeSchema,
type ValueSetTypeSchema,
Expand Down Expand Up @@ -92,33 +96,50 @@ export async function transformValueSet(
};
}

export function extractDependencies(
identifier: TypeIdentifier,
const collectRawDeps = (
base: TypeIdentifier | undefined,
fields: Record<string, Field> | undefined,
nestedTypes: NestedTypeSchema[] | undefined,
): TypeIdentifier[] | undefined {
const deps = [];
): TypeIdentifier[] => {
const deps: TypeIdentifier[] = [];
if (base) deps.push(base);
if (fields) deps.push(...extractFieldDependencies(fields));
if (nestedTypes) deps.push(...extractNestedDependencies(nestedTypes));
return deps;
};

const localNestedTypeUrls = new Set(nestedTypes?.map((nt) => nt.identifier.url));
export const extractDependencies = (
identifier: Identifier,
base: TypeIdentifier | undefined,
fields: Record<string, Field> | undefined,
nestedTypes: NestedTypeSchema[] | undefined,
): Identifier[] | undefined => {
const deps = collectRawDeps(base, fields, nestedTypes);

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);
}
};

export const extractProfileDependencies = (
identifier: ProfileIdentifier,
base: TypeIdentifier | undefined,
fields: Record<string, Field> | undefined,
nestedTypes: NestedTypeSchema[] | undefined,
): TypeIdentifier[] | undefined => {
const deps = collectRawDeps(base, fields, nestedTypes);
const filtered = deps.filter((dep) => dep.url !== identifier.url);
return concatIdentifiers(filtered);
};

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

let base: TypeIdentifier | undefined;
let base: Identifier | undefined;
if (fhirSchema.base) {
const baseFs = register.resolveFs(
fhirSchema.package_meta,
Expand All @@ -128,27 +149,44 @@ export function transformFhirSchema(register: Register, fhirSchema: RichFHIRSche
throw new Error(
`Base resource not found '${fhirSchema.base}' for <${fhirSchema.url}> from ${packageMetaToFhir(fhirSchema.package_meta)}`,
);
base = mkIdentifier(baseFs);
const baseId = mkIdentifier(baseFs);
assert(!isNestedIdentifier(baseId), `Unexpected nested base for ${fhirSchema.url}`);
base = baseId;
}

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.
};
let typeSchema: TypeSchema;
if (fhirSchema.derivation === "constraint") {
if (!base) throw new Error(`Profile ${fhirSchema.url} must have a base type`);
assert(isProfileIdentifier(identifier));
const extensions = extractProfileExtensions(register, fhirSchema, logger);
const extensionDeps = extensions?.flatMap(extractExtensionDeps);
const rawDeps = extractProfileDependencies(identifier, base, fields, nested);
typeSchema = {
identifier,
base,
fields,
nested,
description: fhirSchema.description,
dependencies: concatIdentifiers(rawDeps, extensionDeps),
extensions,
};
} else {
assert(!isNestedIdentifier(identifier), `Unexpected nested identifier for ${fhirSchema.url}`);
const rawDeps = extractDependencies(identifier, base, fields, nested);
const specialization: SpecializationTypeSchema = {
identifier,
base,
fields,
nested,
description: fhirSchema.description,
dependencies: rawDeps,
typeFamily: undefined, // NOTE: should be populateTypeFamily later.
};
typeSchema = specialization;
}

const bindingSchemas = collectBindingSchemas(register, fhirSchema, logger);
return [typeSchema, ...bindingSchemas];
Expand Down
3 changes: 2 additions & 1 deletion src/typeschema/ir/logic-promotion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type Field,
type Identifier,
isChoiceDeclarationField,
isLogicalTypeSchema,
isPrimitiveTypeSchema,
isProfileTypeSchema,
isSpecializationTypeSchema,
Expand All @@ -25,7 +26,7 @@ export const promoteLogical = (tsIndex: TypeSchemaIndex, promotes: LogicalPromot
.map((schema) => {
const promo = promoteSets[schema.identifier.package]?.has(schema.identifier.url);
if (!promo) return undefined;
if (schema.identifier.kind !== "logical")
if (!isLogicalTypeSchema(schema))
throw new Error(`Unexpected schema kind: ${JSON.stringify(schema.identifier)}`);
return [identifierToString(schema.identifier), { ...schema.identifier, kind: "resource" }] as const;
})
Expand Down
24 changes: 14 additions & 10 deletions src/typeschema/ir/tree-shake.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert";
import type { CodegenLog } from "@root/utils/log";
import { extractDependencies } from "../core/transformer";
import { extractDependencies, extractProfileDependencies } from "../core/transformer";
import {
type CanonicalUrl,
concatIdentifiers,
Expand All @@ -15,7 +15,6 @@ import {
isProfileTypeSchema,
isSpecializationTypeSchema,
isValueSetTypeSchema,
type NestedTypeSchema,
type PkgName,
type ProfileTypeSchema,
type SpecializationTypeSchema,
Expand Down Expand Up @@ -78,7 +77,7 @@ export const packageTreeShakeReadme = (report: TypeSchemaIndex | IrReport, pkgNa
return lines.join("\n");
};

const mutableSelectFields = (schema: SpecializationTypeSchema, selectFields: string[]) => {
const mutableSelectFields = (schema: SpecializationTypeSchema | ProfileTypeSchema, selectFields: string[]) => {
const selectedFields: Record<string, Field> = {};

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

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

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

const extDeps = isProfileTypeSchema(schema) ? schema.extensions?.flatMap(extractExtensionDeps) : undefined;
schema.dependencies = concatIdentifiers(
extractDependencies(schema.identifier, schema.base, schema.fields, schema.nested),
extDeps,
);
if (isProfileTypeSchema(schema)) {
const extDeps = schema.extensions?.flatMap(extractExtensionDeps);
schema.dependencies = concatIdentifiers(
extractProfileDependencies(schema.identifier, schema.base, schema.fields, schema.nested),
extDeps,
);
} else {
assert(!isNestedIdentifier(schema.identifier));
schema.dependencies = extractDependencies(schema.identifier, schema.base, schema.fields, schema.nested);
}
return schema;
};

Expand Down
12 changes: 7 additions & 5 deletions src/typeschema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,14 @@ export const isProfileIdentifier = (id: TypeIdentifier | undefined): id is Profi
return id?.kind === "profile";
};

export const concatIdentifiers = (...sources: (TypeIdentifier[] | undefined)[]): TypeIdentifier[] | undefined => {
export const concatIdentifiers = <T extends TypeIdentifier = TypeIdentifier>(
...sources: (T[] | undefined)[]
): T[] | undefined => {
const entries = sources
.filter((s): s is TypeIdentifier[] => s !== undefined)
.flatMap((s) => s.map((id): [string, TypeIdentifier] => [id.url, id]));
.filter((s): s is T[] => s !== undefined)
.flatMap((s) => s.map((id): [string, T] => [id.url, id]));
if (entries.length === 0) return undefined;
const deduped = Object.values(Object.fromEntries(entries) as Record<string, TypeIdentifier>);
const deduped = Object.values(Object.fromEntries(entries) as Record<string, T>);
return deduped.sort((a, b) => a.url.localeCompare(b.url));
};

Expand Down Expand Up @@ -271,7 +273,7 @@ export interface SpecializationTypeSchema {
description?: string;
fields?: { [k: string]: Field };
nested?: NestedTypeSchema[];
dependencies?: TypeIdentifier[];
dependencies?: Identifier[];
/** Transitive children grouped by kind (e.g. Resource → { resources: [DomainResource, Patient, …] }) */
typeFamily?: {
resources?: ResourceIdentifier[];
Expand Down
7 changes: 4 additions & 3 deletions src/typeschema/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ import {
///////////////////////////////////////////////////////////
// TypeSchema processing

export const groupByPackages = (typeSchemas: TypeSchema[]): Record<PkgName, TypeSchema[]> => {
const grouped = {} as Record<PkgName, TypeSchema[]>;
export const groupByPackages = <T extends { identifier: TypeIdentifier }>(typeSchemas: T[]): Record<PkgName, T[]> => {
const grouped = {} as Record<PkgName, T[]>;
for (const ts of typeSchemas) {
const pkgName = ts.identifier.package;
if (!grouped[pkgName]) grouped[pkgName] = [];
grouped[pkgName].push(ts);
}
for (const [packageName, typeSchemas] of Object.entries(grouped)) {
const dict: Record<string, TypeSchema> = {};
const dict: Record<string, T> = {};
for (const ts of typeSchemas) {
dict[JSON.stringify(ts.identifier)] = ts;
}
Expand Down Expand Up @@ -231,6 +231,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
Expand Down
Loading
Loading