UNPKG

@darkobits/formation

Version:
524 lines (459 loc) 16.3 kB
'use strict'; 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 };