alwz
Version:
Extendable library for typecasting
256 lines • 8.06 kB
JavaScript
import EV from './ErrorValue.js';
export function isIS(input) {
return typeof input === 'function';
}
export function assertIS(input) {
if (isIS(input))
return true;
throw new Converter.InvalidTypeCheckFunction('type check must be a function', input);
}
export function isFallback(input) {
return typeof input === 'function';
}
export function assertFallback(input) {
if (isFallback(input))
return true;
throw new Converter.InvalidFallbackFunction('fallback must be a function', input);
}
export function isConversion(input) {
return typeof input === 'function';
}
export function assertConversion(input) {
if (isConversion(input))
return true;
throw new Converter.InvalidConversionFunction('conversion must be a function', input);
}
/**
* @description converts input data to specific type
* - at first checks if conversion is necessary
* - attempts conversion based on the input data type
* - searches for suitable conversions among registered
* - calls a fallback function
*
* @example <caption>converter creation</caption>
* const positive = new Converter(
* (input) => typeof input === 'number' && input > 0,
* (input) => input === 0 ? 0.1 : 0.2
* );
*
* positive
* .undefined(() => 0.3)
* .boolean((i) => i ? 1 : 0.4)
* .number(function(i) {
* const result = Math.abs(i)
* return this.is(result) ? result : this.fallback(i);
* })
* .string((i) => positive.convert(Number(i)))
* .symbol((i) => positive.convert(Symbol.keyFor(i)))
* .bigint((i) => positive.convert(Number(i)))
* .register(Array.isArray, (i) => positive.convert(i[0]))
* .register((i) => i === null, (i) => 0.5);
*
* positive.convert(1); // 1
* positive.convert(0); // 0.1 (fallback)
* positive.convert(NaN); // 0.2 (fallback)
* positive.convert(undefined); // 0.3 (has own handler)
* positive.convert(false); // 0.4 (has own handler)
* positive.convert(null); // 0.5 (has own handler)
* positive.convert(2n); // 2
* positive.convert(-3); // 3
* positive.convert('4'); // 4
* positive.convert([5, 6]); // 5
*
* @example <caption>conversion with prohibited input types</caption>
* const converter = new Converter(
* (input) => typeof input === 'number',
* (input) => {
* throw new Error('unknown input data type:' + input);
* })
* .string((i) => {
* throw new Error('string input is forbidden:' + i);
* })
* .boolean(Number)
* .register(Array.isArray, (i) => converter.convert(i[0]));
*
* converter.convert(true); // 1
* converter.convert(2); // 2
* converter.convert('3'); // Error
* converter.convert([4]); // 4
* converter.convert(Promise.resolve(5)); // Error
*/
export class Converter {
/**
* @param {IS<OUTPUT>} is - initial input data type check (predicate). determines if any conversion is necessary
* @param {Fallback<OUTPUT>} fallback - fallback value generator. runs if none of the available conversions are suitable
*/
constructor(is, fallback) {
/**
* @description converts data according to saved conversion rules
* @param {*} input - input data
*/
this.convert = (input) => {
if (this._is(input))
return input;
const type = (typeof input);
if (type in this._types) {
const conversion = this._types[type];
return conversion.call(this, input);
}
else {
for (const is of this._conversions.keys()) {
if (is(input)) {
const conversion = this._conversions.get(is);
return conversion.call(this, input);
}
}
}
return this._fallback(input);
};
this.is = is;
this.fallback = fallback;
this._types = {};
this._conversions = new Map();
Object.seal(this);
}
get is() {
return this._is;
}
set is(is) {
assertIS(is);
this._is = is;
}
get fallback() {
return this._fallback;
}
set fallback(fallback) {
assertFallback(fallback);
this._fallback = fallback;
}
get types() {
return Object.assign({}, this._types);
}
get conversions() {
return Array.from(this._conversions);
}
/**
* @description adds conversion function for `INPUT` type
* @param {IS<INPUT>} is - input data type check (predicate), determines if input can be processed by `conversion`
* @param {Conversion<INPUT, OUTPUT>} conversion - `INPUT` to `OUTPUT` conversion function
*/
register(is, conversion) {
assertIS(is);
assertConversion(conversion);
this._conversions.set(is, conversion);
return this;
}
/**
* @description removes conversion for `INPUT` type
* @param {IS<INPUT>} is - input type check (predicate)
*/
unregister(is) {
assertIS(is);
this._conversions.delete(is);
return this;
}
/**
* @description set conversion rule for type `name` if `conversion` is defined or unset if undefined
* @param {string} name - one of types (`typeof` result)
* @param {Conversion} [conversion]
*/
type(name, conversion) {
if (conversion === undefined) {
delete this._types[name];
}
else {
assertConversion(conversion);
this._types[name] = conversion;
}
return this;
}
/**
* @description conversion rule setter for `undefined` input
* @param {Conversion} [conversion]
*/
undefined(conversion) {
return this.type('undefined', conversion);
}
/**
* @description conversion rule setter for `boolean` input
* @param {Conversion} [conversion]
*/
boolean(conversion) {
return this.type('boolean', conversion);
}
/**
* @description conversion rule setter for `number` input
* @param {Conversion} [conversion]
*/
number(conversion) {
return this.type('number', conversion);
}
/**
* @description conversion rule setter for `bigint` input
* @param {Conversion} [conversion]
*/
bigint(conversion) {
return this.type('bigint', conversion);
}
/**
* @description conversion rule setter for `string` input
* @param {Conversion} [conversion]
*/
string(conversion) {
return this.type('string', conversion);
}
/**
* @description conversion rule setter for `symbol` input
* @param {Conversion} [conversion]
*/
symbol(conversion) {
return this.type('symbol', conversion);
}
/**
* @example
* const converter = new Converter(
* (i) => typeof i === 'number',
* () => 0
* )
* .undefined(() => 1);
*
* const clone = converter
* .clone()
* .undefined(() => 2);
*
* converter.convert(); // 1
* clone.convert(); // 2
*/
clone() {
return Converter.build(this._is, this._fallback, this._types, this._conversions);
}
static build(is, fallback, types = {}, conversions = []) {
const converter = new Converter(is, fallback);
for (const type in types) {
converter.type(type, types[type]);
}
for (const [is, conversion] of conversions) {
converter.register(is, conversion);
}
return converter;
}
static assert(input) {
if (Converter.is(input))
return true;
throw new Converter.InvalidConverter('input is not a Converter', input);
}
}
Converter.InvalidTypeCheckFunction = class extends EV {
};
Converter.InvalidFallbackFunction = class extends EV {
};
Converter.InvalidConversionFunction = class extends EV {
};
Converter.InvalidConverter = class extends EV {
};
Converter.is = (input) => input instanceof Converter;
export default Converter;
//# sourceMappingURL=Converter.js.map