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
20 changes: 20 additions & 0 deletions assets/api/writer-generator/typescript/profile-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,26 @@ export const getArraySlice = <T>(list: readonly T[] | undefined, match: Record<s
return list.find((item) => matchesValue(item, match));
};

/** Return all elements in `list` that satisfy the slice discriminator `match`. */
export const getArraySliceAll = <T>(list: readonly T[] | undefined, match: Record<string, unknown>): T[] => {
if (!list) return [];
return list.filter((item) => matchesValue(item, match));
};

/**
* Replace all elements matching `match` in `list` with `newItems`.
* Each new item has the discriminator values applied via {@link applySliceMatch}
* before this call, so this helper only handles the array surgery.
*/
export const setArraySliceAll = <T>(list: T[], match: Record<string, unknown>, newItems: T[]): void => {
// Remove all existing items that match the discriminator
for (let i = list.length - 1; i >= 0; i--) {
if (matchesValue(list[i], match)) list.splice(i, 1);
}
// Append new items
list.push(...newItems);
};

// ---------------------------------------------------------------------------
// Validation helpers
//
Expand Down
104 changes: 77 additions & 27 deletions examples/local-package-folder/profile-typed-bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,62 +17,112 @@ import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
const smithPatient: Patient = { resourceType: "Patient", name: [{ family: "Smith" }] };
const activePatient: Patient = { resourceType: "Patient", active: true };
const clinicOrg: Organization = { resourceType: "Organization", name: "Clinic" };
const acmeOrg: Organization = { resourceType: "Organization", name: "Acme" };

