Skip to content

Commit b3b5134

Browse files
authoredNov 28, 2018
Merge pull request #281 from kanongil/symbol-support
Symbol support
2 parents c123e61 + bec8b86 commit b3b5134

File tree

4 files changed

+183
-33
lines changed

4 files changed

+183
-33
lines changed
 

‎API.md

+14-8
Original file line numberDiff line numberDiff line change
@@ -137,28 +137,32 @@ var config = Hoek.applyToDefaults(defaults, options, true); // results in { host
137137
```
138138

139139
### applyToDefaultsWithShallow(defaults, options, keys)
140-
keys is an array of key names to shallow copy
140+
keys is an array of dot-separated, or array-based, key paths to shallow copy
141141

142142
Apply options to a copy of the defaults. Keys specified in the last parameter are shallow copied from options instead of merged.
143143

144144
```javascript
145145

146146
var defaults = {
147+
db: {
147148
server: {
148149
host: "localhost",
149150
port: 8000
150151
},
151152
name: 'example'
152-
};
153+
}
154+
};
153155

154156
var options = { server: { port: 8080 } };
155157

156-
var config = Hoek.applyToDefaultsWithShallow(defaults, options, ['server']); // results in { server: { port: 8080 }, name: 'example' }
158+
var config = Hoek.applyToDefaultsWithShallow(defaults, options, ['db.server']); // results in { db: { server: { port: 8080 }, name: 'example' } }
159+
var config = Hoek.applyToDefaultsWithShallow(defaults, options, [['db', 'server']]); // results in { db: { server: { port: 8080 }, name: 'example' } }
157160
```
158161

159162
### deepEqual(b, a, [options])
160163

161-
Performs a deep comparison of the two values including support for circular dependencies, prototype, and properties. To skip prototype comparisons, use `options.prototype = false`
164+
Performs a deep comparison of the two values including support for circular dependencies, prototype, and enumerable properties.
165+
To skip prototype comparisons, use `options.prototype = false`
162166

163167
```javascript
164168
Hoek.deepEqual({ a: [1, 2], b: 'string', c: { d: true } }, { a: [1, 2], b: 'string', c: { d: true } }); //results in true
@@ -218,16 +222,18 @@ flattenedArray = Hoek.flatten(array, target); // results in [4, [5], 1, 2, 3]
218222

219223
### reach(obj, chain, [options])
220224

221-
Converts an object key chain string to reference
225+
Converts an object key chain string or array to reference
222226

223227
- `options` - optional settings
224228
- `separator` - string to split chain path on, defaults to '.'
225229
- `default` - value to return if the path or value is not present, default is `undefined`
226230
- `strict` - if `true`, will throw an error on missing member, default is `false`
227231
- `functions` - if `true` allow traversing functions for properties. `false` will throw an error if a function is part of the chain.
228232

229-
A chain including negative numbers will work like negative indices on an
230-
array.
233+
A chain can be a string that will be split into key names using `separator`,
234+
or an array containing each individual key name.
235+
236+
A chain including negative numbers will work like negative indices on an array.
231237

232238
If chain is `null`, `undefined` or `false`, the object itself will be returned.
233239

@@ -238,7 +244,7 @@ var obj = {a : {b : { c : 1}}};
238244

239245
Hoek.reach(obj, chain); // returns 1
240246

241-
var chain = 'a.b.-1';
247+
var chain = ['a', 'b', -1];
242248
var obj = {a : {b : [2,3,6]}};
243249

244250
Hoek.reach(obj, chain); // returns 6

