Skip to content
145 changes: 145 additions & 0 deletions helpers/fhirpath_api_engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,143 @@ export function convertOptionsToParametersJavaR5R6(
return parameters;
}

// --- fhirpath.zig WASM engine support ---

const FHIRPATH_ZIG_BASE = "https://joshuamandel.com/fhirpath.zig";

let _zigEnginePromise: Promise<any> | null = null;
let _zigSchemasRegistered: Set<string> = new Set();

async function getZigEngine(): Promise<any> {
if (!_zigEnginePromise) {
_zigEnginePromise = (async () => {
// Dynamic import of the JS wrapper from the deployed site
const mod = await import(/* webpackIgnore: true */ `${FHIRPATH_ZIG_BASE}/fhirpath.js`);
const FhirPathEngine = mod.FhirPathEngine;
const engine = await FhirPathEngine.instantiate();
Comment on lines +969 to +980
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This issue here is the reason why the PR hasn't been merged in.
I'd prefer this engine to be hooked in via a node hosted service similar to https://github.com/brianpos/fhirpath-lab-fhirpath-js-api.
This then
Currently the fhirpath.js engine is the only one that is "local" to the browser, even though others could be done that way.
This removes the burden of updates, or random external js loading and execution in the platform.
Though it does cost in terms of requiring a hosted service to call.
I have also introduced a mechanism to be able to tag them as only visible with "show advanced properties" is on (for less common/more advanced things) and also a new feature for enabling custom engine URLs via URL parameter.

return engine;
})();
}
return _zigEnginePromise;
}

async function ensureZigSchema(engine: any, fhirVersion: string): Promise<string> {
const version = fhirVersion.toLowerCase();
const schemaName = version; // "r4" or "r5"
if (!_zigSchemasRegistered.has(schemaName)) {
await engine.registerSchema({
name: schemaName,
isDefault: _zigSchemasRegistered.size === 0,
});
_zigSchemasRegistered.add(schemaName);
}
Comment on lines +987 to +996
return schemaName;
}

/**
* Evaluate FHIRPath expression using fhirpath.zig WASM engine
*/
export async function evaluateExpressionUsingFhirpathZig(
options: FhirPathEvaluationOptions,
fhirVersion: 'R4' | 'R5' = 'R4'
): Promise<FhirPathEvaluationResult> {
const result: FhirPathEvaluationResult = {
results: [],
debugTraceData: [],
processedByEngine: `fhirpath.zig (${fhirVersion} WASM)`
};

try {
const engine = await getZigEngine();
const schemaName = await ensureZigSchema(engine, fhirVersion);

// Set current time
engine.setNowDate(new Date());

const resourceText = options.resourceJson || '{}';
const isXml = !!options.isXmlResource;

Comment on lines +1003 to +1022
// Helper: call eval or evalXml depending on input format
function zigEval(expr: string, input: string, inputIsXml: boolean) {
if (inputIsXml) {
return engine.evalXml({ expr, xml: input, schema: schemaName });
}
return engine.eval({ expr, json: input, schema: schemaName });
}

// Evaluate context expression to get context nodes, or use root
let contexts: { path?: string; text: string; isXml: boolean }[] = [];
if (options.contextExpression) {
try {
const contextResult = zigEval(options.contextExpression, resourceText, isXml);
for (const node of contextResult) {
// Context results are always JSON (node.data is a JS object)
contexts.push({
path: node.meta?.typeName || undefined,
text: JSON.stringify(node.data),
isXml: false,
});
}
} catch (err: any) {
result.saveOutcome = CreateOperationOutcome('fatal', 'exception',
`Context expression error: ${err.message || err}`);
result.showOutcome = true;
return result;
}
} else {
// No context expression — use the original resource as-is
contexts.push({ text: resourceText, isXml });
}

// Evaluate main expression for each context
for (const ctx of contexts) {
const resData: ResultData = {
context: ctx.path,
result: [],
trace: []
};

try {
const evalResult = zigEval(options.expression, ctx.text, ctx.isXml);

for (const node of evalResult) {
const typeName = node.meta?.typeName || '';
const data = node.data;

let value: any;
if (data !== null && data !== undefined) {
if (typeof data === 'object' && data.value !== undefined && data.unit !== undefined) {
// Quantity
value = `${data.value} '${data.unit}'`;
} else if (typeof data === 'object') {
value = JSON.stringify(data, null, 2);
} else {
value = data;
}
}

resData.result.push({
type: typeName,
value: typeof value === 'string' ? value : JSON.stringify(value),
});
}
} catch (err: any) {
result.saveOutcome = CreateOperationOutcome('fatal', 'exception', err.message || String(err));
result.showOutcome = true;
return result;
}

result.results.push(resData);
}
} catch (err: any) {
result.saveOutcome = CreateOperationOutcome('fatal', 'exception',
`Failed to load fhirpath.zig engine: ${err.message || err}`);
result.showOutcome = true;
}

return result;
}

/**
* Get engine URL and determine if it's a local (non-RESTful) engine
*/
Expand All @@ -981,6 +1118,9 @@ export async function getEngineInfo(selectedEngine: IFhirPathEngineDetails): Pro
if (engineName === "fhirpath.js") {
return { isLocal: true, requiresSpecialParameterHandling: false, astSupported: true };
}
if (engineName === "fhirpath.zig") {
return { isLocal: true, requiresSpecialParameterHandling: false, astSupported: false };
}
}

// RESTful engines - use the URL from the engine details if available
Expand Down Expand Up @@ -1014,6 +1154,11 @@ export async function evaluateFhirPathExpression(
const engineInfo = await getEngineInfo(selectedEngine);

if (engineInfo.isLocal) {
const engineName = selectedEngine.name.toLowerCase();
if (engineName === "fhirpath.zig") {
const fhirVersion = selectedEngine.fhirVersion.toLowerCase() === 'r5' ? 'R5' : 'R4';
return await evaluateExpressionUsingFhirpathZig(options, fhirVersion);
}
// Use local fhirpath.js engine
const fhirVersion = selectedEngine.fhirVersion.toLowerCase() === 'r5' ? 'R5' : 'R4';
return await evaluateExpressionUsingFhirpathJs(options, fhirVersion as 'R4' | 'R5' | 'R6');
Expand Down
25 changes: 25 additions & 0 deletions types/fhirpath_test_engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,31 @@ export let registeredEngines: { [key: string]: IFhirPathEngineDetails } = {
supportsAST: true,
supportsXML: false
},
"fhirpath.zig (R4)": {
name: "fhirpath.zig",
legacyName: "fhirpath.zig (R4)",
fhirVersion: "R4",
appInsightsEngineName: "fhirpath.zig",
publisher: "Joshua Mandel",
githubRepo: "https://github.com/jmandel/fhirpath.zig",
description: "A Zig/WASM FHIRPath engine running locally in the browser.",
external: false,
supportsAST: false,
supportsXML: true
},
Comment on lines +401 to +405
"fhirpath.zig (R5)": {
name: "fhirpath.zig",
legacyName: "fhirpath.zig (R5)",
fhirVersion: "R5",
appInsightsEngineName: "fhirpath.zig",
publisher: "Joshua Mandel",
githubRepo: "https://github.com/jmandel/fhirpath.zig",
description: "A Zig/WASM FHIRPath engine running locally in the browser for FHIR R5.",
external: false,
supportsAST: false,
supportsXML: true
},
Comment on lines +413 to +417

"CQL (R4)": {
name: "CQL-Facade",
legacyName: "CQL (R4)",
Expand Down
Loading