alwz
Version:
Extendable library for typecasting
293 lines • 9.24 kB
JavaScript
import EV from './models/ErrorValue.js';
import Converter, { assertFallback, isConversion, assertConversion } from './models/Converter.js';
import * as presets from './presets.js';
const InvalidArgument = class extends EV {
};
/**
* @namespace utils
* @description extra utils functions
* @example
* const { array, tuple, range, variant, object, dictionary } = a.utils;
*/
/**
* @memberof utils
* @description constrain data to an array elements of a given type
* @example
* const Numbers = array(Number);
*
* Numbers(); // []
* Numbers([]); // []
* Numbers([true, 2, "3", {}]); // [1, 2, 3, NaN]
*
* @example <caption>sparse arrays behavior</caption>
* // Be aware of sparse arrays behavior - conversion is not performed for empty items
* numArray[1, , 3] // [1, , 3]
* @param {Conversion<*, OUTPUT>} conversion - item conversion
* @param {Conversion<*, Array<*>>} initiator - input data initial conversion
* @returns {Conversion<*, Array<OUTPUT>>}
*/
export const array = (conversion, initiator = presets.array.convert) => {
assertConversion(conversion);
if (!isConversion(initiator)) {
throw new Converter.InvalidConversionFunction('initiator must be a function', initiator);
}
return (input) => {
return initiator(input).map(conversion);
};
};
/**
* @memberof utils
* @description constrain data to a tuple with given types
* @example
* const NumStrBool = tuple([Number, String, Boolean]);
*
* NumStrBool(); // [NaN, 'undefined', false]
* NumStrBool(null); // [NaN, 'undefined', false]
* NumStrBool([]); // [NaN, '', false]
* NumStrBool('5'); // [5, 'undefined', false]
* NumStrBool(['1', '2', '3']); // [1, '2', true]
*
* @param {Array<Conversion<*, *>>} conversions - tuple elemets conversions
* @param {Conversion<*, Array<*>>} initiator - input data initial conversion
* @returns {Conversion<*, Array<*>>}
*/
export const tuple = (conversions, initiator = presets.array.convert) => {
if (!Array.isArray(conversions)) {
throw new Converter.InvalidConversionFunction('first argument must be an array', conversions);
}
conversions.forEach((conversion, index) => {
if (!isConversion(conversion)) {
throw new Converter.InvalidConversionFunction('conversion ' + index + ' must be a function', [index, conversion]);
}
});
if (!isConversion(initiator)) {
throw new Converter.InvalidConversionFunction('initiator must be a function', initiator);
}
return (input) => {
const array = initiator(input);
return conversions.map((fn, index) => fn(array[index]));
};
};
/**
* @memberof utils
* @description constrain variable value within a given range
* @example
* const range37 = range(3, 7);
*
* range37(1); // 3
* range37(5); // 5
* range37(9); // 7
*
*
* const range37WithCustomFallback = range(3, 7, () => -1);
*
* range37WithCustomFallback(1); // -1
* range37WithCustomFallback(5); // 5
* range37WithCustomFallback(9); // -1
*
*
* const rangeString = range('k', 'w', undefined, String);
*
* rangeString('a'); // k
* rangeString('n'); // n
* rangeString('z'); // w
*
* @param {OUTPUT} lower - lower range border
* @param {OUTPUT} upper - upper range border
* @param {Fallback<OUTPUT>} fallback - fallback value generator
* @param {Conversion<*, OUTPUT>} conversion - input data conversion
* @returns {Conversion<*, OUTPUT>}
*/
export const range = (lower = -Number.MAX_VALUE, upper = Number.MAX_VALUE, fallback, conversion = presets.double.convert) => {
assertConversion(conversion);
lower = conversion(lower);
upper = conversion(upper);
const fallbackActual = fallback === undefined
? (input) => {
const converted = conversion(input);
return upper <= converted ? upper : lower;
}
: fallback;
assertFallback(fallbackActual);
return (input) => {
const converted = conversion(input);
if ((lower <= converted) && (converted <= upper)) {
return converted;
}
return fallbackActual(input);
};
};
/**
* @memberof utils
* @description constrain variable to given variants
* @example
* const oneOf123 = variant([1, 2, 3]);
*
* oneOf123(1); // 1
* oneOf123(2); // 2
* oneOf123(3); // 3
* oneOf123(4); // 1
* oneOf123(-5); // 1
*
*
* const oneOf123WithCustomFallback = variant([1, 2, 3], () => -1);
*
* oneOf123WithCustomFallback(4); // -1
*
*
* oneOf123Strict([1, 2, 3], () => {
* throw new Error('invalid input');
* });
* oneOf123Strict(4); // throws an Error
*
*
* const oneOfAB = variant(['a', 'b'], (i) => ['a', 'b'][i], String);
*
* oneOfAB('a'); // 'a'
* oneOfAB('b'); // 'b'
* oneOfAB(0); // 'a'
* oneOfAB(1); // 'b'
*
* @param {Array<OUTPUT>} values - valid values list
* @param {Fallback<OUTPUT>} fallback - fallback value generator
* @param {Conversion<*, OUTPUT>} conversion - input data conversion
* @returns {Conversion<*, OUTPUT>}
*/
export const variant = (values, fallback = () => values[0], conversion = presets.double.convert) => {
assertConversion(conversion);
assertFallback(fallback);
if (!Array.isArray(values)) {
throw new InvalidArgument('variant values must be an array of allowed values', values);
}
values = values.map((value) => conversion(value));
return (input) => {
const converted = conversion(input);
if (values.includes(converted)) {
return converted;
}
return fallback(input);
};
};
/**
* @memberof utils
* @description cast data into an object with a given schema
* @example
* const obj = object({
* a: a.ubyte,
* b: array(object({
* c: a.int,
* d: a.string,
* })),
* });
*
* obj(undefined); // { a: 0, b: [] }
* obj({ a: 999, b: [{ c: 2.5, d: 3 }, null] }); // { a: 255, b: [{ c: 2, d: '3' }, { c: 0, d: '' }] }
*
* @param {Record<string, Conversion<any, OUTPUT>>} schema
* @param {Conversion<any, OUTPUT>} conversion - input data conversion
* @returns {Conversion<any, OUTPUT>}
*/
export const object = (schema, conversion = presets.object.convert) => {
if (typeof schema !== 'object' || schema === null) {
throw new InvalidArgument('schema must must be an object', schema);
}
for (const key in schema) {
const conversion = schema[key];
assertConversion(conversion);
}
assertConversion(conversion);
return (input) => {
const result = {};
const source = conversion(input);
for (const key in schema) {
const type = schema[key];
result[key] = type(source === null || source === void 0 ? void 0 : source[key]);
}
return result;
};
};
/**
* @memberof utils
* @description cast data into a dictionary
* @example
* const dictOfInt = utils.dictionary(a.int);
*
* dictOfInt(undefined); // { }
* dictInt({ a: null, b: true, c: '2', d: [3, 4] }); // { a: 0, b: 1, c: 2, d: 3 }
*
* @param {(value: any, key: string | number) => VALUE} conversion - item conversion
* @param {Conversion<any, any>} initiator - input data conversion
* @returns {Conversion<any, Record<string | number, VALUE>>}
*/
export const dictionary = (conversion, initiator = presets.object.convert) => {
if (typeof conversion !== 'function') {
throw new InvalidArgument('conversion must be a function', conversion);
}
assertConversion(initiator);
return (input) => {
const result = {};
const source = initiator(input);
for (const key in source) {
result[key] = conversion(source === null || source === void 0 ? void 0 : source[key], key);
}
return result;
};
};
/**
* @description project data into object according to schema
* @param {Schema} schema
* @returns {Function}
* @example
* const schema = {
* // shallow element
* a: (source) => source.x + 1,
* // nested schema
* b: {
* c: (source) => source.x + 2,
* },
* // options ( second argument )
* d: (source, options) => options,
* // mid-process result access
* e: (source, options, target) => { target._e = source.x + 3; },
* // call context
* f: function() { return this; },
* };
*
* const project = projection(schema);
* const reshape = project(schema);
* const source = { x: 1 };
* const options = { z: 5 };
* const context = { y: 11 };
*
* project.call(context, source, options);
* {
* a: 2,
* b: { c: 3 },
* d: { z: 5 },
* _e: 7, e: undefined,
* f: { y: 11 },
* }
*/
export const projection = (schema) => {
const builders = Object
.entries(schema)
.map(([key, value]) => {
if (typeof value === 'function') {
return [key, value];
}
else if (value && typeof value === 'object') {
return [key, projection(value)];
}
else {
throw new EV('invalid schema item', { key, value });
}
});
return function (source, options) {
return builders
.reduce((target, [key, build]) => {
target[key] = build.call(this, source, options, target);
return target;
}, {});
};
};
//# sourceMappingURL=utils.js.map