@atomic-ehr/fhirpath 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.browser.d.ts +22 -0
- package/dist/index.browser.js +15758 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.node.d.ts +24 -0
- package/dist/{index.js → index.node.js} +5450 -3809
- package/dist/index.node.js.map +1 -0
- package/dist/{index.d.ts → model-provider.common-oir-zg7r.d.ts} +81 -74
- package/package.json +10 -5
- package/src/analyzer.ts +46 -9
- package/src/complex-types/quantity-value.ts +131 -9
- package/src/complex-types/temporal.ts +45 -6
- package/src/errors.ts +4 -0
- package/src/index.browser.ts +4 -0
- package/src/{index.ts → index.common.ts} +18 -14
- package/src/index.node.ts +4 -0
- package/src/interpreter/navigator.ts +12 -0
- package/src/interpreter/runtime-context.ts +60 -25
- package/src/interpreter.ts +118 -33
- package/src/lexer.ts +4 -1
- package/src/model-provider.browser.ts +35 -0
- package/src/{model-provider.ts → model-provider.common.ts} +29 -26
- package/src/model-provider.node.ts +41 -0
- package/src/operations/allTrue-function.ts +6 -10
- package/src/operations/and-operator.ts +2 -2
- package/src/operations/as-function.ts +41 -0
- package/src/operations/combine-operator.ts +17 -4
- package/src/operations/comparison.ts +73 -21
- package/src/operations/convertsToQuantity-function.ts +56 -7
- package/src/operations/decode-function.ts +114 -0
- package/src/operations/divide-operator.ts +3 -3
- package/src/operations/encode-function.ts +110 -0
- package/src/operations/escape-function.ts +114 -0
- package/src/operations/exp-function.ts +65 -0
- package/src/operations/extension-function.ts +88 -0
- package/src/operations/greater-operator.ts +5 -24
- package/src/operations/greater-or-equal-operator.ts +5 -24
- package/src/operations/hasValue-function.ts +84 -0
- package/src/operations/iif-function.ts +7 -1
- package/src/operations/implies-operator.ts +1 -0
- package/src/operations/index.ts +11 -0
- package/src/operations/is-function.ts +11 -0
- package/src/operations/is-operator.ts +187 -5
- package/src/operations/less-operator.ts +6 -24
- package/src/operations/less-or-equal-operator.ts +5 -24
- package/src/operations/less-than.ts +7 -12
- package/src/operations/ln-function.ts +62 -0
- package/src/operations/log-function.ts +113 -0
- package/src/operations/lowBoundary-function.ts +14 -0
- package/src/operations/minus-operator.ts +8 -1
- package/src/operations/mod-operator.ts +7 -1
- package/src/operations/not-function.ts +9 -2
- package/src/operations/ofType-function.ts +35 -0
- package/src/operations/plus-operator.ts +46 -3
- package/src/operations/precision-function.ts +146 -0
- package/src/operations/replace-function.ts +19 -19
- package/src/operations/replaceMatches-function.ts +5 -0
- package/src/operations/sort-function.ts +209 -0
- package/src/operations/take-function.ts +1 -1
- package/src/operations/toQuantity-function.ts +0 -1
- package/src/operations/toString-function.ts +76 -12
- package/src/operations/trace-function.ts +20 -3
- package/src/operations/unescape-function.ts +119 -0
- package/src/operations/where-function.ts +3 -1
- package/src/parser.ts +14 -2
- package/src/types.ts +7 -5
- package/src/utils/decimal.ts +76 -0
- package/dist/index.js.map +0 -1
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
import { Parser } from './parser';
|
|
2
2
|
import { Interpreter } from './interpreter';
|
|
3
3
|
import { Analyzer } from './analyzer';
|
|
4
|
-
import type { AnalysisResult } from './types';
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import type { AnalysisResult, EvaluationResult, ModelProvider, TypeInfo } from './types';
|
|
5
|
+
|
|
6
|
+
declare const __VERSION__: string;
|
|
7
7
|
|
|
8
8
|
export interface EvaluateOptions {
|
|
9
9
|
input?: unknown;
|
|
10
10
|
variables?: Record<string, unknown>;
|
|
11
|
-
modelProvider?:
|
|
12
|
-
inputType?:
|
|
11
|
+
modelProvider?: ModelProvider;
|
|
12
|
+
inputType?: TypeInfo;
|
|
13
|
+
includeMetadata?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export async function evaluate(
|
|
16
|
+
export async function evaluate<T = any>(
|
|
16
17
|
expression: string,
|
|
17
18
|
options: EvaluateOptions = {}
|
|
18
|
-
): Promise<
|
|
19
|
+
): Promise<EvaluationResult<T>['value']> {
|
|
19
20
|
const interpreter = new Interpreter(undefined, options.modelProvider);
|
|
20
21
|
return interpreter.evaluateExpression(expression, {
|
|
21
22
|
input: options.input,
|
|
22
23
|
variables: options.variables,
|
|
23
24
|
inputType: options.inputType,
|
|
24
25
|
modelProvider: options.modelProvider,
|
|
26
|
+
includeMetadata: options.includeMetadata
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
|
|
28
31
|
export async function analyze(
|
|
29
32
|
expression: string,
|
|
30
33
|
options: {
|
|
31
34
|
variables?: Record<string, unknown>;
|
|
32
|
-
modelProvider?:
|
|
33
|
-
inputType?:
|
|
35
|
+
modelProvider?: ModelProvider;
|
|
36
|
+
inputType?: TypeInfo;
|
|
34
37
|
errorRecovery?: boolean;
|
|
35
38
|
} = {}
|
|
36
39
|
): Promise<AnalysisResult> {
|
|
@@ -43,6 +46,10 @@ export async function analyze(
|
|
|
43
46
|
return analysisResult;
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
export function getVersion(): string {
|
|
50
|
+
return __VERSION__;
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
// Export key types and classes
|
|
47
54
|
export { Parser } from './parser';
|
|
48
55
|
export { Interpreter } from './interpreter';
|
|
@@ -59,13 +66,10 @@ export type {
|
|
|
59
66
|
TypeName,
|
|
60
67
|
ModelProvider as ModelTypeProvider,
|
|
61
68
|
OperatorDefinition,
|
|
62
|
-
FunctionDefinition
|
|
69
|
+
FunctionDefinition,
|
|
70
|
+
EvaluationResult
|
|
63
71
|
} from './types';
|
|
64
72
|
|
|
65
|
-
// Export FHIR ModelProvider
|
|
66
|
-
export { FHIRModelProvider } from './model-provider';
|
|
67
|
-
export type { FHIRModelContext, FHIRModelProviderConfig } from './model-provider';
|
|
68
|
-
|
|
69
73
|
// Export inspect API
|
|
70
74
|
export { inspect } from './inspect';
|
|
71
75
|
export type { InspectOptions, InspectResult, ASTMetadata } from './inspect';
|
|
@@ -79,6 +79,18 @@ async function detectChoiceValues(
|
|
|
79
79
|
} else {
|
|
80
80
|
choiceType = { ...choiceType, singleton: !Array.isArray(value) };
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
// For FHIR value[x] polymorphic properties, ensure FHIR namespace and lowercase name
|
|
84
|
+
if (choiceProp.startsWith('value') && modelProvider) {
|
|
85
|
+
// Convert type name to lowercase for FHIR primitive types
|
|
86
|
+
// valueString -> FHIR.string, valueInteger -> FHIR.integer, etc.
|
|
87
|
+
const lowerTypeName = choiceName.charAt(0).toLowerCase() + choiceName.slice(1);
|
|
88
|
+
choiceType = {
|
|
89
|
+
...choiceType,
|
|
90
|
+
namespace: 'FHIR',
|
|
91
|
+
name: lowerTypeName
|
|
92
|
+
};
|
|
93
|
+
}
|
|
82
94
|
if (Array.isArray(value)) {
|
|
83
95
|
for (const v of value) {
|
|
84
96
|
hits.push({ value: v, typeInfo: { ...choiceType, singleton: true }, primitiveElement });
|
|
@@ -33,6 +33,12 @@ export class RuntimeContextManager {
|
|
|
33
33
|
context.variables['%resource'] = input;
|
|
34
34
|
context.variables['%rootResource'] = input;
|
|
35
35
|
|
|
36
|
+
// Set FHIR-specific system variables (standard URLs for code systems)
|
|
37
|
+
context.variables['%sct'] = 'http://snomed.info/sct';
|
|
38
|
+
context.variables['%loinc'] = 'http://loinc.org';
|
|
39
|
+
context.variables['%ucum'] = 'http://unitsofmeasure.org';
|
|
40
|
+
context.variables['%vs-administrative-gender'] = 'http://hl7.org/fhir/ValueSet/administrative-gender';
|
|
41
|
+
|
|
36
42
|
// Add any initial variables (with % prefix for user-defined)
|
|
37
43
|
if (initialVariables) {
|
|
38
44
|
for (const [key, value] of Object.entries(initialVariables)) {
|
|
@@ -91,7 +97,7 @@ export class RuntimeContextManager {
|
|
|
91
97
|
static setVariable(context: RuntimeContext, name: string, value: any, allowRedefinition: boolean = false): RuntimeContext {
|
|
92
98
|
// Ensure value is array for consistency (except for special variables like $index)
|
|
93
99
|
const arrayValue = (name === '$index' || name === '$total') ? value :
|
|
94
|
-
|
|
100
|
+
Array.isArray(value) ? value : [value];
|
|
95
101
|
|
|
96
102
|
// Determine variable key based on prefix
|
|
97
103
|
let varKey = name;
|
|
@@ -100,12 +106,20 @@ export class RuntimeContextManager {
|
|
|
100
106
|
varKey = `%${name}`;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
// Check for system
|
|
104
|
-
const systemVariables = [
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
// Check for system variable override attempts
|
|
110
|
+
const systemVariables = new Set([
|
|
111
|
+
'context', '%context',
|
|
112
|
+
'resource', '%resource',
|
|
113
|
+
'rootResource', '%rootResource',
|
|
114
|
+
'sct', '%sct',
|
|
115
|
+
'loinc', '%loinc',
|
|
116
|
+
'ucum', '%ucum',
|
|
117
|
+
'vs-administrative-gender', '%vs-administrative-gender'
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
if (!allowRedefinition && (systemVariables.has(name) || systemVariables.has(varKey))) {
|
|
121
|
+
// Cannot override system variables
|
|
122
|
+
throw Errors.systemVariableOverride(name);
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
// Check if variable already exists (unless redefinition is allowed)
|
|
@@ -149,12 +163,33 @@ export class RuntimeContextManager {
|
|
|
149
163
|
if (name === 'rootResource' || name === '%rootResource') {
|
|
150
164
|
return context.variables['%rootResource'];
|
|
151
165
|
}
|
|
166
|
+
if (name === 'sct' || name === '%sct') {
|
|
167
|
+
return context.variables['%sct'];
|
|
168
|
+
}
|
|
169
|
+
if (name === 'loinc' || name === '%loinc') {
|
|
170
|
+
return context.variables['%loinc'];
|
|
171
|
+
}
|
|
172
|
+
if (name === 'ucum' || name === '%ucum') {
|
|
173
|
+
return context.variables['%ucum'];
|
|
174
|
+
}
|
|
175
|
+
if (name === 'vs-administrative-gender' || name === '%vs-administrative-gender' || name === '%`vs-administrative-gender`') {
|
|
176
|
+
return context.variables['%vs-administrative-gender'];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Handle user-defined variables
|
|
180
|
+
// Strip backticks from environment variable syntax %`variable`
|
|
181
|
+
let cleanName = name;
|
|
182
|
+
if (name.startsWith('%`') && name.endsWith('`')) {
|
|
183
|
+
// Remove %` from start and ` from end
|
|
184
|
+
cleanName = '%' + name.slice(2, -1);
|
|
185
|
+
} else if (!name.startsWith('%')) {
|
|
186
|
+
// Add % prefix if not present
|
|
187
|
+
cleanName = '%' + name;
|
|
188
|
+
}
|
|
152
189
|
|
|
153
|
-
// Handle user-defined variables (add % prefix if not present)
|
|
154
|
-
const varKey = name.startsWith('%') ? name : `%${name}`;
|
|
155
190
|
// Use 'in' operator to check prototype chain for inherited variables
|
|
156
|
-
if (
|
|
157
|
-
return context.variables[
|
|
191
|
+
if (cleanName in context.variables) {
|
|
192
|
+
return context.variables[cleanName];
|
|
158
193
|
}
|
|
159
194
|
return undefined;
|
|
160
195
|
}
|
|
@@ -250,21 +285,21 @@ export class RuntimeContextManager {
|
|
|
250
285
|
const values = Array.isArray(rawVal) ? rawVal : [rawVal];
|
|
251
286
|
const maybeBoxed = modelProvider
|
|
252
287
|
? await Promise.all(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
288
|
+
values.map(async (v) => {
|
|
289
|
+
if (
|
|
290
|
+
v &&
|
|
291
|
+
typeof v === 'object' &&
|
|
292
|
+
'resourceType' in (v as any) &&
|
|
293
|
+
typeof (v as any).resourceType === 'string'
|
|
294
|
+
) {
|
|
295
|
+
const ti = await modelProvider.getType((v as any).resourceType);
|
|
296
|
+
return ti ? box(v, ti) : v;
|
|
297
|
+
}
|
|
298
|
+
return v;
|
|
299
|
+
})
|
|
300
|
+
)
|
|
266
301
|
: values;
|
|
267
|
-
context = RuntimeContextManager.setVariable(context, key, maybeBoxed);
|
|
302
|
+
context = RuntimeContextManager.setVariable(context, key, maybeBoxed, true);
|
|
268
303
|
}
|
|
269
304
|
}
|
|
270
305
|
|
package/src/interpreter.ts
CHANGED
|
@@ -10,7 +10,8 @@ import type {
|
|
|
10
10
|
IndexNode,
|
|
11
11
|
MembershipTestNode,
|
|
12
12
|
TypeCastNode,
|
|
13
|
-
QuantityNode
|
|
13
|
+
QuantityNode,
|
|
14
|
+
ModelProvider
|
|
14
15
|
} from './types';
|
|
15
16
|
import { NodeType } from './types';
|
|
16
17
|
import { Registry } from './registry';
|
|
@@ -44,7 +45,7 @@ export class Interpreter {
|
|
|
44
45
|
this.modelProvider = modelProvider;
|
|
45
46
|
this.operationEvaluators = new Map();
|
|
46
47
|
this.functionEvaluators = new Map();
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
// Initialize node evaluators using object dispatch pattern
|
|
49
50
|
this.nodeEvaluators = {
|
|
50
51
|
[NodeType.Literal]: this.evaluateLiteral.bind(this),
|
|
@@ -136,10 +137,11 @@ export class Interpreter {
|
|
|
136
137
|
input?: unknown;
|
|
137
138
|
variables?: Record<string, unknown>;
|
|
138
139
|
inputType?: TypeInfo;
|
|
139
|
-
modelProvider?:
|
|
140
|
+
modelProvider?: ModelProvider;
|
|
140
141
|
now?: Date;
|
|
142
|
+
includeMetadata?: boolean;
|
|
141
143
|
} = {}
|
|
142
|
-
): Promise<
|
|
144
|
+
): Promise<EvaluationResult['value']> {
|
|
143
145
|
// Analyze expression first (ensures type info and diagnostics)
|
|
144
146
|
const analysis = await Analyzer.analyzeExpression(expression, {
|
|
145
147
|
variables: options.variables,
|
|
@@ -168,13 +170,24 @@ export class Interpreter {
|
|
|
168
170
|
// Unbox and format temporal outputs for API parity
|
|
169
171
|
return result.value.map((boxedValue) => {
|
|
170
172
|
const value = unbox(boxedValue);
|
|
173
|
+
let overridedValue: any = undefined;
|
|
171
174
|
if (value && typeof value === 'object' && 'kind' in value) {
|
|
172
175
|
if ((value as any).kind === 'FHIRDate' || (value as any).kind === 'FHIRDateTime') {
|
|
173
|
-
|
|
176
|
+
if (options.includeMetadata) {
|
|
177
|
+
overridedValue = '@' + toTemporalString(value as any);
|
|
178
|
+
} else {
|
|
179
|
+
return '@' + toTemporalString(value as any);
|
|
180
|
+
}
|
|
174
181
|
} else if ((value as any).kind === 'FHIRTime') {
|
|
175
|
-
|
|
182
|
+
if (options.includeMetadata) {
|
|
183
|
+
overridedValue = '@T' + toTemporalString(value as any);
|
|
184
|
+
} else {
|
|
185
|
+
return '@T' + toTemporalString(value as any);
|
|
186
|
+
}
|
|
176
187
|
}
|
|
177
188
|
}
|
|
189
|
+
// return options.includeMetadata ? box(overridedValue ?? value, boxedValue.typeInfo, boxedValue.primitiveElement) : value;
|
|
190
|
+
// return box(overridedValue ?? value, boxedValue.typeInfo, boxedValue.primitiveElement);
|
|
178
191
|
return value;
|
|
179
192
|
});
|
|
180
193
|
}
|
|
@@ -201,10 +214,10 @@ export class Interpreter {
|
|
|
201
214
|
// TemporalLiteral node evaluator
|
|
202
215
|
private async evaluateTemporalLiteral(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
|
|
203
216
|
const temporal = node as import('./types').TemporalLiteralNode;
|
|
204
|
-
|
|
217
|
+
|
|
205
218
|
// The value is already parsed in the parser
|
|
206
219
|
let typeInfo: import('./types').TypeInfo;
|
|
207
|
-
|
|
220
|
+
|
|
208
221
|
if (temporal.valueType === 'date') {
|
|
209
222
|
typeInfo = { type: 'Date', singleton: true };
|
|
210
223
|
} else if (temporal.valueType === 'datetime') {
|
|
@@ -212,7 +225,7 @@ export class Interpreter {
|
|
|
212
225
|
} else {
|
|
213
226
|
typeInfo = { type: 'Time', singleton: true };
|
|
214
227
|
}
|
|
215
|
-
|
|
228
|
+
|
|
216
229
|
return {
|
|
217
230
|
value: [box(temporal.value, typeInfo)],
|
|
218
231
|
context
|
|
@@ -222,18 +235,18 @@ export class Interpreter {
|
|
|
222
235
|
// Literal node evaluator
|
|
223
236
|
private async evaluateLiteral(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
|
|
224
237
|
const literal = node as LiteralNode;
|
|
225
|
-
|
|
238
|
+
|
|
226
239
|
// Box the literal value with appropriate type info
|
|
227
240
|
let typeInfo: import('./types').TypeInfo | undefined;
|
|
228
241
|
let value: any = literal.value;
|
|
229
|
-
|
|
242
|
+
|
|
230
243
|
// Handle temporal literals (backwards compatibility - should not reach here with new parser)
|
|
231
244
|
if (literal.valueType === 'date' || literal.valueType === 'datetime' || literal.valueType === 'time') {
|
|
232
245
|
// Import temporal parsing function
|
|
233
246
|
const { parseTemporalLiteral } = await import('./complex-types/temporal');
|
|
234
247
|
// Parse the temporal literal (add @ back since it was stripped by parser)
|
|
235
248
|
const temporalValue = parseTemporalLiteral('@' + literal.value);
|
|
236
|
-
|
|
249
|
+
|
|
237
250
|
// Set appropriate type info
|
|
238
251
|
if (literal.valueType === 'date') {
|
|
239
252
|
typeInfo = { type: 'Date', singleton: true };
|
|
@@ -242,18 +255,20 @@ export class Interpreter {
|
|
|
242
255
|
} else if (literal.valueType === 'time') {
|
|
243
256
|
typeInfo = { type: 'Time', singleton: true };
|
|
244
257
|
}
|
|
245
|
-
|
|
258
|
+
|
|
246
259
|
value = temporalValue;
|
|
247
260
|
} else if (typeof value === 'string') {
|
|
248
261
|
typeInfo = { type: 'String', singleton: true };
|
|
249
262
|
} else if (typeof value === 'number') {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
263
|
+
// Use the valueType from the literal node to determine if it's integer or decimal
|
|
264
|
+
// This preserves the distinction between 1.0 (decimal) and 1 (integer)
|
|
265
|
+
typeInfo = literal.valueType === 'decimal' ?
|
|
266
|
+
{ type: 'Decimal', singleton: true } :
|
|
267
|
+
{ type: 'Integer', singleton: true };
|
|
253
268
|
} else if (typeof value === 'boolean') {
|
|
254
269
|
typeInfo = { type: 'Boolean', singleton: true };
|
|
255
270
|
}
|
|
256
|
-
|
|
271
|
+
|
|
257
272
|
return {
|
|
258
273
|
value: [box(value, typeInfo)],
|
|
259
274
|
context
|
|
@@ -262,7 +277,7 @@ export class Interpreter {
|
|
|
262
277
|
|
|
263
278
|
// Helper: Handle extension elements
|
|
264
279
|
private handleExtension(
|
|
265
|
-
boxedItem: FHIRPathValue,
|
|
280
|
+
boxedItem: FHIRPathValue,
|
|
266
281
|
nodeTypeInfo?: TypeInfo
|
|
267
282
|
): FHIRPathValue[] {
|
|
268
283
|
const results: FHIRPathValue[] = [];
|
|
@@ -326,15 +341,31 @@ export class Interpreter {
|
|
|
326
341
|
item: object,
|
|
327
342
|
name: string,
|
|
328
343
|
nodeTypeInfo: TypeInfo | undefined,
|
|
329
|
-
context: RuntimeContext
|
|
344
|
+
context: RuntimeContext,
|
|
345
|
+
parentTypeInfo?: TypeInfo
|
|
330
346
|
): Promise<FHIRPathValue[]> {
|
|
331
347
|
const results: FHIRPathValue[] = [];
|
|
332
348
|
if (name in (item as any)) {
|
|
333
349
|
const value = (item as any)[name];
|
|
334
350
|
const primitiveElement = getPrimitiveElement(item as Record<string, unknown>, name);
|
|
335
351
|
|
|
352
|
+
// Determine if this is a FHIR primitive - if parent is a FHIR resource and value is primitive
|
|
353
|
+
const isFHIRPrimitive = parentTypeInfo &&
|
|
354
|
+
parentTypeInfo.type &&
|
|
355
|
+
parentTypeInfo.type !== 'Any' &&
|
|
356
|
+
!parentTypeInfo.type.startsWith('System.') &&
|
|
357
|
+
(typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number');
|
|
358
|
+
|
|
336
359
|
if (Array.isArray(value)) {
|
|
337
|
-
|
|
360
|
+
let elementTypeInfo = nodeTypeInfo ? { ...nodeTypeInfo, singleton: true } : undefined;
|
|
361
|
+
|
|
362
|
+
// For FHIR primitives, use FHIR namespace
|
|
363
|
+
if (isFHIRPrimitive && elementTypeInfo) {
|
|
364
|
+
if (elementTypeInfo.type === 'Boolean') {
|
|
365
|
+
elementTypeInfo = { ...elementTypeInfo, type: 'boolean' as any };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
338
369
|
for (const v of value) {
|
|
339
370
|
if (
|
|
340
371
|
v && typeof v === 'object' && 'resourceType' in (v as any) && typeof (v as any).resourceType === 'string'
|
|
@@ -355,8 +386,16 @@ export class Interpreter {
|
|
|
355
386
|
const boxed = await reboxResource(value, true, context.modelProvider);
|
|
356
387
|
results.push(boxed);
|
|
357
388
|
} else {
|
|
358
|
-
|
|
359
|
-
|
|
389
|
+
// For FHIR primitives, use FHIR namespace
|
|
390
|
+
let typeInfo = nodeTypeInfo;
|
|
391
|
+
if (isFHIRPrimitive && typeInfo) {
|
|
392
|
+
if (typeInfo.type === 'Boolean') {
|
|
393
|
+
typeInfo = { ...typeInfo, type: 'boolean' as any };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const val = await maybeParseTemporal(value, typeInfo, context.modelProvider);
|
|
398
|
+
results.push(box(val, typeInfo, primitiveElement));
|
|
360
399
|
}
|
|
361
400
|
}
|
|
362
401
|
}
|
|
@@ -393,7 +432,7 @@ export class Interpreter {
|
|
|
393
432
|
results.push(...unionResults);
|
|
394
433
|
|
|
395
434
|
// 4. Handle standard property access
|
|
396
|
-
const propertyResults = await this.handleStandardProperty(item, name, nodeTypeInfo, context);
|
|
435
|
+
const propertyResults = await this.handleStandardProperty(item, name, nodeTypeInfo, context, boxedItem.typeInfo);
|
|
397
436
|
results.push(...propertyResults);
|
|
398
437
|
}
|
|
399
438
|
}
|
|
@@ -431,9 +470,29 @@ export class Interpreter {
|
|
|
431
470
|
|
|
432
471
|
// Special handling for dot operator (sequential pipeline)
|
|
433
472
|
if (operator === '.') {
|
|
473
|
+
// Check if this is actually a namespaced type in an 'is' expression
|
|
474
|
+
// Parser incorrectly creates: (true is System).Boolean instead of: true is System.Boolean
|
|
475
|
+
if (binary.left.type === NodeType.MembershipTest && binary.right.type === NodeType.Identifier) {
|
|
476
|
+
const membershipTest = binary.left as MembershipTestNode;
|
|
477
|
+
const rightIdent = binary.right as IdentifierNode;
|
|
478
|
+
|
|
479
|
+
// Extract the expression from the membership test
|
|
480
|
+
const expr = membershipTest.expression;
|
|
481
|
+
const typeName = `${membershipTest.targetType}.${rightIdent.name}`;
|
|
482
|
+
|
|
483
|
+
// Evaluate the expression
|
|
484
|
+
const exprResult = await this.evaluate(expr, input, context);
|
|
485
|
+
|
|
486
|
+
// Now apply the is operator with the full type name
|
|
487
|
+
const evaluator = this.operationEvaluators.get('is');
|
|
488
|
+
if (evaluator) {
|
|
489
|
+
return await evaluator(input, context, exprResult.value, [typeName]);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
434
493
|
// Evaluate left with current input/context
|
|
435
494
|
const leftResult = await this.evaluate(binary.left, input, context);
|
|
436
|
-
|
|
495
|
+
|
|
437
496
|
// Use left's output as right's input, and left's context flows to right
|
|
438
497
|
return await this.evaluate(binary.right, leftResult.value, leftResult.context);
|
|
439
498
|
}
|
|
@@ -447,7 +506,7 @@ export class Interpreter {
|
|
|
447
506
|
this.evaluate(binary.left, input, context),
|
|
448
507
|
this.evaluate(binary.right, input, context)
|
|
449
508
|
]);
|
|
450
|
-
|
|
509
|
+
|
|
451
510
|
// Merge the results
|
|
452
511
|
const unionEvaluator = this.operationEvaluators.get('|');
|
|
453
512
|
if (unionEvaluator) {
|
|
@@ -457,6 +516,32 @@ export class Interpreter {
|
|
|
457
516
|
throw Errors.noEvaluatorFound('binary operator', '|');
|
|
458
517
|
}
|
|
459
518
|
|
|
519
|
+
// Special handling for 'is' and 'as' operators - right side is a type identifier, not an expression
|
|
520
|
+
if (operator === 'is' || operator === 'as') {
|
|
521
|
+
const leftResult = await this.evaluate(binary.left, input, context);
|
|
522
|
+
|
|
523
|
+
// Extract type name from right side WITHOUT evaluating it
|
|
524
|
+
let typeName: string;
|
|
525
|
+
if (binary.right.type === NodeType.Identifier) {
|
|
526
|
+
typeName = (binary.right as any).name;
|
|
527
|
+
} else if (binary.right.type === NodeType.Binary && (binary.right as any).operator === '.') {
|
|
528
|
+
// Handle namespaced types like System.Boolean or FHIR.Patient
|
|
529
|
+
const rightBinary = binary.right as any;
|
|
530
|
+
if (rightBinary.left.type === NodeType.Identifier && rightBinary.right.type === NodeType.Identifier) {
|
|
531
|
+
typeName = `${rightBinary.left.name}.${rightBinary.right.name}`;
|
|
532
|
+
} else {
|
|
533
|
+
throw new Error('is operator requires a type name as right operand');
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
throw new Error('is operator requires a type name as right operand');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const evaluator = this.operationEvaluators.get('is');
|
|
540
|
+
if (evaluator) {
|
|
541
|
+
return await evaluator(input, context, leftResult.value, [typeName]);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
460
545
|
// Get operation evaluator
|
|
461
546
|
const evaluator = this.operationEvaluators.get(operator);
|
|
462
547
|
if (evaluator) {
|
|
@@ -541,7 +626,7 @@ export class Interpreter {
|
|
|
541
626
|
private async evaluateUnary(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
|
|
542
627
|
const unary = node as UnaryNode;
|
|
543
628
|
const operator = unary.operator;
|
|
544
|
-
|
|
629
|
+
|
|
545
630
|
const operandResult = await this.evaluate(unary.operand, input, context);
|
|
546
631
|
|
|
547
632
|
// Check for unary operation evaluators
|
|
@@ -566,7 +651,7 @@ export class Interpreter {
|
|
|
566
651
|
const name = variable.name;
|
|
567
652
|
|
|
568
653
|
const value = RuntimeContextManager.getVariable(context, name);
|
|
569
|
-
|
|
654
|
+
|
|
570
655
|
if (value !== undefined) {
|
|
571
656
|
// Ensure value is always an array
|
|
572
657
|
const arrayValue = Array.isArray(value) ? value : [value];
|
|
@@ -599,7 +684,7 @@ export class Interpreter {
|
|
|
599
684
|
|
|
600
685
|
// Get the function definition to check if it propagates empty
|
|
601
686
|
const functionDef = this.registry.getFunction(funcName);
|
|
602
|
-
|
|
687
|
+
|
|
603
688
|
// Check if function is registered with an evaluator
|
|
604
689
|
const functionEvaluator = this.functionEvaluators.get(funcName);
|
|
605
690
|
if (!functionEvaluator) {
|
|
@@ -702,13 +787,13 @@ export class Interpreter {
|
|
|
702
787
|
private async evaluateMembershipTest(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
|
|
703
788
|
const test = node as MembershipTestNode;
|
|
704
789
|
const exprResult = await this.evaluate(test.expression, input, context);
|
|
705
|
-
|
|
790
|
+
|
|
706
791
|
// Use the is-operator implementation for consistency
|
|
707
792
|
const isOperator = this.operationEvaluators.get('is');
|
|
708
793
|
if (isOperator) {
|
|
709
794
|
return isOperator(input, context, exprResult.value, [test.targetType]);
|
|
710
795
|
}
|
|
711
|
-
|
|
796
|
+
|
|
712
797
|
// Fallback - shouldn't reach here normally
|
|
713
798
|
return { value: [], context };
|
|
714
799
|
}
|
|
@@ -717,17 +802,17 @@ export class Interpreter {
|
|
|
717
802
|
private async evaluateTypeCast(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
|
|
718
803
|
const cast = node as TypeCastNode;
|
|
719
804
|
const exprResult = await this.evaluate(cast.expression, input, context);
|
|
720
|
-
|
|
805
|
+
|
|
721
806
|
// Use the as-operator implementation for consistency
|
|
722
807
|
const asOperator = this.operationEvaluators.get('as');
|
|
723
808
|
if (asOperator) {
|
|
724
809
|
return asOperator(input, context, exprResult.value, [cast.targetType]);
|
|
725
810
|
}
|
|
726
|
-
|
|
811
|
+
|
|
727
812
|
// Fallback implementation (shouldn't normally reach here)
|
|
728
813
|
return { value: [], context };
|
|
729
814
|
}
|
|
730
|
-
|
|
815
|
+
|
|
731
816
|
private async evaluateQuantity(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): Promise<EvaluationResult> {
|
|
732
817
|
const quantity = node as QuantityNode;
|
|
733
818
|
const quantityValue = createQuantity(quantity.value, quantity.unit);
|
package/src/lexer.ts
CHANGED
|
@@ -674,10 +674,13 @@ export class Lexer {
|
|
|
674
674
|
if (this.current() === '*' && this.peek() === '/') {
|
|
675
675
|
this.advance(); // Skip *
|
|
676
676
|
this.advance(); // Skip /
|
|
677
|
-
|
|
677
|
+
return;
|
|
678
678
|
}
|
|
679
679
|
this.advance();
|
|
680
680
|
}
|
|
681
|
+
|
|
682
|
+
// If we reached the end without finding */, it's an error
|
|
683
|
+
throw this.error('Unclosed multi-line comment');
|
|
681
684
|
}
|
|
682
685
|
|
|
683
686
|
private advance(): void {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export * from './model-provider.common'
|
|
2
|
+
import { FHIRModelProviderBase, type Resource } from './model-provider.common';
|
|
3
|
+
|
|
4
|
+
export type Resolver = (canonicalUrl: string) => Promise<Resource | null>;
|
|
5
|
+
export type Searcher = (kind: 'primitive-type' | 'complex-type' | 'resource') => Promise<Resource[]>
|
|
6
|
+
|
|
7
|
+
export type Options = {
|
|
8
|
+
resolve: Resolver,
|
|
9
|
+
search: Searcher
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class FHIRModelProvider extends FHIRModelProviderBase {
|
|
13
|
+
private _resolve: Resolver;
|
|
14
|
+
private _search: Searcher;
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
override async resolve(canonicalUrl: string): Promise<Resource | null> {
|
|
18
|
+
return await this._resolve(canonicalUrl);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override async search(params: { kind: 'primitive-type' | 'complex-type' | 'resource' }): Promise<Resource[]> {
|
|
22
|
+
return await this._search(params.kind);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
reconfigure(options: Options) {
|
|
26
|
+
this._resolve = options.resolve;
|
|
27
|
+
this._search = options.search;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
constructor(options: Options) {
|
|
31
|
+
super();
|
|
32
|
+
this._resolve = options.resolve;
|
|
33
|
+
this._search = options.search;
|
|
34
|
+
}
|
|
35
|
+
}
|