describe("type-discriminated bundle slices", () => {
test("create() starts with no entry — PatientEntry must be set by user", () => {
describe("demo: single-element slice (max: 1) — PatientEntry", () => {
test("create, set, and validate a typed patient entry", () => {
const bundle = ExampleTypedBundleProfile.create({ type: "collection" });
expect(bundle.toResource().entry).toBeUndefined();

// No entry yet — validation fails (PatientEntry min: 1)
expect(bundle.validate().errors).toEqual([
"ExampleTypedBundle.entry: slice 'PatientEntry' requires at least 1 item(s), found 0",
]);
});

test("setPatientEntry inserts a typed patient entry", () => {
const bundle = ExampleTypedBundleProfile.create({ type: "collection" });
// Set a single patient entry — typed as BundleEntry<Patient>
bundle.setPatientEntry({ resource: smithPatient });
expect(bundle.validate().errors).toEqual([]);

// Getter returns the entry with resource narrowed to Patient
const entry = bundle.getPatientEntry()!;
expect(entry.resource).toEqual(smithPatient);
expect(bundle.validate().errors).toEqual([]);
});

test("setPatientEntry replaces existing patient entry (no duplicates)", () => {
test("setPatientEntry replaces existing entry (no duplicates)", () => {
const bundle = ExampleTypedBundleProfile.create({ type: "collection" });
bundle.setPatientEntry({ resource: smithPatient });
bundle.setPatientEntry({ resource: activePatient });

const entries = bundle.toResource().entry!;
expect(entries).toHaveLength(1);
expect(entries[0]!.resource).toEqual(activePatient);
// Only one patient entry — the second call replaced the first
expect(bundle.toResource().entry).toHaveLength(1);
expect(bundle.getPatientEntry()!.resource).toEqual(activePatient);
});
});

test("getPatientEntry('flat') returns the entry as-is (no keys stripped)", () => {
const bundle = ExampleTypedBundleProfile.create({ type: "collection" });
bundle.setPatientEntry({ fullUrl: "urn:uuid:patient-1", resource: activePatient });
describe("demo: unbounded slice (max: *) — OrganizationEntry", () => {
test("setter accepts an array, getter returns an array", () => {
const bundle = ExampleTypedBundleProfile.create({ type: "collection" }).setPatientEntry({
resource: activePatient,
});

// Set multiple organization entries at once
bundle.setOrganizationEntry([{ resource: clinicOrg }, { resource: acmeOrg }]);

const flat = bundle.getPatientEntry("flat")!;
expect(flat.fullUrl).toBe("urn:uuid:patient-1");
expect(flat.resource).toEqual(activePatient);
// Getter returns all matching entries as an array (undefined if none)
const orgs = bundle.getOrganizationEntry()!;
expect(orgs).toHaveLength(2);
expect(orgs[0]!.resource).toEqual(clinicOrg);
expect(orgs[1]!.resource).toEqual(acmeOrg);

// Total entries: 1 patient + 2 organizations
expect(bundle.toResource().entry).toHaveLength(3);
});

test("fluent chaining across slice setters", () => {
test("setOrganizationEntry replaces all previous org entries", () => {
const bundle = ExampleTypedBundleProfile.create({ type: "collection" })
.setPatientEntry({ resource: activePatient })
.setOrganizationEntry({ resource: clinicOrg });
.setOrganizationEntry([{ resource: clinicOrg }, { resource: acmeOrg }]);

// Replace with a single org — previous two are removed
bundle.setOrganizationEntry([{ resource: { resourceType: "Organization", name: "NewCo" } }]);

const orgs = bundle.getOrganizationEntry()!;
expect(orgs).toHaveLength(1);
expect(orgs[0]!.resource!.name).toBe("NewCo");

expect(bundle.toResource().entry).toHaveLength(2);
// Patient entry unaffected
expect(bundle.getPatientEntry()!.resource).toEqual(activePatient);
expect(bundle.getOrganizationEntry()!.resource).toEqual(clinicOrg);
});

test("setOrganizationEntry replaces existing org entry (same discriminator)", () => {
test("append to existing entries via spread", () => {
const bundle = ExampleTypedBundleProfile.create({ type: "collection" })
.setPatientEntry({ resource: activePatient })
.setOrganizationEntry({ resource: clinicOrg })
.setOrganizationEntry({ resource: { resourceType: "Organization", name: "Acme" } });
.setOrganizationEntry([{ resource: clinicOrg }]);

// Append a new org by spreading existing entries
bundle.setOrganizationEntry([...(bundle.getOrganizationEntry() ?? []), { resource: acmeOrg }]);

const orgs = bundle.getOrganizationEntry()!;
expect(orgs).toHaveLength(2);
expect(orgs[0]!.resource).toEqual(clinicOrg);
expect(orgs[1]!.resource).toEqual(acmeOrg);
});

test("empty array removes all org entries, getter returns undefined", () => {
const bundle = ExampleTypedBundleProfile.create({ type: "collection" })
.setPatientEntry({ resource: activePatient })
.setOrganizationEntry([{ resource: clinicOrg }]);

bundle.setOrganizationEntry([]);

// No matching entries — returns undefined, not empty array
expect(bundle.getOrganizationEntry()).toBeUndefined();
// Patient entry still present
expect(bundle.toResource().entry).toHaveLength(1);
});
});

describe("fluent chaining across slice types", () => {
test("chain single and array setters", () => {
const bundle = ExampleTypedBundleProfile.create({ type: "collection" })
.setPatientEntry({ fullUrl: "urn:uuid:patient-1", resource: activePatient })
.setOrganizationEntry([
{ fullUrl: "urn:uuid:org-1", resource: clinicOrg },
{ fullUrl: "urn:uuid:org-2", resource: acmeOrg },
]);

const entries = bundle.toResource().entry!;
expect(entries).toHaveLength(2);
expect(bundle.getOrganizationEntry()!.resource!.name).toBe("Acme");
expect(bundle.toResource().entry).toHaveLength(3);
expect(bundle.getPatientEntry()!.fullUrl).toBe("urn:uuid:patient-1");
expect(bundle.getOrganizationEntry()![0]!.fullUrl).toBe("urn:uuid:org-1");
expect(bundle.getOrganizationEntry()![1]!.fullUrl).toBe("urn:uuid:org-2");
});
});
20 changes: 20 additions & 0 deletions examples/typescript-r4/fhir-types/profile-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,26 @@ export const getArraySlice = <T>(list: readonly T[] | undefined, match: Record<s
return list.find((item) => matchesValue(item, match));
};

/** Return all elements in `list` that satisfy the slice discriminator `match`. */
export const getArraySliceAll = <T>(list: readonly T[] | undefined, match: Record<string, unknown>): T[] => {
if (!list) return [];
return list.filter((item) => matchesValue(item, match));
};

/**
* Replace all elements matching `match` in `list` with `newItems`.
* Each new item has the discriminator values applied via {@link applySliceMatch}
* before this call, so this helper only handles the array surgery.
*/
export const setArraySliceAll = <T>(list: T[], match: Record<string, unknown>, newItems: T[]): void => {
// Remove all existing items that match the discriminator
for (let i = list.length - 1; i >= 0; i--) {
if (matchesValue(list[i], match)) list.splice(i, 1);
}
// Append new items
list.push(...newItems);
};

// ---------------------------------------------------------------------------
// Validation helpers
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
setArraySlice,
getArraySlice,
ensureSliceDefaults,
setArraySliceAll,
getArraySliceAll,
wrapSliceChoice,
unwrapSliceChoice,
isExtension,
Expand Down Expand Up @@ -186,15 +188,11 @@ export class USCoreEthnicityExtensionProfile {
return this
}

public setExtensionDetailed (input?: USCoreEthnicityExtension_Extension_DetailedSliceFlat | Extension): this {
public setExtensionDetailed (input: (USCoreEthnicityExtension_Extension_DetailedSliceFlat | Extension)[]): this {
const match = USCoreEthnicityExtensionProfile.detailedSliceMatch
if (input && matchesValue(input, match)) {
setArraySlice(this.resource.extension ??= [], match, input as Extension)
return this
}
const wrapped = wrapSliceChoice<Extension>(input ?? {}, "valueCoding")
const value = applySliceMatch<Extension>(wrapped, match)
setArraySlice(this.resource.extension ??= [], match, value)
const arr = this.resource.extension ??= []
const values = input.map(item => matchesValue(item, match) ? item as Extension : applySliceMatch<Extension>(wrapSliceChoice<Extension>(item, "valueCoding"), match))
setArraySliceAll(arr, match, values)
return this
}

Expand All @@ -220,15 +218,15 @@ export class USCoreEthnicityExtensionProfile {
return unwrapSliceChoice<USCoreEthnicityExtension_Extension_OmbCategorySliceFlatAll>(item, ["url"], "valueCoding")
}

public getExtensionDetailed(mode: 'flat'): USCoreEthnicityExtension_Extension_DetailedSliceFlatAll | undefined;
public getExtensionDetailed(mode: 'raw'): Extension | undefined;
public getExtensionDetailed(): USCoreEthnicityExtension_Extension_DetailedSliceFlatAll | undefined;
public getExtensionDetailed (mode: 'flat' | 'raw' = 'flat'): USCoreEthnicityExtension_Extension_DetailedSliceFlatAll | Extension | undefined {
public getExtensionDetailed(mode: 'flat'): USCoreEthnicityExtension_Extension_DetailedSliceFlatAll[] | undefined;
public getExtensionDetailed(mode: 'raw'): Extension[] | undefined;
public getExtensionDetailed(): USCoreEthnicityExtension_Extension_DetailedSliceFlatAll[] | undefined;
public getExtensionDetailed (mode: 'flat' | 'raw' = 'flat'): (USCoreEthnicityExtension_Extension_DetailedSliceFlatAll | Extension)[] | undefined {
const match = USCoreEthnicityExtensionProfile.detailedSliceMatch
const item = getArraySlice(this.resource.extension, match)
if (!item) return undefined
if (mode === 'raw') return item
return unwrapSliceChoice<USCoreEthnicityExtension_Extension_DetailedSliceFlatAll>(item, ["url"], "valueCoding")
const items = getArraySliceAll(this.resource.extension, match)
if (items.length === 0) return undefined
if (mode === 'raw') return items
return items.map(item => unwrapSliceChoice<USCoreEthnicityExtension_Extension_DetailedSliceFlatAll>(item, ["url"], "valueCoding"))
}

public getExtensionText(mode: 'flat'): USCoreEthnicityExtension_Extension_TextSliceFlatAll | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
setArraySlice,
getArraySlice,
ensureSliceDefaults,
setArraySliceAll,
getArraySliceAll,
wrapSliceChoice,
unwrapSliceChoice,
isExtension,
Expand Down Expand Up @@ -186,15 +188,11 @@ export class USCoreRaceExtensionProfile {
return this
}

public setExtensionDetailed (input?: USCoreRaceExtension_Extension_DetailedSliceFlat | Extension): this {
public setExtensionDetailed (input: (USCoreRaceExtension_Extension_DetailedSliceFlat | Extension)[]): this {
const match = USCoreRaceExtensionProfile.detailedSliceMatch
if (input && matchesValue(input, match)) {
setArraySlice(this.resource.extension ??= [], match, input as Extension)
return this
}
const wrapped = wrapSliceChoice<Extension>(input ?? {}, "valueCoding")
const value = applySliceMatch<Extension>(wrapped, match)
setArraySlice(this.resource.extension ??= [], match, value)
const arr = this.resource.extension ??= []
const values = input.map(item => matchesValue(item, match) ? item as Extension : applySliceMatch<Extension>(wrapSliceChoice<Extension>(item, "valueCoding"), match))
setArraySliceAll(arr, match, values)
return this
}

Expand All @@ -220,15 +218,15 @@ export class USCoreRaceExtensionProfile {
return unwrapSliceChoice<USCoreRaceExtension_Extension_OmbCategorySliceFlatAll>(item, ["url"], "valueCoding")
}

public getExtensionDetailed(mode: 'flat'): USCoreRaceExtension_Extension_DetailedSliceFlatAll | undefined;
public getExtensionDetailed(mode: 'raw'): Extension | undefined;
public getExtensionDetailed(): USCoreRaceExtension_Extension_DetailedSliceFlatAll | undefined;
public getExtensionDetailed (mode: 'flat' | 'raw' = 'flat'): USCoreRaceExtension_Extension_DetailedSliceFlatAll | Extension | undefined {
public getExtensionDetailed(mode: 'flat'): USCoreRaceExtension_Extension_DetailedSliceFlatAll[] | undefined;
public getExtensionDetailed(mode: 'raw'): Extension[] | undefined;
public getExtensionDetailed(): USCoreRaceExtension_Extension_DetailedSliceFlatAll[] | undefined;
public getExtensionDetailed (mode: 'flat' | 'raw' = 'flat'): (USCoreRaceExtension_Extension_DetailedSliceFlatAll | Extension)[] | undefined {
const match = USCoreRaceExtensionProfile.detailedSliceMatch
const item = getArraySlice(this.resource.extension, match)
if (!item) return undefined
if (mode === 'raw') return item
return unwrapSliceChoice<USCoreRaceExtension_Extension_DetailedSliceFlatAll>(item, ["url"], "valueCoding")
const items = getArraySliceAll(this.resource.extension, match)
if (items.length === 0) return undefined
if (mode === 'raw') return items
return items.map(item => unwrapSliceChoice<USCoreRaceExtension_Extension_DetailedSliceFlatAll>(item, ["url"], "valueCoding"))
}

public getExtensionText(mode: 'flat'): USCoreRaceExtension_Extension_TextSliceFlatAll | undefined;
Expand Down
20 changes: 20 additions & 0 deletions examples/typescript-us-core/fhir-types/profile-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,26 @@ export const getArraySlice = <T>(list: readonly T[] | undefined, match: Record<s
return list.find((item) => matchesValue(item, match));
};

/** Return all elements in `list` that satisfy the slice discriminator `match`. */
export const getArraySliceAll = <T>(list: readonly T[] | undefined, match: Record<string, unknown>): T[] => {
if (!list) return [];
return list.filter((item) => matchesValue(item, match));
};

/**
* Replace all elements matching `match` in `list` with `newItems`.
* Each new item has the discriminator values applied via {@link applySliceMatch}
* before this call, so this helper only handles the array surgery.
*/
export const setArraySliceAll = <T>(list: T[], match: Record<string, unknown>, newItems: T[]): void => {
// Remove all existing items that match the discriminator
for (let i = list.length - 1; i >= 0; i--) {
if (matchesValue(list[i], match)) list.splice(i, 1);
}
// Append new items
list.push(...newItems);
};

// ---------------------------------------------------------------------------
// Validation helpers
//
Expand Down
Loading
Loading