|
11 | 11 |
|
12 | 12 | import { describe, expect, test } from "bun:test"; |
13 | 13 | import { ExampleTypedBundleProfile } from "./fhir-types/example-folder-structures/profiles/Bundle_ExampleTypedBundle"; |
14 | | -import type { BundleEntry } from "./fhir-types/hl7-fhir-r4-core/Bundle"; |
15 | | -import type { DomainResource } from "./fhir-types/hl7-fhir-r4-core/DomainResource"; |
16 | 14 | import type { Organization } from "./fhir-types/hl7-fhir-r4-core/Organization"; |
17 | 15 | import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient"; |
18 | 16 |
|
19 | | -const createBundle = () => ExampleTypedBundleProfile.create({ type: "collection" }); |
20 | | - |
21 | 17 | const smithPatient: Patient = { resourceType: "Patient", name: [{ family: "Smith" }] }; |
22 | | -const jonesPatient: Patient = { resourceType: "Patient", name: [{ family: "Jones" }] }; |
23 | 18 | const activePatient: Patient = { resourceType: "Patient", active: true }; |
24 | | -const acmeOrg: Organization = { resourceType: "Organization", name: "Acme Corp" }; |
25 | 19 | const clinicOrg: Organization = { resourceType: "Organization", name: "Clinic" }; |
26 | 20 |
|
27 | 21 | describe("type-discriminated bundle slices", () => { |
28 | | - test("create() auto-populates a PatientEntry stub (min: 1)", () => { |
29 | | - const bundle = createBundle(); |
30 | | - const entry = bundle.toResource().entry; |
31 | | - expect(entry).toHaveLength(1); |
32 | | - expect(entry![0]!.resource).toEqual({ resourceType: "Patient" }); |
| 22 | + test("create() starts with no entry — PatientEntry must be set by user", () => { |
| 23 | + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }); |
| 24 | + expect(bundle.toResource().entry).toBeUndefined(); |
| 25 | + expect(bundle.validate().errors).toEqual([ |
| 26 | + "ExampleTypedBundle.entry: slice 'PatientEntry' requires at least 1 item(s), found 0", |
| 27 | + ]); |
33 | 28 | }); |
34 | 29 |
|
35 | 30 | test("setPatientEntry inserts a typed patient entry", () => { |
36 | | - const bundle = createBundle(); |
| 31 | + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }); |
37 | 32 | bundle.setPatientEntry({ resource: smithPatient }); |
38 | 33 |
|
39 | 34 | const entry = bundle.getPatientEntry()!; |
40 | 35 | expect(entry.resource).toEqual(smithPatient); |
| 36 | + expect(bundle.validate().errors).toEqual([]); |
41 | 37 | }); |
42 | 38 |
|
43 | 39 | test("setPatientEntry replaces existing patient entry (no duplicates)", () => { |
44 | | - const bundle = createBundle(); |
| 40 | + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }); |
45 | 41 | bundle.setPatientEntry({ resource: smithPatient }); |
46 | | - bundle.setPatientEntry({ resource: jonesPatient }); |
47 | | - |
48 | | - const patients = bundle.toResource().entry!.filter((e) => e.resource?.resourceType === "Patient"); |
49 | | - expect(patients).toHaveLength(1); |
50 | | - expect(bundle.getPatientEntry()!.resource).toEqual(jonesPatient); |
51 | | - }); |
52 | | - |
53 | | - test("setOrganizationEntry adds an organization entry", () => { |
54 | | - const bundle = createBundle(); |
55 | | - bundle.setOrganizationEntry({ resource: acmeOrg }); |
| 42 | + bundle.setPatientEntry({ resource: activePatient }); |
56 | 43 |
|
57 | | - expect(bundle.getOrganizationEntry()!.resource).toEqual(acmeOrg); |
| 44 | + const entries = bundle.toResource().entry!; |
| 45 | + expect(entries).toHaveLength(1); |
| 46 | + expect(entries[0]!.resource).toEqual(activePatient); |
58 | 47 | }); |
59 | 48 |
|
60 | 49 | test("getPatientEntry('flat') returns the entry as-is (no keys stripped)", () => { |
61 | | - const bundle = createBundle(); |
| 50 | + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }); |
62 | 51 | bundle.setPatientEntry({ fullUrl: "urn:uuid:patient-1", resource: activePatient }); |
63 | 52 |
|
64 | 53 | const flat = bundle.getPatientEntry("flat")!; |
65 | 54 | expect(flat.fullUrl).toBe("urn:uuid:patient-1"); |
66 | 55 | expect(flat.resource).toEqual(activePatient); |
67 | 56 | }); |
68 | 57 |
|
69 | | - test("validate() checks PatientEntry cardinality", () => { |
70 | | - const bundle = ExampleTypedBundleProfile.apply({ |
71 | | - resourceType: "Bundle", |
72 | | - type: "collection", |
73 | | - }); |
74 | | - const { errors } = bundle.validate(); |
75 | | - expect(errors).toEqual(["ExampleTypedBundle.entry: slice 'PatientEntry' requires at least 1 item(s), found 0"]); |
76 | | - }); |
77 | | - |
78 | 58 | test("fluent chaining across slice setters", () => { |
79 | | - const bundle = createBundle() |
| 59 | + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }) |
80 | 60 | .setPatientEntry({ resource: activePatient }) |
81 | 61 | .setOrganizationEntry({ resource: clinicOrg }); |
82 | 62 |
|
| 63 | + expect(bundle.toResource().entry).toHaveLength(2); |
83 | 64 | expect(bundle.getPatientEntry()!.resource).toEqual(activePatient); |
84 | 65 | expect(bundle.getOrganizationEntry()!.resource).toEqual(clinicOrg); |
85 | | - expect(bundle.toResource().entry).toHaveLength(2); |
86 | 66 | }); |
87 | 67 |
|
88 | | - test("set/get PatientEntry with full BundleEntry<Patient> input", () => { |
89 | | - const bundle = createBundle(); |
90 | | - const input: BundleEntry<Patient> = { fullUrl: "urn:uuid:p1", resource: smithPatient }; |
91 | | - bundle.setPatientEntry(input); |
92 | | - |
93 | | - const raw = bundle.getPatientEntry("raw")!; |
94 | | - expect(raw.fullUrl).toBe("urn:uuid:p1"); |
95 | | - expect(raw.resource).toEqual(smithPatient); |
96 | | - |
97 | | - const flat = bundle.getPatientEntry("flat")!; |
98 | | - expect(flat.fullUrl).toBe("urn:uuid:p1"); |
99 | | - expect(flat.resource).toEqual(smithPatient); |
100 | | - }); |
101 | | - |
102 | | - test("set/get OrganizationEntry with full BundleEntry<Organization> input", () => { |
103 | | - const bundle = createBundle(); |
104 | | - const input: BundleEntry<Organization> = { fullUrl: "urn:uuid:o1", resource: acmeOrg }; |
105 | | - bundle.setOrganizationEntry(input); |
106 | | - |
107 | | - const raw = bundle.getOrganizationEntry("raw")!; |
108 | | - expect(raw.fullUrl).toBe("urn:uuid:o1"); |
109 | | - expect(raw.resource).toEqual(acmeOrg); |
110 | | - |
111 | | - const flat = bundle.getOrganizationEntry("flat")!; |
112 | | - expect(flat.fullUrl).toBe("urn:uuid:o1"); |
113 | | - expect(flat.resource).toEqual(acmeOrg); |
114 | | - }); |
115 | | -}); |
116 | | - |
117 | | -describe("generic type-family fields — compile-time narrowing", () => { |
118 | | - test("BundleEntry<Patient>.resource is Patient (access Patient-specific fields without cast)", () => { |
119 | | - const bundle = createBundle(); |
120 | | - bundle.setPatientEntry({ resource: smithPatient }); |
121 | | - |
122 | | - const entry = bundle.getPatientEntry()!; |
123 | | - // entry.resource is Patient — .name is available directly, no cast needed |
124 | | - const family: string | undefined = entry.resource?.name?.[0]?.family; |
125 | | - expect(family).toBe("Smith"); |
126 | | - }); |
127 | | - |
128 | | - test("BundleEntry<Organization>.resource is Organization (access Organization-specific fields without cast)", () => { |
129 | | - const bundle = createBundle(); |
130 | | - bundle.setOrganizationEntry({ resource: acmeOrg }); |
131 | | - |
132 | | - const entry = bundle.getOrganizationEntry()!; |
133 | | - // entry.resource is Organization — .name is string, not HumanName[] |
134 | | - const name: string | undefined = entry.resource?.name; |
135 | | - expect(name).toBe("Acme Corp"); |
136 | | - }); |
137 | | - |
138 | | - test("BundleEntry<T> defaults to BundleEntry<Resource> — unparameterized usage unchanged", () => { |
139 | | - const entry: BundleEntry = { resource: smithPatient }; |
140 | | - expect(entry.resource?.resourceType).toBe("Patient"); |
141 | | - }); |
142 | | - |
143 | | - test("DomainResource<T> narrows contained to T[]", () => { |
144 | | - const container: DomainResource<Patient> = { |
145 | | - resourceType: "Patient", |
146 | | - contained: [smithPatient, jonesPatient], |
147 | | - }; |
148 | | - // contained is Patient[] — .name available directly |
149 | | - const family: string | undefined = container.contained?.[0]?.name?.[0]?.family; |
150 | | - expect(family).toBe("Smith"); |
151 | | - }); |
152 | | - |
153 | | - test("BundleEntry<Patient> rejects Organization at compile time", () => { |
154 | | - const patientEntry: BundleEntry<Patient> = { resource: smithPatient }; |
155 | | - expect(patientEntry.resource?.resourceType).toBe("Patient"); |
| 68 | + test("setOrganizationEntry replaces existing org entry (same discriminator)", () => { |
| 69 | + const bundle = ExampleTypedBundleProfile.create({ type: "collection" }) |
| 70 | + .setPatientEntry({ resource: activePatient }) |
| 71 | + .setOrganizationEntry({ resource: clinicOrg }) |
| 72 | + .setOrganizationEntry({ resource: { resourceType: "Organization", name: "Acme" } }); |
156 | 73 |
|
157 | | - // Uncomment to verify compile error: |
158 | | - // @ts-expect-error — Organization is not assignable to Patient |
159 | | - const _bad: BundleEntry<Patient> = { resource: acmeOrg }; |
160 | | - void _bad; |
| 74 | + const entries = bundle.toResource().entry!; |
| 75 | + expect(entries).toHaveLength(2); |
| 76 | + expect(bundle.getOrganizationEntry()!.resource!.name).toBe("Acme"); |
161 | 77 | }); |
162 | 78 | }); |
0 commit comments