import Exception from './Exception.js';

/**
 * Thrown to indicate that the type of a value isn't expected.
 *
 * @example <caption>Basic Usage</caption>
 * function greet(name) {
 *   var checked = InvalidType.check(name, 'string');
 *   alert('Hello, ' + checked);
 * }
 * greet('John'); // "Hello, John"
 * greet(1234);   // Throws: "unexpected type of value ..."
 *
 * @example <caption>Multiple Expectations</caption>
 * var value = 'ABC';
 * InvalidType.check(value, 'boolean', 'string', 'object'); // OK
 *
 * @example <caption>Class Checking</caption>
 * class Beagle extends Dog {
 *   ...
 * }
 * var oliver = new Beagle();
 * InvalidType.check(oliver, Dog);      // OK
 * InvalidType.check(oliver, Beagle);   // OK
 * InvalidType.check(oliver, ShibaInu); // Throws
 *
 * @example <caption>Using .info for debug inside 'catch'</caption>
 * value = 123;
 * try {
 *   InvalidType.check(value, 'string', 'object');
 * } catch (e) {
 *   console.debug( e.info.checked  ); // 123
 *   console.debug( e.info.expected ); // ['string', 'object']
 *   console.debug( e.info.actual   ); // 'number'
 * }
 *
 * @extends Exception
 * @hideconstructor
 */
class InvalidType extends Exception {
	/**
	 * Returns if this exception expected `type`.
	 * @param {string|class} type
	 * @return {boolean}
	 * @see InvalidType.check
	 */
	expects(type) {
		return super.expects(type);
	}
	static failed(checked, ...expected) {
		return new this(`unexpected type of value`, {
			checked,
			expected: expected.length > 1 ? expected : expected[0],
			actual: typeof checked
		});
	}
	/**
	 * Checks if the type of `value` matches with `expected`. If it does, just returns `value`.
	 * Otherwise, [triggers]{@link InvalidType#trigger} an exception.
	 *
	 * The triggered exception holds `value` and `expected` as `.info.checked` and `.info.expected`.
	 * And the actual type is stored in `.info.actual`.
	 *
	 * @param {any} value A value to check the type
	 * @param {...string|class} expected The expected type(s)
	 * ##### Available Types
	 * | Type | Description |
	 * |-----:|:------------|
	 * | Any type that `typeof` returns | See: {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof} |
	 * | `bool` | Alias of `boolean` |
	 * | `int`, `integer` | Matches with integer numbers |
	 * | `iterable` | Matches with an array or array-like object |
	 * | A class (constructor function) | Matches with an instance of the class |
	 *
	 * @return {any} Just returns the `value` argument if there's no problem
	 */
	static check(value, ...expected) {
		for (let type of expected) {
			if (isTypeOf(value, type)) return value;
		}
		return this.failed(value, expected).trigger();
	}
}

function isTypeOf(value, expected) {
	if (typeof expected == 'function') return value instanceof expected;

	if (expected == 'iterable') {
		if (value === null) return false;
		if (value === undefined) return false;
		return typeof value[Symbol.iterator] == 'function';
	}

	let actual = typeof value;
	if (actual === expected) return true;

	switch (actual) {
	case 'boolean':
		return expected == 'bool';
	case 'number':
		switch (expected) {
		case 'int':
		case 'integer':
			return isFinite(value) && Math.floor(value) === value;
		}
		break;
	}
	return false;
}

export default InvalidType;

Documentation generated by JSDoc 3.6.6
on
using docolatte theme