diff --git a/examples/typescript-r4/extension-profile.test.ts b/examples/typescript-r4/extension-profile.test.ts index 1dee2811..42c428b4 100644 --- a/examples/typescript-r4/extension-profile.test.ts +++ b/examples/typescript-r4/extension-profile.test.ts @@ -74,3 +74,12 @@ test("create() with no required params", () => { const profile = nationalityProfile.create(); expect(profile.toResource().url).toBe("http://hl7.org/fhir/StructureDefinition/patient-nationality"); }); + +test("is() matches extensions by url", () => { + const ext = birthPlaceProfile.createResource({ valueAddress: { city: "Bonn" } }); + expect(birthPlaceProfile.is(ext)).toBe(true); + expect(birthTimeProfile.is(ext)).toBe(false); + expect(birthPlaceProfile.is({ url: "http://example.com/other" })).toBe(false); + expect(birthPlaceProfile.is(null)).toBe(false); + expect(birthPlaceProfile.is("not an object")).toBe(false); +}); diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthPlace.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthPlace.ts index e2729645..11c3994e 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthPlace.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthPlace.ts @@ -38,6 +38,11 @@ export class birthPlaceProfile { return profile; } + static is (resource: unknown) : resource is Extension { + if (typeof resource !== "object" || resource === null) return false; + return (resource as { url?: string }).url === birthPlaceProfile.canonicalUrl; + } + static apply (resource: Extension) : birthPlaceProfile { resource.url = birthPlaceProfile.canonicalUrl; Object.assign(resource, { diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthTime.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthTime.ts index 088028b9..53e7f2b0 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthTime.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_birthTime.ts @@ -37,6 +37,11 @@ export class birthTimeProfile { return profile; } + static is (resource: unknown) : resource is Extension { + if (typeof resource !== "object" || resource === null) return false; + return (resource as { url?: string }).url === birthTimeProfile.canonicalUrl; + } + static apply (resource: Extension) : birthTimeProfile { resource.url = birthTimeProfile.canonicalUrl; Object.assign(resource, { diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_nationality.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_nationality.ts index 3990ad4e..bec4decc 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_nationality.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_nationality.ts @@ -49,6 +49,11 @@ export class nationalityProfile { return profile; } + static is (resource: unknown) : resource is Extension { + if (typeof resource !== "object" || resource === null) return false; + return (resource as { url?: string }).url === nationalityProfile.canonicalUrl; + } + static apply (resource: Extension) : nationalityProfile { resource.url = nationalityProfile.canonicalUrl; Object.assign(resource, { diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_own_prefix.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_own_prefix.ts index eeb18eb6..8e81b487 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_own_prefix.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Extension_own_prefix.ts @@ -37,6 +37,11 @@ export class own_prefixProfile { return profile; } + static is (resource: unknown) : resource is Extension { + if (typeof resource !== "object" || resource === null) return false; + return (resource as { url?: string }).url === own_prefixProfile.canonicalUrl; + } + static apply (resource: Extension) : own_prefixProfile { resource.url = own_prefixProfile.canonicalUrl; Object.assign(resource, { diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bodyweight.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bodyweight.ts index a6fa90ea..362bf460 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bodyweight.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bodyweight.ts @@ -61,6 +61,13 @@ export class observation_bodyweightProfile { return profile; } + static is (resource: unknown) : resource is Observation { + if (typeof resource !== "object" || resource === null) return false; + const r = resource as { resourceType?: string; meta?: { profile?: string[] } }; + if (r.resourceType !== "Observation") return false; + return (r.meta?.profile ?? []).includes(observation_bodyweightProfile.canonicalUrl); + } + static apply (resource: Observation) : observation_bodyweightProfile { ensureProfile(resource, observation_bodyweightProfile.canonicalUrl); Object.assign(resource, { diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bp.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bp.ts index d303dd4a..6e0655f4 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bp.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_bp.ts @@ -76,6 +76,13 @@ export class observation_bpProfile { return profile; } + static is (resource: unknown) : resource is Observation { + if (typeof resource !== "object" || resource === null) return false; + const r = resource as { resourceType?: string; meta?: { profile?: string[] } }; + if (r.resourceType !== "Observation") return false; + return (r.meta?.profile ?? []).includes(observation_bpProfile.canonicalUrl); + } + static apply (resource: Observation) : observation_bpProfile { ensureProfile(resource, observation_bpProfile.canonicalUrl); Object.assign(resource, { diff --git a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts index 146c333d..2ddde60d 100644 --- a/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts +++ b/examples/typescript-r4/fhir-types/hl7-fhir-r4-core/profiles/Observation_observation_vitalsigns.ts @@ -61,6 +61,13 @@ export class observation_vitalsignsProfile { return profile; } + static is (resource: unknown) : resource is Observation { + if (typeof resource !== "object" || resource === null) return false; + const r = resource as { resourceType?: string; meta?: { profile?: string[] } }; + if (r.resourceType !== "Observation") return false; + return (r.meta?.profile ?? []).includes(observation_vitalsignsProfile.canonicalUrl); + } + static apply (resource: Observation) : observation_vitalsignsProfile { ensureProfile(resource, observation_vitalsignsProfile.canonicalUrl); resource.category = ensureSliceDefaults( diff --git a/examples/typescript-r4/profile-bodyweight.test.ts b/examples/typescript-r4/profile-bodyweight.test.ts index dac9b017..18c65da6 100644 --- a/examples/typescript-r4/profile-bodyweight.test.ts +++ b/examples/typescript-r4/profile-bodyweight.test.ts @@ -108,6 +108,73 @@ describe("demo: read a bodyweight observation from JSON", () => { }); }); +describe("demo: filter a mixed collection with is()", () => { + test("is() narrows unknown values to profile matches", () => { + // A mixed bag: a matching bodyweight, a non-matching Observation, a non-Observation, and junk + const bodyweight: Observation = { + resourceType: "Observation", + meta: { profile: ["http://hl7.org/fhir/StructureDefinition/bodyweight"] }, + status: "final", + code: { coding: [{ code: "29463-7", system: "http://loinc.org" }] }, + subject: { reference: "Patient/pt-1" }, + effectiveDateTime: "2024-06-15", + valueQuantity: { value: 75, unit: "kg" }, + }; + const otherObs: Observation = { + resourceType: "Observation", + status: "final", + code: { coding: [{ code: "8867-4", system: "http://loinc.org" }] }, + }; + const resources: unknown[] = [bodyweight, otherObs, { resourceType: "Patient" }, null, "not a resource"]; + + // is() is a cheap type guard — no validation, no instance constructed + const matches = resources.filter(bodyweightProfile.is); + + expect(matches).toEqual([bodyweight]); + }); +}); + +describe("is() type guard", () => { + test("returns true for Observation with matching meta.profile", () => { + const obs: Observation = { + resourceType: "Observation", + meta: { profile: ["http://hl7.org/fhir/StructureDefinition/bodyweight"] }, + status: "final", + code: { coding: [{ code: "29463-7", system: "http://loinc.org" }] }, + }; + expect(bodyweightProfile.is(obs)).toBe(true); + }); + + test("returns false when meta.profile does not include canonical url", () => { + const obs: Observation = { + resourceType: "Observation", + status: "final", + code: { coding: [{ code: "29463-7", system: "http://loinc.org" }] }, + }; + expect(bodyweightProfile.is(obs)).toBe(false); + }); + + test("returns false for wrong resourceType", () => { + expect(bodyweightProfile.is({ resourceType: "Patient" })).toBe(false); + }); + + test("returns false for non-object inputs", () => { + expect(bodyweightProfile.is(null)).toBe(false); + expect(bodyweightProfile.is(undefined)).toBe(false); + expect(bodyweightProfile.is("string")).toBe(false); + expect(bodyweightProfile.is(42)).toBe(false); + }); + + test("does not validate — only checks identity", () => { + // A resource that claims the profile but is otherwise invalid still passes is() + const bogus = { + resourceType: "Observation", + meta: { profile: ["http://hl7.org/fhir/StructureDefinition/bodyweight"] }, + }; + expect(bodyweightProfile.is(bogus)).toBe(true); + }); +}); + describe("factory method equivalence", () => { test("create(), createResource(), and apply() produce equal resources", () => { const args = { status: "final" as const, subject: { reference: "Patient/pt-1" as const } }; diff --git a/src/api/writer-generator/typescript/profile.ts b/src/api/writer-generator/typescript/profile.ts index 613bef65..d7057daf 100644 --- a/src/api/writer-generator/typescript/profile.ts +++ b/src/api/writer-generator/typescript/profile.ts @@ -391,6 +391,20 @@ const generateFactoryMethods = ( w.lineSM("return profile"); }); w.line(); + const canEmitIs = (hasMeta && isResourceIdentifier(flatProfile.base)) || flatProfile.base.name === "Extension"; + if (canEmitIs) { + w.curlyBlock(["static", "is", "(resource: unknown)", `: resource is ${tsBaseResourceName}`], () => { + w.line(`if (typeof resource !== "object" || resource === null) return false;`); + if (hasMeta && isResourceIdentifier(flatProfile.base)) { + w.line(`const r = resource as { resourceType?: string; meta?: { profile?: string[] } };`); + w.line(`if (r.resourceType !== ${JSON.stringify(flatProfile.base.name)}) return false;`); + w.lineSM(`return (r.meta?.profile ?? []).includes(${profileClassName}.canonicalUrl)`); + } else { + w.lineSM(`return (resource as { url?: string }).url === ${profileClassName}.canonicalUrl`); + } + }); + w.line(); + } w.curlyBlock(["static", "apply", `(resource: ${tsBaseResourceName})`, `: ${profileClassName}`], () => { if (hasMeta) { w.lineSM(`ensureProfile(resource, ${profileClassName}.canonicalUrl)`); diff --git a/test/api/write-generator/__snapshots__/typescript.test.ts.snap b/test/api/write-generator/__snapshots__/typescript.test.ts.snap index e6153df9..3098279f 100644 --- a/test/api/write-generator/__snapshots__/typescript.test.ts.snap +++ b/test/api/write-generator/__snapshots__/typescript.test.ts.snap @@ -375,6 +375,13 @@ export class observation_bodyweightProfile { return profile; } + static is (resource: unknown) : resource is Observation { + if (typeof resource !== "object" || resource === null) return false; + const r = resource as { resourceType?: string; meta?: { profile?: string[] } }; + if (r.resourceType !== "Observation") return false; + return (r.meta?.profile ?? []).includes(observation_bodyweightProfile.canonicalUrl); + } + static apply (resource: Observation) : observation_bodyweightProfile { ensureProfile(resource, observation_bodyweightProfile.canonicalUrl); Object.assign(resource, { @@ -607,6 +614,13 @@ export class observation_bpProfile { return profile; } + static is (resource: unknown) : resource is Observation { + if (typeof resource !== "object" || resource === null) return false; + const r = resource as { resourceType?: string; meta?: { profile?: string[] } }; + if (r.resourceType !== "Observation") return false; + return (r.meta?.profile ?? []).includes(observation_bpProfile.canonicalUrl); + } + static apply (resource: Observation) : observation_bpProfile { ensureProfile(resource, observation_bpProfile.canonicalUrl); Object.assign(resource, { @@ -897,6 +911,13 @@ export class USCorePatientProfile { return profile; } + static is (resource: unknown) : resource is Patient { + if (typeof resource !== "object" || resource === null) return false; + const r = resource as { resourceType?: string; meta?: { profile?: string[] } }; + if (r.resourceType !== "Patient") return false; + return (r.meta?.profile ?? []).includes(USCorePatientProfile.canonicalUrl); + } + static apply (resource: Patient) : USCorePatientProfile { ensureProfile(resource, USCorePatientProfile.canonicalUrl); return new USCorePatientProfile(resource); @@ -1168,6 +1189,13 @@ export class USCoreBloodPressureProfile { return profile; } + static is (resource: unknown) : resource is Observation { + if (typeof resource !== "object" || resource === null) return false; + const r = resource as { resourceType?: string; meta?: { profile?: string[] } }; + if (r.resourceType !== "Observation") return false; + return (r.meta?.profile ?? []).includes(USCoreBloodPressureProfile.canonicalUrl); + } + static apply (resource: Observation) : USCoreBloodPressureProfile { ensureProfile(resource, USCoreBloodPressureProfile.canonicalUrl); Object.assign(resource, { @@ -1550,6 +1578,13 @@ export class USCoreBodyWeightProfile { return profile; } + static is (resource: unknown) : resource is Observation { + if (typeof resource !== "object" || resource === null) return false; + const r = resource as { resourceType?: string; meta?: { profile?: string[] } }; + if (r.resourceType !== "Observation") return false; + return (r.meta?.profile ?? []).includes(USCoreBodyWeightProfile.canonicalUrl); + } + static apply (resource: Observation) : USCoreBodyWeightProfile { ensureProfile(resource, USCoreBodyWeightProfile.canonicalUrl); Object.assign(resource, { @@ -1881,6 +1916,11 @@ export class USCoreRaceExtensionProfile { return profile; } + static is (resource: unknown) : resource is Extension { + if (typeof resource !== "object" || resource === null) return false; + return (resource as { url?: string }).url === USCoreRaceExtensionProfile.canonicalUrl; + } + static apply (resource: Extension) : USCoreRaceExtensionProfile { resource.url = USCoreRaceExtensionProfile.canonicalUrl; Object.assign(resource, { diff --git a/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap b/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap index 3dc9737b..ed2d9111 100644 --- a/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap +++ b/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap @@ -92,6 +92,13 @@ export class ExampleTypedBundleProfile { return profile; } + static is (resource: unknown) : resource is Bundle { + if (typeof resource !== "object" || resource === null) return false; + const r = resource as { resourceType?: string; meta?: { profile?: string[] } }; + if (r.resourceType !== "Bundle") return false; + return (r.meta?.profile ?? []).includes(ExampleTypedBundleProfile.canonicalUrl); + } + static apply (resource: Bundle) : ExampleTypedBundleProfile { ensureProfile(resource, ExampleTypedBundleProfile.canonicalUrl); return new ExampleTypedBundleProfile(resource);