@atomic-ehr/fhirpath
Version:
A TypeScript implementation of FHIRPath
247 lines (200 loc) • 10.9 kB
text/typescript
import type { Range, Diagnostic } from './types';
import { DiagnosticSeverity } from './types';
/**
* Base error class for all FHIRPath errors
*/
export class FHIRPathError extends Error {
constructor(
public code: string,
message: string,
public location?: Range
) {
super(message);
this.name = 'FHIRPathError';
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
/**
* Convert FHIRPathError to Diagnostic for analyzer
*/
export function toDiagnostic(error: FHIRPathError, severity: DiagnosticSeverity = DiagnosticSeverity.Error): Diagnostic {
return {
code: error.code,
message: error.message,
severity,
range: error.location!,
source: 'fhirpath'
};
}
/**
* Error factory with specialized constructors
*/
export const Errors = {
// Resolution errors (1000-1999)
unknownOperator(operator: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.UNKNOWN_OPERATOR, `Unknown operator: ${operator}`, location);
},
unknownFunction(name: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.UNKNOWN_FUNCTION, `Unknown function: ${name}`, location);
},
unknownVariable(name: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.UNKNOWN_VARIABLE, `Unknown variable: ${name}`, location);
},
unknownUserVariable(name: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.UNKNOWN_USER_VARIABLE, `Unknown user variable: ${name}`, location);
},
unknownProperty(property: string, type: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.UNKNOWN_PROPERTY, `Unknown property '${property}' on type ${type}`, location);
},
unknownNodeType(nodeType: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.UNKNOWN_NODE_TYPE, `Unknown node type: ${nodeType}`, location);
},
noEvaluatorFound(evaluatorType: string, name: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.NO_EVALUATOR_FOUND, `No evaluator found for ${evaluatorType}: ${name}`, location);
},
variableNotDefined(name: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.VARIABLE_NOT_DEFINED, `Variable '${name}' is not defined in the current scope`, location);
},
// Arity errors (2000-2999)
wrongArgumentCount(funcName: string, expected: number, actual: number, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.WRONG_ARGUMENT_COUNT, `${funcName} expects ${expected} arguments, got ${actual}`, location);
},
wrongArgumentCountRange(funcName: string, min: number, max: number, actual: number, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.WRONG_ARGUMENT_COUNT_RANGE, `${funcName} expects ${min} to ${max} arguments, got ${actual}`, location);
},
singletonRequired(funcName: string, actualCount: number, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.SINGLETON_REQUIRED, `${funcName} requires a single item, but collection has ${actualCount} items`, location);
},
stringSingletonRequired(funcName: string, actualCount: number, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.SINGLETON_REQUIRED, `${funcName} can only be used on a single string, but collection has ${actualCount} items`, location);
},
emptyNotAllowed(funcName: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.EMPTY_NOT_ALLOWED, `${funcName} cannot operate on empty collection`, location);
},
argumentRequired(funcName: string, argumentName: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.ARGUMENT_REQUIRED, `${funcName} requires ${argumentName}`, location);
},
singletonTypeRequired(funcName: string, actualType: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.SINGLETON_REQUIRED, `${funcName} expects a singleton value, but received collection type ${actualType}`, location);
},
// Type errors (3000-3999)
operatorTypeMismatch(operator: string, leftType: string, rightType: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.OPERATOR_TYPE_MISMATCH, `Operator '${operator}' cannot be applied to types ${leftType} and ${rightType}`, location);
},
argumentTypeMismatch(argIndex: number, funcName: string, expected: string, actual: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.ARGUMENT_TYPE_MISMATCH, `Argument ${argIndex} of ${funcName}: expected ${expected}, got ${actual}`, location);
},
conversionFailed(value: string, targetType: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.CONVERSION_FAILED, `Cannot convert '${value}' to ${targetType}`, location);
},
invalidValueType(expected: string, actual: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.INVALID_VALUE_TYPE, `${expected} expected, got ${actual}`, location);
},
invalidOperandType(operation: string, type: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.INVALID_OPERAND_TYPE, `Cannot apply ${operation} to ${type}`, location);
},
stringOperationOnNonString(operation: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.STRING_OPERATION_ON_NON_STRING, `${operation} can only be used on string values`, location);
},
numericOperationOnNonNumeric(operation: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.NUMERIC_OPERATION_ON_NON_NUMERIC, `${operation} can only be applied to numeric values`, location);
},
booleanOperationOnNonBoolean(operation: string, index: number, actualType: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.BOOLEAN_OPERATION_ON_NON_BOOLEAN, `${operation} expects all items to be Boolean values, but item at index ${index} is ${actualType}`, location);
},
// Configuration errors (4000-4999)
modelProviderRequired(operation: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.MODEL_PROVIDER_REQUIRED,
`ModelProvider required for '${operation}' operation. Even primitive types like Boolean can fail due to choice types (e.g., Patient.deceased is actually deceasedBoolean in FHIR data)`,
location);
},
// Syntax errors (5000-5999)
unexpectedToken(token: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.UNEXPECTED_TOKEN, `Unexpected token: ${token}`, location);
},
expectedToken(expected: string, actual: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.EXPECTED_TOKEN, `Expected ${expected}, got ${actual}`, location);
},
invalidSyntax(details: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.INVALID_SYNTAX, `Invalid syntax: ${details}`, location);
},
expectedIdentifier(after: string, actual: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.EXPECTED_IDENTIFIER, `Expected identifier after '${after}', got: ${actual}`, location);
},
expectedTypeName(actual: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.EXPECTED_TYPE_NAME, `Expected type name, got: ${actual}`, location);
},
// Domain errors (6000-6999)
divisionByZero(location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.DIVISION_BY_ZERO, 'Division by zero', location);
},
invalidDateTimeFormat(format: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.INVALID_DATE_TIME_FORMAT, `Invalid date/time format: '${format}'`, location);
},
incompatibleUnits(unit1: string, unit2: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.INCOMPATIBLE_UNITS, `Cannot perform operation on incompatible units: ${unit1} and ${unit2}`, location);
},
indexOutOfBounds(index: number, size: number, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.INDEX_OUT_OF_BOUNDS, `Index ${index} out of bounds for collection of size ${size}`, location);
},
invalidOperation(details: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.INVALID_OPERATION, `Invalid operation: ${details}`, location);
},
invalidPrecision(operation: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.INVALID_PRECISION, `${operation} precision must be a non-negative integer`, location);
},
invalidStringOperation(operation: string, paramName: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.INVALID_STRING_OPERATION, `${operation} ${paramName} must be a string`, location);
},
invalidNumericOperation(operation: string, paramName: string, expectedType: string, location?: Range): FHIRPathError {
return new FHIRPathError(ErrorCodes.INVALID_NUMERIC_OPERATION, `${operation} ${paramName} must be ${expectedType}`, location);
}
};
// Export error codes as enum for external use
export enum ErrorCodes {
// Resolution errors (1000-1999)
UNKNOWN_OPERATOR = 'FP1001',
UNKNOWN_FUNCTION = 'FP1002',
UNKNOWN_VARIABLE = 'FP1003',
UNKNOWN_USER_VARIABLE = 'FP1004',
UNKNOWN_PROPERTY = 'FP1005',
UNKNOWN_NODE_TYPE = 'FP1006',
NO_EVALUATOR_FOUND = 'FP1007',
VARIABLE_NOT_DEFINED = 'FP1008',
// Arity errors (2000-2999)
WRONG_ARGUMENT_COUNT = 'FP2001',
WRONG_ARGUMENT_COUNT_RANGE = 'FP2002',
SINGLETON_REQUIRED = 'FP2003',
EMPTY_NOT_ALLOWED = 'FP2004',
ARGUMENT_REQUIRED = 'FP2005',
// Type errors (3000-3999)
// FP3001 - removed (unified with FP3006)
OPERATOR_TYPE_MISMATCH = 'FP3002',
ARGUMENT_TYPE_MISMATCH = 'FP3003',
CONVERSION_FAILED = 'FP3004',
INVALID_VALUE_TYPE = 'FP3005',
INVALID_OPERAND_TYPE = 'FP3006',
STRING_OPERATION_ON_NON_STRING = 'FP3007',
NUMERIC_OPERATION_ON_NON_NUMERIC = 'FP3008',
BOOLEAN_OPERATION_ON_NON_BOOLEAN = 'FP3009',
// Configuration errors (4000-4999)
MODEL_PROVIDER_REQUIRED = 'FP4001',
// Syntax errors (5000-5999)
UNEXPECTED_TOKEN = 'FP5001',
EXPECTED_TOKEN = 'FP5002',
INVALID_SYNTAX = 'FP5003',
EXPECTED_IDENTIFIER = 'FP5004',
EXPECTED_TYPE_NAME = 'FP5005',
// Domain errors (6000-6999)
DIVISION_BY_ZERO = 'FP6001',
INVALID_DATE_TIME_FORMAT = 'FP6002',
INCOMPATIBLE_UNITS = 'FP6003',
INDEX_OUT_OF_BOUNDS = 'FP6004',
INVALID_OPERATION = 'FP6005',
INVALID_PRECISION = 'FP6006',
INVALID_STRING_OPERATION = 'FP6007',
INVALID_NUMERIC_OPERATION = 'FP6008'
}