Skip to content

Commit

Permalink
Merge pull request #25 from blackflux/dev
Browse files Browse the repository at this point in the history
[Gally]: master <- dev
  • Loading branch information
simlu authored Jan 30, 2021
2 parents 446ace8 + 9da44e2 commit 609aeab
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 7 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@ align(obj, ref);
// obj => { k2: 1, k1: 2 }
```

### clone(obj: Object[], needles: Array<String> = [])

Deep clone object.

Fields targeted by passed needles are created as a reference and not cloned.

Fields targeted by excluded needles are removed entirely from the result.

Needles are declared using the [object-scan](https://github.com/blackflux/object-scan) syntax.

_Example:_
<!-- eslint-disable import/no-unresolved,no-console -->
```js
const { clone } = require('object-lib');

const data = { a: {}, b: {}, c: {} };
const cloned = clone(data, ['b', '!c']);

console.log(cloned);
// => { a: {}, b: {} }
console.log(cloned.a !== data.a);
// => true
console.log(cloned.b === data.b);
// => true
```

### contains(tree: Object, subtree: Object)

Check if `subtree` is contained in `tree` recursively.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"eslint-plugin-mocha": "8.0.0",
"js-gardener": "2.0.184",
"lodash.clonedeep": "4.5.0",
"lodash.samplesize": "4.2.0",
"node-tdd": "2.19.1",
"nyc": "15.1.0",
"semantic-release": "17.3.7"
Expand Down
32 changes: 32 additions & 0 deletions src/core/clone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const objectScan = require('object-scan');
const last = require('../util/last');
const mkChild = require('../util/mk-child');

module.exports = (obj, needles = []) => {
const hasDoubleStar = needles.includes('**');
const breakLength = hasDoubleStar ? 0 : 1;
return objectScan(hasDoubleStar ? needles : ['**', ...needles], {
reverse: false,
breakFn: ({
isMatch, property, value, context, getMatchedBy
}) => {
if (!isMatch) {
return property !== undefined;
}
const ref = last(context);
const doBreak = getMatchedBy().length > breakLength;
const v = doBreak ? value : mkChild(value);
if (Array.isArray(ref)) {
ref.push(v);
context.push(last(ref));
} else {
ref[property] = v;
context.push(ref[property]);
}
return doBreak;
},
filterFn: ({ context }) => {
context.pop();
}
})(obj, [mkChild(obj)])[0];
};
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const align = require('./core/align');
const clone = require('./core/clone');
const contains = require('./core/contains');
const Merge = require('./core/merge');

module.exports = {
align,
clone,
contains,
Merge
};
2 changes: 1 addition & 1 deletion test/core/align.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { describe } = require('node-tdd');
const align = require('../../src/core/align');
const genData = require('./gen-data');

describe('Testing align', () => {
describe('Testing align', { timeout: 100000 }, () => {
const convert = (input) => {
if (input === undefined) {
return input;
Expand Down
127 changes: 127 additions & 0 deletions test/core/clone.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const expect = require('chai').expect;
const { describe } = require('node-tdd');
const objectScan = require('object-scan');
const sampleSize = require('lodash.samplesize');
const cloneDeep = require('lodash.clonedeep');
const clone = require('../../src/core/clone');
const genData = require('./gen-data');

describe('Testing clone', { timeout: 100000 }, () => {
it('Batch test (deep)', ({ fixture }) => {
const refDiff = fixture('ref-diff');
const asRefDiff = fixture('as-ref-diff');
for (let x = 0; x < 5000; x += 1) {
const data = genData();
const dataX = cloneDeep(data);
const cloned = clone(data);
expect(dataX).to.deep.equal(data);
expect(data).to.deep.equal(cloned);
expect(refDiff(data, cloned)).to.deep.equal(asRefDiff(data));
}
});

it('Batch test (shallow)', ({ fixture }) => {
const refDiff = fixture('ref-diff');
const asRefDiff = fixture('as-ref-diff');
for (let x = 0; x < 5000; x += 1) {
const data = genData();
const dataX = cloneDeep(data);
const cloned = clone(data, ['**']);
expect(dataX).to.deep.equal(data);
expect(data).to.deep.equal(cloned);
expect(refDiff(data, cloned)).to.deep.equal(asRefDiff(data, ['**']));
}
});

it('Batch test (random shallow)', ({ fixture }) => {
const refDiff = fixture('ref-diff');
const asRefDiff = fixture('as-ref-diff');
for (let x = 0; x < 5000; x += 1) {
const data = genData();
const dataX = cloneDeep(data);
const allKeys = objectScan(['**'], { joined: true })(data);
const selectedKeys = sampleSize(allKeys, Math.floor(Math.random() * allKeys.length) + 1);
const cloned = clone(data, selectedKeys);
expect(dataX).to.deep.equal(data);
expect(data).to.deep.equal(cloned);
expect(refDiff(data, cloned)).to.deep.equal(asRefDiff(data, selectedKeys));
}
});

it('Batch test (random exclude)', ({ fixture }) => {
const cloneWithout = fixture('clone-without');
for (let x = 0; x < 5000; x += 1) {
const data = genData();
const dataX = cloneDeep(data);
const allKeys = objectScan(['**'], { joined: true })(data);
const selectedKeys = sampleSize(allKeys, Math.floor(Math.random() * allKeys.length) + 1);
const excludeKeys = selectedKeys.map((k) => `!${k}`);
const cloned = clone(data, excludeKeys);
expect(dataX).to.deep.equal(data);
expect(cloned).to.deep.equal(cloneWithout(data, selectedKeys));
}
});

it('Test simple', () => {
const data = {
a: 1,
b: { x: 2, y: { /* complex object */ } },
c: [{ /* complex object */ }, { z: 3 }]
};
const cloned = clone(data, ['b.y', 'c[0]']);
expect(data).to.deep.equal(cloned);
expect(data).to.not.equal(cloned);
expect(data.b).to.not.equal(cloned.b);
expect(data.y).to.equal(cloned.y);
expect(data.c[0]).to.equal(cloned.c[0]);
expect(data.c[1]).to.not.equal(cloned.c[1]);
});

it('Test shallow clone', () => {
const data = { a: {} };
const cloned = clone(data, ['**']);
expect(data).to.deep.equal(cloned);
expect(data).to.not.equal(cloned);
expect(data.a).to.equal(cloned.a);
});

it('Test exclude', () => {
const data = { a: {} };
const cloned = clone(data, ['!a']);
expect(cloned).to.deep.equal({});
});

it('Test complex exclude one', ({ fixture }) => {
const cloneWithout = fixture('clone-without');
const data = { C: { A: undefined, C: [] }, B: [] };
const cloned = clone(data, ['!C', '!C.A', '!B']);
const excluded = cloneWithout(data, ['C', 'C.A', 'B']);
expect(cloned).to.deep.equal(excluded);
});

it('Test complex exclude two', ({ fixture }) => {
const cloneWithout = fixture('clone-without');
const data = { B: {}, C: {} };
const cloned = clone(data, ['!C']);
const excluded = cloneWithout(data, ['C']);
expect(cloned).to.deep.equal(excluded);
});

it('Test complex exclude three', ({ fixture }) => {
const cloneWithout = fixture('clone-without');
const data = { B: [2, []] };
const cloned = clone(data, ['!B[0]']);
const excluded = cloneWithout(data, ['B[0]']);
expect(cloned).to.deep.equal(excluded);
});

it('Test exclude, shallow and deep clone', () => {
const data = { a: {}, b: {}, c: [{}, {}] };
const cloned = clone(data, ['b', '!c[0]', 'c[1]']);
expect(cloned).to.deep.equal({ a: {}, b: {}, c: [{}] });
expect(data.a).to.not.equal(cloned.a);
expect(data.b).to.equal(cloned.b);
expect(data.c).to.not.equal(cloned.c);
expect(data.c[1]).to.equal(cloned.c[0]);
});
});
27 changes: 27 additions & 0 deletions test/core/clone.spec.js__fixtures/as-ref-diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const clonedeep = require('lodash.clonedeep');
const objectScan = require('object-scan');

module.exports = (obj_, needles = []) => {
if (!(obj_ instanceof Object)) {
return true;
}
const obj = clonedeep(obj_);
const hasDoubleStar = needles.includes('**');
const breakLength = hasDoubleStar ? 0 : 1;
objectScan(hasDoubleStar ? needles : ['**', ...needles], {
breakFn: ({
isMatch, parent, property, isLeaf, matchedBy
}) => {
if (!isMatch) {
return false;
}
if (matchedBy.length > breakLength || isLeaf) {
// eslint-disable-next-line no-param-reassign
parent[property] = true;
return true;
}
return false;
}
})(obj);
return obj;
};
24 changes: 24 additions & 0 deletions test/core/clone.spec.js__fixtures/clone-without.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const clonedeep = require('lodash.clonedeep');
const objectScan = require('object-scan');

module.exports = (obj_, needles) => {
if (!(obj_ instanceof Object)) {
return obj_;
}
const obj = clonedeep(obj_);
objectScan(needles, {
breakFn: ({ isMatch, parent, property }) => {
if (!isMatch) {
return false;
}
if (Array.isArray(parent)) {
parent.splice(property, 1);
} else {
// eslint-disable-next-line no-param-reassign
delete parent[property];
}
return true;
}
})(obj);
return obj;
};
17 changes: 17 additions & 0 deletions test/core/clone.spec.js__fixtures/ref-diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const refDiff = (o1, o2) => {
if (o1 === o2) {
return true;
}
if (Array.isArray(o1)) {
return Object.entries(o1)
.map(([k, v]) => refDiff(v, o2[k]));
}
return Object.entries(o1)
.reduce((prev, [k, v]) => {
// eslint-disable-next-line no-param-reassign
prev[k] = refDiff(v, o2[k]);
return prev;
}, {});
};

module.exports = refDiff;
12 changes: 6 additions & 6 deletions test/core/merge.spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const expect = require('chai').expect;
const { describe } = require('node-tdd');
const clonedeep = require('lodash.clonedeep');
const cloneDeep = require('lodash.clonedeep');
const Merge = require('../../src/core/merge');
const genData = require('./gen-data');

describe('Testing Merge', () => {
describe('Testing Merge', { timeout: 100000 }, () => {
describe('Default Merge', () => {
let merge;
before(() => {
Expand All @@ -16,8 +16,8 @@ describe('Testing Merge', () => {
for (let x = 0; x < 10000; x += 1) {
const tree = genData();
const subtree = genData();
const treeX = clonedeep(tree);
const subtreeX = clonedeep(subtree);
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);
Expand Down Expand Up @@ -78,8 +78,8 @@ describe('Testing Merge', () => {
for (let x = 0; x < 10000; x += 1) {
const tree = genData();
const subtree = genData();
const treeX = clonedeep(tree);
const subtreeX = clonedeep(subtree);
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);
Expand Down
1 change: 1 addition & 0 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ describe('Testing index.js', () => {
it('Testing exported', () => {
expect(Object.keys(index)).to.deep.equal([
'align',
'clone',
'contains',
'Merge'
]);
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4495,6 +4495,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==

[email protected]:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.samplesize/-/lodash.samplesize-4.2.0.tgz#460762fbb2b342290517499e90d51586db465ff9"
integrity sha1-Rgdi+7KzQikFF0mekNUVhttGX/k=

[email protected], lodash.set@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
Expand Down

0 comments on commit 609aeab

Please sign in to comment.