Skip to content

Commit

Permalink
Merge pull request #10 from blackflux/dev
Browse files Browse the repository at this point in the history
[Gally]: master <- dev
  • Loading branch information
simlu authored Jan 23, 2021
2 parents 46c1425 + 4a07cf9 commit 38ce429
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 6 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:_
<!-- eslint-disable import/no-unresolved -->
```js
Expand All @@ -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:_
<!-- eslint-disable import/no-unresolved -->
```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 } ] }
```
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -105,5 +106,8 @@
},
"engines": {
"node": ">= 10"
},
"dependencies": {
"object-scan": "13.8.0"
}
}
67 changes: 67 additions & 0 deletions src/core/merge.js
Original file line number Diff line number Diff line change
@@ -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;
};
};
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -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
};
39 changes: 39 additions & 0 deletions test/core/merge.spec.js
Original file line number Diff line number Diff line change
@@ -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]
]);
});
});
30 changes: 30 additions & 0 deletions test/core/merge.spec.js__fixtures/json1.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
30 changes: 30 additions & 0 deletions test/core/merge.spec.js__fixtures/json2.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
48 changes: 48 additions & 0 deletions test/core/merge.spec.js__fixtures/result.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
3 changes: 2 additions & 1 deletion test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ describe('Testing index.js', () => {
it('Testing exported', () => {
expect(Object.keys(index)).to.deep.equal([
'align',
'contain'
'contain',
'Merge'
]);
});
});
Loading

0 comments on commit 38ce429

Please sign in to comment.