Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/typescript-r4/fhir-types/type-tree.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ hl7.fhir.r4.core:
http://hl7.org/fhir/StructureDefinition/bp: {}
http://hl7.org/fhir/StructureDefinition/vitalsigns: {}
http://hl7.org/fhir/StructureDefinition/humanname-own-prefix: {}
profile-snapshot: {}
logical: {}
shared:
primitive-type: {}
Expand Down Expand Up @@ -137,4 +138,5 @@ shared:
urn:fhir:binding:UsageContextType: {}
urn:fhir:binding:VitalSigns: {}
profile: {}
profile-snapshot: {}
logical: {}
3 changes: 3 additions & 0 deletions examples/typescript-us-core/fhir-types/type-tree.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ hl7.fhir.us.core:
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: {}
profile-snapshot: {}
logical: {}
hl7.fhir.r4.core:
primitive-type:
Expand Down Expand Up @@ -90,6 +91,7 @@ hl7.fhir.r4.core:
binding: {}
profile:
http://hl7.org/fhir/StructureDefinition/vitalsigns: {}
profile-snapshot: {}
logical: {}
shared:
primitive-type: {}
Expand Down Expand Up @@ -150,4 +152,5 @@ shared:
urn:fhir:binding:UsageContextType: {}
urn:fhir:binding:VitalSigns: {}
profile: {}
profile-snapshot: {}
logical: {}
12 changes: 6 additions & 6 deletions src/api/writer-generator/typescript/name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import {
type CanonicalUrl,
extractNameFromCanonical,
type ProfileTypeSchema,
type SnapshotProfileTypeSchema,
type TypeIdentifier,
} from "@root/typeschema/types";
import type { TypeSchemaIndex } from "@root/typeschema/utils";
Expand Down Expand Up @@ -77,17 +77,17 @@ export const tsFieldName = (n: string): string => {
return n;
};

