All files / src/bridge type-coercion.ts

100% Statements 47/47
100% Branches 79/79
100% Functions 2/2
100% Lines 38/38

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                            41x 5x       36x 35x 34x     33x 7x 7x 1x   6x       26x 5x       21x 6x 6x 3x 1x       15x 2x       13x 6x       7x 4x       3x 3x 3x                       18x 13x 13x 12x 11x   10x 2x   8x 2x   6x 4x   2x      
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;
  }
}