All files / src/bridge type-coercion.ts

100% Statements 62/62
100% Branches 71/71
100% Functions 2/2
100% Lines 62/62

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 941x 1x             1x       1x   41x 5x 5x     41x 41x 41x     41x 7x 7x 1x 1x 6x 6x     41x 5x 5x     41x 6x 6x 5x 1x 1x     41x 2x 2x     41x 6x 6x     41x 4x 4x     3x 3x 3x 3x 3x 3x 3x 3x 41x         1x 18x 13x 18x 18x 18x   18x 2x 2x 18x 2x 2x 18x 4x 4x 2x 18x 1x  
import { SpelEvaluationException } from '../error/spel-evaluation-exception.js';
import { SpelMessage } from '../error/spel-message.js';
 
/**
 * Parallels Spring TypeConverter / SpelTypeConverter
 *
 * Type coercion, handles implicit type conversion in SpEL.
 */
export class SpelTypeConverter {
  /**
   * Try converting value to target type
   */
  public convertValue(value: unknown, targetType: new (...args: unknown[]) => unknown): unknown {
    // null → null (any type)
    if (value === null || value === undefined) {
      return null;
    }
 
    // Already matching type
    if (typeof value === 'string' && targetType === String) return value;
    if (typeof value === 'number' && targetType === Number) return value;
    if (typeof value === 'boolean' && targetType === Boolean) return value;
 
    // String → Number
    if (typeof value === 'string' && targetType === Number) {
      const num = parseFloat(value);
      if (isNaN(num)) {
        throw new SpelEvaluationException(-1, SpelMessage.TYPE_CONVERSION_ERROR, value, 'Number');
      }
      return num;
    }
 
    // Number → String
    if (typeof value === 'number' && targetType === String) {
      return String(value);
    }
 
    // String → Boolean
    if (typeof value === 'string' && targetType === Boolean) {
      const s = value.toLowerCase();
      if (s === 'true') return true;
      if (s === 'false') return false;
      throw new SpelEvaluationException(-1, SpelMessage.TYPE_CONVERSION_ERROR, value, 'Boolean');
    }
 
    // Boolean → String
    if (typeof value === 'boolean' && targetType === String) {
      return String(value);
    }
 
    // Number → Boolean (non-zero is truthy)
    if (typeof value === 'number' && targetType === Boolean) {
      return value !== 0;
    }
 
    // Boolean → Number
    if (typeof value === 'boolean' && targetType === Number) {
      return value ? 1 : 0;
    }
 
    // Cannot convert — value must be object/function at this point
    const typeLabel = typeof value;
    const valueStr = typeLabel;
    throw new SpelEvaluationException(
      -1,
      SpelMessage.TYPE_CONVERSION_ERROR,
      `${valueStr} (${typeLabel})`,
      targetType.name,
    );
  }
 
  /**
   * Check if conversion is possible
   */
  public canConvert(value: unknown, targetType: new (...args: unknown[]) => unknown): boolean {
    if (value === null || value === undefined) return true;
    const valueType = typeof value;
    if (valueType === 'string' && targetType === String) return true;
    if (valueType === 'number' && targetType === Number) return true;
    if (valueType === 'boolean' && targetType === Boolean) return true;
 
    if (valueType === 'string' && (targetType === Number || targetType === Boolean)) {
      return true;
    }
    if (valueType === 'number' && (targetType === String || targetType === Boolean)) {
      return true;
    }
    if (valueType === 'boolean' && (targetType === String || targetType === Number)) {
      return true;
    }
    return false;
  }
}