export const tsProfileModuleName = (tsIndex: TypeSchemaIndex, schema: ProfileTypeSchema): string => {
const resourceSchema = tsIndex.findLastSpecialization(schema);
const resourceName = uppercaseFirstLetter(normalizeTsName(resourceSchema.identifier.name));
export const tsProfileModuleName = (tsIndex: TypeSchemaIndex, schema: SnapshotProfileTypeSchema): string => {
const resourceId = tsIndex.findLastSpecializationByIdentifier(schema.identifier);
const resourceName = uppercaseFirstLetter(normalizeTsName(resourceId.name));
return `${resourceName}_${normalizeTsName(schema.identifier.name)}`;
};

export const tsProfileModuleFileName = (tsIndex: TypeSchemaIndex, schema: ProfileTypeSchema): string => {
export const tsProfileModuleFileName = (tsIndex: TypeSchemaIndex, schema: SnapshotProfileTypeSchema): string => {
return `${tsProfileModuleName(tsIndex, schema)}.ts`;
};

export const tsProfileClassName = (schema: ProfileTypeSchema): string => {
export const tsProfileClassName = (schema: SnapshotProfileTypeSchema): string => {
const name = normalizeTsName(schema.identifier.name);
return name.endsWith("Profile") ? name : `${name}Profile`;
};
Expand Down
62 changes: 34 additions & 28 deletions src/api/writer-generator/typescript/profile-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
isChoiceDeclarationField,
isProfileTypeSchema,
type ProfileExtension,
type ProfileTypeSchema,
type SnapshotProfileTypeSchema,
snapshotIdentifier,
type TypeIdentifier,
} from "@root/typeschema/types";
import type { TypeSchemaIndex } from "@root/typeschema/utils";
Expand Down Expand Up @@ -31,7 +32,7 @@ export type SubExtensionSliceInfo = {
export type ExtensionProfileInfo = {
className: string;
modulePath: string;
flatProfile: ProfileTypeSchema;
snapshot: SnapshotProfileTypeSchema;
};

/**
Expand Down Expand Up @@ -77,8 +78,8 @@ export const valueFieldToTsType = (valueField: string): string => {
* 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;
export const collectSubExtensionSlices = (extProfile: SnapshotProfileTypeSchema): 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)) {
Expand Down Expand Up @@ -112,10 +113,11 @@ export const resolveExtensionProfile = (
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 };
const snapshot = tsIndex.resolve(snapshotIdentifier(schema.identifier));
if (!snapshot) return undefined;
const className = tsProfileClassName(snapshot);
const modulePath = `./${tsProfileModuleName(tsIndex, snapshot)}`;
return { className, modulePath, snapshot };
};

/** Generate the body of a raw Extension branch: validate url, then push/upsert. */
Expand Down Expand Up @@ -208,7 +210,7 @@ const generateExtensionGetterOverloads = (

type ExtensionMethodInfo = {
ext: ProfileExtension;
flatProfile: ProfileTypeSchema;
snapshot: SnapshotProfileTypeSchema;
setMethodName: string;
getMethodName: string;
targetPath: string[];
Expand All @@ -218,11 +220,11 @@ type ExtensionMethodInfo = {
// 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 { ext, snapshot, setMethodName, targetPath, extProfileInfo } = info;
const tsProfileName = tsResourceName(snapshot.identifier);
const inputTypeName = tsExtensionFlatTypeName(tsProfileName, ext.name);
const extProfileHasFlatInput = extProfileInfo
? collectSubExtensionSlices(extProfileInfo.flatProfile).length > 0
? collectSubExtensionSlices(extProfileInfo.snapshot).length > 0
: false;
const useUpsert = ext.max === "1";

Expand Down Expand Up @@ -286,11 +288,11 @@ const generateComplexExtensionSetter = (w: TypeScript, info: ExtensionMethodInfo
};

const generateComplexExtensionGetter = (w: TypeScript, info: ExtensionMethodInfo) => {
const { ext, flatProfile, getMethodName, targetPath, extProfileInfo } = info;
const tsProfileName = tsResourceName(flatProfile.identifier);
const { ext, snapshot, getMethodName, targetPath, extProfileInfo } = info;
const tsProfileName = tsResourceName(snapshot.identifier);
const inputTypeName = tsExtensionFlatTypeName(tsProfileName, ext.name);
const extProfileHasFlatInput = extProfileInfo
? collectSubExtensionSlices(extProfileInfo.flatProfile).length > 0
? collectSubExtensionSlices(extProfileInfo.snapshot).length > 0
: false;
const inputType = extProfileHasFlatInput && extProfileInfo ? `${extProfileInfo.className}Flat` : inputTypeName;

Expand All @@ -316,7 +318,7 @@ const generateSingleValueExtensionSetter = (w: TypeScript, tsIndex: TypeSchemaIn
const useUpsert = ext.max === "1";

if (extProfileInfo) {
const extFactoryInfo = collectProfileFactoryInfo(tsIndex, extProfileInfo.flatProfile);
const extFactoryInfo = collectProfileFactoryInfo(tsIndex, extProfileInfo.snapshot);
const extValueParam = extFactoryInfo.params.find((p) => p.name === valueField);
const resolvedValueType = extValueParam?.tsType ?? valueType;
const paramType = `${extProfileInfo.className} | Extension | ${resolvedValueType}`;
Expand Down Expand Up @@ -395,15 +397,19 @@ const generateGenericExtensionGetter = (w: TypeScript, info: ExtensionMethodInfo
});
};

export const generateExtensionMethods = (w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema) => {
for (const ext of flatProfile.extensions ?? []) {
export const generateExtensionMethods = (
w: TypeScript,
tsIndex: TypeSchemaIndex,
snapshot: SnapshotProfileTypeSchema,
) => {
for (const ext of snapshot.extensions ?? []) {
if (!ext.url) continue;
const baseName = ext.nameCandidates.recommended;
const targetPath = ext.path.split(".").filter((segment) => segment !== "extension");
const extProfileInfo = resolveExtensionProfile(tsIndex, flatProfile.identifier.package, ext.url);
const extProfileInfo = resolveExtensionProfile(tsIndex, snapshot.identifier.package, ext.url);
const info: ExtensionMethodInfo = {
ext,
flatProfile,
snapshot,
setMethodName: `set${baseName}`,
getMethodName: `get${baseName}`,
targetPath,
Expand All @@ -429,18 +435,18 @@ export const generateExtensionMethods = (w: TypeScript, tsIndex: TypeSchemaIndex

export const collectTypesFromExtensions = (
tsIndex: TypeSchemaIndex,
flatProfile: ProfileTypeSchema,
snapshot: SnapshotProfileTypeSchema,
addType: (typeId: TypeIdentifier) => void,
): boolean => {
let needsExtensionType = false;

for (const ext of flatProfile.extensions ?? []) {
for (const ext of snapshot.extensions ?? []) {
if (ext.isComplex && ext.subExtensions) {
needsExtensionType = true;
for (const sub of ext.subExtensions) {
if (!sub.valueFieldType) continue;
const resolvedType = tsIndex.resolveByUrl(
flatProfile.identifier.package,
snapshot.identifier.package,
sub.valueFieldType.url as CanonicalUrl,
);
addType(resolvedType?.identifier ?? sub.valueFieldType);
Expand All @@ -449,7 +455,7 @@ export const collectTypesFromExtensions = (
needsExtensionType = true;
if (ext.valueFieldTypes[0]) {
const resolvedType = tsIndex.resolveByUrl(
flatProfile.identifier.package,
snapshot.identifier.package,
ext.valueFieldTypes[0].url as CanonicalUrl,
);
addType(resolvedType?.identifier ?? ext.valueFieldTypes[0]);
Expand All @@ -465,18 +471,18 @@ export const collectTypesFromExtensions = (
/** Collect types used in the FlatInput of extension profiles. */
export const collectTypesFromFlatInput = (
tsIndex: TypeSchemaIndex,
flatProfile: ProfileTypeSchema,
snapshot: SnapshotProfileTypeSchema,
addType: (typeId: TypeIdentifier) => void,
) => {
if (flatProfile.base.name !== "Extension") return;
const subSlices = collectSubExtensionSlices(flatProfile);
if (snapshot.base.name !== "Extension") return;
const subSlices = collectSubExtensionSlices(snapshot);
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);
const schema = tsIndex.resolveByUrl(snapshot.identifier.package, fhirUrl);
if (schema) addType(schema.identifier);
}
};
26 changes: 13 additions & 13 deletions src/api/writer-generator/typescript/profile-slices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
isChoiceDeclarationField,
isNotChoiceDeclarationField,
isPrimitiveIdentifier,
type ProfileTypeSchema,
type RegularField,
type SnapshotProfileTypeSchema,
type TypeIdentifier,
} from "@root/typeschema/types";
import type { TypeSchemaIndex } from "@root/typeschema/utils";
Expand Down Expand Up @@ -45,11 +45,11 @@ export const extractResourceTypeFromMatch = (match: Record<string, unknown>): st

export const collectTypesFromSlices = (
tsIndex: TypeSchemaIndex,
flatProfile: ProfileTypeSchema,
snapshot: SnapshotProfileTypeSchema,
addType: (typeId: TypeIdentifier) => void,
) => {
const pkgName = flatProfile.identifier.package;
for (const field of Object.values(flatProfile.fields ?? {})) {
const pkgName = snapshot.identifier.package;
for (const field of Object.values(snapshot.fields)) {
if (!isNotChoiceDeclarationField(field) || !field.slicing?.slices || !field.type) continue;
const isTypeDisc = field.slicing.discriminator?.some((d) => d.type === "type") ?? false;
for (const slice of Object.values(field.slicing.slices)) {
Expand Down Expand Up @@ -114,13 +114,13 @@ export type SliceDef = {
max: number;
};

export const collectSliceDefs = (tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema): SliceDef[] =>
Object.entries(flatProfile.fields ?? {})
export const collectSliceDefs = (tsIndex: TypeSchemaIndex, snapshot: SnapshotProfileTypeSchema): SliceDef[] =>
Object.entries(snapshot.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 pkgName = snapshot.identifier.package;
const choiceBaseNames = collectChoiceBaseNames(tsIndex, field.type);
const isTypeDisc = field.slicing.discriminator?.some((d) => d.type === "type") ?? false;
return Object.entries(field.slicing.slices)
Expand Down Expand Up @@ -154,9 +154,9 @@ export const collectSliceDefs = (tsIndex: TypeSchemaIndex, flatProfile: ProfileT
});
});

export const generateSliceSetters = (w: TypeScript, sliceDefs: SliceDef[], flatProfile: ProfileTypeSchema) => {
const profileClassName = tsProfileClassName(flatProfile);
const tsProfileName = tsResourceName(flatProfile.identifier);
export const generateSliceSetters = (w: TypeScript, sliceDefs: SliceDef[], snapshot: SnapshotProfileTypeSchema) => {
const profileClassName = tsProfileClassName(snapshot);
const tsProfileName = tsResourceName(snapshot.identifier);
for (const sliceDef of sliceDefs) {
const baseName = sliceDef.baseName;
const methodName = `set${baseName}`;
Expand Down Expand Up @@ -222,9 +222,9 @@ export const generateSliceSetters = (w: TypeScript, sliceDefs: SliceDef[], flatP
}
};

export const generateSliceGetters = (w: TypeScript, sliceDefs: SliceDef[], flatProfile: ProfileTypeSchema) => {
const profileClassName = tsProfileClassName(flatProfile);
const tsProfileName = tsResourceName(flatProfile.identifier);
export const generateSliceGetters = (w: TypeScript, sliceDefs: SliceDef[], snapshot: SnapshotProfileTypeSchema) => {
const profileClassName = tsProfileClassName(snapshot);
const tsProfileName = tsResourceName(snapshot.identifier);
const defaultMode = w.opts.sliceGetterDefault ?? "flat";
for (const sliceDef of sliceDefs) {
const baseName = sliceDef.baseName;
Expand Down
16 changes: 10 additions & 6 deletions src/api/writer-generator/typescript/profile-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {
type ChoiceFieldInstance,
isChoiceDeclarationField,
isChoiceInstanceField,
type ProfileTypeSchema,
type RegularField,
type SnapshotProfileTypeSchema,
type TypeIdentifier,
} from "@root/typeschema/types";
import type { TypeSchemaIndex } from "@root/typeschema/utils";
Expand Down Expand Up @@ -78,12 +78,16 @@ export const collectRegularFieldValidation = (
}
};

export const generateValidateMethod = (w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema) => {
const fields = flatProfile.fields ?? {};
const profileName = flatProfile.identifier.name;
const canonicalUrl = flatProfile.identifier.url;
export const generateValidateMethod = (
w: TypeScript,
tsIndex: TypeSchemaIndex,
snapshot: SnapshotProfileTypeSchema,
) => {
const fields = snapshot.fields;
const profileName = snapshot.identifier.name;
const canonicalUrl = snapshot.identifier.url;
const canonicalUrlExpr = canonicalUrl
? { url: canonicalUrl, expr: `${tsProfileClassName(flatProfile)}.canonicalUrl` }
? { url: canonicalUrl, expr: `${tsProfileClassName(snapshot)}.canonicalUrl` }
: undefined;
w.curlyBlock(["validate(): { errors: string[]; warnings: string[] }"], () => {
w.line(`const profileName = "${profileName}"`);
Expand Down
Loading
Loading