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
13 changes: 7 additions & 6 deletions examples/typescript-us-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ US Core FHIR profile generation with type-safe profile wrapper classes.
bun run examples/typescript-us-core/generate.ts
```

Edit [`generate.ts`](generate.ts) to customize which profiles to include. Tree shaking keeps only the specified profiles and their transitive dependencies. Output goes to `./fhir-types/`.
`generate.ts` calls `prettyReport(report)` to print a grouped summary of what was emitted. Edit the script to add/remove profiles; tree shaking keeps only what you list. Output: `./fhir-types/`.

## Profile Class API

Each profile class provides:

- **`from(resource)`** -- validate the resource conforms to the profile (meta.profile + required fields), throw on errors
- **`apply(resource)`** -- attach meta.profile without validation, useful for incremental construction
- **`create(args)`** -- build a new resource from typed input, auto-sets fixed values
- **`validate()`** -- check required fields, return error list
- **`toResource()`** -- get the underlying FHIR resource
- **`from(resource)`** -- validate against the profile, throw on errors
- **`apply(resource)`** -- attach meta.profile, no validation
- **`create(args)`** -- build a new resource, auto-set fixed values
- **`is(value)`** -- non-throwing type guard for `.filter()`
- **`validate()`** -- return `{ errors, warnings }`
- **`toResource()`** -- the underlying FHIR resource

Generated accessors depend on what the profile defines:

Expand Down
3 changes: 0 additions & 3 deletions examples/typescript-us-core/fhir-types/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2219,7 +2219,6 @@
- `urn:fhir:binding:BodyStructureQualifier`
- `urn:fhir:binding:BodyTempUnits`
- `urn:fhir:binding:BodyWeightUnits`
- `urn:fhir:binding:BundleType`
- `urn:fhir:binding:CapabilityStatementKind`
- `urn:fhir:binding:CarePlanActivityKind`
- `urn:fhir:binding:CarePlanActivityOutcome`
Expand Down Expand Up @@ -2534,7 +2533,6 @@
- `urn:fhir:binding:GuidanceResponseStatus`
- `urn:fhir:binding:GuidePageGeneration`
- `urn:fhir:binding:GuideParameterCode`
- `urn:fhir:binding:HTTPVerb`
- `urn:fhir:binding:HandlingConditionSet`
- `urn:fhir:binding:HumanRefSeqNCBIBuildId`
- `urn:fhir:binding:IANATimezone`
Expand Down Expand Up @@ -2842,7 +2840,6 @@
- `urn:fhir:binding:SPDXLicense`
- `urn:fhir:binding:Safety`
- `urn:fhir:binding:SearchComparator`
- `urn:fhir:binding:SearchEntryMode`
- `urn:fhir:binding:SearchModifierCode`
- `urn:fhir:binding:SearchParamType`
- `urn:fhir:binding:SearchProcessingModeType`
Expand Down
68 changes: 68 additions & 0 deletions examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// WARNING: This file is autogenerated by @atomic-ehr/codegen.
// GitHub: https://github.com/atomic-ehr/codegen
// Any manual changes made to this file may be overwritten.

import type { BackboneElement } from "../hl7-fhir-r4-core/BackboneElement";
import type { Identifier } from "../hl7-fhir-r4-core/Identifier";
import type { Resource } from "../hl7-fhir-r4-core/Resource";
import type { Signature } from "../hl7-fhir-r4-core/Signature";

import type { Element } from "../hl7-fhir-r4-core/Element";
export type { BackboneElement } from "../hl7-fhir-r4-core/BackboneElement";
export type { Identifier } from "../hl7-fhir-r4-core/Identifier";
export type { Signature } from "../hl7-fhir-r4-core/Signature";

export interface BundleEntry<T1 extends Resource = Resource, T2 extends Resource = Resource> extends BackboneElement {
fullUrl?: string;
link?: BundleLink[];
request?: BundleEntryRequest;
resource?: T1;
response?: BundleEntryResponse<T2>;
search?: BundleEntrySearch;
}

export interface BundleEntryRequest extends BackboneElement {
ifMatch?: string;
ifModifiedSince?: string;
ifNoneExist?: string;
ifNoneMatch?: string;
method: ("GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH");
url: string;
}

export interface BundleEntryResponse<T extends Resource = Resource> extends BackboneElement {
etag?: string;
lastModified?: string;
location?: string;
outcome?: T;
status: string;
}

export interface BundleEntrySearch extends BackboneElement {
mode?: ("match" | "include" | "outcome");
score?: number;
}

export interface BundleLink extends BackboneElement {
relation: string;
url: string;
}

// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Bundle (pkg: hl7.fhir.r4.core#4.0.1)
export interface Bundle<T1 extends Resource = Resource, T2 extends Resource = Resource> extends Resource {
resourceType: "Bundle";

entry?: BundleEntry<T1, T2>[];
identifier?: Identifier;
link?: BundleLink[];
signature?: Signature;
timestamp?: string;
_timestamp?: Element;
total?: number;
_total?: Element;
type: ("document" | "message" | "transaction" | "transaction-response" | "batch" | "batch-response" | "history" | "searchset" | "collection");
_type?: Element;
}
export const isBundle = (resource: unknown): resource is Bundle => {
return resource !== null && typeof resource === "object" && (resource as {resourceType: string}).resourceType === "Bundle";
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type { Meta } from "../hl7-fhir-r4-core/Meta";

// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Resource (pkg: hl7.fhir.r4.core#4.0.1)
export interface Resource {
resourceType: "DomainResource" | "Observation" | "Patient" | "Resource";
resourceType: "Bundle" | "DomainResource" | "Observation" | "Patient" | "Resource";

id?: string;
_id?: Element;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export type { Age } from "./Age";
export type { Annotation } from "./Annotation";
export type { Attachment } from "./Attachment";
export type { BackboneElement } from "./BackboneElement";
export type { Bundle, BundleEntry, BundleEntryRequest, BundleEntryResponse, BundleEntrySearch, BundleLink } from "./Bundle";
export { isBundle } from "./Bundle";
export type { CodeableConcept } from "./CodeableConcept";
export type { Coding } from "./Coding";
export type { ContactDetail } from "./ContactDetail";
Expand Down
4 changes: 4 additions & 0 deletions examples/typescript-us-core/fhir-types/type-tree.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ hl7.fhir.r4.core:
http://hl7.org/fhir/StructureDefinition/TriggerDefinition: {}
http://hl7.org/fhir/StructureDefinition/UsageContext: {}
resource:
http://hl7.org/fhir/StructureDefinition/Bundle: {}
http://hl7.org/fhir/StructureDefinition/DomainResource: {}
http://hl7.org/fhir/StructureDefinition/Observation: {}
http://hl7.org/fhir/StructureDefinition/Patient: {}
Expand All @@ -102,6 +103,7 @@ shared:
urn:fhir:binding:AddressUse: {}
urn:fhir:binding:AdministrativeGender: {}
urn:fhir:binding:BodySite: {}
urn:fhir:binding:BundleType: {}
urn:fhir:binding:ContactPointSystem: {}
urn:fhir:binding:ContactPointUse: {}
urn:fhir:binding:ContactRelationship: {}
Expand All @@ -113,6 +115,7 @@ shared:
urn:fhir:binding:ExpressionLanguage: {}
urn:fhir:binding:FHIRAllTypes: {}
urn:fhir:binding:FHIRResourceTypeExt: {}
urn:fhir:binding:HTTPVerb: {}
urn:fhir:binding:IdentifierType: {}
urn:fhir:binding:IdentifierUse: {}
urn:fhir:binding:Language: {}
Expand All @@ -135,6 +138,7 @@ shared:
urn:fhir:binding:QuantityComparator: {}
urn:fhir:binding:RelatedArtifactType: {}
urn:fhir:binding:RouteOfAdministration: {}
urn:fhir:binding:SearchEntryMode: {}
urn:fhir:binding:SecurityLabels: {}
urn:fhir:binding:SignatureType: {}
urn:fhir:binding:SortDirection: {}
Expand Down
21 changes: 10 additions & 11 deletions examples/typescript-us-core/generate.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Run this script using Bun CLI with:
// bun run scripts/generate-fhir-types.ts

import { APIBuilder } from "../../src/api/builder";
import { APIBuilder, mkCodegenLogger, prettyReport } from "../../src/api";

if (require.main === module) {
console.log("📦 Generating US Core Types...");
const logger = mkCodegenLogger({
suppressTags: ["#fieldTypeNotFound", "#duplicateSchema", "#duplicateCanonical", "#largeValueSet"],
});

const builder = new APIBuilder()
const builder = new APIBuilder({ logger })
.throwException()
.fromPackage("hl7.fhir.us.core", "8.0.1")
.typeSchema({
Expand All @@ -16,6 +18,9 @@ if (require.main === module) {
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure": {},
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight": {},
},
"hl7.fhir.r4.core": {
"http://hl7.org/fhir/StructureDefinition/Bundle": {},
},
},
})
.typescript({
Expand All @@ -31,13 +36,7 @@ if (require.main === module) {
.cleanOutput(true);

const report = await builder.generate();
console.log(prettyReport(report));

// console.log(report);

if (report.success) {
console.log("✅ FHIR US Core types generated successfully!");
} else {
console.error("❌ FHIR US Core types generation failed.");
process.exit(1);
}
if (!report.success) process.exit(1);
}
68 changes: 67 additions & 1 deletion examples/typescript-us-core/profile-bp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
*/

import { describe, expect, test } from "bun:test";
import type { Bundle } from "./fhir-types/hl7-fhir-r4-core/Bundle";
import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation";
import { USCoreBloodPressureProfile } from "./fhir-types/hl7-fhir-us-core/profiles";
import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
import { USCoreBloodPressureProfile, USCorePatientProfile } from "./fhir-types/hl7-fhir-us-core/profiles";

describe("demo: create a US Core blood pressure observation", () => {
test("build a valid BP resource step by step", () => {
Expand Down Expand Up @@ -121,6 +123,70 @@ describe("demo: read a US Core blood pressure observation from JSON", () => {
});
});

describe("demo: filter Bundle<T> with profile is()", () => {
test("is() narrows by resourceType + meta.profile across a typed bundle and a raw collection", () => {
const patient: Patient = {
resourceType: "Patient",
meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"] },
identifier: [{ system: "http://hospital.example.org/mrn", value: "MRN-001" }],
name: [{ family: "Lovelace", given: ["Ada"] }],
};
const bp: Observation = {
resourceType: "Observation",
meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure"] },
status: "final",
category: [
{
coding: [
{ code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" },
],
},
],
code: { coding: [{ code: "85354-9", system: "http://loinc.org" }] },
subject: { reference: "Patient/pt-1" },
effectiveDateTime: "2024-06-15",
component: [
{
code: { coding: [{ code: "8480-6", system: "http://loinc.org" }] },
valueQuantity: { value: 120, unit: "mmHg" },
},
{
code: { coding: [{ code: "8462-4", system: "http://loinc.org" }] },
valueQuantity: { value: 80, unit: "mmHg" },
},
],
};
const otherObs: Observation = {
resourceType: "Observation",
status: "final",
code: { coding: [{ code: "29463-7", system: "http://loinc.org" }] },
};

// Bundle<T> propagation narrows entry[].resource to Patient | Observation at the type level
const bundle: Bundle<Patient | Observation> = {
resourceType: "Bundle",
type: "transaction",
entry: [{ resource: patient }, { resource: bp }, { resource: otherObs }],
};

// is() adds runtime narrowing on top of Bundle<T>: filters by resourceType + meta.profile
const bps = (bundle.entry ?? []).map((e) => e.resource).filter(USCoreBloodPressureProfile.is);
expect(bps).toEqual([bp]);

const patients = (bundle.entry ?? []).map((e) => e.resource).filter(USCorePatientProfile.is);
expect(patients).toEqual([patient]);

// Same guard also works on a plain unknown[] — no Bundle<T> required
const mixed: unknown[] = [patient, bp, otherObs, { resourceType: "Patient" }, null, "not a resource"];
expect(mixed.filter(USCoreBloodPressureProfile.is)).toEqual([bp]);

// Hand survivors to from() for typed reads
const wrapped = bps.map((o) => USCoreBloodPressureProfile.from(o));
expect(wrapped[0]!.getSystolic()!.value).toBe(120);
expect(wrapped[0]!.getDiastolic()!.value).toBe(80);
});
});

describe("category auto-population", () => {
test("custom category is preserved, VSCat is appended", () => {
const profile = USCoreBloodPressureProfile.create({
Expand Down
Loading