Skip to content

Commit

Permalink
Merge pull request #245 from jkcfg/2019-07-25-merge2
Browse files Browse the repository at this point in the history
std: Introduce a mergeFull function
  • Loading branch information
dlespiau authored Aug 14, 2019
2 parents a3f028f + 578460a commit 41d0bea
Show file tree
Hide file tree
Showing 4 changed files with 413 additions and 2 deletions.
10 changes: 10 additions & 0 deletions docs/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Deprecated in 0.2.x (will be removed in 0.3.0)

### merge and patch std functions

*Deprecated in 0.2.10*

The `merge` and `patch` function of the `@jkcfg/std/merge` module have been
deprecated in favour of the more general `mergeFull` function.

- `merge` and `patch` will be removed in `0.3.0`.
- `mergeFull` will be renamed to `merge` in `0.3.0`.

### std import

*Deprecated in 0.2.5*
Expand Down
3 changes: 2 additions & 1 deletion std/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"],
"no-use-before-define": ["error", { "functions": false }],
"no-shadow": 0,
"no-param-reassign": 0
"no-param-reassign": 0,
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
}
}
322 changes: 322 additions & 0 deletions std/merge.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,328 @@ function objectMerge(obj, mergeObj) {
return r;
}

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, mergeFull)(a[key], value);
}
return r;
}

/**
* 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 [[mergeFull]].
*/
export function deep(rules) {
return (a, b) => 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',
* },
* };
*
* mergeFull(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',
* },
* };
*
* mergeFull(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 { mergeFull, 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',
* }],
* },
* };
*
* mergeFull(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) => 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.
*
* `mergeFull` 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, `mergeFull` will throw an error.
*
* **Examples**:
*
* Merge primitive values with the default rules:
*
* ```js
* mergeFull(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',
* },
* }
*
* mergeFull(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 `mergeFull` is a
* function:
*
* ```js
* const add = (a, b) => a + b;
* mergeFull(1, 2, add);
*
* > 3
* ```
*
* For objects, each own property can be merged with different strategies. The
* third argument of `mergeFull` is an object associating properties with merge
* functions.
*
*
* ```js
* // merge a and b, adding the values of the `k0` property.
* mergeFull(a, b, { k0: add });
*
* >
* {
* k0: 3,
* k1: true,
* o: {
* o0: 'another string',
* }
* }
* ```
*/
export function mergeFull(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);
}

// Interpret a series of transformations expressed either as object
// patches (as in the argument to `patch` in this module), or
// functions. Usually the first argument will be an object,
Expand Down
Loading

0 comments on commit 41d0bea

Please sign in to comment.