@atomic-ehr/fhirpath 0.0.1-canary.8687028.20250724113707
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/README.md +307 -0
- package/dist/index.js +7975 -0
- package/package.json +48 -0
- package/src/analyzer/analyzer.ts +486 -0
- package/src/analyzer/model-provider.ts +244 -0
- package/src/analyzer/schemas/index.ts +2 -0
- package/src/analyzer/schemas/types.ts +40 -0
- package/src/analyzer/types.ts +142 -0
- package/src/api/builder.ts +148 -0
- package/src/api/errors.ts +134 -0
- package/src/api/expression.ts +149 -0
- package/src/api/index.ts +57 -0
- package/src/api/registry.ts +128 -0
- package/src/api/types.ts +154 -0
- package/src/compiler/compiler.ts +550 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/types.ts +23 -0
- package/src/index.ts +52 -0
- package/src/interpreter/README.md +78 -0
- package/src/interpreter/context.ts +181 -0
- package/src/interpreter/interpreter.ts +429 -0
- package/src/interpreter/types.ts +132 -0
- package/src/lexer/char-tables.ts +37 -0
- package/src/lexer/errors.ts +31 -0
- package/src/lexer/index.ts +5 -0
- package/src/lexer/lexer.ts +745 -0
- package/src/lexer/token.ts +104 -0
- package/src/parser/ast.ts +123 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/parser.ts +701 -0
- package/src/parser/pprint.ts +169 -0
- package/src/registry/default-analyzers.ts +257 -0
- package/src/registry/default-compilers.ts +31 -0
- package/src/registry/index.ts +93 -0
- package/src/registry/operations/arithmetic.ts +506 -0
- package/src/registry/operations/collection.ts +384 -0
- package/src/registry/operations/comparison.ts +432 -0
- package/src/registry/operations/existence.ts +719 -0
- package/src/registry/operations/filtering.ts +374 -0
- package/src/registry/operations/literals.ts +341 -0
- package/src/registry/operations/logical.ts +402 -0
- package/src/registry/operations/math.ts +128 -0
- package/src/registry/operations/membership.ts +132 -0
- package/src/registry/operations/string.ts +507 -0
- package/src/registry/operations/subsetting.ts +174 -0
- package/src/registry/operations/type-checking.ts +162 -0
- package/src/registry/operations/type-conversion.ts +404 -0
- package/src/registry/operations/type-operators.ts +307 -0
- package/src/registry/operations/utility.ts +510 -0
- package/src/registry/registry.ts +146 -0
- package/src/registry/types.ts +162 -0
- package/src/registry/utils/evaluation-helpers.ts +93 -0
- package/src/registry/utils/index.ts +3 -0
- package/src/registry/utils/type-system.ts +173 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { Context } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context management utilities for FHIRPath interpreter.
|
|
5
|
+
* Context flows parallel to data through expressions.
|
|
6
|
+
*
|
|
7
|
+
* Uses JavaScript prototype chain for efficient inheritance.
|
|
8
|
+
*/
|
|
9
|
+
export class ContextManager {
|
|
10
|
+
/**
|
|
11
|
+
* Create a new empty context
|
|
12
|
+
*/
|
|
13
|
+
static create(initialInput?: any[]): Context {
|
|
14
|
+
// Create base context with null prototype to avoid Object.prototype pollution
|
|
15
|
+
const context = Object.create(null) as Context;
|
|
16
|
+
|
|
17
|
+
// Initialize with prototype-based objects
|
|
18
|
+
context.variables = Object.create(null);
|
|
19
|
+
context.env = Object.create(null);
|
|
20
|
+
|
|
21
|
+
// Set root variables
|
|
22
|
+
context.$context = initialInput;
|
|
23
|
+
context.$resource = initialInput;
|
|
24
|
+
context.$rootResource = initialInput;
|
|
25
|
+
|
|
26
|
+
return context;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a child context inheriting from parent via prototype chain
|
|
31
|
+
* O(1) operation - no copying needed
|
|
32
|
+
*/
|
|
33
|
+
static copy(context: Context): Context {
|
|
34
|
+
// Create child context with parent as prototype
|
|
35
|
+
const newContext = Object.create(context) as Context;
|
|
36
|
+
|
|
37
|
+
// Create child objects that inherit from parent's objects
|
|
38
|
+
newContext.variables = Object.create(context.variables);
|
|
39
|
+
newContext.env = Object.create(context.env);
|
|
40
|
+
|
|
41
|
+
// Root variables are inherited automatically through prototype chain
|
|
42
|
+
// No need to copy them unless they change
|
|
43
|
+
|
|
44
|
+
return newContext;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Add or update a variable in context
|
|
49
|
+
* Only sets on current context level, shadowing parent values
|
|
50
|
+
*/
|
|
51
|
+
static setVariable(context: Context, name: string, value: any[]): Context {
|
|
52
|
+
const newContext = this.copy(context);
|
|
53
|
+
newContext.variables[name] = value;
|
|
54
|
+
return newContext;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get a variable value from context
|
|
59
|
+
* Uses prototype chain for lookup
|
|
60
|
+
*/
|
|
61
|
+
static getVariable(context: Context, name: string): any[] | undefined {
|
|
62
|
+
// Check user-defined variables first (prototype chain handles inheritance)
|
|
63
|
+
if (name in context.variables) {
|
|
64
|
+
return context.variables[name];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check special root variables
|
|
68
|
+
switch (name) {
|
|
69
|
+
case 'context':
|
|
70
|
+
return context.$context;
|
|
71
|
+
case 'resource':
|
|
72
|
+
return context.$resource;
|
|
73
|
+
case 'rootResource':
|
|
74
|
+
return context.$rootResource;
|
|
75
|
+
default:
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set iterator context ($this, $index) - used by where(), select(), etc.
|
|
82
|
+
*/
|
|
83
|
+
static setIteratorContext(
|
|
84
|
+
context: Context,
|
|
85
|
+
item: any,
|
|
86
|
+
index: number
|
|
87
|
+
): Context {
|
|
88
|
+
const newContext = this.copy(context);
|
|
89
|
+
// Only set changed values - prototype provides the rest
|
|
90
|
+
newContext.env.$this = [item];
|
|
91
|
+
newContext.env.$index = index;
|
|
92
|
+
return newContext;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Set aggregate context ($total) - used by aggregate()
|
|
97
|
+
*/
|
|
98
|
+
static setAggregateContext(
|
|
99
|
+
context: Context,
|
|
100
|
+
total: any[]
|
|
101
|
+
): Context {
|
|
102
|
+
const newContext = this.copy(context);
|
|
103
|
+
newContext.env.$total = total;
|
|
104
|
+
return newContext;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clear iterator/aggregate context - restore to original
|
|
109
|
+
*/
|
|
110
|
+
static clearEnv(context: Context): Context {
|
|
111
|
+
const newContext = this.copy(context);
|
|
112
|
+
// Create fresh env object, effectively hiding parent's env values
|
|
113
|
+
newContext.env = Object.create(null);
|
|
114
|
+
return newContext;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get special environment variable
|
|
119
|
+
*/
|
|
120
|
+
static getEnvVariable(context: Context, name: '$this' | '$index' | '$total'): any {
|
|
121
|
+
switch (name) {
|
|
122
|
+
case '$this':
|
|
123
|
+
return context.env.$this;
|
|
124
|
+
case '$index':
|
|
125
|
+
return context.env.$index;
|
|
126
|
+
case '$total':
|
|
127
|
+
return context.env.$total;
|
|
128
|
+
default:
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if a variable exists in context (including inherited)
|
|
135
|
+
*/
|
|
136
|
+
static hasVariable(context: Context, name: string): boolean {
|
|
137
|
+
return (name in context.variables) ||
|
|
138
|
+
['context', 'resource', 'rootResource'].includes(name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Debug helper - get all variables including inherited ones
|
|
143
|
+
* Traverses prototype chain to collect all variables
|
|
144
|
+
*/
|
|
145
|
+
static getAllVariables(context: Context): Record<string, any[]> {
|
|
146
|
+
const result: Record<string, any[]> = {};
|
|
147
|
+
|
|
148
|
+
// Traverse prototype chain for user variables
|
|
149
|
+
let currentVars = context.variables;
|
|
150
|
+
while (currentVars) {
|
|
151
|
+
for (const key in currentVars) {
|
|
152
|
+
if (!(key in result) && Object.prototype.hasOwnProperty.call(currentVars, key)) {
|
|
153
|
+
result[`%${key}`] = currentVars[key]!;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
currentVars = Object.getPrototypeOf(currentVars);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Root variables
|
|
160
|
+
if (context.$context) result['%context'] = context.$context;
|
|
161
|
+
if (context.$resource) result['%resource'] = context.$resource;
|
|
162
|
+
if (context.$rootResource) result['%rootResource'] = context.$rootResource;
|
|
163
|
+
|
|
164
|
+
// Environment variables (also traverse prototype chain)
|
|
165
|
+
let currentEnv = context.env;
|
|
166
|
+
while (currentEnv) {
|
|
167
|
+
if (currentEnv.$this !== undefined && !('$this' in result)) {
|
|
168
|
+
result['$this'] = currentEnv.$this;
|
|
169
|
+
}
|
|
170
|
+
if (currentEnv.$index !== undefined && !('$index' in result)) {
|
|
171
|
+
result['$index'] = [currentEnv.$index];
|
|
172
|
+
}
|
|
173
|
+
if (currentEnv.$total !== undefined && !('$total' in result)) {
|
|
174
|
+
result['$total'] = currentEnv.$total;
|
|
175
|
+
}
|
|
176
|
+
currentEnv = Object.getPrototypeOf(currentEnv);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ASTNode,
|
|
3
|
+
LiteralNode,
|
|
4
|
+
IdentifierNode,
|
|
5
|
+
VariableNode,
|
|
6
|
+
BinaryNode,
|
|
7
|
+
UnaryNode,
|
|
8
|
+
FunctionNode,
|
|
9
|
+
CollectionNode,
|
|
10
|
+
IndexNode,
|
|
11
|
+
UnionNode,
|
|
12
|
+
MembershipTestNode,
|
|
13
|
+
TypeCastNode,
|
|
14
|
+
TypeReferenceNode,
|
|
15
|
+
TypeOrIdentifierNode
|
|
16
|
+
} from '../parser/ast';
|
|
17
|
+
import { NodeType } from '../parser/ast';
|
|
18
|
+
import { TokenType } from '../lexer/token';
|
|
19
|
+
import type { Context, EvaluationResult } from './types';
|
|
20
|
+
import { EvaluationError, CollectionUtils } from './types';
|
|
21
|
+
import { ContextManager } from './context';
|
|
22
|
+
import { TypeSystem } from '../registry/utils/type-system';
|
|
23
|
+
import { Registry } from '../registry';
|
|
24
|
+
import type { Interpreter as IInterpreter } from '../registry/types';
|
|
25
|
+
|
|
26
|
+
// Import registry to trigger operation registration
|
|
27
|
+
import '../registry';
|
|
28
|
+
|
|
29
|
+
// Type for node evaluator functions
|
|
30
|
+
type NodeEvaluator = (node: any, input: any[], context: Context) => EvaluationResult;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* FHIRPath Interpreter - evaluates AST nodes following the stream-processing model.
|
|
34
|
+
* Every node is a processing unit: (input, context) → (output, new context)
|
|
35
|
+
*
|
|
36
|
+
* This refactored version uses object lookup instead of switch statements.
|
|
37
|
+
*/
|
|
38
|
+
export class Interpreter implements IInterpreter {
|
|
39
|
+
// Object lookup for node evaluators
|
|
40
|
+
private readonly nodeEvaluators: Record<NodeType, NodeEvaluator> = {
|
|
41
|
+
[NodeType.Literal]: this.evaluateLiteral.bind(this),
|
|
42
|
+
[NodeType.Identifier]: this.evaluateIdentifier.bind(this),
|
|
43
|
+
[NodeType.TypeOrIdentifier]: this.evaluateTypeOrIdentifier.bind(this),
|
|
44
|
+
[NodeType.Variable]: this.evaluateVariable.bind(this),
|
|
45
|
+
[NodeType.Binary]: this.evaluateBinary.bind(this),
|
|
46
|
+
[NodeType.Unary]: this.evaluateUnary.bind(this),
|
|
47
|
+
[NodeType.Function]: this.evaluateFunction.bind(this),
|
|
48
|
+
[NodeType.Collection]: this.evaluateCollection.bind(this),
|
|
49
|
+
[NodeType.Index]: this.evaluateIndex.bind(this),
|
|
50
|
+
[NodeType.Union]: this.evaluateUnion.bind(this),
|
|
51
|
+
[NodeType.MembershipTest]: this.evaluateMembershipTest.bind(this),
|
|
52
|
+
[NodeType.TypeCast]: this.evaluateTypeCast.bind(this),
|
|
53
|
+
[NodeType.TypeReference]: this.evaluateTypeReference.bind(this),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Main evaluation method - uses object lookup instead of switch
|
|
58
|
+
*/
|
|
59
|
+
evaluate(node: ASTNode, input: any[], context: Context): EvaluationResult {
|
|
60
|
+
try {
|
|
61
|
+
// Ensure $this is set in the context if not already present
|
|
62
|
+
if (!context.env.$this) {
|
|
63
|
+
context = {
|
|
64
|
+
...context,
|
|
65
|
+
env: {
|
|
66
|
+
...context.env,
|
|
67
|
+
$this: input
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const evaluator = this.nodeEvaluators[node.type];
|
|
73
|
+
|
|
74
|
+
if (!evaluator) {
|
|
75
|
+
throw new EvaluationError(
|
|
76
|
+
`Unknown node type: ${node.type}`,
|
|
77
|
+
node.position
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return evaluator(node, input, context);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Add position information if not already present
|
|
84
|
+
if (error instanceof EvaluationError && !error.position && node.position) {
|
|
85
|
+
error.position = node.position;
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private evaluateLiteral(node: LiteralNode, input: any[], context: Context): EvaluationResult {
|
|
92
|
+
// If literal has operation reference from parser
|
|
93
|
+
if (node.operation && node.operation.kind === 'literal') {
|
|
94
|
+
return node.operation.evaluate(this, context, input);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Fallback for legacy literals
|
|
98
|
+
const value = node.value === null ? [] : [node.value];
|
|
99
|
+
return { value, context };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private evaluateIdentifier(node: IdentifierNode, input: any[], context: Context): EvaluationResult {
|
|
103
|
+
// Identifier performs property navigation on each item in input
|
|
104
|
+
const results: any[] = [];
|
|
105
|
+
|
|
106
|
+
for (const item of input) {
|
|
107
|
+
if (item == null || typeof item !== 'object') {
|
|
108
|
+
// Primitives don't have properties - skip
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const value = item[node.name];
|
|
113
|
+
if (value !== undefined) {
|
|
114
|
+
// Add to results - flatten if array
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
results.push(...value);
|
|
117
|
+
} else {
|
|
118
|
+
results.push(value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Missing properties return empty (not added to results)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { value: results, context };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private evaluateTypeOrIdentifier(node: TypeOrIdentifierNode, input: any[], context: Context): EvaluationResult {
|
|
128
|
+
// TypeOrIdentifier can act as either a type reference or property navigation
|
|
129
|
+
// For now, treat it as an identifier
|
|
130
|
+
return this.evaluateIdentifier(node as any, input, context);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private evaluateVariable(node: VariableNode, input: any[], context: Context): EvaluationResult {
|
|
134
|
+
// Variables ignore input and return value from context
|
|
135
|
+
let value: any[] = [];
|
|
136
|
+
|
|
137
|
+
if (node.name.startsWith('$')) {
|
|
138
|
+
// Special environment variables - use object lookup
|
|
139
|
+
const envVarHandlers: Record<string, () => any[]> = {
|
|
140
|
+
'$this': () => context.env.$this || [],
|
|
141
|
+
'$index': () => context.env.$index !== undefined ? [context.env.$index] : [],
|
|
142
|
+
'$total': () => context.env.$total || [],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handler = envVarHandlers[node.name];
|
|
146
|
+
if (!handler) {
|
|
147
|
+
throw new EvaluationError(`Unknown special variable: ${node.name}`, node.position);
|
|
148
|
+
}
|
|
149
|
+
value = handler();
|
|
150
|
+
} else {
|
|
151
|
+
// User-defined variables (remove % prefix if present)
|
|
152
|
+
const varName = node.name.startsWith('%') ? node.name.substring(1) : node.name;
|
|
153
|
+
value = ContextManager.getVariable(context, varName) || [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { value, context };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private evaluateBinary(node: BinaryNode, input: any[], context: Context): EvaluationResult {
|
|
160
|
+
// Special handling for dot operator - it's a pipeline
|
|
161
|
+
if (node.operator === TokenType.DOT) {
|
|
162
|
+
// Phase 1: Evaluate left with original input/context
|
|
163
|
+
const leftResult = this.evaluate(node.left, input, context);
|
|
164
|
+
|
|
165
|
+
// Phase 2: Evaluate right with left's output as input
|
|
166
|
+
const rightResult = this.evaluate(node.right, leftResult.value, leftResult.context);
|
|
167
|
+
|
|
168
|
+
return rightResult;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Handle case where parser incorrectly creates BinaryNode for unary minus
|
|
172
|
+
if (!node.left && !node.right && (node as any).operand) {
|
|
173
|
+
// This is actually a unary operation
|
|
174
|
+
const unaryOp = Registry.getByToken(node.operator, 'prefix');
|
|
175
|
+
if (unaryOp && unaryOp.kind === 'operator') {
|
|
176
|
+
const operandResult = this.evaluate((node as any).operand, input, context);
|
|
177
|
+
return unaryOp.evaluate(this, operandResult.context, input, operandResult.value);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Get operation from registry (binary operators are infix)
|
|
182
|
+
const operation = node.operation || Registry.getByToken(node.operator, 'infix');
|
|
183
|
+
if (!operation || operation.kind !== 'operator') {
|
|
184
|
+
throw new EvaluationError(`Unknown operator: ${node.operator}`, node.position);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!node.left || !node.right) {
|
|
188
|
+
throw new EvaluationError(`Binary operator ${node.operator} missing operands`, node.position);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const leftResult = this.evaluate(node.left, input, context);
|
|
192
|
+
const rightResult = this.evaluate(node.right, input, leftResult.context);
|
|
193
|
+
|
|
194
|
+
// Use operation's evaluate method
|
|
195
|
+
return operation.evaluate(this, rightResult.context, input, leftResult.value, rightResult.value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private evaluateUnary(node: UnaryNode, input: any[], context: Context): EvaluationResult {
|
|
199
|
+
// Get operation from registry (unary operators are prefix)
|
|
200
|
+
// Don't use node.operation as parser might have assigned wrong operation
|
|
201
|
+
const operation = Registry.getByToken(node.operator, 'prefix');
|
|
202
|
+
if (!operation || operation.kind !== 'operator') {
|
|
203
|
+
throw new EvaluationError(`Unknown unary operator: ${node.operator}`, node.position);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Evaluate operand
|
|
207
|
+
const operandResult = this.evaluate(node.operand, input, context);
|
|
208
|
+
|
|
209
|
+
// Use operation's evaluate method
|
|
210
|
+
return operation.evaluate(this, operandResult.context, input, operandResult.value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private evaluateFunction(node: FunctionNode, input: any[], context: Context): EvaluationResult {
|
|
214
|
+
// Extract function name and handle method call syntax
|
|
215
|
+
let funcName: string;
|
|
216
|
+
let functionInput = input;
|
|
217
|
+
|
|
218
|
+
if (node.name.type === NodeType.Identifier) {
|
|
219
|
+
funcName = (node.name as IdentifierNode).name;
|
|
220
|
+
} else if (node.name.type === NodeType.Binary && (node.name as BinaryNode).operator === TokenType.DOT) {
|
|
221
|
+
// Method call syntax: expression.function(args)
|
|
222
|
+
const binaryNode = node.name as BinaryNode;
|
|
223
|
+
|
|
224
|
+
// Evaluate the left side to get the input
|
|
225
|
+
const leftResult = this.evaluate(binaryNode.left, input, context);
|
|
226
|
+
functionInput = leftResult.value;
|
|
227
|
+
context = leftResult.context;
|
|
228
|
+
|
|
229
|
+
// Get the function name from the right side
|
|
230
|
+
if (binaryNode.right.type === NodeType.Identifier) {
|
|
231
|
+
funcName = (binaryNode.right as IdentifierNode).name;
|
|
232
|
+
} else {
|
|
233
|
+
throw new EvaluationError('Invalid method call syntax', node.position);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
throw new EvaluationError('Complex function names not yet supported', node.position);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check for custom functions first
|
|
240
|
+
if (context.customFunctions && funcName in context.customFunctions) {
|
|
241
|
+
const customFunc = context.customFunctions[funcName];
|
|
242
|
+
|
|
243
|
+
// Evaluate all arguments
|
|
244
|
+
const evaluatedArgs: any[] = [];
|
|
245
|
+
for (const arg of node.arguments) {
|
|
246
|
+
const argResult = this.evaluate(arg, functionInput, context);
|
|
247
|
+
evaluatedArgs.push(argResult.value);
|
|
248
|
+
context = argResult.context;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Call custom function
|
|
252
|
+
const result = customFunc!(context, functionInput, ...evaluatedArgs);
|
|
253
|
+
return { value: result, context };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Get function from registry
|
|
257
|
+
const operation = Registry.get(funcName);
|
|
258
|
+
if (!operation || operation.kind !== 'function') {
|
|
259
|
+
throw new EvaluationError(`Unknown function: ${funcName}`, node.position);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check propagateEmptyInput flag
|
|
263
|
+
if (operation.signature.propagatesEmpty && functionInput.length === 0) {
|
|
264
|
+
return { value: [], context };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Evaluate arguments based on parameter definitions
|
|
268
|
+
const evaluatedArgs: any[] = [];
|
|
269
|
+
for (let i = 0; i < node.arguments.length; i++) {
|
|
270
|
+
const arg = node.arguments[i];
|
|
271
|
+
const param = operation.signature.parameters[i];
|
|
272
|
+
|
|
273
|
+
if (param && param.kind === 'expression') {
|
|
274
|
+
// Pass expression as-is, will be evaluated by the function
|
|
275
|
+
evaluatedArgs.push(arg);
|
|
276
|
+
} else {
|
|
277
|
+
// Evaluate the argument to get its value
|
|
278
|
+
const argResult = this.evaluate(arg!, functionInput, context);
|
|
279
|
+
evaluatedArgs.push(argResult.value);
|
|
280
|
+
context = argResult.context;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Use operation's evaluate method
|
|
285
|
+
return operation.evaluate(this, context, functionInput, ...evaluatedArgs);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private evaluateCollection(node: CollectionNode, input: any[], context: Context): EvaluationResult {
|
|
289
|
+
// Evaluate each element and combine results
|
|
290
|
+
const results: any[] = [];
|
|
291
|
+
let currentContext = context;
|
|
292
|
+
|
|
293
|
+
for (const element of node.elements) {
|
|
294
|
+
const result = this.evaluate(element, input, currentContext);
|
|
295
|
+
results.push(...result.value);
|
|
296
|
+
currentContext = result.context;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { value: results, context: currentContext };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private evaluateIndex(node: IndexNode, input: any[], context: Context): EvaluationResult {
|
|
303
|
+
// Evaluate the expression being indexed
|
|
304
|
+
const exprResult = this.evaluate(node.expression, input, context);
|
|
305
|
+
|
|
306
|
+
// Evaluate the index expression in the original context
|
|
307
|
+
const indexResult = this.evaluate(node.index, input, context);
|
|
308
|
+
|
|
309
|
+
// Index must be a single integer
|
|
310
|
+
if (indexResult.value.length === 0) {
|
|
311
|
+
return { value: [], context: indexResult.context };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const index = CollectionUtils.toSingleton(indexResult.value);
|
|
315
|
+
if (typeof index !== 'number' || !Number.isInteger(index)) {
|
|
316
|
+
throw new EvaluationError('Index must be an integer', node.position);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// FHIRPath uses 0-based indexing
|
|
320
|
+
if (index < 0 || index >= exprResult.value.length) {
|
|
321
|
+
// Out of bounds returns empty
|
|
322
|
+
return { value: [], context: indexResult.context };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { value: [exprResult.value[index]], context: indexResult.context };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private evaluateUnion(node: UnionNode, input: any[], context: Context): EvaluationResult {
|
|
329
|
+
// Union combines results from all operands
|
|
330
|
+
const results: any[] = [];
|
|
331
|
+
let currentContext = context;
|
|
332
|
+
|
|
333
|
+
for (const operand of node.operands) {
|
|
334
|
+
const result = this.evaluate(operand, input, currentContext);
|
|
335
|
+
results.push(...result.value);
|
|
336
|
+
// Thread context through operands
|
|
337
|
+
currentContext = result.context;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { value: results, context: currentContext };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private evaluateMembershipTest(node: MembershipTestNode, input: any[], context: Context): EvaluationResult {
|
|
344
|
+
// Evaluate the expression to get values to test
|
|
345
|
+
const exprResult = this.evaluate(node.expression, input, context);
|
|
346
|
+
|
|
347
|
+
// Empty collection: is returns empty
|
|
348
|
+
if (exprResult.value.length === 0) {
|
|
349
|
+
return { value: [], context: exprResult.context };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Check if ALL values match the type
|
|
353
|
+
for (const value of exprResult.value) {
|
|
354
|
+
if (!TypeSystem.isType(value, node.targetType)) {
|
|
355
|
+
return { value: [false], context: exprResult.context };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// All values match the type
|
|
360
|
+
return { value: [true], context: exprResult.context };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private evaluateTypeCast(node: TypeCastNode, input: any[], context: Context): EvaluationResult {
|
|
364
|
+
// Evaluate the expression to get values to cast
|
|
365
|
+
const exprResult = this.evaluate(node.expression, input, context);
|
|
366
|
+
|
|
367
|
+
// For each value, attempt to cast to the target type
|
|
368
|
+
const results: any[] = [];
|
|
369
|
+
for (const value of exprResult.value) {
|
|
370
|
+
// If already the correct type, keep it
|
|
371
|
+
if (TypeSystem.isType(value, node.targetType)) {
|
|
372
|
+
results.push(value);
|
|
373
|
+
}
|
|
374
|
+
// Otherwise, try to cast (returns null if fails)
|
|
375
|
+
else {
|
|
376
|
+
const castValue = TypeSystem.cast(value, node.targetType);
|
|
377
|
+
if (castValue !== null) {
|
|
378
|
+
results.push(castValue);
|
|
379
|
+
}
|
|
380
|
+
// Failed casts are filtered out (not added to results)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Return filtered collection
|
|
385
|
+
return { value: results, context: exprResult.context };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private evaluateTypeReference(node: TypeReferenceNode, input: any[], context: Context): EvaluationResult {
|
|
389
|
+
// Type references don't evaluate to values directly
|
|
390
|
+
throw new EvaluationError(`Type reference cannot be evaluated: ${node.typeName}`, node.position);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Helper function to evaluate a FHIRPath expression
|
|
396
|
+
*/
|
|
397
|
+
export function evaluateFHIRPath(
|
|
398
|
+
expression: string | ASTNode,
|
|
399
|
+
input: any,
|
|
400
|
+
context?: Context
|
|
401
|
+
): any[] {
|
|
402
|
+
// Parse if string
|
|
403
|
+
const ast = typeof expression === 'string'
|
|
404
|
+
? require('../parser').parse(expression)
|
|
405
|
+
: expression;
|
|
406
|
+
|
|
407
|
+
// Convert input to collection
|
|
408
|
+
const inputCollection = CollectionUtils.toCollection(input);
|
|
409
|
+
|
|
410
|
+
// Create context if not provided and set initial $this
|
|
411
|
+
let evalContext = context || ContextManager.create(inputCollection);
|
|
412
|
+
|
|
413
|
+
// Set initial $this to the input collection if not already set
|
|
414
|
+
if (!evalContext.env.$this) {
|
|
415
|
+
evalContext = {
|
|
416
|
+
...evalContext,
|
|
417
|
+
env: {
|
|
418
|
+
...evalContext.env,
|
|
419
|
+
$this: inputCollection
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Create interpreter and evaluate
|
|
425
|
+
const interpreter = new Interpreter();
|
|
426
|
+
const result = interpreter.evaluate(ast, inputCollection, evalContext);
|
|
427
|
+
|
|
428
|
+
return result.value;
|
|
429
|
+
}
|