‎lib/deep-equal.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ internals.isSetSimpleEqual = function (obj, ref) {
116116
internals.isDeepEqualObj = function (instanceType, obj, ref, options, seen) {
117117

118118
const { isDeepEqual, valueOf, hasOwnEnumerableProperty } = internals;
119-
const { keys } = Object;
119+
const { keys, getOwnPropertySymbols } = Object;
120120

121121
if (instanceType === internals.arrayType) {
122122
if (options.part) {
@@ -226,6 +226,36 @@ internals.isDeepEqualObj = function (instanceType, obj, ref, options, seen) {
226226
}
227227
}
228228

229+
// Check symbols
230+
231+
const objSymbols = getOwnPropertySymbols(obj);
232+
const refSymbols = new Set(getOwnPropertySymbols(ref));
233+
234+
for (let i = 0; i < objSymbols.length; ++i) {
235+
const key = objSymbols[i];
236+
237+
if (hasOwnEnumerableProperty(obj, key)) {
238+
if (!hasOwnEnumerableProperty(ref, key)) {
239+
return false;
240+
}
241+
242+
if (!isDeepEqual(obj[key], ref[key], options, seen)) {
243+
return false;
244+
}
245+
}
246+
else if (hasOwnEnumerableProperty(ref, key)) {
247+
return false;
248+
}
249+
250+
refSymbols.delete(key);
251+
}
252+
253+
for (const key of refSymbols) {
254+
if (hasOwnEnumerableProperty(ref, key)) {
255+
return false;
256+
}
257+
}
258+
229259
return true;
230260
};
231261

‎lib/index.js

+30-19
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ exports.clone = function (obj, seen) {
6666
seen.set(obj, newObj);
6767

6868
if (cloneDeep) {
69-
const keys = Object.getOwnPropertyNames(obj);
69+
const keys = Reflect.ownKeys(obj);
7070
for (let i = 0; i < keys.length; ++i) {
7171
const key = keys[i];
7272

@@ -126,10 +126,12 @@ exports.merge = function (target, source, isNullOverride /* = true */, isMergeAr
126126
return target;
127127
}
128128

129-
const keys = Object.keys(source);
129+
const keys = Reflect.ownKeys(source);
130130
for (let i = 0; i < keys.length; ++i) {
131131
const key = keys[i];
132-
if (key === '__proto__') {
132+
if (key === '__proto__' ||
133+
!Object.prototype.propertyIsEnumerable.call(source, key)) {
134+
133135
continue;
134136
}
135137

@@ -199,19 +201,21 @@ exports.cloneWithShallow = function (source, keys) {
199201

200202
const storage = internals.store(source, keys); // Move shallow copy items to storage
201203
const copy = exports.clone(source); // Deep copy the rest
202-
internals.restore(copy, source, storage); // Shallow copy the stored items and restore
204+
internals.restore(copy, source, storage); // Shallow copy the stored items and restore
203205
return copy;
204206
};
205207

206208

207209
internals.store = function (source, keys) {
208210

209-
const storage = {};
211+
const storage = new Map();
210212
for (let i = 0; i < keys.length; ++i) {
211213
const key = keys[i];
212214
const value = exports.reach(source, key);
213-
if (value !== undefined) {
214-
storage[key] = value;
215+
if (typeof value === 'object' ||
216+
typeof value === 'function') {
217+
218+
storage.set(key, value);
215219
internals.reachSet(source, key, undefined);
216220
}
217221
}
@@ -222,18 +226,16 @@ internals.store = function (source, keys) {
222226

223227
internals.restore = function (copy, source, storage) {
224228

225-
const keys = Object.keys(storage);
226-
for (let i = 0; i < keys.length; ++i) {
227-
const key = keys[i];
228-
internals.reachSet(copy, key, storage[key]);
229-
internals.reachSet(source, key, storage[key]);
229+
for (const [key, value] of storage) {
230+
internals.reachSet(copy, key, value);
231+
internals.reachSet(source, key, value);
230232
}
231233
};
232234

233235

234236
internals.reachSet = function (obj, key, value) {
235237

236-
const path = key.split('.');
238+
const path = Array.isArray(key) ? key : key.split('.');
237239
let ref = obj;
238240
for (let i = 0; i < path.length; ++i) {
239241
const segment = path[i];
@@ -334,7 +336,8 @@ exports.contain = function (ref, values, options) {
334336
!Array.isArray(values)) {
335337

336338
valuePairs = values;
337-
values = Object.keys(values);
339+
const symbols = Object.getOwnPropertySymbols(values).filter(Object.prototype.propertyIsEnumerable.bind(values));
340+
values = [...Object.keys(values), ...symbols];
338341
}
339342
else {
340343
values = [].concat(values);
@@ -409,7 +412,7 @@ exports.contain = function (ref, values, options) {
409412
}
410413
}
411414
else {
412-
const keys = Object.getOwnPropertyNames(ref);
415+
const keys = Reflect.ownKeys(ref);
413416
for (let i = 0; i < keys.length; ++i) {
414417
const key = keys[i];
415418
const pos = values.indexOf(key);
@@ -483,13 +486,21 @@ exports.reach = function (obj, chain, options) {
483486
options = { separator: options };
484487
}
485488

486-
const path = chain.split(options.separator || '.');
489+
const isChainArray = Array.isArray(chain);
490+
491+
exports.assert(!isChainArray || !options.separator, 'Separator option no valid for array-based chain');
492+
493+
const path = isChainArray ? chain : chain.split(options.separator || '.');
487494
let ref = obj;
488495
for (let i = 0; i < path.length; ++i) {
489496
let key = path[i];
490-
if (key[0] === '-' && Array.isArray(ref)) {
491-
key = key.slice(1, key.length);
492-
key = ref.length - key;
497+
498+
if (Array.isArray(ref)) {
499+
const number = Number(key);
500+
501+
if (Number.isInteger(number) && number < 0) {
502+
key = ref.length + number;
503+
}
493504
}
494505

495506
if (!ref ||

‎test/index.js

+108-5
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,21 @@ describe('clone()', () => {
135135
expect(a).to.equal(b);
136136
});
137137

138+
it('clones symbol properties', () => {
139+
140+
const sym1 = Symbol(1);
141+
const sym2 = Symbol(2);
142+
const a = { [sym1]: 1 };
143+
Object.defineProperty(a, sym2, { value: 2 });
144+
145+
const b = Hoek.clone(a);
146+
147+
expect(a).to.equal(b);
148+
expect(Hoek.deepEqual(a, b)).to.be.true();
149+
expect(b[sym1]).to.be.equal(1);
150+
expect(b[sym2]).to.be.equal(2);
151+
});
152+
138153
it('performs actual copy for shallow keys (no pass by reference)', () => {
139154

140155
const x = Hoek.clone(nestedObj);
@@ -617,6 +632,15 @@ describe('merge()', () => {
617632
expect(a.x).to.equal(/test/);
618633
});
619634

635+
it('overrides Symbol properties', () => {
636+
637+
const sym = Symbol();
638+
const a = { [sym]: 1 };
639+
640+
Hoek.merge({ [sym]: {} }, a);
641+
expect(a[sym]).to.equal(1);
642+
});
643+
620644
it('skips __proto__', () => {
621645

622646
const a = '{ "ok": "value", "__proto__": { "test": "value" } }';
@@ -725,14 +749,16 @@ describe('cloneWithShallow()', () => {
725749
},
726750
c: {
727751
d: 6
728-
}
752+
},
753+
e() {}
729754
};
730755

731-
const copy = Hoek.cloneWithShallow(source, ['c']);
756+
const copy = Hoek.cloneWithShallow(source, ['c', 'e']);
732757
expect(copy).to.equal(source);
733758
expect(copy).to.not.shallow.equal(source);
734759
expect(copy.a).to.not.shallow.equal(source.a);
735-
expect(copy.b).to.equal(source.b);
760+
expect(copy.c).to.shallow.equal(source.c);
761+
expect(copy.e).to.shallow.equal(source.e);
736762
});
737763

738764
it('returns immutable value', () => {
@@ -767,6 +793,25 @@ describe('cloneWithShallow()', () => {
767793
expect(copy.a).to.not.shallow.equal(source.a);
768794
expect(copy.b).to.equal(source.b);
769795
});
796+
797+
it('supports symbols', () => {
798+
799+
const sym = Symbol();
800+
const source = {
801+
a: {
802+
b: 5
803+
},
804+
[sym]: {
805+
d: 6
806+
}
807+
};
808+
809+
const copy = Hoek.cloneWithShallow(source, [[sym]]);
810+
expect(copy).to.equal(source);
811+
expect(copy).to.not.shallow.equal(source);
812+
expect(copy.a).to.not.shallow.equal(source.a);
813+
expect(copy[sym]).to.equal(source[sym]);
814+
});
770815
});
771816

772817
describe('applyToDefaultsWithShallow()', () => {
@@ -1083,6 +1128,27 @@ describe('deepEqual()', () => {
10831128
compare();
10841129
});
10851130

1131+
it('compares symbol object properties', () => {
1132+
1133+
const sym = Symbol();
1134+
1135+
const ne = {};
1136+
Object.defineProperty(ne, sym, { value: true });
1137+
1138+
expect(Hoek.deepEqual({ [sym]: { c: true } }, { [sym]: { c: true } })).to.be.true();
1139+
expect(Hoek.deepEqual({ [sym]: { c: true } }, { [sym]: { c: false } })).to.be.false();
1140+
expect(Hoek.deepEqual({ [sym]: { c: true } }, { [sym]: true })).to.be.false();
1141+
expect(Hoek.deepEqual({ [sym]: undefined }, { [sym]: undefined })).to.be.true();
1142+
expect(Hoek.deepEqual({ [sym]: undefined }, {})).to.be.false();
1143+
expect(Hoek.deepEqual({}, { [sym]: undefined })).to.be.false();
1144+
1145+
expect(Hoek.deepEqual({}, ne)).to.be.true();
1146+
expect(Hoek.deepEqual(ne, {})).to.be.true();
1147+
expect(Hoek.deepEqual({ [sym]: true }, ne)).to.be.false();
1148+
expect(Hoek.deepEqual(ne, { [sym]: true })).to.be.false();
1149+
expect(Hoek.deepEqual(ne, { [Symbol()]: undefined })).to.be.false();
1150+
});
1151+
10861152
it('compares dates', () => {
10871153

10881154
expect(Hoek.deepEqual(new Date(2015, 1, 1), new Date('2015/02/01'))).to.be.true();
@@ -1709,6 +1775,18 @@ describe('contain()', () => {
17091775
expect(Hoek.contain(foo, { 'a': 1, 'b': 2, 'c': 3 }, { only: true })).to.be.true();
17101776
}
17111777
});
1778+
1779+
it('supports symbols', () => {
1780+
1781+
const sym = Symbol();
1782+
1783+
expect(Hoek.contain([sym], sym)).to.be.true();
1784+
expect(Hoek.contain({ [sym]: 1 }, sym)).to.be.true();
1785+
expect(Hoek.contain({ [sym]: 1, a: 2 }, { [sym]: 1 })).to.be.true();
1786+
1787+
expect(Hoek.contain([sym], Symbol())).to.be.false();
1788+
expect(Hoek.contain({ [sym]: 1 }, Symbol())).to.be.false();
1789+
});
17121790
});
17131791

17141792
describe('flatten()', () => {
@@ -1723,6 +1801,7 @@ describe('flatten()', () => {
17231801

17241802
describe('reach()', () => {
17251803

1804+
const sym = Symbol();
17261805
const obj = {
17271806
a: {
17281807
b: {
@@ -1735,7 +1814,10 @@ describe('reach()', () => {
17351814
g: {
17361815
h: 3
17371816
},
1738-
'-2': true
1817+
'-2': true,
1818+
[sym]: {
1819+
v: true
1820+
}
17391821
},
17401822
i: function () { },
17411823
j: null,
@@ -1749,15 +1831,18 @@ describe('reach()', () => {
17491831
expect(Hoek.reach(obj, null)).to.equal(obj);
17501832
expect(Hoek.reach(obj, false)).to.equal(obj);
17511833
expect(Hoek.reach(obj)).to.equal(obj);
1834+
expect(Hoek.reach(obj, [])).to.equal(obj);
17521835
});
17531836

1754-
it('returns first value of array', () => {
1837+
it('returns values of array', () => {
17551838

17561839
expect(Hoek.reach(obj, 'k.0')).to.equal(4);
1840+
expect(Hoek.reach(obj, 'k.1')).to.equal(8);
17571841
});
17581842

17591843
it('returns last value of array using negative index', () => {
17601844

1845+
expect(Hoek.reach(obj, 'k.-1')).to.equal(1);
17611846
expect(Hoek.reach(obj, 'k.-2')).to.equal(9);
17621847
});
17631848

@@ -1803,6 +1888,9 @@ describe('reach()', () => {
18031888
it('returns undefined on invalid member', () => {
18041889

18051890
expect(Hoek.reach(obj, 'a.b.c.d-.x')).to.equal(undefined);
1891+
expect(Hoek.reach(obj, 'k.x')).to.equal(undefined);
1892+
expect(Hoek.reach(obj, 'k.1000')).to.equal(undefined);
1893+
expect(Hoek.reach(obj, 'k/0.5', '/')).to.equal(undefined);
18061894
});
18071895

18081896
it('returns function member', () => {
@@ -1842,6 +1930,21 @@ describe('reach()', () => {
18421930

18431931
expect(Hoek.reach(obj, 'q', { default: '' })).to.equal('');
18441932
});
1933+
1934+
it('allows array-based lookup', () => {
1935+
1936+
expect(Hoek.reach(obj, ['a', 'b', 'c', 'd'])).to.equal(1);
1937+
expect(Hoek.reach(obj, ['k', '1'])).to.equal(8);
1938+
expect(Hoek.reach(obj, ['k', 1])).to.equal(8);
1939+
expect(Hoek.reach(obj, ['k', '-2'])).to.equal(9);
1940+
expect(Hoek.reach(obj, ['k', -2])).to.equal(9);
1941+
});
1942+
1943+
it('allows array-based lookup with symbols', () => {
1944+
1945+
expect(Hoek.reach(obj, ['a', sym, 'v'])).to.equal(true);
1946+
expect(Hoek.reach(obj, ['a', Symbol(), 'v'])).to.equal(undefined);
1947+
});
18451948
});
18461949

18471950
describe('reachTemplate()', () => {

0 commit comments

Comments
 (0)
Please sign in to comment.