All files / src/language spel-evaluator-adapter.ts

95.71% Statements 134/140
81.63% Branches 40/49
100% Functions 12/12
95.71% Lines 134/140

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169            1x 1x 1x 1x 1x 1x 1x   1x 32x   32x 32x 32x   32x 3x 3x   32x 5x 5x 5x 5x 5x 4x 2x 2x 2x 2x 2x 4x 4x 4x 5x   32x 4x 4x   32x 2x 2x 2x 2x 2x   32x 1x 1x   32x 13x 13x 13x 14x 14x 3x 3x   14x 3x 14x 14x 3x 14x 2x 14x 2x 14x 1x 14x 13x 13x 13x 13x 13x 13x 13x 13x   32x 4x 4x 4x 4x 4x 4x 4x 4x   32x 1x 1x   32x 7x 7x 7x 7x 7x 7x 7x 7x         7x     7x 7x 3x 3x 3x   3x 3x 3x 3x 3x 3x 4x 4x 4x 4x 4x   4x   4x 3x 1x   1x   1x 4x 4x     4x 4x 3x 3x 3x 3x 3x 3x       3x 3x 3x 5x 7x 2x 2x 7x 32x  
import type { SpelEvaluator, ParseResult } from './spel-evaluator.js';
import type { ContextSchema } from '../types/context-schema.js';
import type { SpelReference } from './reference-extractor.js';
import type { ContextValidationResult } from './diagnostic-engine.js';
import type { CompletionItem } from './completion-engine.js';
import type { FormatOptions } from './spel-formatter.js';
import { SpelExpressionParser } from '../spel-expression-parser.js';
import { SpelParseException } from '../error/spel-parse-exception.js';
import { StandardEvaluationContext } from '../standard-evaluation-context.js';
import { SpelReferenceExtractor, SpelReferenceKind } from './reference-extractor.js';
import { SpelDiagnosticEngine } from './diagnostic-engine.js';
import { SpelCompletionEngine } from './completion-engine.js';
import { SpelFormatter } from './spel-formatter.js';
 
export class SpelEvaluatorAdapter implements SpelEvaluator {
  private context: StandardEvaluationContext;
 
  constructor(context: StandardEvaluationContext) {
    this.context = context;
  }
 
  static fromContext(ctx: StandardEvaluationContext): SpelEvaluatorAdapter {
    return new SpelEvaluatorAdapter(ctx);
  }
 
  parse(expression: string): ParseResult {
    try {
      const parser = new SpelExpressionParser();
      const expr = parser.parseExpression(expression);
      return { valid: true, errors: [], ast: expr.getAST() };
    } catch (e) {
      if (e instanceof SpelParseException) {
        return {
          valid: false,
          errors: [{ message: e.message, position: e.position, code: String(e.messageCode) }],
        };
      }
      const err = e instanceof Error ? e : new Error(String(e));
      return { valid: false, errors: [{ message: err.message, position: 0, code: 'UNKNOWN' }] };
    }
  }
 
  getContextSchema(): ContextSchema | null {
    return SpelEvaluatorAdapter.#extractContextSchema(this.context);
  }
 
  evaluate(expression: string, context: Record<string, unknown>): unknown {
    const parser = new SpelExpressionParser();
    const expr = parser.parseExpression(expression);
    const evalCtx = new StandardEvaluationContext(context);
    return expr.getValueWithContext(evalCtx);
  }
 
  extractReferences(expression: string): SpelReference[] {
    return SpelReferenceExtractor.extract(expression);
  }
 
  validateContext(expression: string, contextSchema: ContextSchema): ContextValidationResult {
    const diagnostics = SpelDiagnosticEngine.checkContext(expression, contextSchema);
    const refs = SpelReferenceExtractor.extract(expression);
    const missingRefs = refs.filter((ref) => {
      switch (ref.kind) {
        case SpelReferenceKind.VARIABLE:
          return (
            ref.name !== 'root' && ref.name !== 'this' && !(ref.name in contextSchema.variables)
          );
        case SpelReferenceKind.ROOT_PROPERTY:
          return contextSchema.root ? !(ref.name in contextSchema.root.fields) : false;
        case SpelReferenceKind.BEAN:
        case SpelReferenceKind.BEAN_FACTORY:
          return !(ref.name in contextSchema.beans);
        case SpelReferenceKind.TYPE:
          return !(ref.name in contextSchema.types);
        case SpelReferenceKind.FUNCTION:
          return !(ref.name in contextSchema.functions);
        default:
          return false;
      }
    });
    return {
      valid: diagnostics.length === 0,
      diagnostics,
      missingReferences: missingRefs,
      typeMismatches: [],
    };
  }
 
  getCompletions(
    expression: string,
    position: number,
    contextSchema?: ContextSchema,
  ): CompletionItem[] {
    const schema =
      contextSchema ?? SpelEvaluatorAdapter.#extractContextSchema(this.context) ?? undefined;
    return SpelCompletionEngine.getCompletions(expression, position, schema);
  }
 
  format(expression: string, options?: FormatOptions): string {
    return SpelFormatter.format(expression, options);
  }
 
  static #extractContextSchema(ctx: StandardEvaluationContext): ContextSchema | null {
    try {
      const schema: ContextSchema = {
        root: null,
        variables: {},
        beans: {},
        types: {},
        functions: {},
      };
 
      interface StandardContextInternals {
        getRootObject?(): { getValue?(): unknown };
      }
      const internals = ctx as unknown as StandardContextInternals;
      // typeof null === 'object' is true in JS, so both checks are required
      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
      const rootObj = internals.getRootObject?.()?.getValue?.();
      if (typeof rootObj === 'object' && rootObj !== null) {
        const extractFields = (
          obj: Record<string, unknown>,
          visited: WeakSet<object> = new WeakSet(),
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
        ): any => {
          if (typeof obj !== 'object') return {};
          if (visited.has(obj)) return {};
          visited.add(obj);
          const fields: Record<string, unknown> = {};
          for (const key of Object.keys(obj)) {
            const val = obj[key];
            const t = typeof val;
            const field: Record<string, unknown> = {
              type:
                val === null
                  ? 'string'
                  : Array.isArray(val)
                    ? 'array'
                    : t === 'number'
                      ? 'number'
                      : t === 'boolean'
                        ? 'boolean'
                        : t === 'object'
                          ? 'object'
                          : 'string',
            };
            if (t === 'object' && val !== null && !Array.isArray(val)) {
              field.fields = extractFields(val as Record<string, unknown>, visited);
            }
            fields[key] = field;
          }
          return fields;
        };
        schema.root = {
          name: 'root',
          type: (rootObj as { constructor?: { name?: string } }).constructor?.name ?? 'object',
          fields: extractFields(rootObj as Record<string, unknown>) as Record<
            string,
            { type: 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array' | 'map' }
          >,
          methods: {},
        };
      }
      return schema;
    } catch {
      return null;
    }
  }
}