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

98.11% Statements 52/53
97.77% Branches 44/45
100% Functions 12/12
100% Lines 51/51

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                                    36x       7x       5x 5x 5x 5x   4x 2x         2x 4x         8x       2x 2x 2x 2x       1x       13x 13x 13x 14x   3x       3x     3x   2x   2x   1x     13x                           4x 4x       1x       11x 11x                     11x     11x 11x 7x         10x 10x 9x 9x 9x 17x 17x 17x                           17x 3x   17x   9x   7x                   9x   2x        
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 => {
          Iif (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;
    }
  }
}