@darkobits/formation
Version:
524 lines (459 loc) • 16.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.assignToScope = exports.mergeDeep = exports.assertType = undefined;
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
exports.assertIsEntry = assertIsEntry;
exports.isDefined = isDefined;
exports.isFunction = isFunction;
exports.throwError = throwError;
exports.capitalizeFirst = capitalizeFirst;
exports.lowercaseFirst = lowercaseFirst;
exports.mergeWithDeep = mergeWithDeep;
exports.parseFlags = parseFlags;
exports.onReady = onReady;
exports.toPairsWith = toPairsWith;
exports.mergeEntries = mergeEntries;
exports.invoke = invoke;
exports.greaterScopeId = greaterScopeId;
exports.applyToCollection = applyToCollection;
var _ramda = require('ramda');
var _constants = require('./constants');
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } // -----------------------------------------------------------------------------
// ----- Utilities -------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* TODO: Consider replacing mergeDeep with webpack-merge et. al.
*/
/**
* Returns true if the provided value is (likely) a plain object.
*
* @private
*
* @param {any} value
* @return {boolean}
*/
function isPlainObject(value) {
try {
return Object.getPrototypeOf(value).constructor.name === 'Object';
} catch (err) {
if (/cannot read property/ig.test(err.message)) {
return true;
}
throw err;
}
}
/**
* Throws an error if the provided value is not a [key, value] entry. Otherwise,
* returns true.
*
* @param {any} value
* @param {string} [label] - Optional label.
* @return {boolean}
*/
function assertIsEntry(value, label) {
if (!Array.isArray(value) || value.length !== 2) {
throwError(['Expected ' + (label ? label + ' to be a ' : '') + '[key, value] entry,', 'but got ' + (0, _ramda.type)(value) + '.'].join(' '));
}
return true;
}
/**
* Returns true if the provided value is not undefined.
*
* @param {any} value
* @return {boolean}
*/
function isDefined(value) {
return value !== undefined;
}
/**
* Because Jest's mocked functions fail a Function type check, we need to
* additionally check their "typeof" property.
*
* @param {any} value
* @return {boolean}
*/
function isFunction(value) {
return (0, _ramda.is)(Function, value) || typeof value === 'function';
}
/**
* Throws a new error with the provided message, prefixed with the module
* name.
*
* @param {string} message
*/
function throwError(message) {
throw new Error('[' + _constants.MODULE_NAME + '] ' + message);
}
/**
* Checks the type of a value and throws an error if it does not match one of
* the provided types.
*
* @param {string} callee - Label of the method/process to use in errors.
* @param {function|array} types - Constructor/class or list of constructors
* and classes to check against.
* @param {string} label - Label for the value being checked, used in errors.
* @param {any} value - Value to check.
*
* @return {boolean} - True if types match, throws otherwise.
*/
var assertType = exports.assertType = (0, _ramda.curry)(function (callee, types, label, value) {
types = [].concat(types);
var match = types.reduce(function (accumulator, type) {
var predicateFn = void 0;
switch (type) {
case Function:
predicateFn = isFunction;
break;
case Array:
predicateFn = Array.isArray;
break;
case undefined:
predicateFn = (0, _ramda.equals)(undefined);
break;
case null:
predicateFn = (0, _ramda.equals)(null);
break;
default:
predicateFn = (0, _ramda.is)(type);
break;
}
return accumulator || predicateFn(value);
}, false);
if (!match) {
var typeNames = types.map(function (ctor) {
try {
return ctor.prototype.constructor.name;
} catch (err) {
return (0, _ramda.type)(ctor);
}
}).join(' or ');
throwError([callee + ' expected ' + label + ' to be of type', typeNames + ',', 'but got ' + (0, _ramda.type)(value) + '.'].join(' '));
}
return true;
});
/**
* Capitalizes the first character in the provided string.
*
* @param {string} str
* @return {string}
*/
function capitalizeFirst(str) {
return str && String(str).substr(0, 1).toUpperCase() + String(str).substr(1);
}
/**
* Lowercases the first character in the provided string.
*
* @param {string} str
* @return {string}
*/
function lowercaseFirst(str) {
return str && String(str).substr(0, 1).toLowerCase() + String(str).substr(1);
}
/**
* Recursive version of R.mergeWith.
*
* @param {function} f - Merging function.
* @param {arglist} objs - Objects to merge (right to left.)
* @return {object}
*/
function mergeWithDeep(f) {
for (var _len = arguments.length, objs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
objs[_key - 1] = arguments[_key];
}
if (objs.length >= 2) {
var d = (0, _ramda.nth)(-2, objs) || {};
var s = (0, _ramda.nth)(-1, objs) || {};
var merged = (0, _ramda.mergeWith)(f, d, s);
if (objs.length === 2) {
return merged;
}
var rest = (0, _ramda.slice)(0, -2, objs);
return mergeWithDeep.apply(undefined, [f].concat(_toConsumableArray((0, _ramda.append)(merged, rest))));
} else if (objs.length === 1) {
return (0, _ramda.head)(objs);
}
return {};
}
/**
* Default merging function.
*
* - If values are primitives, use the value from source object, overwriting the
* value in the destination object.
* - If values are arrays, the source array is appended to the destination
* array. (Important for merging arrays of ngMessages.)
* - If values are objects, deep merge them.
*
* @param {object} d - Destination object.
* @param {object} s - Source object.
* @return {object} - Merged object.
*/
var DEFAULT_MERGER = function DEFAULT_MERGER(d, s) {
if (Array.isArray(d) && Array.isArray(s)) {
// Concat arrays.
return (0, _ramda.concat)(d, s);
} else if (isPlainObject(d) && isPlainObject(s)) {
// Deep-merge plain objects.
return mergeWithDeep(DEFAULT_MERGER, d, s);
}
// Otherwise, return the source value.
return s;
};
/**
* Partially-applied version of mergeWithDeep using the default merger.
*
* @param {arglist} objs - Objects to merge.
* @return {object} - Merged object.
*/
var mergeDeep = exports.mergeDeep = function mergeDeep() {
for (var _len2 = arguments.length, objs = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
objs[_key2] = arguments[_key2];
}
return mergeWithDeep.apply(undefined, [DEFAULT_MERGER].concat(objs));
};
/**
* Accepts a comma/space-delimited list of strings and returns an array of
* $-prefixed strings.
*
* @example
*
* "touched, submitted" => ['$touched', '$submitted']
*
* @private
*
* @param {string} string
* @return {array}
*/
function parseFlags(string) {
if (!string || string === '') {
return;
}
var states = (0, _ramda.map)(function (state) {
return state.length && '$' + state.replace(/[, ]/g, '');
}, String(string).split(/, ?| /g));
return (0, _ramda.filter)(_ramda.identity, states);
}
/**
* Spies on a key in the provided object and returns a promise that resolves
* when the value becomes defined or rejects when a timeout is reached.
*
* @param {object} obj - Base object.
* @param {string} key - Key in base object to spy on.
* @param {number} [timeout=10000] - Timeout period. Default is 10 seconds.
* @return {promise<*>} - Promise that resolves with the value at the named key
* once it is defined.
*/
function onReady(obj, key) {
var timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1000;
var start = new Date().getTime() / 1000;
return new Promise(function (resolve, reject) {
var cancel = setInterval(function () {
var now = new Date().getTime() / 1000;
if (obj[key] !== undefined) {
resolve(obj[key]);
clearInterval(cancel);
} else if (now - start > timeout) {
reject(new Error('[onReady] Timed-out.'));
clearInterval(cancel);
}
}, 1);
});
}
/**
* Assigns a value to an expression on the provided scope.
*
* @param {object} $parse - Angular $parse service.
* @param {object} scope - Angular scope to assign to.
* @param {*} value - Value to assign to scope.
* @param {string} expression - Expression in scope's parent to assign value to.
*/
var assignToScope = exports.assignToScope = (0, _ramda.curry)(function ($parse, scope, value, expression) {
var setter = void 0;
if (expression === '') {
setter = $parse('this[""]').assign;
} else {
setter = $parse(expression).assign;
}
if (setter) {
setter(scope, value);
}
});
/**
* Generates a list of pairs/entries from a collection using the provided
* key/value generation functions.
*
* If called with 2 arguments, they will be interpreted as [keyFn, collection],
* and values will be each member in the collection.
*
* If called with 3 arguments, they will be interpreted as
* [keyFn, valueFn, collection].
*
* @param {arglist} args - Key generation function, optional value generation
* function, and collection.
* @return {array}
*/
function toPairsWith() {
for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
var keyFn = _ramda.identity;
var valueFn = _ramda.identity;
var collection = [];
switch (args.length) {
case 1:
throw new Error('toPairsWith expects at least 2 arguments.');
case 3:
keyFn = args[0];
valueFn = args[1];
collection = args[2];
break;
case 2:
default:
keyFn = args[0];
collection = args[1];
break;
}
if (!isFunction(keyFn)) {
throwError('Expected key generation function to be of type "Function", but got "' + (typeof keyFn === 'undefined' ? 'undefined' : _typeof(keyFn)) + '".');
}
if (!isFunction(valueFn)) {
throwError('Expected key value generation function to be of type "Function", but got "' + (typeof valueFn === 'undefined' ? 'undefined' : _typeof(valueFn)) + '".');
}
if (!Array.isArray(collection)) {
throwError('Expected collection to be of type "Array", but got "' + (typeof collection === 'undefined' ? 'undefined' : _typeof(collection)) + '".');
}
return collection.map(function () {
return [String(keyFn.apply(undefined, arguments)), valueFn.apply(undefined, arguments)];
});
}
/**
* Provided two lists of [key, value] entries, such as those generated using
* Object.entries or Map.prototype.entries, returns a list of
* [key, valueA, valueB] triplets by matching each entry in the source list with
* each of its corresponding entries in the destination list. Extraneous entries
* in the source list will be dropped.
*
* @param {array} dest - Destination set of entries.
* @param {array} src - Source set of entries.
* @return {array}
*/
function mergeEntries() {
var dest = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
var src = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
var check = assertType('mergeEntries');
check(Array, 'first argument', dest);
check(Array, 'second argument', src);
return dest.map(function (destEntry) {
assertIsEntry(destEntry);
var match = src.find(function (srcEntry) {
return assertIsEntry(srcEntry) && srcEntry[0] === destEntry[0];
});
return [destEntry[0], destEntry[1], match ? match[1] : undefined];
});
}
/**
* Invokes the named method on the provided object (if it exists), optionally
* passing any additional arguments as parameters to the method.
*
* @param {string} method - Method name to invoke.
* @param {object} obj - Target object.
* @param {arglist} [args] - Additional arguments to pass to 'method'.
* @return {*}
*/
function invoke(method, obj) {
for (var _len4 = arguments.length, args = Array(_len4 > 2 ? _len4 - 2 : 0), _key4 = 2; _key4 < _len4; _key4++) {
args[_key4 - 2] = arguments[_key4];
}
return obj && isFunction(obj[method]) && obj[method].apply(obj, args);
}
/**
* Provided two objects that implement a '$getScope' method, returns the
* object with the greater $scope id. This is used to determine which object
* is likely to be lowe in the scope hierarchy.
*
* @param {object} a
* @param {object} b
* @return {object}
*/
function greaterScopeId(a, b) {
var aId = (0, _ramda.path)(['$id'], invoke('$getScope', a)) || 0;
var bId = (0, _ramda.path)(['$id'], invoke('$getScope', b)) || 0;
return Number(aId) > Number(bId) ? a : b;
}
/**
* Applies a set of data to each member in a collection by matching data to
* members and invoking a method on each member, passing it a data fragment.
*
* @example
*
* const collection = [
* {
* id: '1',
* setName: name => {
* this.name = name;
* }
* },
* {
* id: '2',
* setName: => {
* this.name = name;
* }
* }
* ];
*
* const data = {
* '1': 'foo',
* '2': 'bar'
* };
*
* // This will set the first item's name to 'foo', and the second item's name
* // to 'bar', based on matching keys in 'data' to 'id' in collection members.
* applyToCollection(collection, R.prop('id'), 'setName', data);
*
* @param {array} collection - Collection to apply data to.
* @param {function} entryFn - Function to pass to toPairsWith to generate the
* key (left hand side) for each entry in 'collection'.
* @param {string} memberFn - The function to invoke on each member in the
* collection to pass matched data fragments to.
* @param {object|array} data - Data to disperse to members of 'collection'.
*/
function applyToCollection(collection, entryFn, memberFn, data) {
// Convert collection to entries using the provided entry generation function.
var collectionEntries = toPairsWith(entryFn, collection);
// Convert data object to entries in the format [key, value].
var dataEntries = Object.entries(data || {});
// Correlate data to registry members by common name/key, generating
// triplets in the format [name, member, data].
var mergedEntries = mergeEntries(collectionEntries, dataEntries);
// For each triplet, invoke the provided method name on the collection member,
// passing it its matching data. Return an entry in the format
// [name, returnValue].
return (0, _ramda.map)(function (_ref) {
var _ref2 = _slicedToArray(_ref, 3),
name = _ref2[0],
member = _ref2[1],
data = _ref2[2];
return [name, invoke(memberFn, member, data)];
}, mergedEntries);
}
exports.default = {
applyToCollection: applyToCollection,
assertIsEntry: assertIsEntry,
assertType: assertType,
assignToScope: assignToScope,
capitalizeFirst: capitalizeFirst,
greaterScopeId: greaterScopeId,
invoke: invoke,
isDefined: isDefined,
isFunction: isFunction,
lowercaseFirst: lowercaseFirst,
mergeDeep: mergeDeep,
mergeEntries: mergeEntries,
mergeWithDeep: mergeWithDeep,
onReady: onReady,
parseFlags: parseFlags,
throwError: throwError,
toPairsWith: toPairsWith
};