Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a60c67f
API: thread CM `patches` through APIBuilder (P1.0)
ryukzak Jun 5, 2026
eb42944
API: add injectDependency patch helper, migrate kbv/norge dep injecti…
ryukzak Jun 5, 2026
0a9eb44
chore: pin CM to 0.0.25 (patches API) for Phase 1
ryukzak Jun 5, 2026
1663fd8
API: add renamePackage patch helper, migrate norge name-typo fix (P1.2)
ryukzak Jun 5, 2026
cb0f429
API: add renameCanonical/renameReferenceTarget helpers (P1.3)
ryukzak Jun 5, 2026
8b2fc70
API: add swapBinding + patchCodeSystem, finish ccda migration (P1.4+P…
ryukzak Jun 5, 2026
15d1348
API: scoping combinators forPackage/forResource, scope-free helpers (…
ryukzak Jun 5, 2026
a5565bc
ref: rename patch combinators to inPackage/inResource, Patches fields…
ryukzak Jun 5, 2026
22a58ec
ref: inline packageNameFixes into renamePackage (norge example)
ryukzak Jun 8, 2026
e950240
ref: simplify norge core-dependency predicate to exclude only core it…
ryukzak Jun 8, 2026
da6844c
ref: use array form for patches in kbv/norge examples
ryukzak Jun 8, 2026
1bb935e
API: require array form for patches phases (drop single-handler short…
ryukzak Jun 8, 2026
ab2c668
ref: flatten src/api/patches/index.ts to src/api/patches.ts
ryukzak Jul 29, 2026
650b3ab
API: thread packageIndex mode through APIBuilder, migrate kbv off ign…
ryukzak Jul 29, 2026
f5f2e10
API: surface CM input-fix report in prettyReport (P1.7)
ryukzak Jul 29, 2026
0519ae0
test(api): cover prettyReport Input fixes section
ryukzak Jul 29, 2026
33c0cb7
API: add ensureDependency patch helper, migrate norge off inPackage+i…
ryukzak Jul 29, 2026
cd22dd5
test(api): cover ensureDependency
ryukzak Jul 29, 2026
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
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 26 additions & 46 deletions examples/on-the-fly/ccda/generate.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,41 @@
// Run this script using Bun CLI with:
// bun run scripts/generate-fhir-types.ts

import { CanonicalManager, type PreprocessContext } from "@atomic-ehr/fhir-canonical-manager";
import { CanonicalManager } from "@atomic-ehr/fhir-canonical-manager";
import { registerFromManager } from "@root/typeschema/register";
import { APIBuilder, prettyReport } from "../../../src/api/builder";

const preprocessPackage = (ctx: PreprocessContext): PreprocessContext => {
if (ctx.kind !== "resource") return ctx;
if (ctx.package.name === "hl7.cda.uv.core") {
let str = JSON.stringify(ctx.resource);
str = str.replaceAll(
"http://hl7.org/cda/stds/core/StructureDefinition/IVL_TS",
"http://hl7.org/cda/stds/core/StructureDefinition/IVL-TS",
);
return { ...ctx, resource: JSON.parse(str) };
}
// CarePlanAct profile binds moodCode to an external NLM ValueSet that
// isn't available in any loaded package. Reuse the base Act binding.
if (ctx.package.name === "hl7.cda.us.ccda") {
const res = ctx.resource as { url?: string };
if (res.url === "http://hl7.org/cda/us/ccda/StructureDefinition/CarePlanAct") {
let str = JSON.stringify(ctx.resource);
str = str.replaceAll(
"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.37",
"http://terminology.hl7.org/ValueSet/v3-xDocumentActMood",
);
return { ...ctx, resource: JSON.parse(str) };
}
}
// The bundle-type CodeSystem is missing codes "bundle" and
// "subscription-notification" used by BatchBundle and
// SubscriptionNotificationBundle profiles. Patch all instances since
// resolveAny may pick any package's copy of the CodeSystem.
const res = ctx.resource as { url?: string; concept?: { code: string }[] };
if (res.url === "http://hl7.org/fhir/bundle-type" && res.concept) {
const existing = new Set(res.concept.map((c) => c.code));
const missing = ["bundle", "subscription-notification"].filter((c) => !existing.has(c));
if (missing.length > 0) {
return {
...ctx,
resource: {
...ctx.resource,
concept: [...res.concept, ...missing.map((code) => ({ code }))],
},
};
}
}
return ctx;
};
import { inPackage, inResource, patchCodeSystem, renameCanonical, swapBinding } from "../../../src/api/patches";

if (require.main === module) {
console.log("📦 Generating CCDA Types...");

const manager = CanonicalManager({
packages: [],
workingDir: ".codegen-cache/canonical-manager-cache",
preprocessPackage,
patches: {
fhirResource: [
// IVL_TS is a typo'd canonical in hl7.cda.uv.core (should be IVL-TS).
inPackage("hl7.cda.uv.core", [
renameCanonical({
"http://hl7.org/cda/stds/core/StructureDefinition/IVL_TS":
"http://hl7.org/cda/stds/core/StructureDefinition/IVL-TS",
}),
]),
// CarePlanAct binds moodCode to an external NLM ValueSet absent from every loaded
// package; reuse the base Act binding.
inPackage("hl7.cda.us.ccda", [
inResource("http://hl7.org/cda/us/ccda/StructureDefinition/CarePlanAct", [
swapBinding({
"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.37":
"http://terminology.hl7.org/ValueSet/v3-xDocumentActMood",
}),
]),
]),
// The bundle-type CodeSystem omits codes used by BatchBundle /
// SubscriptionNotificationBundle; patch every copy (resolveAny may pick any).
patchCodeSystem("http://hl7.org/fhir/bundle-type", ["bundle", "subscription-notification"]),
],
},
});

// Initialize manager with packages to discover CDA resources
Expand Down
30 changes: 4 additions & 26 deletions examples/on-the-fly/kbv-r4/generate.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,14 @@
import type { PreprocessContext } from "@atomic-ehr/fhir-canonical-manager";
import { APIBuilder, prettyReport } from "../../../src/api/builder";

const preprocessPackage = (ctx: PreprocessContext): PreprocessContext => {
if (ctx.kind !== "package") return ctx;
const json = ctx.packageJson;
const name = json.name as string;

// de.basisprofil.r4 doesn't declare hl7.fhir.r4.core as a dependency
if (name === "de.basisprofil.r4") {
const deps = (json.dependencies as Record<string, string>) || {};
if (!deps["hl7.fhir.r4.core"]) {
return {
...ctx,
kind: "package",
packageJson: {
...json,
dependencies: { ...deps, "hl7.fhir.r4.core": "4.0.1" },
},
};
}
}

return ctx;
};
import { injectDependency, inPackage } from "../../../src/api/patches";

if (require.main === module) {
console.log("Generating KBV R4 types...");

const builder = new APIBuilder({
preprocessPackage,
// de.basisprofil.r4 references core types without declaring hl7.fhir.r4.core.
patches: { packageJson: [inPackage("de.basisprofil.r4", [injectDependency({ "hl7.fhir.r4.core": "4.0.1" })])] },
registry: "https://packages.simplifier.net",
ignorePackageIndex: true,
packageIndex: "recover",
})
.fromPackage("kbv.ita.for", "1.3.1")
.fromPackage("de.basisprofil.r4", "1.6.0-ballot2")
Expand Down
91 changes: 24 additions & 67 deletions examples/on-the-fly/norge-r4/generate.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,33 @@
import type { PreprocessContext } from "@atomic-ehr/fhir-canonical-manager";
import { APIBuilder, prettyReport } from "../../../src/api/builder";

// Fix known package name typos (in-memory transformation)
const packageNameFixes: Record<string, string> = {
"simplifier.core.r4.rResources": "simplifier.core.r4.resources",
};

// Packages that need hl7.fhir.r4.core dependency injected
const needsCoreDependency = (name: string): boolean => {
return (
name.startsWith("simplifier.core.r4.") ||
name === "simplifier.core.r4" ||
name.startsWith("hl7.fhir.no.") ||
name.startsWith("ehelse.fhir.no.") ||
name.startsWith("nhn.fhir.no.") ||
name.startsWith("sfm.")
);
};

const preprocessPackage = (ctx: PreprocessContext): PreprocessContext => {
// GdRelatedPerson widens patient reference to include Person, but the
// base R4 RelatedPerson.patient only allows Patient. Drop the Person targets.
if (ctx.kind === "resource") {
const res = ctx.resource as { url?: string };
if (res.url === "http://ehelse.no/fhir/StructureDefinition/gd-RelatedPerson") {
let str = JSON.stringify(ctx.resource);
str = str.replaceAll(
"http://hl7.org/fhir/StructureDefinition/Person",
"http://hl7.org/fhir/StructureDefinition/Patient",
);
str = str.replaceAll(
"http://hl7.no/fhir/StructureDefinition/no-basis-Person",
"http://hl7.org/fhir/StructureDefinition/Patient",
);
str = str.replaceAll(
"http://ehelse.no/fhir/StructureDefinition/gd-Person",
"http://hl7.org/fhir/StructureDefinition/Patient",
);
return { ...ctx, resource: JSON.parse(str) };
}
return ctx;
}
let json = ctx.packageJson;
const name = json.name as string;

// Fix package name typos
const fixedName = packageNameFixes[name];
if (fixedName) {
console.log(`Fixed package name: ${name} -> ${fixedName}`);
json = { ...json, name: fixedName };
}

// Add missing core dependency to packages that don't properly declare it
if (needsCoreDependency(name)) {
const deps = (json.dependencies as Record<string, string>) || {};
if (!deps["hl7.fhir.r4.core"]) {
console.log(`Injecting hl7.fhir.r4.core dependency into ${name}`);
json = {
...json,
dependencies: { ...deps, "hl7.fhir.r4.core": "4.0.1" },
};
}
}

return { ...ctx, kind: "package", packageJson: json };
};
import { ensureDependency, inResource, renamePackage, renameReferenceTarget } from "../../../src/api/patches";

if (require.main === module) {
console.log("Generating Norge R4 types...");

const builder = new APIBuilder({
preprocessPackage,
patches: {
packageJson: [
// Many Norge packages reference core types without declaring the dependency;
// make every package (except core itself) depend on core.
ensureDependency({ "hl7.fhir.r4.core": "4.0.1" }),
// Fix known package name typo.
renamePackage({ "simplifier.core.r4.rResources": "simplifier.core.r4.resources" }),
],
// gd-RelatedPerson widens patient to include Person, but base R4 RelatedPerson.patient
// only allows Patient — narrow the Person targets back to Patient.
fhirResource: [
inResource("http://ehelse.no/fhir/StructureDefinition/gd-RelatedPerson", [
renameReferenceTarget({
"http://hl7.org/fhir/StructureDefinition/Person":
"http://hl7.org/fhir/StructureDefinition/Patient",
"http://hl7.no/fhir/StructureDefinition/no-basis-Person":
"http://hl7.org/fhir/StructureDefinition/Patient",
"http://ehelse.no/fhir/StructureDefinition/gd-Person":
"http://hl7.org/fhir/StructureDefinition/Patient",
}),
]),
],
},
registry: "https://packages.simplifier.net",
})
.fromPackage("hl7.fhir.r4.core", "4.0.1")
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
},
"homepage": "https://github.com/atomic-ehr/codegen#readme",
"dependencies": {
"@atomic-ehr/fhir-canonical-manager": "0.0.24",
"@atomic-ehr/fhir-canonical-manager": "0.0.25",
"@atomic-ehr/fhirschema": "0.0.14",
"brace-expansion": "^5.0.8",
"mustache": "^4.2.0",
Expand Down
51 changes: 50 additions & 1 deletion src/api/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import * as Path from "node:path";
import {
CanonicalManager,
type LocalPackageConfig,
type PackageId,
type PackageIndexMode,
type Patches,
type PreprocessContext,
type ReportEntry,
type TgzPackageConfig,
} from "@atomic-ehr/fhir-canonical-manager";
import { CSharp, type CSharpGeneratorOptions } from "@root/api/writer-generator/csharp/csharp";
Expand All @@ -32,6 +36,13 @@ import * as Mustache from "./writer-generator/mustache";
import { TypeScript, type TypeScriptOptions } from "./writer-generator/typescript/writer";
import type { FileBuffer, FileSystemWriter, FileSystemWriterOptions, WriterOptions } from "./writer-generator/writer";

/**
* Per-phase patch handlers for the `APIBuilder` `patches` option. Each phase is a list of
* handlers (typically `inPackage`/`inResource` combinators), passed straight to the
* CanonicalManager `Patches` config.
*/
export type PatchesInput = Partial<Patches>;

/**
* Configuration options for the API builder
*/
Expand All @@ -55,6 +66,9 @@ export type GenerationReport = {
errors: string[];
warnings: string[];
duration: number;
/** CanonicalManager input-fix diagnostics (exclusions, index recoveries, deprecations).
* Absent when a prebuilt `register` is used; empty for packages served from cache. */
inputReport?: ReportEntry[];
};

function countLinesByMatches(text: string): number {
Expand All @@ -74,11 +88,31 @@ export interface PrettyReportOptions {
fileLimit?: number;
}

const formatReportEntry = (entry: ReportEntry): string => {
const pkg = (p: PackageId): string => `${p.name}@${p.version}`;
switch (entry.kind) {
case "exclusion":
return `excluded ${entry.url} (${pkg(entry.package)}): ${entry.reason}`;
case "index-recovery":
return `recovered index for ${pkg(entry.package)}: ${entry.reason}, ${entry.recovered} resources`;
case "deprecation":
return `deprecation: ${entry.message}`;
default:
return JSON.stringify(entry);
}
};

export const prettyReport = (report: GenerationReport, options: PrettyReportOptions = {}): string => {
const { success, filesGenerated, errors, warnings, duration } = report;
const { success, filesGenerated, errors, warnings, duration, inputReport } = report;
const fileLimit = options.fileLimit ?? 20;
const errorsStr = errors.length > 0 ? `Errors: ${errors.join(", ")}` : undefined;
const warningsStr = warnings.length > 0 ? `Warnings: ${warnings.join(", ")}` : undefined;
const inputFixesStr =
inputReport && inputReport.length > 0
? [`Input fixes (${inputReport.length}):`, ...inputReport.map((e) => ` - ${formatReportEntry(e)}`)].join(
"\n",
)
: undefined;

let totalFiles = 0;
let totalLoc = 0;
Expand Down Expand Up @@ -124,6 +158,7 @@ export const prettyReport = (report: GenerationReport, options: PrettyReportOpti
return [
`Generated files (${totalFiles} files, ${formatLoc(totalLoc)}):`,
...groupStrs,
inputFixesStr,
errorsStr,
warningsStr,
`Duration: ${Math.round(duration)}ms`,
Expand Down Expand Up @@ -171,7 +206,13 @@ export class APIBuilder {
userOpts: Partial<APIBuilderOptions> & {
manager?: ReturnType<typeof CanonicalManager>;
register?: Register;
/** Per-phase patch handlers passed to the CanonicalManager (package-defect fixes). */
patches?: PatchesInput;
preprocessPackage?: (context: PreprocessContext) => PreprocessContext;
/** How the CanonicalManager treats a package's `.index.json`: trust it (`"use"`, default),
* heal a broken one (`"recover"`), or rebuild it unconditionally (`"regenerate"`). */
packageIndex?: PackageIndexMode;
/** @deprecated Use `packageIndex: "regenerate"` instead. */
ignorePackageIndex?: boolean;
logger?: CodegenLogManager;
} = {},
Expand Down Expand Up @@ -213,10 +254,17 @@ export class APIBuilder {
workingDir: ".codegen-cache/canonical-manager-cache",
registry: userOpts.registry,
dropCache: userOpts.dropCanonicalManagerCache,
patches: userOpts.patches,
preprocessPackage: userOpts.preprocessPackage,
packageIndex: userOpts.packageIndex,
ignorePackageIndex: userOpts.ignorePackageIndex,
});
this.logger = userOpts.logger ?? mkLogger({ prefix: "api" });
// `patches` only apply to a CM that this builder constructs; an injected
// manager/register owns its own patch wiring.
if (userOpts.patches && (userOpts.manager || userOpts.register)) {
this.logger.warn("`patches` is ignored when a prebuilt `manager`/`register` is provided.");
}
this.options = opts;
}

Expand Down Expand Up @@ -468,6 +516,7 @@ export class APIBuilder {
logger: this.logger.fork("reg"),
focusedPackages: packageMetas,
});
result.inputReport = this.manager.report();
}

const tsLogger = this.logger.fork("ts");
Expand Down
Loading
Loading