Skip to content

Commit 9da44e2

Browse files
authored
Merge pull request #24 from simlu/master
feat: added "selective" clone function
2 parents 729b81a + 437f72e commit 9da44e2

12 files changed

+269
-7
lines changed

README.md

+26
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,32 @@ align(obj, ref);
3333
// obj => { k2: 1, k1: 2 }
3434
```
3535

36+
### clone(obj: Object[], needles: Array<String> = [])
37+
38+
Deep clone object.
39+
40+
Fields targeted by passed needles are created as a reference and not cloned.
41+
42+
Fields targeted by excluded needles are removed entirely from the result.
43+
44+
Needles are declared using the [object-scan](https://github.com/blackflux/object-scan) syntax.
45+
46+
_Example:_
47+
<!-- eslint-disable import/no-unresolved,no-console -->
48+
```js
49+
const { clone } = require('object-lib');
50+
51+
const data = { a: {}, b: {}, c: {} };
52+
const cloned = clone(data, ['b', '!c']);
53+
54+
console.log(cloned);
55+
// => { a: {}, b: {} }
56+
console.log(cloned.a !== data.a);
57+
// => true
58+
console.log(cloned.b === data.b);
59+
// => true
60+
```
61+
3662
### contains(tree: Object, subtree: Object)
3763

3864
Check if `subtree` is contained in `tree` recursively.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"eslint-plugin-mocha": "8.0.0",
2727
"js-gardener": "2.0.184",
2828
"lodash.clonedeep": "4.5.0",
29+
"lodash.samplesize": "4.2.0",
2930
"node-tdd": "2.19.1",
3031
"nyc": "15.1.0",
3132
"semantic-release": "17.3.7"

src/core/clone.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const objectScan = require('object-scan');
2+
const last = require('../util/last');
3+
const mkChild = require('../util/mk-child');
4+
5+
module.exports = (obj, needles = []) => {
6+
const hasDoubleStar = needles.includes('**');
7+
const breakLength = hasDoubleStar ? 0 : 1;
8+
return objectScan(hasDoubleStar ? needles : ['**', ...needles], {
9+
reverse: false,
10+
breakFn: ({
11+
isMatch, property, value, context, getMatchedBy
12+
}) => {
13+
if (!isMatch) {
14+
return property !== undefined;
15+
}
16+
const ref = last(context);
17+
const doBreak = getMatchedBy().length > breakLength;
18+
const v = doBreak ? value : mkChild(value);
19+
if (Array.isArray(ref)) {
20+
ref.push(v);
21+
context.push(last(ref));
22+
} else {
23+
ref[property] = v;
24+
context.push(ref[property]);
25+
}
26+
return doBreak;
27+
},
28+
filterFn: ({ context }) => {
29+
context.pop();
30+
}
31+
})(obj, [mkChild(obj)])[0];
32+
};

src/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
const align = require('./core/align');
2+
const clone = require('./core/clone');
23
const contains = require('./core/contains');
34
const Merge = require('./core/merge');
45

56
module.exports = {
67
align,
8+
clone,
79
contains,
810
Merge
911
};

test/core/align.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { describe } = require('node-tdd');
44
const align = require('../../src/core/align');
55
const genData = require('./gen-data');
66

7-
describe('Testing align', () => {
7+
describe('Testing align', { timeout: 100000 }, () => {
88
const convert = (input) => {
99
if (input === undefined) {
1010
return input;

test/core/clone.spec.js

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
const expect = require('chai').expect;
2+
const { describe } = require('node-tdd');
3+
const objectScan = require('object-scan');
4+
const sampleSize = require('lodash.samplesize');
5+
const cloneDeep = require('lodash.clonedeep');
6+
const clone = require('../../src/core/clone');
7+
const genData = require('./gen-data');
8+
9+
describe('Testing clone', { timeout: 100000 }, () => {
10+
it('Batch test (deep)', ({ fixture }) => {
11+
const refDiff = fixture('ref-diff');
12+
const asRefDiff = fixture('as-ref-diff');
13+
for (let x = 0; x < 5000; x += 1) {
14+
const data = genData();
15+
const dataX = cloneDeep(data);
16+
const cloned = clone(data);
17+
expect(dataX).to.deep.equal(data);
18+
expect(data).to.deep.equal(cloned);
19+
expect(refDiff(data, cloned)).to.deep.equal(asRefDiff(data));
20+
}
21+
});
22+
23+
it('Batch test (shallow)', ({ fixture }) => {
24+
const refDiff = fixture('ref-diff');
25+
const asRefDiff = fixture('as-ref-diff');
26+
for (let x = 0; x < 5000; x += 1) {
27+
const data = genData();
28+
const dataX = cloneDeep(data);
29+
const cloned = clone(data, ['**']);
30+
expect(dataX).to.deep.equal(data);
31+
expect(data).to.deep.equal(cloned);
32+
expect(refDiff(data, cloned)).to.deep.equal(asRefDiff(data, ['**']));
33+
}
34+
});
35+
36+
it('Batch test (random shallow)', ({ fixture }) => {
37+
const refDiff = fixture('ref-diff');
38+
const asRefDiff = fixture('as-ref-diff');
39+
for (let x = 0; x < 5000; x += 1) {
40+
const data = genData();
41+
const dataX = cloneDeep(data);
42+
const allKeys = objectScan(['**'], { joined: true })(data);
43+
const selectedKeys = sampleSize(allKeys, Math.floor(Math.random() * allKeys.length) + 1);
44+
const cloned = clone(data, selectedKeys);
45+
expect(dataX).to.deep.equal(data);
46+
expect(data).to.deep.equal(cloned);
47+
expect(refDiff(data, cloned)).to.deep.equal(asRefDiff(data, selectedKeys));
48+
}
49+
});
50+
51+
it('Batch test (random exclude)', ({ fixture }) => {
52+
const cloneWithout = fixture('clone-without');
53+
for (let x = 0; x < 5000; x += 1) {
54+
const data = genData();
55+
const dataX = cloneDeep(data);
56+
const allKeys = objectScan(['**'], { joined: true })(data);
57+
const selectedKeys = sampleSize(allKeys, Math.floor(Math.random() * allKeys.length) + 1);
58+
const excludeKeys = selectedKeys.map((k) => `!${k}`);
59+
const cloned = clone(data, excludeKeys);
60+
expect(dataX).to.deep.equal(data);
61+
expect(cloned).to.deep.equal(cloneWithout(data, selectedKeys));
62+
}
63+
});
64+
65+
it('Test simple', () => {
66+
const data = {
67+
a: 1,
68+
b: { x: 2, y: { /* complex object */ } },
69+
c: [{ /* complex object */ }, { z: 3 }]
70+
};
71+
const cloned = clone(data, ['b.y', 'c[0]']);
72+
expect(data).to.deep.equal(cloned);
73+
expect(data).to.not.equal(cloned);
74+
expect(data.b).to.not.equal(cloned.b);
75+
expect(data.y).to.equal(cloned.y);
76+
expect(data.c[0]).to.equal(cloned.c[0]);
77+
expect(data.c[1]).to.not.equal(cloned.c[1]);
78+
});
79+
80+
it('Test shallow clone', () => {
81+
const data = { a: {} };
82+
const cloned = clone(data, ['**']);
83+
expect(data).to.deep.equal(cloned);
84+
expect(data).to.not.equal(cloned);
85+
expect(data.a).to.equal(cloned.a);
86+
});
87+
88+
it('Test exclude', () => {
89+
const data = { a: {} };
90+
const cloned = clone(data, ['!a']);
91+
expect(cloned).to.deep.equal({});
92+
});
93+
94+
it('Test complex exclude one', ({ fixture }) => {
95+
const cloneWithout = fixture('clone-without');
96+
const data = { C: { A: undefined, C: [] }, B: [] };
97+
const cloned = clone(data, ['!C', '!C.A', '!B']);
98+
const excluded = cloneWithout(data, ['C', 'C.A', 'B']);
99+
expect(cloned).to.deep.equal(excluded);
100+
});
101+
102+
it('Test complex exclude two', ({ fixture }) => {
103+
const cloneWithout = fixture('clone-without');
104+
const data = { B: {}, C: {} };
105+
const cloned = clone(data, ['!C']);
106+
const excluded = cloneWithout(data, ['C']);
107+
expect(cloned).to.deep.equal(excluded);
108+
});
109+
110+
it('Test complex exclude three', ({ fixture }) => {
111+
const cloneWithout = fixture('clone-without');
112+
const data = { B: [2, []] };
113+
const cloned = clone(data, ['!B[0]']);
114+
const excluded = cloneWithout(data, ['B[0]']);
115+
expect(cloned).to.deep.equal(excluded);
116+
});
117+
118+
it('Test exclude, shallow and deep clone', () => {
119+
const data = { a: {}, b: {}, c: [{}, {}] };
120+
const cloned = clone(data, ['b', '!c[0]', 'c[1]']);
121+
expect(cloned).to.deep.equal({ a: {}, b: {}, c: [{}] });
122+
expect(data.a).to.not.equal(cloned.a);
123+
expect(data.b).to.equal(cloned.b);
124+
expect(data.c).to.not.equal(cloned.c);
125+
expect(data.c[1]).to.equal(cloned.c[0]);
126+
});
127+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const clonedeep = require('lodash.clonedeep');
2+
const objectScan = require('object-scan');
3+
4+
module.exports = (obj_, needles = []) => {
5+
if (!(obj_ instanceof Object)) {
6+
return true;
7+
}
8+
const obj = clonedeep(obj_);
9+
const hasDoubleStar = needles.includes('**');
10+
const breakLength = hasDoubleStar ? 0 : 1;
11+
objectScan(hasDoubleStar ? needles : ['**', ...needles], {
12+
breakFn: ({
13+
isMatch, parent, property, isLeaf, matchedBy
14+
}) => {
15+
if (!isMatch) {
16+
return false;
17+
}
18+
if (matchedBy.length > breakLength || isLeaf) {
19+
// eslint-disable-next-line no-param-reassign
20+
parent[property] = true;
21+
return true;
22+
}
23+
return false;
24+
}
25+
})(obj);
26+
return obj;
27+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const clonedeep = require('lodash.clonedeep');
2+
const objectScan = require('object-scan');
3+
4+
module.exports = (obj_, needles) => {
5+
if (!(obj_ instanceof Object)) {
6+
return obj_;
7+
}
8+
const obj = clonedeep(obj_);
9+
objectScan(needles, {
10+
breakFn: ({ isMatch, parent, property }) => {
11+
if (!isMatch) {
12+
return false;
13+
}
14+
if (Array.isArray(parent)) {
15+
parent.splice(property, 1);
16+
} else {
17+
// eslint-disable-next-line no-param-reassign
18+
delete parent[property];
19+
}
20+
return true;
21+
}
22+
})(obj);
23+
return obj;
24+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const refDiff = (o1, o2) => {
2+
if (o1 === o2) {
3+
return true;
4+
}
5+
if (Array.isArray(o1)) {
6+
return Object.entries(o1)
7+
.map(([k, v]) => refDiff(v, o2[k]));
8+
}
9+
return Object.entries(o1)
10+
.reduce((prev, [k, v]) => {
11+
// eslint-disable-next-line no-param-reassign
12+
prev[k] = refDiff(v, o2[k]);
13+
return prev;
14+
}, {});
15+
};
16+
17+
module.exports = refDiff;

test/core/merge.spec.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
const expect = require('chai').expect;
22
const { describe } = require('node-tdd');
3-
const clonedeep = require('lodash.clonedeep');
3+
const cloneDeep = require('lodash.clonedeep');
44
const Merge = require('../../src/core/merge');
55
const genData = require('./gen-data');
66

7-
describe('Testing Merge', () => {
7+
describe('Testing Merge', { timeout: 100000 }, () => {
88
describe('Default Merge', () => {
99
let merge;
1010
before(() => {
@@ -16,8 +16,8 @@ describe('Testing Merge', () => {
1616
for (let x = 0; x < 10000; x += 1) {
1717
const tree = genData();
1818
const subtree = genData();
19-
const treeX = clonedeep(tree);
20-
const subtreeX = clonedeep(subtree);
19+
const treeX = cloneDeep(tree);
20+
const subtreeX = cloneDeep(subtree);
2121
const r1 = merge(tree, subtree);
2222
expect(treeX).to.deep.equal(tree);
2323
expect(subtreeX).to.deep.equal(subtree);
@@ -78,8 +78,8 @@ describe('Testing Merge', () => {
7878
for (let x = 0; x < 10000; x += 1) {
7979
const tree = genData();
8080
const subtree = genData();
81-
const treeX = clonedeep(tree);
82-
const subtreeX = clonedeep(subtree);
81+
const treeX = cloneDeep(tree);
82+
const subtreeX = cloneDeep(subtree);
8383
expect(() => merge(tree, subtree)).to.not.throw();
8484
expect(treeX).to.deep.equal(tree);
8585
expect(subtreeX).to.deep.equal(subtree);

test/index.spec.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ describe('Testing index.js', () => {
55
it('Testing exported', () => {
66
expect(Object.keys(index)).to.deep.equal([
77
'align',
8+
'clone',
89
'contains',
910
'Merge'
1011
]);

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -4495,6 +4495,11 @@ [email protected]:
44954495
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
44964496
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
44974497

4498+
4499+
version "4.2.0"
4500+
resolved "https://registry.yarnpkg.com/lodash.samplesize/-/lodash.samplesize-4.2.0.tgz#460762fbb2b342290517499e90d51586db465ff9"
4501+
integrity sha1-Rgdi+7KzQikFF0mekNUVhttGX/k=
4502+
44984503
[email protected], lodash.set@^4.3.2:
44994504
version "4.3.2"
45004505
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"

0 commit comments

Comments
 (0)