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
24 changes: 18 additions & 6 deletions helpers/fhirliquid-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,9 @@ export class FhirLiquidEngine {
expression: ExpressionContext,
state: EvaluationState,
): boolean {
const values = this.evaluateExpression(expression, state);
const values = this.evaluateExpression(expression, state)
.map(value => fhirpath.resolveInternalTypes(value))
.filter(value => value !== null && value !== undefined);
if (values.length === 0) return false;
if (values.length !== 1 || typeof values[0] !== "boolean") {
throw new Error(
Expand Down Expand Up @@ -599,7 +601,7 @@ export class FhirLiquidEngine {
expressionText,
variables,
this.model,
{ async: false },
{ async: false, resolveInternalTypes: false },
);
}

Expand All @@ -616,13 +618,14 @@ export class FhirLiquidEngine {
): string {
const context: FhirLiquidRenderContext = {
resource: state.resource,
variables: variablesFromState(state),
variables: resolvedVariablesFromState(state),
};
const rendered: string[] = [];
for (const value of values) {
if (value === null || value === undefined) continue;
const custom = this.renderValue?.(value, context);
rendered.push(custom ?? defaultValueToString(value));
const resolvedValue = fhirpath.resolveInternalTypes(value);
if (resolvedValue === null || resolvedValue === undefined) continue;
const custom = this.renderValue?.(resolvedValue, context);
rendered.push(custom ?? defaultValueToString(resolvedValue));
}
return rendered.join(", ");
}
Expand All @@ -644,6 +647,15 @@ function variablesFromState(state: EvaluationState): Record<string, unknown> {
};
}

function resolvedVariablesFromState(
state: EvaluationState,
): Record<string, unknown> {
return Object.fromEntries(
Object.entries(variablesFromState(state))
.map(([name, value]) => [name, fhirpath.resolveInternalTypes(value)]),
);
}

function itemsBetween<TItem extends ParserRuleContext>(
items: TItem[],
startExclusive: number,
Expand Down
46 changes: 46 additions & 0 deletions test/fhirliquid-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ const patient = {
],
};

const patientWithExtensions = {
...patient,
meta: {
extension: [
{
url: "u1",
valueString: "sample extension string",
},
{
url: "u12",
valueMarkdown: "### a simple header",
},
],
},
};

describe("FhirLiquidEngine", () => {
it("renders FHIRPath output and Liquid filters", () => {
const engine = new FhirLiquidEngine();
Expand Down Expand Up @@ -59,6 +75,18 @@ describe("FhirLiquidEngine", () => {
expect(evaluateFhirLiquid(template, patient)).toBe("1=Chalmers,3=Jones;");
});

it("retains FHIR type information for loop variables", () => {
const template = "{% for i in Patient.meta.extension %}"
+ "[{{ i.value }}|{{ %i.value }}|"
+ "{{ i.valueString }}|{{ i.valueMarkdown }}]"
+ "{% endfor %}";

expect(evaluateFhirLiquid(template, patientWithExtensions)).toBe(
"[sample extension string|sample extension string|sample extension string|]"
+ "[### a simple header|### a simple header||### a simple header]",
);
});

it("applies loop modifiers and renders the empty branch", () => {
expect(evaluateFhirLiquid(
"{% for name in Patient.name reversed offset: 1 limit: 2 %}"
Expand Down Expand Up @@ -92,6 +120,24 @@ describe("FhirLiquidEngine", () => {
)).toContain("<strong>Important</strong>");
});

it("passes resolved values to custom renderers", () => {
const renderValue = jest.fn(() => undefined);
const engine = new FhirLiquidEngine({ renderValue });

expect(engine.evaluate(
"{% for name in Patient.name limit: 1 %}{{ name }}{% endfor %}",
patient,
))
.toContain("\"family\":\"Chalmers\"");
expect(renderValue).toHaveBeenCalledWith(
patient.name[0],
expect.objectContaining({
resource: patient,
variables: expect.objectContaining({ name: patient.name[0] }),
}),
);
});

it("rejects invalid templates and multi-value assignments", () => {
const engine = new FhirLiquidEngine();

Expand Down
4 changes: 2 additions & 2 deletions vue3-src/app/components/TwinPaneTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,8 @@ const getActiveTabs = (): TabData[] => {
} else {
const lockedTabData = props.tabs[lockedTab.value]
const selectableTabData = props.tabs[selectableTab.value]
if (lockedTabData) result.push(lockedTabData)
if (selectableTabData) result.push(selectableTabData)
if (lockedTabData && lockedTabData.show) result.push(lockedTabData)
if (selectableTabData && selectableTabData.show) result.push(selectableTabData)
}
return result
}
Expand Down
10 changes: 8 additions & 2 deletions vue3-src/app/pages/fhir-liquid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ useHead({

interface TwinPaneControl {
selectTab(tabIndex: number): void;
getActiveTabs(): TabData[];
}

interface ResourceEditorControl {
Expand All @@ -119,6 +120,7 @@ const tabSpaces = 2;
const TEMPLATE_TAB = 0;
const RESOURCE_TAB = 1;
const OUTPUT_TAB = 2;
const OUTPUT_HTML_TAB = 3;
const ERRORS_TAB = 4;
const twinTabControl = ref<TwinPaneControl>();
const templateEditor = ref<ResourceEditorControl>();
Expand Down Expand Up @@ -235,8 +237,12 @@ function evaluateTemplate(): void {
}

errorOutcome.value = undefined;
twinTabControl.value?.selectTab(OUTPUT_TAB);
showSuccessMessage("Template evaluated successfully.");
var activeTabs = twinTabControl.value?.getActiveTabs() ?? [];
// only switch tabs if either of the output tabs are not currently active
if (!activeTabs.some(tab => tab.tabName === "Output" || tab.tabName === "Output HTML")) {
twinTabControl.value?.selectTab(hasHtmlOutput.value ? OUTPUT_HTML_TAB : OUTPUT_TAB);
}
// twinTabControl.value?.selectTab(OUTPUT_TAB);
}

function ctrlEnterHandler(event: KeyboardEvent): void {
Expand Down
Loading