Skip to content

Commit

Permalink
Merge pull request #21 from blackflux/dev
Browse files Browse the repository at this point in the history
[Gally]: master <- dev
  • Loading branch information
simlu authored Jan 29, 2021
2 parents 692b0e0 + f34734d commit 93c15b3
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 69 deletions.
95 changes: 59 additions & 36 deletions src/core/merge.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,76 @@
const assert = require('assert');
const objectScan = require('object-scan');

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) => {
if (!(key in obj)) {
// eslint-disable-next-line no-param-reassign
obj[key] = fn();
return true;
}
return false;
};
const incompatible = (a, b) => (
!(a instanceof Object)
|| !(b instanceof Object)
|| Array.isArray(a) !== Array.isArray(b)
);

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);

if (!isMatch) {
if (incompatible(current, value)) {
stack[0] = mkChild(value);
}
return false;
}
if (!(current instanceof Object)) {
stack.push(null);
return true;
}
if (!Array.isArray(current)) {
if (!(property in current) || incompatible(current[property], value)) {
current[property] = mkChild(value);
}
stack.push(current[property]);
return false;
}

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]);
if (groupBy === null) {
current.push(value);
stack.push(null);
return true;
}

const groupId = `${bestNeedle}.${groupBy}: ${path.join('.')}`;
populate(groups, groupId, () => ({}));
const groupEntryId = value instanceof Object ? value[groupBy] : undefined;
if (populate(groups[groupId], groupEntryId, () => mkChild(value))) {
current.push(groups[groupId][groupEntryId]);
}
path.push(`${groupBy}=${groupEntryId}`);
stack.push(groups[groupId][groupEntryId]);
return false;
},
filterFn: ({ matchedBy, context }) => {
const { stack, path } = context;
Expand All @@ -59,9 +81,10 @@ module.exports = (logic_ = {}) => {
}
});
return (...args) => {
const result = mkChild(last(args));
const stack = [undefined];
const groups = {};
args.forEach((arg) => scanner(arg, { stack: [result], groups, path: [] }));
return result;
args.forEach((arg) => scanner(arg, { stack, groups, path: [] }));
assert(stack.length === 1);
return stack[0];
};
};
152 changes: 119 additions & 33 deletions test/core/merge.spec.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,131 @@
const expect = require('chai').expect;
const { describe } = require('node-tdd');
const clonedeep = require('lodash.clonedeep');
const Merge = require('../../src/core/merge');
const genData = require('./gen-data');

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'));
});
describe('Default Merge', () => {
let merge;
before(() => {
merge = Merge();
});

it('Testing string merge', () => {
const d1 = 'A';
const d2 = 'B';
expect(Merge()(d1, d2)).to.deep.equal('B');
});
it('Batch test', ({ fixture }) => {
const mergeRec = fixture('merge-rec');
for (let x = 0; x < 10000; x += 1) {
const tree = genData();
const subtree = genData();
const treeX = clonedeep(tree);
const subtreeX = clonedeep(subtree);
const r1 = merge(tree, subtree);
expect(treeX).to.deep.equal(tree);
expect(subtreeX).to.deep.equal(subtree);
const r2 = mergeRec(tree, subtree);
expect(treeX).to.deep.equal(tree);
expect(subtreeX).to.deep.equal(subtree);
expect(r1).to.deep.equal(r2);
}
});

it('Testing array concat', () => {
const d1 = [{ a: 1 }];
const d2 = [{ a: 2 }];
expect(Merge()(d1, d2)).to.deep.equal([{ a: 1 }, { a: 2 }]);
});
it('Nested empty array concat', () => {
expect(merge([[]], [[]])).to.deep.equal([[], []]);
});

it('Nested arrays in object concat', () => {
expect(merge(
{ B: [{}, [2]] },
{ B: [1] }
)).to.deep.equal({ B: [{}, [2], 1] });
});

it('Nested array concat objects', () => {
expect(merge([{ A: [] }], [0])).to.deep.equal([{ A: [] }, 0]);
});

it('Nested array overwrite with object', () => {
expect(merge({ B: [{}, {}] }, { B: {} })).to.deep.equal({ B: {} });
});

it('Nested nested array concat', () => {
expect(merge({ B: [[2]] }, { B: [[2]] })).to.deep.equal({ B: [[2], [2]] });
});

it('Object overwrite array', () => {
expect(merge([1], { a: 1 })).to.deep.equal({ a: 1 });
});

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('Array overwrite object', () => {
expect(merge({ a: 1 }, [1])).to.deep.equal([1]);
});

it('Testing string merge', () => {
const d1 = 'A';
const d2 = 'B';
expect(merge(d1, d2)).to.deep.equal('B');
});

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 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]
]);
describe('Custom Merge', () => {
it('Batch test', () => {
const merge = Merge({ '**': 'A' });
for (let x = 0; x < 10000; x += 1) {
const tree = genData();
const subtree = genData();
const treeX = clonedeep(tree);
const subtreeX = clonedeep(subtree);
expect(() => merge(tree, subtree)).to.not.throw();
expect(treeX).to.deep.equal(tree);
expect(subtreeX).to.deep.equal(subtree);
}
});

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 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]
]);
});

it('Testing incompatible', () => {
const d1 = [{ a: 1 }];
const d2 = [undefined];
const d3 = ['A'];
expect(Merge({ '[*]': 'a' })(d1, d2, d3)).to.deep.equal([{ a: 1 }, undefined]);
});

it('Testing overwrite', () => {
const d1 = { a: [{ a: 1 }] };
const d2 = { a: [{ a: 2 }] };
const d3 = { a: null };
expect(Merge({ '[*]': 'a' })(d1, d2, d3)).to.deep.equal({ a: null });
});
});
});
28 changes: 28 additions & 0 deletions test/core/merge.spec.js__fixtures/merge-rec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const mergeRec = (tree, subtree) => {
if (!(subtree instanceof Object)) {
return subtree;
}
const treeIsArray = Array.isArray(tree);
const subtreeIsArray = Array.isArray(subtree);
if (treeIsArray !== subtreeIsArray) {
return subtreeIsArray ? [...subtree] : { ...subtree };
}
if (tree instanceof Object && subtree instanceof Object) {
const e1 = Object.entries(tree);
const e2 = Object.entries(subtree);
const r = Array.isArray(subtree) ? [] : {};
[e1, e2].forEach((e) => {
e.forEach(([k, v]) => {
if (Array.isArray(r)) {
r.push(v);
} else {
r[k] = k in r ? mergeRec(r[k], v) : v;
}
});
});
return r;
}
return subtree;
};

module.exports = mergeRec;

0 comments on commit 93c15b3

Please sign in to comment.