@atomic-ehr/fhirpath 0.0.2 → 0.0.3
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 +716 -238
- package/dist/index.d.ts +225 -119
- package/dist/index.js +10911 -5600
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +921 -1208
- package/src/completion-provider.ts +209 -191
- package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
- package/src/complex-types/temporal.ts +1737 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +435 -469
- package/src/lexer.ts +188 -210
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +58 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +692 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +116 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +20 -3
- package/src/operations/greater-or-equal-operator.ts +20 -3
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +186 -7
- package/src/operations/implies-operator.ts +1 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +41 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +59 -0
- package/src/operations/is-operator.ts +20 -9
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +20 -3
- package/src/operations/less-or-equal-operator.ts +20 -3
- package/src/operations/less-than.ts +2 -2
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +69 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +1 -1
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +1 -1
- package/src/operations/ofType-function.ts +8 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +120 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +45 -3
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +248 -501
- package/src/registry.ts +53 -42
- package/src/types.ts +128 -16
- package/src/utils/pprint.ts +151 -0
package/src/model-provider.ts
CHANGED
|
@@ -172,7 +172,8 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
|
|
|
172
172
|
|
|
173
173
|
return schema;
|
|
174
174
|
} catch (error) {
|
|
175
|
-
|
|
175
|
+
// Silently fail for unknown types - this is expected for non-FHIR types
|
|
176
|
+
// like function names or property names that get speculatively checked
|
|
176
177
|
return undefined;
|
|
177
178
|
}
|
|
178
179
|
}
|
|
@@ -189,7 +190,8 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
|
|
|
189
190
|
let current = schema;
|
|
190
191
|
|
|
191
192
|
// Walk up the inheritance chain
|
|
192
|
-
|
|
193
|
+
// Include all base types including DomainResource, Resource, and Element
|
|
194
|
+
while (current.base) {
|
|
193
195
|
// Extract just the type name from the base URL if it's a full URL
|
|
194
196
|
let baseTypeName = current.base;
|
|
195
197
|
if (baseTypeName && baseTypeName.startsWith('http://')) {
|
|
@@ -277,10 +279,54 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
|
|
|
277
279
|
|
|
278
280
|
// Async implementation with lazy loading
|
|
279
281
|
async getType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined> {
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
+
// First check if this is a FHIRPath primitive type name
|
|
283
|
+
const fhirpathPrimitives = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time', 'Any'];
|
|
284
|
+
if (fhirpathPrimitives.includes(typeName)) {
|
|
285
|
+
// Return the FHIRPath primitive type directly without trying to load a schema
|
|
282
286
|
return {
|
|
283
|
-
type:
|
|
287
|
+
type: typeName as TypeName,
|
|
288
|
+
namespace: 'FHIR',
|
|
289
|
+
name: typeName,
|
|
290
|
+
singleton: true,
|
|
291
|
+
modelContext: {
|
|
292
|
+
path: typeName,
|
|
293
|
+
schemaHierarchy: []
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check if it's a FHIR primitive type that maps to a FHIRPath type
|
|
299
|
+
const mappedType = this.typeMapping[typeName];
|
|
300
|
+
if (mappedType) {
|
|
301
|
+
// For types that are both FHIRPath types and complex FHIR types (like Quantity),
|
|
302
|
+
// we need to check if they have a schema
|
|
303
|
+
// But skip primitive FHIRPath types that definitely don't have schemas
|
|
304
|
+
const primitiveTypes = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time'];
|
|
305
|
+
const isPrimitive = primitiveTypes.includes(mappedType);
|
|
306
|
+
|
|
307
|
+
if (this.initialized && !isPrimitive) {
|
|
308
|
+
const schema = await this.getSchema(typeName);
|
|
309
|
+
if (schema) {
|
|
310
|
+
// It's a complex type with properties
|
|
311
|
+
const schemaHierarchy = await this.getSchemaHierarchyAsync(schema);
|
|
312
|
+
return {
|
|
313
|
+
type: mappedType,
|
|
314
|
+
namespace: 'FHIR',
|
|
315
|
+
name: typeName,
|
|
316
|
+
singleton: true,
|
|
317
|
+
modelContext: {
|
|
318
|
+
path: typeName,
|
|
319
|
+
schemaHierarchy,
|
|
320
|
+
canonicalUrl: schema.url,
|
|
321
|
+
version: schema.version
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// It's a pure primitive type without properties
|
|
328
|
+
return {
|
|
329
|
+
type: mappedType,
|
|
284
330
|
namespace: 'FHIR',
|
|
285
331
|
name: typeName,
|
|
286
332
|
singleton: true,
|
|
@@ -324,6 +370,26 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
|
|
|
324
370
|
const context = parentType.modelContext;
|
|
325
371
|
if (!context) return undefined;
|
|
326
372
|
|
|
373
|
+
// Handle union types (polymorphic fields like value[x])
|
|
374
|
+
if (context.isUnion && context.choices) {
|
|
375
|
+
// For union types, check if the property exists on any of the choice types
|
|
376
|
+
for (const choice of context.choices) {
|
|
377
|
+
// Get the type for this choice - use type field which has the proper case
|
|
378
|
+
const choiceTypeName = choice.type;
|
|
379
|
+
const choiceType = await this.getType(choiceTypeName);
|
|
380
|
+
if (choiceType) {
|
|
381
|
+
// Try to get the property from this choice type
|
|
382
|
+
const elementType = await this.getElementType(choiceType, propertyName);
|
|
383
|
+
if (elementType) {
|
|
384
|
+
// Found the property on this choice type
|
|
385
|
+
return elementType;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Property not found on any choice type
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
327
393
|
// Search through schema hierarchy for the property
|
|
328
394
|
for (const schema of context.schemaHierarchy) {
|
|
329
395
|
const element = schema.elements?.[propertyName];
|
|
@@ -609,42 +675,4 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
|
|
|
609
675
|
return this.primitiveTypesCache || [];
|
|
610
676
|
}
|
|
611
677
|
|
|
612
|
-
// Synchronous method to get type from cache (for analyzer)
|
|
613
|
-
getTypeFromCache(typeName: string): TypeInfo<FHIRModelContext> | undefined {
|
|
614
|
-
// Check if it's a primitive type - these don't require initialization
|
|
615
|
-
if (this.typeMapping[typeName]) {
|
|
616
|
-
return {
|
|
617
|
-
type: this.typeMapping[typeName],
|
|
618
|
-
namespace: 'FHIR',
|
|
619
|
-
name: typeName,
|
|
620
|
-
singleton: true,
|
|
621
|
-
modelContext: {
|
|
622
|
-
path: typeName,
|
|
623
|
-
schemaHierarchy: []
|
|
624
|
-
}
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// For complex types, check if schema is in cache
|
|
629
|
-
const schema = this.schemaCache.get(typeName);
|
|
630
|
-
if (!schema) {
|
|
631
|
-
return undefined;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Get cached hierarchy or at least the current schema
|
|
635
|
-
const schemaHierarchy = this.hierarchyCache.get(schema.name || schema.url) || [schema];
|
|
636
|
-
|
|
637
|
-
return {
|
|
638
|
-
type: 'Any', // Complex types are 'Any' in FHIRPath
|
|
639
|
-
namespace: 'FHIR',
|
|
640
|
-
name: typeName,
|
|
641
|
-
singleton: true,
|
|
642
|
-
modelContext: {
|
|
643
|
-
path: typeName,
|
|
644
|
-
schemaHierarchy,
|
|
645
|
-
canonicalUrl: schema.url,
|
|
646
|
-
version: schema.version
|
|
647
|
-
}
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
678
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
// abs() takes no arguments
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
1
|
+
import type { FunctionDefinition, FunctionEvaluator, TypeInfo } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { RuntimeContextManager } from '../interpreter';
|
|
4
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { RuntimeContextManager } from '../interpreter/runtime-context';
|
|
4
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
5
5
|
|
|
6
6
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
7
7
|
// aggregator expression is required
|
|
@@ -21,6 +21,11 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
21
21
|
total = [];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// If input is empty and init is provided, return the init value
|
|
25
|
+
if (input.length === 0 && initExpr) {
|
|
26
|
+
return { value: total, context };
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
// For each item in the input collection, evaluate the aggregator expression
|
|
25
30
|
for (let index = 0; index < input.length; index++) {
|
|
26
31
|
const item = input[index]!;
|
|
@@ -46,6 +51,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
46
51
|
|
|
47
52
|
export const aggregateFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
48
53
|
name: 'aggregate',
|
|
54
|
+
doesNotPropagateEmpty: true, // aggregate with init should return init value for empty input
|
|
49
55
|
category: ['aggregates'],
|
|
50
56
|
description: 'Performs general-purpose aggregation by evaluating the aggregator expression for each element of the input collection',
|
|
51
57
|
examples: [
|
|
@@ -63,5 +69,78 @@ export const aggregateFunction: FunctionDefinition & { evaluate: FunctionEvaluat
|
|
|
63
69
|
],
|
|
64
70
|
result: { type: 'Any', singleton: false }
|
|
65
71
|
}],
|
|
66
|
-
evaluate
|
|
67
|
-
|
|
72
|
+
evaluate,
|
|
73
|
+
async inferResultType(analyzer, node, inputType) {
|
|
74
|
+
// If init parameter is provided, use its type to infer result type
|
|
75
|
+
if (node.arguments.length >= 2) {
|
|
76
|
+
const initType = await (analyzer as any).inferType(node.arguments[1]!, inputType);
|
|
77
|
+
// The result type is the same as init type
|
|
78
|
+
return initType;
|
|
79
|
+
}
|
|
80
|
+
// Without init, we can't fully infer the type without running annotation
|
|
81
|
+
// This is a limitation - the actual type will be set during annotateAST
|
|
82
|
+
if (node.arguments.length >= 1) {
|
|
83
|
+
// We could try to infer, but it would require setting up system variables
|
|
84
|
+
// For now, return Any and let annotateAST handle proper typing
|
|
85
|
+
return { type: 'Any', singleton: false };
|
|
86
|
+
}
|
|
87
|
+
// No arguments at all
|
|
88
|
+
return { type: 'Any', singleton: false };
|
|
89
|
+
},
|
|
90
|
+
async analyze(context, args) {
|
|
91
|
+
const diagnostics: any[] = [];
|
|
92
|
+
const itemType = { ...context.inputType, singleton: true };
|
|
93
|
+
|
|
94
|
+
// Determine $total type: from init (arg[1]) if provided; otherwise from aggregator result after first iteration (approximate with Any)
|
|
95
|
+
let totalType = { type: 'Any', singleton: false } as TypeInfo;
|
|
96
|
+
|
|
97
|
+
if (args.length >= 2 && args[1]) {
|
|
98
|
+
const initResult = await context
|
|
99
|
+
.withSystemVariable('$this', itemType)
|
|
100
|
+
.withSystemVariable('$index', { type: 'Integer', singleton: true })
|
|
101
|
+
.analyzeNode(args[1]!);
|
|
102
|
+
diagnostics.push(...initResult.diagnostics);
|
|
103
|
+
totalType = initResult.type;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Analyze aggregator with $this (item) and $total (init or inferred seed)
|
|
107
|
+
if (args.length >= 1 && args[0]) {
|
|
108
|
+
// If we don't have init, seed $total with a heuristic:
|
|
109
|
+
// - If aggregator contains string operations, seed as String
|
|
110
|
+
// - Else seed as item type
|
|
111
|
+
const containsStringHints = (function hasStringHints(node: any): boolean {
|
|
112
|
+
if (!node) return false;
|
|
113
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return true;
|
|
114
|
+
if (node.type === 'Function' && node.name?.type === 'Identifier' && node.name.name === 'toString') return true;
|
|
115
|
+
if (node.children) return node.children.some((c: any) => hasStringHints(c));
|
|
116
|
+
if (node.arguments) return (node.arguments as any[]).some(a => hasStringHints(a));
|
|
117
|
+
if (node.left && node.right) return hasStringHints(node.left) || hasStringHints(node.right);
|
|
118
|
+
if (node.expression) return hasStringHints(node.expression);
|
|
119
|
+
return false;
|
|
120
|
+
})(args[0]);
|
|
121
|
+
|
|
122
|
+
const seededTotal = (args.length < 2)
|
|
123
|
+
? (containsStringHints ? { type: 'String', singleton: true } as TypeInfo : itemType)
|
|
124
|
+
: totalType;
|
|
125
|
+
let aggregatorCtx = context
|
|
126
|
+
.withSystemVariable('$this', itemType)
|
|
127
|
+
.withSystemVariable('$index', { type: 'Integer', singleton: true })
|
|
128
|
+
.withSystemVariable('$total', seededTotal);
|
|
129
|
+
|
|
130
|
+
const firstPass = await aggregatorCtx.analyzeNode(args[0]!);
|
|
131
|
+
diagnostics.push(...firstPass.diagnostics);
|
|
132
|
+
|
|
133
|
+
// If no init provided, refine $total type to aggregator result and re-analyze aggregator
|
|
134
|
+
if (args.length < 2) {
|
|
135
|
+
aggregatorCtx = aggregatorCtx.withSystemVariable('$total', firstPass.type);
|
|
136
|
+
const secondPass = await aggregatorCtx.analyzeNode(args[0]!);
|
|
137
|
+
diagnostics.push(...secondPass.diagnostics);
|
|
138
|
+
return { type: secondPass.type, diagnostics, context };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { type: firstPass.type, diagnostics, context };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { type: context.inputType, diagnostics, context };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { RuntimeContextManager } from '../interpreter';
|
|
4
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { RuntimeContextManager } from '../interpreter/runtime-context';
|
|
4
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
5
5
|
|
|
6
6
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
7
7
|
// all() requires exactly one argument (the criteria expression)
|
|
@@ -51,6 +51,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
51
51
|
|
|
52
52
|
export const allFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
53
53
|
name: 'all',
|
|
54
|
+
doesNotPropagateEmpty: true, // Returns true for empty collections
|
|
54
55
|
category: ['existence'],
|
|
55
56
|
description: 'Returns true if for every element in the input collection, criteria evaluates to true. Otherwise, the result is false. If the input collection is empty, the result is true.',
|
|
56
57
|
examples: [
|
|
@@ -66,4 +67,4 @@ export const allFunction: FunctionDefinition & { evaluate: FunctionEvaluator } =
|
|
|
66
67
|
result: { type: 'Boolean', singleton: true }
|
|
67
68
|
}],
|
|
68
69
|
evaluate
|
|
69
|
-
};
|
|
70
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
// If the input is empty, the result is true
|
|
@@ -24,6 +24,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
24
24
|
|
|
25
25
|
export const allFalseFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
26
26
|
name: 'allFalse',
|
|
27
|
+
doesNotPropagateEmpty: true,
|
|
27
28
|
category: ['existence'],
|
|
28
29
|
description: 'Takes a collection of Boolean values and returns true if all the items are false. If any items are true, the result is false. If the input is empty, the result is true.',
|
|
29
30
|
examples: [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
// If the input is empty, the result is true
|
|
@@ -24,6 +24,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
24
24
|
|
|
25
25
|
export const allTrueFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
26
26
|
name: 'allTrue',
|
|
27
|
+
doesNotPropagateEmpty: true,
|
|
27
28
|
category: ['existence'],
|
|
28
29
|
description: 'Takes a collection of Boolean values and returns true if all the items are true. If any items are false, the result is false. If the input is empty, the result is true.',
|
|
29
30
|
examples: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
5
5
|
|
|
6
6
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
7
7
|
// Three-valued logic implementation
|
|
@@ -30,6 +30,7 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
30
30
|
export const andOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
|
|
31
31
|
symbol: 'and',
|
|
32
32
|
name: 'and',
|
|
33
|
+
doesNotPropagateEmpty: true, // Three-valued logic: false and empty = false
|
|
33
34
|
category: ['logical'],
|
|
34
35
|
precedence: PRECEDENCE.AND,
|
|
35
36
|
associativity: 'left',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
|
-
import { box, unbox } from '../boxing';
|
|
2
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
3
3
|
|
|
4
4
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
5
5
|
// Empty input returns false per spec
|
|
@@ -24,6 +24,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
24
24
|
|
|
25
25
|
export const anyFalseFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
26
26
|
name: 'anyFalse',
|
|
27
|
+
doesNotPropagateEmpty: true,
|
|
27
28
|
category: ['existence'],
|
|
28
29
|
description: 'Takes a collection of Boolean values and returns true if any of the items are false. If all the items are true, or if the input is empty, the result is false.',
|
|
29
30
|
examples: [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
|
-
import { box, unbox } from '../boxing';
|
|
2
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
3
3
|
|
|
4
4
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
5
5
|
// Empty input returns false per spec
|
|
@@ -24,6 +24,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
24
24
|
|
|
25
25
|
export const anyTrueFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
26
26
|
name: 'anyTrue',
|
|
27
|
+
doesNotPropagateEmpty: true,
|
|
27
28
|
category: ['existence'],
|
|
28
29
|
description: 'Takes a collection of Boolean values and returns true if any of the items are true. If all the items are false, or if the input is empty, the result is false.',
|
|
29
30
|
examples: [
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { FunctionDefinition, FunctionEvaluator, ASTNode, RuntimeContext, NodeEvaluator } from '../types';
|
|
2
|
+
import type { FHIRPathValue } from '../interpreter/boxing';
|
|
3
|
+
import { NodeType, isIdentifierNode } from '../types';
|
|
4
|
+
import { evaluate as asOperatorEvaluate } from './as-operator';
|
|
5
|
+
|
|
6
|
+
const asEvaluator: FunctionEvaluator = async (
|
|
7
|
+
input: FHIRPathValue[],
|
|
8
|
+
context: RuntimeContext,
|
|
9
|
+
args: ASTNode[],
|
|
10
|
+
evaluator: NodeEvaluator
|
|
11
|
+
) => {
|
|
12
|
+
// as() function takes one argument - the type name
|
|
13
|
+
if (args.length !== 1) {
|
|
14
|
+
return { value: [], context };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const typeArg = args[0];
|
|
18
|
+
if (!typeArg) {
|
|
19
|
+
return { value: [], context };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Extract type name from the argument AST node
|
|
23
|
+
let typeName: string;
|
|
24
|
+
|
|
25
|
+
if (isIdentifierNode(typeArg)) {
|
|
26
|
+
typeName = typeArg.name;
|
|
27
|
+
} else {
|
|
28
|
+
// For other node types, try to get the name
|
|
29
|
+
throw new Error(`as() requires a type name as argument, got ${typeArg.type}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Use the as operator implementation with the type name
|
|
33
|
+
return asOperatorEvaluate(input, context, input, [typeName]);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export { asEvaluator };
|
|
37
|
+
|
|
38
|
+
export const asFunction: FunctionDefinition & { evaluate: typeof asEvaluator } = {
|
|
39
|
+
name: 'as',
|
|
40
|
+
category: ['type'],
|
|
41
|
+
description: 'Casts the input to the specified type, returning empty if the cast fails',
|
|
42
|
+
examples: ['Patient.name.as(HumanName)', '"hello".as(String)', '5.as(Integer)'],
|
|
43
|
+
signatures: [
|
|
44
|
+
{
|
|
45
|
+
name: 'as-type-cast',
|
|
46
|
+
parameters: [{
|
|
47
|
+
name: 'type',
|
|
48
|
+
type: { type: 'Any', singleton: true },
|
|
49
|
+
expression: true,
|
|
50
|
+
typeReference: true
|
|
51
|
+
}],
|
|
52
|
+
input: { type: 'Any', singleton: false },
|
|
53
|
+
result: { type: 'Any', singleton: false }
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
doesNotPropagateEmpty: false,
|
|
57
|
+
evaluate: asEvaluator
|
|
58
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
5
|
+
import { isFHIRDate, isFHIRDateTime, isFHIRTime } from '../complex-types/temporal';
|
|
5
6
|
|
|
6
7
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
7
8
|
// 'as' operator performs type casting/filtering
|
|
@@ -10,29 +11,66 @@ export const evaluate: OperationEvaluator = async (input, context, left, right)
|
|
|
10
11
|
return { value: [], context };
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
// TODO: Implement proper FHIRPath type casting
|
|
14
|
-
// For now, just return the value as-is if it matches the type
|
|
15
14
|
const results: any[] = [];
|
|
16
|
-
const typeName = right[0]; // Should be a type name
|
|
15
|
+
const typeName = right[0] as string; // Should be a type name
|
|
17
16
|
|
|
18
|
-
for (const
|
|
17
|
+
for (const boxedItem of left) {
|
|
18
|
+
const item = unbox(boxedItem);
|
|
19
19
|
let matches = false;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
|
|
21
|
+
// Check typeInfo first if available
|
|
22
|
+
if (boxedItem?.typeInfo) {
|
|
23
|
+
// If we have a ModelProvider, use it for accurate type checking (handles subtypes)
|
|
24
|
+
if (context.modelProvider) {
|
|
25
|
+
const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, typeName as any);
|
|
26
|
+
matches = matchingType !== undefined;
|
|
27
|
+
} else {
|
|
28
|
+
// Without ModelProvider, just check exact match
|
|
29
|
+
matches = boxedItem.typeInfo.type === typeName;
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
// Fallback to primitive type checking
|
|
33
|
+
switch (typeName) {
|
|
34
|
+
case 'String':
|
|
35
|
+
matches = typeof item === 'string';
|
|
36
|
+
break;
|
|
37
|
+
case 'Boolean':
|
|
38
|
+
matches = typeof item === 'boolean';
|
|
39
|
+
break;
|
|
40
|
+
case 'Integer':
|
|
41
|
+
matches = typeof item === 'number' && Number.isInteger(item);
|
|
42
|
+
break;
|
|
43
|
+
case 'Decimal':
|
|
44
|
+
matches = typeof item === 'number';
|
|
45
|
+
break;
|
|
46
|
+
case 'Date':
|
|
47
|
+
// Check if it's a FHIRDate instance or has Date typeInfo
|
|
48
|
+
if (item && typeof item === 'object') {
|
|
49
|
+
matches = isFHIRDate(item) || (item as any).kind === 'FHIRDate';
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case 'DateTime':
|
|
53
|
+
// Check if it's a FHIRDateTime instance or has DateTime typeInfo
|
|
54
|
+
if (item && typeof item === 'object') {
|
|
55
|
+
matches = isFHIRDateTime(item) || (item as any).kind === 'FHIRDateTime';
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
case 'Time':
|
|
59
|
+
// Check if it's a FHIRTime instance or has Time typeInfo
|
|
60
|
+
if (item && typeof item === 'object') {
|
|
61
|
+
matches = isFHIRTime(item) || (item as any).kind === 'FHIRTime';
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
// For complex types, check resourceType
|
|
66
|
+
if (item && typeof item === 'object' && 'resourceType' in item) {
|
|
67
|
+
matches = item.resourceType === typeName;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
33
70
|
}
|
|
71
|
+
|
|
34
72
|
if (matches) {
|
|
35
|
-
results.push(
|
|
73
|
+
results.push(boxedItem);
|
|
36
74
|
}
|
|
37
75
|
}
|
|
38
76
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
// ceiling() takes no arguments
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator, TypeInfo } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
if (args.length !== 0) {
|
|
@@ -22,9 +22,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
22
22
|
parentTypeInfo = boxedItem.typeInfo;
|
|
23
23
|
} else if (modelProvider && 'resourceType' in item && typeof item.resourceType === 'string') {
|
|
24
24
|
// Try to get type info from resourceType (use cached version)
|
|
25
|
-
parentTypeInfo =
|
|
26
|
-
? (modelProvider as any).getTypeFromCache(item.resourceType)
|
|
27
|
-
: undefined;
|
|
25
|
+
parentTypeInfo = await modelProvider.getType(item.resourceType);
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
// Collect all child properties
|
|
@@ -94,5 +92,16 @@ export const childrenFunction: FunctionDefinition & { evaluate: FunctionEvaluato
|
|
|
94
92
|
parameters: [],
|
|
95
93
|
result: { type: 'Any', singleton: false }
|
|
96
94
|
}],
|
|
97
|
-
evaluate
|
|
95
|
+
evaluate,
|
|
96
|
+
async inferResultType(analyzer, node, inputType) {
|
|
97
|
+
const modelProvider = (analyzer as any).modelProvider;
|
|
98
|
+
if (inputType && modelProvider && 'getChildrenType' in modelProvider) {
|
|
99
|
+
const childrenType = await modelProvider.getChildrenType(inputType);
|
|
100
|
+
if (childrenType) {
|
|
101
|
+
return childrenType;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Fallback to Any collection
|
|
105
|
+
return { type: 'Any', singleton: false };
|
|
106
|
+
}
|
|
98
107
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FunctionDefinition, FunctionEvaluator } from '../types';
|
|
2
2
|
import { Errors } from '../errors';
|
|
3
|
-
import { box, unbox } from '../boxing';
|
|
3
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
4
4
|
|
|
5
5
|
export const evaluate: FunctionEvaluator = async (input, context, args, evaluator) => {
|
|
6
6
|
// combine() requires exactly one argument (the other collection)
|
|
@@ -16,8 +16,10 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
16
16
|
|
|
17
17
|
// Evaluate the argument expression
|
|
18
18
|
// The argument should be evaluated in the root context, not the current input
|
|
19
|
-
// Use
|
|
20
|
-
const
|
|
19
|
+
// Use the original context data
|
|
20
|
+
const rootInput = context.variables?.['%context'] || context.input || [];
|
|
21
|
+
const rootInputArray = Array.isArray(rootInput) ? rootInput : [rootInput];
|
|
22
|
+
const otherResult = await evaluator(otherArg, rootInputArray, context);
|
|
21
23
|
const otherCollection = otherResult.value;
|
|
22
24
|
|
|
23
25
|
// Merge the input and other collections into a single collection
|
|
@@ -29,6 +31,7 @@ export const evaluate: FunctionEvaluator = async (input, context, args, evaluato
|
|
|
29
31
|
|
|
30
32
|
export const combineFunction: FunctionDefinition & { evaluate: FunctionEvaluator } = {
|
|
31
33
|
name: 'combine',
|
|
34
|
+
doesNotPropagateEmpty: true, // Combine accepts empty collections as valid input
|
|
32
35
|
category: ['collection'],
|
|
33
36
|
description: 'Merge the input and other collections into a single collection without eliminating duplicate values. Combining an empty collection with a non-empty collection will return the non-empty collection. There is no expectation of order in the resulting collection.',
|
|
34
37
|
examples: [
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import type { OperatorDefinition } from '../types';
|
|
2
2
|
import { PRECEDENCE } from '../types';
|
|
3
3
|
import type { OperationEvaluator } from '../types';
|
|
4
|
-
import { box, unbox } from '../boxing';
|
|
4
|
+
import { box, unbox } from '../interpreter/boxing';
|
|
5
5
|
|
|
6
6
|
export const evaluate: OperationEvaluator = async (input, context, left, right) => {
|
|
7
7
|
// Combine operator concatenates all values as strings
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
if (leftStr === '' && rightStr === '') {
|
|
12
|
-
return { value: [], context };
|
|
13
|
-
}
|
|
8
|
+
// Empty collections are treated as empty string
|
|
9
|
+
const leftStr = left.length === 0 ? '' : left.map(v => String(unbox(v))).join('');
|
|
10
|
+
const rightStr = right.length === 0 ? '' : right.map(v => String(unbox(v))).join('');
|
|
14
11
|
|
|
12
|
+
// Always return a string, even if both are empty
|
|
15
13
|
return { value: [box(leftStr + rightStr, { type: 'String', singleton: true })], context };
|
|
16
14
|
};
|
|
17
15
|
|
|
18
16
|
export const combineOperator: OperatorDefinition & { evaluate: OperationEvaluator } = {
|
|
19
17
|
symbol: '&',
|
|
20
18
|
name: 'combine',
|
|
19
|
+
doesNotPropagateEmpty: true, // Treats empty as empty string, always returns a string
|
|
21
20
|
category: ['string'],
|
|
22
21
|
precedence: PRECEDENCE.ADDITIVE,
|
|
23
22
|
associativity: 'left',
|