@jkcfg/std
Version:
jk standard library
334 lines (333 loc) • 6.77 kB
JavaScript
/**
* @module std/merge
*/
function mergeFunc(rule, key, defaultFunc) {
const f = rule && rule[key];
if (f === undefined) {
return defaultFunc;
}
const t = typeof f;
if (t === 'object' && t !== 'function') {
return deep(f);
}
if (t !== 'function') {
throw new Error(`merge: expected a function in the rules objects but found a ${t}`);
}
return f;
}
function objectMerge2(a, b, rules) {
const r = {};
Object.assign(r, a);
for (const [key, value] of Object.entries(b)) {
r[key] = mergeFunc(rules, key, merge)(a[key], value);
}
return r;
}
function isObject(o) {
const t = typeof o;
return t === 'object' && !Array.isArray(o) && !!o;
}
function assertObject(o, prefix) {
if (!isObject(o)) {
throw new Error(`${prefix}: input value is not an object`);
}
}
function isArray(a) {
return Array.isArray(a);
}
function assertArray(o, prefix) {
if (!isArray(o)) {
throw new Error(`${prefix}: input is not an array`);
}
}
/**
* Merge strategy deep merging objects.
*
* @param rules optional set of merging rules.
*
* `deep` will deep merge objects. This is the default merging strategy of
* objects. It's possible to provide a set of rules to override the merge
* strategy for some properties. See [[merge]].
*/
export function deep(rules) {
return (a, b) => {
assertObject(a, 'deep');
assertObject(b, 'deep');
return objectMerge2(a, b, rules);
};
}
/**
* Merge strategy merging two values by selecting the first value.
*
* **Example**:
*
* ```js
* let a = {
* k0: 1,
* o: {
* o0: 'a string',
* },
* };
*
* let b = {
* k0: 2,
* k1: true,
* o: {
* o0: 'another string',
* },
* };
*
* merge(a, b, { o: first() });
* ```
*
* Will give the result:
*
* ```js
* {
* k0: 2,
* k1: true,
* o: {
* o0: 'a string',
* },
* }
* ```
*/
export function first() {
return (a, _) => a;
}
/**
* Merge strategy merging two values by selecting the second value.
*
* **Example**:
*
* ```js
* let a = {
* k0: 1,
* o: {
* o0: 'a string',
* o1: 'this will go away!',
* },
* };
*
* let b = {
* k0: 2,
* k1: true,
* o: {
* o0: 'another string',
* },
* };
*
* merge(a, b, { o: replace() });
* ```
*
* Will give the result:
*
* ```js
* {
* k0: 2,
* k1: true,
* o: {
* o0: 'another string',
* },
* }
* ```
*/
export function replace() {
return (_, b) => b;
}
function arrayMergeWithKey(a, b, mergeKey, rules) {
const r = Array.from(a);
const toAppend = [];
for (const value of b) {
const i = a.findIndex(o => o[mergeKey] === value[mergeKey]);
if (i === -1) {
// Object doesn't exist in a, save it in the list of objects to append.
toAppend.push(value);
continue;
}
r[i] = objectMerge2(a[i], value, rules);
}
Array.prototype.push.apply(r, toAppend);
return r;
}
/**
* Merge strategy for arrays of objects, deep merging objects having the same
* `mergeKey`.
*
* @param mergeKey key used to identify the same object.
* @param rules optional set of rules to merge each object.
*
* **Example**:
*
* ```js
* import { merge, deep, deepWithKey } from '@jkcfg/std/merge';
*
* const pod = {
* spec: {
* containers: [{
* name: 'my-app',
* image: 'busybox',
* command: ['sh', '-c', 'echo Hello Kubernetes!'],
* },{
* name: 'sidecar',
* image: 'sidecar:v1',
* }],
* },
* };
*
* const sidecarImage = {
* spec: {
* containers: [{
* name: 'sidecar',
* image: 'sidecar:v2',
* }],
* },
* };
*
* merge(pod, sidecarImage, {
* spec: {
* containers: deepWithKey('name'),
* },
* });
* ```
*
* Will give the result:
*
* ```js
* {
* spec: {
* containers: [
* {
* command: [
* 'sh',
* '-c',
* 'echo Hello Kubernetes!',
* ],
* image: 'busybox',
* name: 'my-app',
* },
* {
* image: 'sidecar:v2',
* name: 'sidecar',
* },
* ],
* },
* }
* ```
*/
export function deepWithKey(mergeKey, rules) {
return (a, b) => {
assertArray(a, 'deepWithKey');
assertArray(b, 'deepWithKey');
return arrayMergeWithKey(a, b, mergeKey, rules);
};
}
/**
* Merges `b` into `a` with optional merging rule(s).
*
* @param a Base value.
* @param b Merge value.
* @param rule Set of merge rules.
*
* `merge` will recursively merge two values `a` and `b`. By default:
*
* - if `a` and `b` are primitive types, `b` is the result of the merge.
* - if `a` and `b` are arrays, `b` is the result of the merge.
* - if `a` and `b` are objects, every own property is merged with this very
* set of default rules.
* - the process is recursive, effectively deep merging objects.
*
* if `a` and `b` have different types, `merge` will throw an error.
*
* **Examples**:
*
* Merge primitive values with the default rules:
*
* ```js
* merge(1, 2);
*
* > 2
* ```
*
* Merge objects with the default rules:
*
* ```js
* const a = {
* k0: 1,
* o: {
* o0: 'a string',
* },
* };
*
* let b = {
* k0: 2,
* k1: true,
* o: {
* o0: 'another string',
* },
* }
*
* merge(a, b);
*
* >
* {
* k0: 2,
* k1: true,
* o: {
* o0: 'another string',
* }
* }
* ```
*
* **Merge strategies**
*
* It's possible to override the default merging rules by specifying a merge
* strategy, a function that will compute the result of the merge.
*
* For primitive values and arrays, the third argument of `merge` is a
* function:
*
* ```js
* const add = (a, b) => a + b;
* merge(1, 2, add);
*
* > 3
* ```
*
* For objects, each own property can be merged with different strategies. The
* third argument of `merge` is an object associating properties with merge
* functions.
*
*
* ```js
* // merge a and b, adding the values of the `k0` property.
* merge(a, b, { k0: add });
*
* >
* {
* k0: 3,
* k1: true,
* o: {
* o0: 'another string',
* }
* }
* ```
*/
export function merge(a, b, rule) {
const [typeA, typeB] = [typeof a, typeof b];
if (a === undefined) {
return b;
}
if (typeA !== typeB) {
throw new Error(`merge cannot combine values of types ${typeA} and ${typeB}`);
}
// Primitive types and arrays default to being replaced.
if (Array.isArray(a) || typeA !== 'object') {
if (typeof rule === 'function') {
return rule(a, b);
}
return b;
}
// Objects.
return objectMerge2(a, b, rule);
}