From 88fa0705fe0f6e0041e01959f853f6d2816bc429 Mon Sep 17 00:00:00 2001 From: Damien Lespiau Date: Wed, 14 Aug 2019 14:29:26 +0100 Subject: [PATCH 1/9] std: Introduce a mergeFull function --- std/merge.js | 241 ++++++++++++++++++++++++++++++++++++++++++++++ std/merge.test.js | 46 ++++++++- 2 files changed, 286 insertions(+), 1 deletion(-) diff --git a/std/merge.js b/std/merge.js index 626a2da0..8388f7c7 100644 --- a/std/merge.js +++ b/std/merge.js @@ -81,6 +81,247 @@ function objectMerge(obj, mergeObj) { return r; } +function mergeFunc(rule, key, defaultFunc) { + if (rule === undefined) { + return defaultFunc; + } + + const f = rule[key]; + if (f === undefined) { + return defaultFunc; + } + + if (typeof f !== 'function') { + throw new Error(`merge: expected a function in the rules objects but found a ${typeof f}`); + } + + 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); +} + +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: deep({ + * containers: deepWithKey('name'), + * }), + * }); + * ``` + * + * Will result to: + * + * ```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, diff --git a/std/merge.test.js b/std/merge.test.js index d3273161..1ae9a5d8 100644 --- a/std/merge.test.js +++ b/std/merge.test.js @@ -1,4 +1,6 @@ -import { mix, patch, merge } from './merge'; +import { + mix, patch, merge, mergeFull, deep, deepWithKey, +} from './merge'; test('mix objects', () => { const r = mix({ foo: 1 }, { bar: 2 }, { foo: 3 }); @@ -144,3 +146,45 @@ test('array patch', () => { baz: 2, }); }); + +test('mergeFull: default merging of primitive values', () => { + expect(mergeFull(1, 2)).toEqual(2); + expect(mergeFull('a', 'b')).toEqual('b'); + expect(() => mergeFull('a', 1)).toThrow(); + expect(() => mergeFull(true, 'b')).toThrow(); + expect(mergeFull([1, 2], [3, 4])).toEqual([3, 4]); + expect(mergeFull({ foo: 1 }, { bar: 2 })).toEqual({ foo: 1, bar: 2 }); +}); + +const pod = { + spec: { + containers: [{ + name: 'my-app', + image: 'busybox', + command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600'], + }, { + name: 'sidecar', + image: 'sidecar:v1', + }], + }, +}; + +test('mergeFull: array of objects, merging objects identified by a key', () => { + const sidecarImage = { + spec: { + containers: [{ + name: 'sidecar', + image: 'sidecar:v2', + }], + }, + }; + + const result = mergeFull(pod, sidecarImage, { + spec: deep({ + containers: deepWithKey('name'), + }), + }); + + expect(result.spec.containers.length).toEqual(2); + expect(result.spec.containers[1].image).toEqual('sidecar:v2'); +}); From 6bc1196e700ab396fe92cd0e67c6e1ab26e965de Mon Sep 17 00:00:00 2001 From: Damien Lespiau Date: Wed, 14 Aug 2019 14:48:07 +0100 Subject: [PATCH 2/9] docs: Add a deprecation note for merge and patch --- docs/deprecations.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/deprecations.md b/docs/deprecations.md index 9da55601..a71fe803 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -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* From df47adc5c3fd92568ac0d8b55e0138bf89d28ca6 Mon Sep 17 00:00:00 2001 From: Damien Lespiau Date: Wed, 14 Aug 2019 14:50:21 +0100 Subject: [PATCH 3/9] merge: Simplify logic computing the default merge function --- std/merge.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/std/merge.js b/std/merge.js index 8388f7c7..ef88cf2f 100644 --- a/std/merge.js +++ b/std/merge.js @@ -82,11 +82,7 @@ function objectMerge(obj, mergeObj) { } function mergeFunc(rule, key, defaultFunc) { - if (rule === undefined) { - return defaultFunc; - } - - const f = rule[key]; + const f = rule && rule[key]; if (f === undefined) { return defaultFunc; } From 52755e949870f100dc1ac817caf92fba419d9d5b Mon Sep 17 00:00:00 2001 From: Damien Lespiau Date: Wed, 14 Aug 2019 15:13:09 +0100 Subject: [PATCH 4/9] merge: Treat objects in rules as the rules for the deep merge strategy --- std/merge.js | 12 ++++++++---- std/merge.test.js | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/std/merge.js b/std/merge.js index ef88cf2f..9eb4ca67 100644 --- a/std/merge.js +++ b/std/merge.js @@ -87,8 +87,12 @@ function mergeFunc(rule, key, defaultFunc) { return defaultFunc; } - if (typeof f !== 'function') { - throw new Error(`merge: expected a function in the rules objects but found a ${typeof f}`); + 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; @@ -170,9 +174,9 @@ function arrayMergeWithKey(a, b, mergeKey, rules) { * }; * * mergeFull(pod, sidecarImage, { - * spec: deep({ + * spec: { * containers: deepWithKey('name'), - * }), + * }, * }); * ``` * diff --git a/std/merge.test.js b/std/merge.test.js index 1ae9a5d8..a6695b51 100644 --- a/std/merge.test.js +++ b/std/merge.test.js @@ -188,3 +188,23 @@ test('mergeFull: array of objects, merging objects identified by a key', () => { expect(result.spec.containers.length).toEqual(2); expect(result.spec.containers[1].image).toEqual('sidecar:v2'); }); + +test('mergeFull: pick the deep merge strategy when encountering an object as rule', () => { + const sidecarImage = { + spec: { + containers: [{ + name: 'sidecar', + image: 'sidecar:v2', + }], + }, + }; + + const result = mergeFull(pod, sidecarImage, { + spec: { + containers: deepWithKey('name'), + }, + }); + + expect(result.spec.containers.length).toEqual(2); + expect(result.spec.containers[1].image).toEqual('sidecar:v2'); +}); From e5518dcdfd8d2f04c0ca7da79e414662fd90bd2c Mon Sep 17 00:00:00 2001 From: Damien Lespiau Date: Wed, 14 Aug 2019 15:38:55 +0100 Subject: [PATCH 5/9] tests: Factor out the sidecarImage variable: --- std/merge.test.js | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/std/merge.test.js b/std/merge.test.js index a6695b51..63e3cc6d 100644 --- a/std/merge.test.js +++ b/std/merge.test.js @@ -169,16 +169,16 @@ const pod = { }, }; -test('mergeFull: array of objects, merging objects identified by a key', () => { - const sidecarImage = { - spec: { - containers: [{ - name: 'sidecar', - image: 'sidecar:v2', - }], - }, - }; +const sidecarImage = { + spec: { + containers: [{ + name: 'sidecar', + image: 'sidecar:v2', + }], + }, +}; +test('mergeFull: array of objects, merging objects identified by a key', () => { const result = mergeFull(pod, sidecarImage, { spec: deep({ containers: deepWithKey('name'), @@ -190,15 +190,6 @@ test('mergeFull: array of objects, merging objects identified by a key', () => { }); test('mergeFull: pick the deep merge strategy when encountering an object as rule', () => { - const sidecarImage = { - spec: { - containers: [{ - name: 'sidecar', - image: 'sidecar:v2', - }], - }, - }; - const result = mergeFull(pod, sidecarImage, { spec: { containers: deepWithKey('name'), From 8aab6c1949feb054271d051b5612b770c05f47ae Mon Sep 17 00:00:00 2001 From: Damien Lespiau Date: Wed, 14 Aug 2019 15:53:11 +0100 Subject: [PATCH 6/9] merge: Fix broken english in API doc --- std/merge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/merge.js b/std/merge.js index 9eb4ca67..af8dd893 100644 --- a/std/merge.js +++ b/std/merge.js @@ -180,7 +180,7 @@ function arrayMergeWithKey(a, b, mergeKey, rules) { * }); * ``` * - * Will result to: + * Will give the result: * * ```js * { From f08597148039b0b32bce44817d6c7ab189a0fd87 Mon Sep 17 00:00:00 2001 From: Damien Lespiau Date: Wed, 14 Aug 2019 15:55:51 +0100 Subject: [PATCH 7/9] merge: Add a replace merge strategy --- std/merge.js | 41 +++++++++++++++++++++++++++++++++++++++++ std/merge.test.js | 13 ++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/std/merge.js b/std/merge.js index af8dd893..9604fe4b 100644 --- a/std/merge.js +++ b/std/merge.js @@ -121,6 +121,47 @@ export function deep(rules) { return (a, b) => objectMerge2(a, b, rules); } +/** + * 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 = []; diff --git a/std/merge.test.js b/std/merge.test.js index 63e3cc6d..8e2b6d9f 100644 --- a/std/merge.test.js +++ b/std/merge.test.js @@ -1,5 +1,5 @@ import { - mix, patch, merge, mergeFull, deep, deepWithKey, + mix, patch, merge, mergeFull, deep, replace, deepWithKey, } from './merge'; test('mix objects', () => { @@ -199,3 +199,14 @@ test('mergeFull: pick the deep merge strategy when encountering an object as rul expect(result.spec.containers.length).toEqual(2); expect(result.spec.containers[1].image).toEqual('sidecar:v2'); }); + +test('replace: basic', () => { + const result = mergeFull(pod, sidecarImage, { + spec: { + containers: replace(), + }, + }); + + expect(result.spec.containers.length).toEqual(1); + expect(result.spec.containers[0].name).toEqual('sidecar'); +}); From d22ec5010bd1d11312f2a3f0c11b1d167e80a5b6 Mon Sep 17 00:00:00 2001 From: Damien Lespiau Date: Wed, 14 Aug 2019 16:03:34 +0100 Subject: [PATCH 8/9] build: Ignore no-unused-vars when names are starting with _ --- std/.eslintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/std/.eslintrc b/std/.eslintrc index 5c1303cb..fb0b7362 100644 --- a/std/.eslintrc +++ b/std/.eslintrc @@ -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": "^_" }] } } From 578460a1f51bf0d0b4035aeb215f353cb111b5a5 Mon Sep 17 00:00:00 2001 From: Damien Lespiau Date: Wed, 14 Aug 2019 16:04:13 +0100 Subject: [PATCH 9/9] merge: Add a first merge strategy --- std/merge.js | 40 ++++++++++++++++++++++++++++++++++++++++ std/merge.test.js | 14 +++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/std/merge.js b/std/merge.js index 9604fe4b..1598aee5 100644 --- a/std/merge.js +++ b/std/merge.js @@ -121,6 +121,46 @@ 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. * diff --git a/std/merge.test.js b/std/merge.test.js index 8e2b6d9f..35eedf45 100644 --- a/std/merge.test.js +++ b/std/merge.test.js @@ -1,5 +1,5 @@ import { - mix, patch, merge, mergeFull, deep, replace, deepWithKey, + mix, patch, merge, mergeFull, deep, first, replace, deepWithKey, } from './merge'; test('mix objects', () => { @@ -200,6 +200,18 @@ test('mergeFull: pick the deep merge strategy when encountering an object as rul expect(result.spec.containers[1].image).toEqual('sidecar:v2'); }); +test('first: basic', () => { + const result = mergeFull(pod, sidecarImage, { + spec: { + containers: first(), + }, + }); + + expect(result.spec.containers.length).toEqual(2); + expect(result.spec.containers[0].name).toEqual('my-app'); + expect(result.spec.containers[1].image).toEqual('sidecar:v1'); +}); + test('replace: basic', () => { const result = mergeFull(pod, sidecarImage, { spec: {