Skip to content

Commit

Permalink
Fix case where multiple sections with same settings could modify shar…
Browse files Browse the repository at this point in the history
…ed configuration (#48)

* Fix case where multiple sections with same settings could modify shared configuration

* Optimize safe merge of overridden settings

- Add safe option to mergeDeep that prevents mutation from to object
- Use safe option in non-hot code paths
  • Loading branch information
mridgway committed May 13, 2016
1 parent bf6f55e commit f4f9313
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 7 deletions.
5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Ycb.prototype = {
config = {};

context = context || {};
options = mergeDeep(this.options, options || {});
options = mergeDeep(this.options, options || {}, true);

lookupPaths = this._getLookupPaths(context, options);

Expand Down Expand Up @@ -409,7 +409,8 @@ Ycb.prototype = {
this.settings[key] ? (' onto ' + this.settings[key].__ycb_source__) : ''
));
}
mergeDeep(section, this.settings[key]);
// Clone original settings so that we don't override shared settings
this.settings[key] = mergeDeep(section, this.settings[key], true);
}
}, this);
},
Expand Down
19 changes: 14 additions & 5 deletions lib/mergeDeep.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
var isA = require('./isA');
var cloneDeep = require('./cloneDeep');

module.exports = function mergeDeep(from, to) {
module.exports = function mergeDeep(from, to, safe) {
var newTo = to;
var key;
if (safe) {
newTo = {};
for (key in to) {
if (to.hasOwnProperty(key)) {
newTo[key] = to[key];
}
}
}
for (key in from) {
if (from.hasOwnProperty(key)) {
// Property in destination object set; update its value.
Expand All @@ -13,14 +22,14 @@ module.exports = function mergeDeep(from, to) {
} else {
mergeToObj = {};
}
to[key] = mergeDeep(from[key], mergeToObj);
newTo[key] = mergeDeep(from[key], mergeToObj, safe);
} else if (isA(from[key], Array)) {
to[key] = cloneDeep(from[key]);
newTo[key] = cloneDeep(from[key]);
} else {
// Other literals are copied
to[key] = from[key];
newTo[key] = from[key];
}
}
}
return to;
return newTo;
};
27 changes: 27 additions & 0 deletions tests/fixtures/overridden-multisetting.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[
{
"settings": ["master"],
"foo": null,
"bar": 0,
"baz": false,
"oof": 4,
"rab": 5,
"zab": 6,
"__ycb_source__": "tests/fixtures/simple-4.json"
},
{
"settings": ["lang:fr,es"],
"foo": 1,
"bar": 2,
"baz": 3,
"oof": null,
"rab": 0,
"zab": false,
"__ycb_source__": "tests/fixtures/simple-4.json"
},
{
"settings": ["lang:es"],
"foo": 3,
"__ycb_source__": "tests/fixtures/simple-4.json"
}
]
28 changes: 28 additions & 0 deletions tests/unit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,34 @@ describe('ycb unit tests', function () {
}, config);
});

it('should not merge matched settings', function () {
var bundle,
ycb;

bundle = readFixtureFile('dimensions.json')
.concat(readFixtureFile('overridden-multisetting.json'));
ycb = new libycb.Ycb(bundle);
var config = ycb.read({
'lang': 'fr'
});
assert.equal(config.foo, 1, 'fr should be 1');

config = ycb.read({
'lang': 'es'
});
assert.equal(config.foo, 3, 'es should be 3');

config = ycb.read({
'lang': ['es', 'fr']
});
assert.equal(config.foo, 3, '[es,fr] should be 3');

config = ycb.read({
'lang': ['fr', 'es']
});
assert.equal(config.foo, 1, '[fr,es] should be 1');
});

it('should not pollute master settings with dimension values', function () {
var bundle,
ycb,
Expand Down

0 comments on commit f4f9313

Please sign in to comment.