diff --git a/examples/typescript-us-core/README.md b/examples/typescript-us-core/README.md index db8f257a..98e45f1f 100644 --- a/examples/typescript-us-core/README.md +++ b/examples/typescript-us-core/README.md @@ -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: diff --git a/examples/typescript-us-core/fhir-types/README.md b/examples/typescript-us-core/fhir-types/README.md index cc230de3..3eeeacca 100644 --- a/examples/typescript-us-core/fhir-types/README.md +++ b/examples/typescript-us-core/fhir-types/README.md @@ -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` @@ -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` @@ -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` diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Bundle.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Bundle.ts new file mode 100644 index 00000000..4683d070 --- /dev/null +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Bundle.ts @@ -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 extends BackboneElement { + fullUrl?: string; + link?: BundleLink[]; + request?: BundleEntryRequest; + resource?: T1; + response?: BundleEntryResponse; + 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 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 extends Resource { + resourceType: "Bundle"; + + entry?: BundleEntry[]; + 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"; +} diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Resource.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Resource.ts index 56b489b7..b79403fc 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Resource.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Resource.ts @@ -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; diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/index.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/index.ts index 82c1509a..a837c5f7 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/index.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/index.ts @@ -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"; diff --git a/examples/typescript-us-core/fhir-types/type-tree.yaml b/examples/typescript-us-core/fhir-types/type-tree.yaml index 7142c3d9..d4aa5017 100644 --- a/examples/typescript-us-core/fhir-types/type-tree.yaml +++ b/examples/typescript-us-core/fhir-types/type-tree.yaml @@ -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: {} @@ -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: {} @@ -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: {} @@ -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: {} diff --git a/examples/typescript-us-core/generate.ts b/examples/typescript-us-core/generate.ts index 30622342..ab09f1b3 100644 --- a/examples/typescript-us-core/generate.ts +++ b/examples/typescript-us-core/generate.ts @@ -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({ @@ -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({ @@ -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); } diff --git a/examples/typescript-us-core/profile-bp.test.ts b/examples/typescript-us-core/profile-bp.test.ts index 9824d555..30c68abf 100644 --- a/examples/typescript-us-core/profile-bp.test.ts +++ b/examples/typescript-us-core/profile-bp.test.ts @@ -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", () => { @@ -121,6 +123,70 @@ describe("demo: read a US Core blood pressure observation from JSON", () => { }); }); +describe("demo: filter Bundle 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 propagation narrows entry[].resource to Patient | Observation at the type level + const bundle: Bundle = { + resourceType: "Bundle", + type: "transaction", + entry: [{ resource: patient }, { resource: bp }, { resource: otherObs }], + }; + + // is() adds runtime narrowing on top of Bundle: 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 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({