You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
133 lines
3.9 KiB
JavaScript
133 lines
3.9 KiB
JavaScript
4 years ago
|
"use strict";
|
||
|
|
||
|
const util = require("util");
|
||
|
const range = require("range").range;
|
||
|
const fromEntries = require("fromentries");
|
||
|
|
||
|
const { validateArguments, validateValue } = require("@validatem/core");
|
||
|
const isArray = require("@validatem/is-array");
|
||
|
const isPlainObject = require("@validatem/is-plain-object");
|
||
|
const defaultTo = require("@validatem/default-to");
|
||
|
const hasLengthOf = require("@validatem/has-length-of");
|
||
|
const removeNullishItems = require("@validatem/remove-nullish-items");
|
||
|
const virtualProperty = require("@validatem/virtual-property");
|
||
|
const wrapPath = require("@validatem/wrap-path");
|
||
|
|
||
|
/* NOTE: In some cases below, we explicitly check for `undefined` only, rather than for both `undefined` and `null`. This is to allow explicitly overriding existent values with `null` during a merge. */
|
||
|
|
||
|
function wrapValidationPath(basePathSegments, lastProperty, rules) {
|
||
|
let combinedPath = basePathSegments.concat(virtualProperty(lastProperty));
|
||
|
|
||
|
return wrapPath(combinedPath, rules);
|
||
|
}
|
||
|
|
||
|
function mapToObject(items, mapper) {
|
||
|
return fromEntries(items.map(mapper));
|
||
|
}
|
||
|
|
||
|
function combineKeys(...objects) {
|
||
|
let allKeys = new Set();
|
||
|
|
||
|
for (let object of objects) {
|
||
|
for (let key of Object.keys(object)) {
|
||
|
allKeys.add(key);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return Array.from(allKeys);
|
||
|
}
|
||
|
|
||
|
let optionalArray = [ defaultTo([]), isArray ];
|
||
|
let optionalObject = [ defaultTo({}), isPlainObject ];
|
||
|
|
||
|
function mergeArray(subTemplate, aInput, bInput, path) {
|
||
|
let aItems = validateValue(aInput, wrapValidationPath(path, "a", [ optionalArray ]));
|
||
|
let bItems = validateValue(bInput, wrapValidationPath(path, "b", [ optionalArray ]));
|
||
|
|
||
|
let valueRule = subTemplate[0];
|
||
|
|
||
|
if (valueRule == null) {
|
||
|
/* No object merging rule specified, so just concatenate the items. */
|
||
|
return aItems.concat(bItems);
|
||
|
} else {
|
||
|
/* Object merging rule specified, so we should invoke that merging rule for each pair of objects. */
|
||
|
let itemCount = Math.max(aItems.length, bItems.length);
|
||
|
|
||
|
return range(0, itemCount).map((i) => {
|
||
|
return mergeValue(valueRule, aItems[i], bItems[i], path.concat([ i ]));
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function mergeObject(subTemplate, aInput, bInput, path) {
|
||
|
let a = validateValue(aInput, wrapValidationPath(path, "a", [ optionalObject ]));
|
||
|
let b = validateValue(bInput, wrapValidationPath(path, "b", [ optionalObject ]));
|
||
|
|
||
|
let allKeys = combineKeys(a, b, subTemplate);
|
||
|
|
||
|
return mapToObject(allKeys, (key) => {
|
||
|
let rule = subTemplate[key];
|
||
|
let value = mergeValue(rule, a[key], b[key], path.concat([ key ]));
|
||
|
|
||
|
return [ key, value ];
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function mergeValue(rule, a, b, path) {
|
||
|
if (rule == null) {
|
||
|
if (b !== undefined) {
|
||
|
return b;
|
||
|
} else {
|
||
|
return a;
|
||
|
}
|
||
|
} else if (typeof rule === "function") {
|
||
|
if (a === undefined) {
|
||
|
return b;
|
||
|
} else if (b === undefined) {
|
||
|
return a;
|
||
|
} else {
|
||
|
return rule(a, b);
|
||
|
}
|
||
|
} else if (typeof rule === "object") {
|
||
|
if (Array.isArray(rule)) {
|
||
|
return mergeArray(rule, a, b, path);
|
||
|
} else {
|
||
|
return mergeObject(rule, a, b, path);
|
||
|
}
|
||
|
} else {
|
||
|
throw new Error(`Unrecognized rule: ${util.inspect(rule)}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
createMerger: function createMerger(template) {
|
||
|
return function merge(_items) {
|
||
|
let [ items ] = validateArguments(arguments, [
|
||
|
[ "items", [
|
||
|
isArray,
|
||
|
removeNullishItems,
|
||
|
hasLengthOf(2)
|
||
|
]]
|
||
|
]);
|
||
|
|
||
|
return items.slice(1).reduce((merged, item) => {
|
||
|
return mergeValue(template, merged, item, []);
|
||
|
}, items[0]);
|
||
|
};
|
||
|
},
|
||
|
anyProperty: function (template) {
|
||
|
/* Used for cases where an object is used like a key->value map */
|
||
|
return function merge(aInput, bInput, path) {
|
||
|
let a = validateValue(aInput, wrapValidationPath(path, "a", [ optionalObject ]));
|
||
|
let b = validateValue(bInput, wrapValidationPath(path, "b", [ optionalObject ]));
|
||
|
|
||
|
let allKeys = combineKeys(a, b);
|
||
|
|
||
|
return mapToObject(allKeys, (key) => {
|
||
|
let value = mergeValue(template, a[key], b[key], path.concat([ key ]));
|
||
|
return [ key, value ];
|
||
|
});
|
||
|
};
|
||
|
}
|
||
|
};
|