@atomic-ehr/fhirpath 0.0.1-canary.35b105d.20250724165800
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.d.ts +225 -0
- package/dist/index.js +8185 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -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 +152 -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 +579 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prototype-context-adapter.ts +99 -0
- package/src/compiler/types.ts +23 -0
- package/src/index.ts +52 -0
- package/src/interpreter/README.md +78 -0
- package/src/interpreter/interpreter.ts +485 -0
- package/src/interpreter/types.ts +110 -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 +425 -0
- package/src/registry/operations/comparison.ts +432 -0
- package/src/registry/operations/existence.ts +703 -0
- package/src/registry/operations/filtering.ts +358 -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 +542 -0
- package/src/registry/registry.ts +146 -0
- package/src/registry/types.ts +161 -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
- package/src/runtime/context.ts +179 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { ModelProvider as IModelProvider, PropertyInfo, TypeRef } from './types';
|
|
2
|
+
import type { FHIRSchema, SchemaRegistry, SchemaTypeRef } from './schemas/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Model provider implementation using FHIR schemas
|
|
6
|
+
* Resolves types and properties through a schema registry
|
|
7
|
+
*/
|
|
8
|
+
export class ModelProvider implements IModelProvider {
|
|
9
|
+
private registry: SchemaRegistry;
|
|
10
|
+
private schemaListCache = new Map<string, FHIRSchema[]>();
|
|
11
|
+
|
|
12
|
+
constructor(registry: SchemaRegistry) {
|
|
13
|
+
this.registry = registry;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
resolveType(typeName: string): TypeRef | undefined {
|
|
17
|
+
// Normalize type names - handle both "String" and "string" formats
|
|
18
|
+
const normalizedName = this.normalizeTypeName(typeName);
|
|
19
|
+
|
|
20
|
+
const schemas = this.getSchemaList(normalizedName);
|
|
21
|
+
if (!schemas.length) return undefined;
|
|
22
|
+
|
|
23
|
+
// Return opaque TypeRef (which is actually SchemaTypeRef)
|
|
24
|
+
const schemaTypeRef: SchemaTypeRef = {
|
|
25
|
+
schemas,
|
|
26
|
+
type: this.isPrimitiveType(normalizedName) ? normalizedName : 'complex-type'
|
|
27
|
+
// array is not set here - it's property-specific
|
|
28
|
+
};
|
|
29
|
+
return schemaTypeRef as unknown as TypeRef;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getPropertyType(type: TypeRef, propertyName: string): PropertyInfo | undefined {
|
|
33
|
+
const schemaRef = type as unknown as SchemaTypeRef;
|
|
34
|
+
|
|
35
|
+
// Search through type.schemas for the property
|
|
36
|
+
for (const schema of schemaRef.schemas) {
|
|
37
|
+
const element = schema.elements?.[propertyName];
|
|
38
|
+
if (element) {
|
|
39
|
+
// Handle union types
|
|
40
|
+
if (element.union === true && element.types) {
|
|
41
|
+
// For now, return Any type for unions
|
|
42
|
+
// In future, this could return a proper union type
|
|
43
|
+
return {
|
|
44
|
+
type: this.resolveType('Any')!,
|
|
45
|
+
isSingleton: !element.array
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Build TypeRef for the property's type
|
|
50
|
+
let propSchemas: FHIRSchema[];
|
|
51
|
+
|
|
52
|
+
if (element.elements) {
|
|
53
|
+
// Inline type with nested elements
|
|
54
|
+
const inlineSchema: FHIRSchema = {
|
|
55
|
+
name: `${this.getSchemaName(schema)}.${propertyName}`,
|
|
56
|
+
base: element.type || 'BackboneElement',
|
|
57
|
+
elements: element.elements
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Get base type schemas if any
|
|
61
|
+
const baseSchemas = element.type ? this.getSchemaList(element.type) : [];
|
|
62
|
+
propSchemas = [inlineSchema, ...baseSchemas];
|
|
63
|
+
} else if (element.type) {
|
|
64
|
+
// Simple type reference
|
|
65
|
+
propSchemas = this.getSchemaList(element.type);
|
|
66
|
+
} else {
|
|
67
|
+
// No type specified - shouldn't happen in valid schemas
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const propTypeRef: SchemaTypeRef = {
|
|
72
|
+
schemas: propSchemas,
|
|
73
|
+
type: element.type && this.isPrimitiveType(element.type) ? element.type : 'complex-type',
|
|
74
|
+
array: element.array
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
type: propTypeRef as unknown as TypeRef,
|
|
79
|
+
isSingleton: !element.array
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isAssignable(from: TypeRef, to: TypeRef): boolean {
|
|
88
|
+
const fromRef = from as unknown as SchemaTypeRef;
|
|
89
|
+
const toRef = to as unknown as SchemaTypeRef;
|
|
90
|
+
|
|
91
|
+
// Same type is always assignable
|
|
92
|
+
if (fromRef.type === toRef.type && fromRef.type !== 'complex-type') {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Any is assignable to/from everything
|
|
97
|
+
if (fromRef.type === 'Any' || toRef.type === 'Any') {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// For complex types, check if 'from' is a subtype of 'to'
|
|
102
|
+
if (fromRef.type === 'complex-type' && toRef.type === 'complex-type') {
|
|
103
|
+
const fromSchema = fromRef.schemas?.[0];
|
|
104
|
+
const toSchema = toRef.schemas?.[0];
|
|
105
|
+
|
|
106
|
+
if (!fromSchema || !toSchema) return false;
|
|
107
|
+
|
|
108
|
+
const fromName = this.getSchemaName(fromSchema);
|
|
109
|
+
const toName = this.getSchemaName(toSchema);
|
|
110
|
+
|
|
111
|
+
// Check if 'to' appears in 'from's schema list (inheritance chain)
|
|
112
|
+
return fromRef.schemas.some(schema => this.getSchemaName(schema) === toName);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle primitive type inheritance (e.g., code extends string)
|
|
116
|
+
if (this.isPrimitiveType(fromRef.type) && this.isPrimitiveType(toRef.type)) {
|
|
117
|
+
const fromSchemas = this.getSchemaList(fromRef.type);
|
|
118
|
+
return fromSchemas.some(schema => schema.name === toRef.type);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getTypeName(type: TypeRef): string {
|
|
125
|
+
// Handle undefined type
|
|
126
|
+
if (!type || typeof type !== 'object') {
|
|
127
|
+
return 'Unknown';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Cast to our internal SchemaTypeRef structure
|
|
131
|
+
const typeRef = type as unknown as SchemaTypeRef;
|
|
132
|
+
|
|
133
|
+
if (typeRef.type !== 'complex-type') {
|
|
134
|
+
// Capitalize primitive type names for display
|
|
135
|
+
return typeRef.type.charAt(0).toUpperCase() + typeRef.type.slice(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// For complex types, use the name of the first schema
|
|
139
|
+
const schema = typeRef.schemas?.[0];
|
|
140
|
+
if (!schema) return 'Unknown';
|
|
141
|
+
return this.getSchemaName(schema);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
isCollectionType(type: TypeRef): boolean {
|
|
145
|
+
// The collection nature is carried in the TypeRef itself
|
|
146
|
+
const typeRef = type as unknown as SchemaTypeRef;
|
|
147
|
+
return typeRef.array || false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getCommonType(types: TypeRef[]): TypeRef | undefined {
|
|
151
|
+
if (types.length === 0) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (types.length === 1) {
|
|
156
|
+
return types[0];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// For simplicity, if all types are the same, return that type
|
|
160
|
+
const typeNames = types.map(t => this.getTypeName(t));
|
|
161
|
+
if (typeNames.every(name => name === typeNames[0])) {
|
|
162
|
+
return types[0];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Otherwise return Any
|
|
166
|
+
return this.resolveType('Any');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private getSchemaList(typeName: string): FHIRSchema[] {
|
|
170
|
+
// Check cache first
|
|
171
|
+
if (this.schemaListCache.has(typeName)) {
|
|
172
|
+
return this.schemaListCache.get(typeName)!;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Build schema list by following base chain
|
|
176
|
+
const schemaList: FHIRSchema[] = [];
|
|
177
|
+
let currentType = typeName;
|
|
178
|
+
|
|
179
|
+
while (currentType) {
|
|
180
|
+
const schema = this.registry.resolve(currentType);
|
|
181
|
+
if (!schema) break;
|
|
182
|
+
|
|
183
|
+
// Add name to schema if not present (for primitives)
|
|
184
|
+
if (!schema.name) {
|
|
185
|
+
schema.name = currentType;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
schemaList.push(schema);
|
|
189
|
+
currentType = schema.base || '';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Cache the result
|
|
193
|
+
this.schemaListCache.set(typeName, schemaList);
|
|
194
|
+
return schemaList;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private getSchemaName(schema: FHIRSchema): string {
|
|
198
|
+
// Schema should have a name, either explicit or set during resolution
|
|
199
|
+
return schema.name || 'Unknown';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private isPrimitiveType(typeName: string): boolean {
|
|
203
|
+
// List of known primitive types
|
|
204
|
+
const primitives = [
|
|
205
|
+
'string', 'integer', 'decimal', 'boolean',
|
|
206
|
+
'date', 'dateTime', 'time', 'instant',
|
|
207
|
+
'code', 'id', 'uri', 'url', 'canonical',
|
|
208
|
+
'base64Binary', 'markdown', 'unsignedInt', 'positiveInt',
|
|
209
|
+
'Any'
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
return primitives.includes(typeName);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private normalizeTypeName(typeName: string): string {
|
|
216
|
+
// Handle different naming conventions
|
|
217
|
+
// "String" -> "string", "Integer" -> "integer", etc.
|
|
218
|
+
|
|
219
|
+
// First check if it's a primitive type with capital letter
|
|
220
|
+
const lowerName = typeName.toLowerCase();
|
|
221
|
+
if (this.isPrimitiveType(lowerName)) {
|
|
222
|
+
return lowerName;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Handle System namespace
|
|
226
|
+
if (typeName.startsWith('System.')) {
|
|
227
|
+
const baseName = typeName.substring(7);
|
|
228
|
+
const lowerBase = baseName.toLowerCase();
|
|
229
|
+
if (this.isPrimitiveType(lowerBase)) {
|
|
230
|
+
return lowerBase;
|
|
231
|
+
}
|
|
232
|
+
// System.Quantity -> Quantity
|
|
233
|
+
return baseName;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Handle FHIR namespace
|
|
237
|
+
if (typeName.startsWith('FHIR.')) {
|
|
238
|
+
return typeName.substring(5);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Default - return as is
|
|
242
|
+
return typeName;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// FHIR Schema type definitions
|
|
2
|
+
// Note: Actual schema definitions are not included in the analyzer
|
|
3
|
+
// In production, schemas should be loaded from the fhirschema library
|
|
4
|
+
// Test schemas are located in test/test-schemas.ts
|
|
5
|
+
|
|
6
|
+
export interface FHIRSchemaElement {
|
|
7
|
+
type?: string; // Simple type reference
|
|
8
|
+
array?: boolean; // Is this a collection?
|
|
9
|
+
union?: boolean | string; // Union type indicator or discriminator
|
|
10
|
+
types?: string[]; // Union type options
|
|
11
|
+
elements?: Record<string, FHIRSchemaElement>; // Nested elements
|
|
12
|
+
valueset?: string; // Terminology binding
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FHIRSchema {
|
|
16
|
+
name?: string; // Schema name (for primitives and inline types)
|
|
17
|
+
base?: string; // Parent type
|
|
18
|
+
primitive?: boolean; // Is this a primitive type?
|
|
19
|
+
elements?: Record<string, FHIRSchemaElement>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SchemaRegistry {
|
|
23
|
+
resolve(typeName: string): FHIRSchema | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Static implementation for testing
|
|
27
|
+
export class StaticSchemaRegistry implements SchemaRegistry {
|
|
28
|
+
constructor(private schemas: Record<string, FHIRSchema>) {}
|
|
29
|
+
|
|
30
|
+
resolve(typeName: string): FHIRSchema | undefined {
|
|
31
|
+
return this.schemas[typeName];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Schema-based type reference used internally by ModelProvider
|
|
36
|
+
export interface SchemaTypeRef {
|
|
37
|
+
schemas: FHIRSchema[]; // List of schemas in inheritance order
|
|
38
|
+
type: string; // 'complex-type' for FHIR types, or primitive name
|
|
39
|
+
array?: boolean; // Optional, indicates if this is an array type
|
|
40
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type system interfaces for FHIRPath type analysis
|
|
3
|
+
*
|
|
4
|
+
* Uses opaque type references to allow flexible implementation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Opaque type reference - implementation details hidden from analyzer
|
|
8
|
+
export type TypeRef = unknown;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Information about a property on a type
|
|
12
|
+
*/
|
|
13
|
+
export interface PropertyInfo {
|
|
14
|
+
type: TypeRef;
|
|
15
|
+
isSingleton: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Model provider interface for type resolution and navigation
|
|
20
|
+
*/
|
|
21
|
+
export interface ModelProvider {
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a type name to an opaque type reference
|
|
24
|
+
*/
|
|
25
|
+
resolveType(typeName: string): TypeRef | undefined;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get property type from an opaque type reference
|
|
29
|
+
*/
|
|
30
|
+
getPropertyType(type: TypeRef, propertyName: string): PropertyInfo | undefined;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if one type is assignable to another
|
|
34
|
+
*/
|
|
35
|
+
isAssignable(from: TypeRef, to: TypeRef): boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get a human-readable name for a type (for error messages)
|
|
39
|
+
*/
|
|
40
|
+
getTypeName(type: TypeRef): string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a type is a collection type
|
|
44
|
+
*/
|
|
45
|
+
isCollectionType?(type: TypeRef): boolean;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get common base type for union types
|
|
49
|
+
*/
|
|
50
|
+
getCommonType?(types: TypeRef[]): TypeRef | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parameter type information for functions
|
|
55
|
+
*/
|
|
56
|
+
export interface ParameterTypeInfo {
|
|
57
|
+
type?: TypeRef | 'expression' | 'any';
|
|
58
|
+
requiresSingleton?: boolean;
|
|
59
|
+
optional?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Function type signature
|
|
64
|
+
*/
|
|
65
|
+
export interface FunctionTypeSignature {
|
|
66
|
+
// Input requirements
|
|
67
|
+
requiresSingleton: boolean;
|
|
68
|
+
propagateEmptyInput?: boolean;
|
|
69
|
+
requiresInputType?: string; // Required input type (e.g., 'string' for string functions)
|
|
70
|
+
|
|
71
|
+
// Parameter type information
|
|
72
|
+
parameters?: ParameterTypeInfo[];
|
|
73
|
+
|
|
74
|
+
// Return type calculation
|
|
75
|
+
returnType: (
|
|
76
|
+
inputType: TypeRef | undefined,
|
|
77
|
+
paramTypes: (TypeRef | undefined)[],
|
|
78
|
+
provider: ModelProvider
|
|
79
|
+
) => TypeRef | undefined;
|
|
80
|
+
|
|
81
|
+
// Return cardinality calculation
|
|
82
|
+
returnsSingleton: (
|
|
83
|
+
inputIsSingleton: boolean,
|
|
84
|
+
paramAreSingleton: boolean[]
|
|
85
|
+
) => boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Operator type signature
|
|
90
|
+
*/
|
|
91
|
+
export interface OperatorTypeSignature {
|
|
92
|
+
requiresLeftSingleton: boolean;
|
|
93
|
+
requiresRightSingleton: boolean;
|
|
94
|
+
|
|
95
|
+
// For simple operators with fixed types
|
|
96
|
+
acceptedTypes?: Array<{
|
|
97
|
+
left: string;
|
|
98
|
+
right: string;
|
|
99
|
+
result: string;
|
|
100
|
+
}> | 'any';
|
|
101
|
+
|
|
102
|
+
// For complex operators that need custom logic
|
|
103
|
+
returnType?: (
|
|
104
|
+
leftType: TypeRef | undefined,
|
|
105
|
+
rightType: TypeRef | undefined,
|
|
106
|
+
provider: ModelProvider
|
|
107
|
+
) => TypeRef | undefined;
|
|
108
|
+
|
|
109
|
+
returnsSingleton: boolean | ((left: boolean, right: boolean) => boolean);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Type analysis mode
|
|
114
|
+
*/
|
|
115
|
+
export enum AnalysisMode {
|
|
116
|
+
Strict = 'strict', // Type mismatches are errors
|
|
117
|
+
Lenient = 'lenient' // Type mismatches are warnings, continue with Any
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Type analysis result
|
|
122
|
+
*/
|
|
123
|
+
export interface TypeAnalysisResult {
|
|
124
|
+
// Annotated AST with type information
|
|
125
|
+
ast: import('../parser/ast').ASTNode;
|
|
126
|
+
|
|
127
|
+
// Any errors or warnings encountered
|
|
128
|
+
diagnostics: TypeDiagnostic[];
|
|
129
|
+
|
|
130
|
+
// Overall result type
|
|
131
|
+
resultType?: TypeRef;
|
|
132
|
+
resultIsSingleton?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Type diagnostic (error or warning)
|
|
137
|
+
*/
|
|
138
|
+
export interface TypeDiagnostic {
|
|
139
|
+
severity: 'error' | 'warning';
|
|
140
|
+
message: string;
|
|
141
|
+
position?: import('../parser/ast').Position;
|
|
142
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FHIRPathBuilder,
|
|
3
|
+
FHIRPathAPI,
|
|
4
|
+
ModelProvider,
|
|
5
|
+
CustomFunction,
|
|
6
|
+
FHIRPathExpression,
|
|
7
|
+
CompiledExpression,
|
|
8
|
+
AnalysisResult,
|
|
9
|
+
EvaluationContext,
|
|
10
|
+
CompileOptions,
|
|
11
|
+
AnalyzeOptions
|
|
12
|
+
} from './types';
|
|
13
|
+
import { parse, evaluate, compile, analyze } from './index';
|
|
14
|
+
import { PublicRegistryAPI } from './registry';
|
|
15
|
+
import { Registry } from '../registry/registry';
|
|
16
|
+
import { invalidArgument } from './errors';
|
|
17
|
+
|
|
18
|
+
export class FHIRPath {
|
|
19
|
+
static builder(): FHIRPathBuilder {
|
|
20
|
+
return new Builder();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class Builder implements FHIRPathBuilder {
|
|
25
|
+
private modelProvider?: ModelProvider;
|
|
26
|
+
private customFunctions: Map<string, CustomFunction> = new Map();
|
|
27
|
+
private variables: Map<string, any> = new Map();
|
|
28
|
+
|
|
29
|
+
withModelProvider(provider: ModelProvider): this {
|
|
30
|
+
this.modelProvider = provider;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
withCustomFunction(name: string, fn: CustomFunction): this {
|
|
35
|
+
// Validate function name
|
|
36
|
+
if (!name || typeof name !== 'string') {
|
|
37
|
+
throw invalidArgument('Function name must be a non-empty string');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
41
|
+
throw invalidArgument(`Invalid function name: ${name}. Must start with letter or underscore, followed by letters, numbers, or underscores`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check for conflicts with built-ins
|
|
45
|
+
if (Registry.get(name)) {
|
|
46
|
+
throw invalidArgument(`Cannot override built-in operation: ${name}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.customFunctions.set(name, fn);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
withVariable(name: string, value: any): this {
|
|
54
|
+
if (!name || typeof name !== 'string') {
|
|
55
|
+
throw invalidArgument('Variable name must be a non-empty string');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.variables.set(name, value);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
build(): FHIRPathAPI {
|
|
63
|
+
return new BuiltAPI(
|
|
64
|
+
this.modelProvider,
|
|
65
|
+
this.customFunctions,
|
|
66
|
+
this.variables
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class BuiltAPI implements FHIRPathAPI {
|
|
72
|
+
public readonly registry: PublicRegistryAPI;
|
|
73
|
+
|
|
74
|
+
constructor(
|
|
75
|
+
private readonly modelProvider: ModelProvider | undefined,
|
|
76
|
+
private readonly customFunctions: Map<string, CustomFunction>,
|
|
77
|
+
private readonly defaultVariables: Map<string, any>
|
|
78
|
+
) {
|
|
79
|
+
// Create a registry wrapper that includes custom functions
|
|
80
|
+
this.registry = new BuiltRegistryAPI(this.customFunctions);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
parse(expression: string): FHIRPathExpression {
|
|
84
|
+
return parse(expression);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
evaluate(expression: string | FHIRPathExpression, input?: any): any[] {
|
|
88
|
+
const context = this.createContext();
|
|
89
|
+
return evaluate(expression, input, context);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
compile(expression: string | FHIRPathExpression, options?: CompileOptions): CompiledExpression {
|
|
93
|
+
// TODO: Integrate custom functions into compiled code
|
|
94
|
+
return compile(expression, options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
analyze(expression: string | FHIRPathExpression, options?: AnalyzeOptions): AnalysisResult {
|
|
98
|
+
const mergedOptions: AnalyzeOptions = {
|
|
99
|
+
...options,
|
|
100
|
+
modelProvider: options?.modelProvider || this.modelProvider
|
|
101
|
+
};
|
|
102
|
+
return analyze(expression, mergedOptions);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private createContext(): EvaluationContext {
|
|
106
|
+
const context: EvaluationContext = {};
|
|
107
|
+
|
|
108
|
+
if (this.modelProvider) {
|
|
109
|
+
context.modelProvider = this.modelProvider;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (this.defaultVariables.size > 0) {
|
|
113
|
+
context.variables = {};
|
|
114
|
+
for (const [name, value] of this.defaultVariables) {
|
|
115
|
+
context.variables[name] = value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.customFunctions.size > 0) {
|
|
120
|
+
context.customFunctions = {};
|
|
121
|
+
for (const [name, fn] of this.customFunctions) {
|
|
122
|
+
context.customFunctions[name] = fn;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return context;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Extended registry that includes custom functions
|
|
131
|
+
class BuiltRegistryAPI extends PublicRegistryAPI {
|
|
132
|
+
constructor(private customFunctions: Map<string, CustomFunction>) {
|
|
133
|
+
super();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
override hasFunction(name: string): boolean {
|
|
137
|
+
return super.hasFunction(name) || this.customFunctions.has(name);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
override hasOperation(name: string): boolean {
|
|
141
|
+
return super.hasOperation(name) || this.customFunctions.has(name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override canRegisterFunction(name: string): boolean {
|
|
145
|
+
// Can't register if it conflicts with built-ins or already registered custom
|
|
146
|
+
return super.canRegisterFunction(name) && !this.customFunctions.has(name);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Location } from './types';
|
|
2
|
+
|
|
3
|
+
export enum ErrorCode {
|
|
4
|
+
// Parse errors
|
|
5
|
+
PARSE_ERROR = 'PARSE_ERROR',
|
|
6
|
+
SYNTAX_ERROR = 'SYNTAX_ERROR',
|
|
7
|
+
UNEXPECTED_TOKEN = 'UNEXPECTED_TOKEN',
|
|
8
|
+
UNTERMINATED_STRING = 'UNTERMINATED_STRING',
|
|
9
|
+
INVALID_ESCAPE = 'INVALID_ESCAPE',
|
|
10
|
+
|
|
11
|
+
// Type errors
|
|
12
|
+
TYPE_ERROR = 'TYPE_ERROR',
|
|
13
|
+
TYPE_MISMATCH = 'TYPE_MISMATCH',
|
|
14
|
+
UNKNOWN_TYPE = 'UNKNOWN_TYPE',
|
|
15
|
+
CARDINALITY_ERROR = 'CARDINALITY_ERROR',
|
|
16
|
+
|
|
17
|
+
// Runtime errors
|
|
18
|
+
RUNTIME_ERROR = 'RUNTIME_ERROR',
|
|
19
|
+
UNDEFINED_VARIABLE = 'UNDEFINED_VARIABLE',
|
|
20
|
+
UNDEFINED_FUNCTION = 'UNDEFINED_FUNCTION',
|
|
21
|
+
INVALID_ARGUMENT = 'INVALID_ARGUMENT',
|
|
22
|
+
DIVISION_BY_ZERO = 'DIVISION_BY_ZERO',
|
|
23
|
+
|
|
24
|
+
// Analysis errors
|
|
25
|
+
ANALYSIS_ERROR = 'ANALYSIS_ERROR',
|
|
26
|
+
UNREACHABLE_CODE = 'UNREACHABLE_CODE',
|
|
27
|
+
AMBIGUOUS_TYPE = 'AMBIGUOUS_TYPE',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class FHIRPathError extends Error {
|
|
31
|
+
constructor(
|
|
32
|
+
message: string,
|
|
33
|
+
public code: ErrorCode,
|
|
34
|
+
public location?: Location,
|
|
35
|
+
public expression?: string
|
|
36
|
+
) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = 'FHIRPathError';
|
|
39
|
+
|
|
40
|
+
// Ensure proper prototype chain
|
|
41
|
+
Object.setPrototypeOf(this, FHIRPathError.prototype);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override toString(): string {
|
|
45
|
+
let result = `${this.name}: ${this.message}`;
|
|
46
|
+
|
|
47
|
+
if (this.expression && this.location) {
|
|
48
|
+
result += `\n\n${this.expression}`;
|
|
49
|
+
if (this.location.offset >= 0 && this.location.length > 0) {
|
|
50
|
+
const indent = ' '.repeat(this.location.offset);
|
|
51
|
+
const marker = '^'.repeat(this.location.length);
|
|
52
|
+
result += `\n${indent}${marker}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (this.location) {
|
|
57
|
+
result += `\n\nAt line ${this.location.line}, column ${this.location.column}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Error factory functions
|
|
65
|
+
export function parseError(
|
|
66
|
+
message: string,
|
|
67
|
+
location?: Location,
|
|
68
|
+
expression?: string
|
|
69
|
+
): FHIRPathError {
|
|
70
|
+
return new FHIRPathError(message, ErrorCode.PARSE_ERROR, location, expression);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function syntaxError(
|
|
74
|
+
message: string,
|
|
75
|
+
location?: Location,
|
|
76
|
+
expression?: string
|
|
77
|
+
): FHIRPathError {
|
|
78
|
+
return new FHIRPathError(message, ErrorCode.SYNTAX_ERROR, location, expression);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function typeError(
|
|
82
|
+
message: string,
|
|
83
|
+
location?: Location,
|
|
84
|
+
expression?: string
|
|
85
|
+
): FHIRPathError {
|
|
86
|
+
return new FHIRPathError(message, ErrorCode.TYPE_ERROR, location, expression);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function runtimeError(
|
|
90
|
+
message: string,
|
|
91
|
+
location?: Location,
|
|
92
|
+
expression?: string
|
|
93
|
+
): FHIRPathError {
|
|
94
|
+
return new FHIRPathError(message, ErrorCode.RUNTIME_ERROR, location, expression);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function undefinedVariable(
|
|
98
|
+
name: string,
|
|
99
|
+
location?: Location,
|
|
100
|
+
expression?: string
|
|
101
|
+
): FHIRPathError {
|
|
102
|
+
return new FHIRPathError(
|
|
103
|
+
`Undefined variable: ${name}`,
|
|
104
|
+
ErrorCode.UNDEFINED_VARIABLE,
|
|
105
|
+
location,
|
|
106
|
+
expression
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function undefinedFunction(
|
|
111
|
+
name: string,
|
|
112
|
+
location?: Location,
|
|
113
|
+
expression?: string
|
|
114
|
+
): FHIRPathError {
|
|
115
|
+
return new FHIRPathError(
|
|
116
|
+
`Undefined function: ${name}`,
|
|
117
|
+
ErrorCode.UNDEFINED_FUNCTION,
|
|
118
|
+
location,
|
|
119
|
+
expression
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function invalidArgument(
|
|
124
|
+
message: string,
|
|
125
|
+
location?: Location,
|
|
126
|
+
expression?: string
|
|
127
|
+
): FHIRPathError {
|
|
128
|
+
return new FHIRPathError(
|
|
129
|
+
`Invalid argument: ${message}`,
|
|
130
|
+
ErrorCode.INVALID_ARGUMENT,
|
|
131
|
+
location,
|
|
132
|
+
expression
|
|
133
|
+
);
|
|
134
|
+
}
|