diff --git a/README.md b/README.md index 00a3981..1835ab8 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ ## Functions +For more extensive examples, please refer to the tests. + ### align(obj: Object, ref: Object) Align the ordering of one object recursively to a reference object. @@ -33,6 +35,18 @@ align(obj, ref); ### contain(tree: Object, subtree: Object) +Check if `subtree` is contained in `tree` recursively. + +Different types are never considered _contained_. + +Arrays are _contained_ iff they are the same length and every +element is _contained_ in the corresponding element. + +Objects are _contained_ if the keys are a subset, +and the respective values are _contained_. + +All other types are contained if they match exactly (`===`). + _Example:_ ```js @@ -44,3 +58,29 @@ contain({ a: [1, 2], b: 'c' }, { a: [1, 2] }); contain({ a: [1, 2], b: 'c' }, { a: [1] }); // => false ``` + +### Merge(logic: Object = {})(...obj: Object[]) + +Allows merging of objects. The logic defines paths that map to a field, or a function, to merge by. + +If a function is passed, it is invoked with the value, and the result is used as the merge identifier. + +The paths are defined using [object-scan](https://github.com/blackflux/object-scan) syntax. + +_Example:_ + +```js +const { Merge } = require('object-lig'); + +Merge()( + { children: [{ id: 1 }, { id: 2 }] }, + { children: [{ id: 2 }, { id: 3 }] } +); +// => { children: [ { id: 1 }, { id: 2 }, { id: 2 }, { id: 3 } ] } + +Merge({ '**[*]': 'id' })( + { children: [{ id: 1 }, { id: 2 }] }, + { children: [{ id: 2 }, { id: 3 }] } +); +// => { children: [ { id: 1 }, { id: 2 }, { id: 3 } ] } +``` diff --git a/package.json b/package.json index 918f93c..4321b99 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "eslint-plugin-markdown": "1.0.2", "eslint-plugin-mocha": "8.0.0", "js-gardener": "2.0.184", + "node-tdd": "2.19.1", "nyc": "15.1.0", "semantic-release": "17.3.7" }, @@ -105,5 +106,8 @@ }, "engines": { "node": ">= 10" + }, + "dependencies": { + "object-scan": "13.8.0" } } diff --git a/src/core/merge.js b/src/core/merge.js new file mode 100644 index 0000000..9e51ed2 --- /dev/null +++ b/src/core/merge.js @@ -0,0 +1,67 @@ +const objectScan = require('object-scan'); + +module.exports = (logic_ = {}) => { + const logic = { '**': null, ...logic_ }; + const last = (arr) => arr[arr.length - 1]; + const mkChild = (ref) => { + if (!(ref instanceof Object)) { + return ref; + } + return Array.isArray(ref) ? [] : {}; + }; + const populate = (obj, key, fn, force = false) => { + if (force === true || !(key in obj)) { + // eslint-disable-next-line no-param-reassign + obj[key] = fn(); + return true; + } + return false; + }; + + const scanner = objectScan(Object.keys(logic), { + reverse: false, + breakFn: ({ + isMatch, property, value, matchedBy, context + }) => { + if (!isMatch) return; + const { stack, groups, path } = context; + const current = last(stack); + const bestNeedle = last(matchedBy); + const groupBy = typeof logic[bestNeedle] === 'function' + ? logic[bestNeedle](value) + : logic[bestNeedle]; + + if (!Array.isArray(current) || groupBy === null) { + if (Array.isArray(current)) { + current.push(mkChild(value)); + stack.push(last(current)); + } else { + populate(current, property, () => mkChild(value), !(value instanceof Object)); + stack.push(current[property]); + } + } else { + const groupId = `${bestNeedle}.${groupBy}: ${path.join('.')}`; + populate(groups, groupId, () => ({})); + const groupEntryId = value[groupBy]; + if (populate(groups[groupId], groupEntryId, () => mkChild(value))) { + current.push(groups[groupId][groupEntryId]); + } + path.push(`${groupBy}=${groupEntryId}`); + stack.push(groups[groupId][groupEntryId]); + } + }, + filterFn: ({ matchedBy, context }) => { + const { stack, path } = context; + stack.pop(); + if (logic[last(matchedBy)] !== null) { + path.pop(); + } + } + }); + return (...args) => { + const result = mkChild(args[0]); + const groups = {}; + args.forEach((arg) => scanner(arg, { stack: [result], groups, path: [] })); + return result; + }; +}; diff --git a/src/index.js b/src/index.js index e168254..3e7a302 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,9 @@ const align = require('./core/align'); const contain = require('./core/contain'); +const Merge = require('./core/merge'); module.exports = { align, - contain + contain, + Merge }; diff --git a/test/core/merge.spec.js b/test/core/merge.spec.js new file mode 100644 index 0000000..3c09b26 --- /dev/null +++ b/test/core/merge.spec.js @@ -0,0 +1,39 @@ +const expect = require('chai').expect; +const { describe } = require('node-tdd'); +const Merge = require('../../src/core/merge'); + +describe('Testing Merge', () => { + it('Testing SO question: https://stackoverflow.com/questions/65822248', ({ fixture }) => { + const json1 = fixture('json1'); + const json2 = fixture('json2'); + const merge = Merge({ + '[*]': 'id', + '[*].addresses[*]': 'type' + }); + expect(merge(json1, json2)).to.deep.equal(fixture('result')); + }); + + it('Testing array concat', () => { + const d1 = [{ a: 1 }]; + const d2 = [{ a: 2 }]; + expect(Merge()(d1, d2)).to.deep.equal([{ a: 1 }, { a: 2 }]); + }); + + it('Testing array merge', () => { + const d1 = [{ a: 1, b: 1, c: 3 }]; + const d2 = [{ a: 1, b: 2, d: 4 }]; + expect(Merge({ '[*]': 'a' })(d1, d2)).to.deep.equal([{ ...d1[0], ...d2[0] }]); + }); + + it('Testing merge by sum', () => { + const d1 = [[1, 2, 3], [2, 4], [1, 2]]; + const d2 = [[3, 3], [1, 5], [3, 2]]; + expect(Merge({ + '[*]': (o) => o.reduce((a, b) => a + b, 0) + })(d1, d2)).to.deep.equal([ + [1, 2, 3, 2, 4, 3, 3, 1, 5], + [1, 2], + [3, 2] + ]); + }); +}); diff --git a/test/core/merge.spec.js__fixtures/json1.json b/test/core/merge.spec.js__fixtures/json1.json new file mode 100644 index 0000000..c9f5e06 --- /dev/null +++ b/test/core/merge.spec.js__fixtures/json1.json @@ -0,0 +1,30 @@ +[ + { + "id": 1, + "name": "aaa", + "addresses": [ + { + "type": "office", + "city": "office city" + }, + { + "type": "home1", + "city": "home1 city" + } + ] + }, + { + "id": 2, + "name": "bbb", + "addresses": [ + { + "type": "office", + "city": "office city" + }, + { + "type": "home1", + "city": "home1 city" + } + ] + } +] diff --git a/test/core/merge.spec.js__fixtures/json2.json b/test/core/merge.spec.js__fixtures/json2.json new file mode 100644 index 0000000..a82a56a --- /dev/null +++ b/test/core/merge.spec.js__fixtures/json2.json @@ -0,0 +1,30 @@ +[ + { + "id": 1, + "name": "aaa1", + "addresses": [ + { + "type": "home1", + "city": "home1 new city" + }, + { + "type": "home2", + "city": "home2 city" + } + ] + }, + { + "id": 3, + "name": "ccc", + "addresses": [ + { + "type": "home1", + "city": "home1 city" + }, + { + "type": "home2", + "city": "home2 city" + } + ] + } +] diff --git a/test/core/merge.spec.js__fixtures/result.json b/test/core/merge.spec.js__fixtures/result.json new file mode 100644 index 0000000..057a7a0 --- /dev/null +++ b/test/core/merge.spec.js__fixtures/result.json @@ -0,0 +1,48 @@ +[ + { + "id": 1, + "name": "aaa1", + "addresses": [ + { + "type": "office", + "city": "office city" + }, + { + "type": "home1", + "city": "home1 new city" + }, + { + "type": "home2", + "city": "home2 city" + } + ] + }, + { + "id": 2, + "name": "bbb", + "addresses": [ + { + "type": "office", + "city": "office city" + }, + { + "type": "home1", + "city": "home1 city" + } + ] + }, + { + "id": 3, + "name": "ccc", + "addresses": [ + { + "type": "home1", + "city": "home1 city" + }, + { + "type": "home2", + "city": "home2 city" + } + ] + } +] diff --git a/test/index.spec.js b/test/index.spec.js index 8584847..5926fb6 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -5,7 +5,8 @@ describe('Testing index.js', () => { it('Testing exported', () => { expect(Object.keys(index)).to.deep.equal([ 'align', - 'contain' + 'contain', + 'Merge' ]); }); }); diff --git a/yarn.lock b/yarn.lock index ed42402..8a50184 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1281,7 +1281,7 @@ callsite@^1.0.0: resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= -callsites@^3.0.0: +callsites@3.1.0, callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== @@ -4495,7 +4495,7 @@ lodash.mergewith@4.6.2: resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== -lodash.set@4.3.2: +lodash.set@4.3.2, lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= @@ -4790,7 +4790,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: +minimist@1.2.5, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -4956,6 +4956,16 @@ nerf-dart@^1.0.0: resolved "https://registry.yarnpkg.com/nerf-dart/-/nerf-dart-1.0.0.tgz#e6dab7febf5ad816ea81cf5c629c5a0ebde72c1a" integrity sha1-5tq3/r9a2Bbqgc9cYpxaDr3nLBo= +nock@13.0.5: + version "13.0.5" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.5.tgz#a618c6f86372cb79fac04ca9a2d1e4baccdb2414" + integrity sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash.set "^4.3.2" + propagate "^2.0.0" + node-emoji@^1.0.3, node-emoji@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" @@ -5013,6 +5023,24 @@ node-sass-tilde-importer@^1.0.2: dependencies: find-parent-dir "^0.3.0" +node-tdd@2.19.1: + version "2.19.1" + resolved "https://registry.yarnpkg.com/node-tdd/-/node-tdd-2.19.1.tgz#1b1ff2503ecaae9f39b92a877b6db8e8088be3a0" + integrity sha512-fEtIFSyVT4ftAA/P3raH/ZIbH4jpPvuYkiV2rteujYOBVpT+KHu0GMKjuFnoaNqNVffNoEz/cptXZKeWhoLCPA== + dependencies: + callsites "3.1.0" + joi-strict "1.2.9" + lodash.clonedeep "4.5.0" + lodash.get "4.4.2" + minimist "1.2.5" + nock "13.0.5" + object-scan "13.7.1" + smart-fs "1.12.3" + timekeeper "2.2.0" + tmp "0.2.1" + uuid "8.3.0" + xml2js "0.4.23" + nopt@^4.0.1, nopt@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" @@ -5435,6 +5463,11 @@ object-scan@13.7.1: resolved "https://registry.yarnpkg.com/object-scan/-/object-scan-13.7.1.tgz#2ce78454ae6a35ea45da7f26f2209f5545613122" integrity sha512-akfqDKVs77wfaRhcln8p9IVIjfQ6MdgfsqsstAAgBWHHO9OqWjs74IUUqiGihDStpvgKA3fFUqdLpdD2Oi7YkQ== +object-scan@13.8.0: + version "13.8.0" + resolved "https://registry.yarnpkg.com/object-scan/-/object-scan-13.8.0.tgz#70ec43620b76fbc580856beb5275ee28321f781c" + integrity sha512-ljh4WKzs/TTBML0kl+qfTpu13lSZ0Q41aj5XiJa0ue9FkEdP5fi5/FTHbZgsGBiXo/AeyPRh1XZDOuGQLffC1A== + object-treeify@1.1.31: version "1.1.31" resolved "https://registry.yarnpkg.com/object-treeify/-/object-treeify-1.1.31.tgz#eb083c8eb25b512c9feea088e72b03aa13032d5e" @@ -6028,6 +6061,11 @@ promzard@^0.3.0: dependencies: read "1" +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -6649,7 +6687,7 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@^1.2.4: +sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -7382,11 +7420,23 @@ timed-out@^4.0.0: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= +timekeeper@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/timekeeper/-/timekeeper-2.2.0.tgz#9645731fce9e3280a18614a57a9d1b72af3ca368" + integrity sha512-W3AmPTJWZkRwu+iSNxPIsLZ2ByADsOLbbLxe46UJyWj3mlYLlwucKiq+/dPm0l9wTzqoF3/2PH0AGFCebjq23A== + tiny-relative-date@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== +tmp@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -7765,6 +7815,11 @@ util-promisify@^2.1.0: dependencies: object.getownpropertydescriptors "^2.0.3" +uuid@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + uuid@^3.3.2, uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -8011,6 +8066,19 @@ xml-js@1.6.11: dependencies: sax "^1.2.4" +xml2js@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"