diff --git a/lib/nconf/stores/memory.js b/lib/nconf/stores/memory.js index b4cbe9b..d65a0b8 100644 --- a/lib/nconf/stores/memory.js +++ b/lib/nconf/stores/memory.js @@ -67,13 +67,13 @@ Memory.prototype.set = function (key, value) { // while (path.length > 1) { key = path.shift(); - if (!target[key]) { + if (!target[key] || typeof target[key] !== 'object') { target[key] = {}; } target = target[key]; } - + // Set the specified value in the nested JSON structure key = path.shift(); target[key] = value; @@ -112,6 +112,55 @@ Memory.prototype.clear = function (key) { return true; }; +Memory.prototype.merge = function (key, value) { + // + // If the key is not an `Object` or is an `Array`, + // then simply set it. Merging is for Objects. + // + if (typeof value !== 'object' || Array.isArray(value)) { + return this.set(key, value); + } + + var self = this, + target = this.store, + path = nconf.path(key), + fullKey = key; + + // + // Update the `mtime` (modified time) of the key + // + this.mtimes[key] = Date.now(); + + // + // Scope into the object to get the appropriate nested context + // + while (path.length > 1) { + key = path.shift(); + if (!target[key]) { + target[key] = {}; + } + + target = target[key]; + } + + // Set the specified value in the nested JSON structure + key = path.shift(); + + // + // If the current value at the key target is not an `Object`, + // of is an `Array` then simply override it because the new value + // is an Object. + // + if (typeof target[key] !== 'object' || Array.isArray(target[key])) { + target[key] = value; + return true; + } + + return Object.keys(value).every(function (nested) { + return self.merge(fullKey + ':' + nested, value[nested]); + }); +}; + // // ### function reset (callback) // Clears all keys associated with this instance. diff --git a/lib/nconf/stores/redis.js b/lib/nconf/stores/redis.js index 015f458..e990a57 100644 --- a/lib/nconf/stores/redis.js +++ b/lib/nconf/stores/redis.js @@ -132,19 +132,7 @@ Redis.prototype.set = function (key, value, callback) { // Set the callback if not provided for "fire and forget" callback = callback || function () { }; - function addKey (partial, next) { - var index = path.indexOf(partial), - base = [self.namespace].concat(path.slice(0, index)), - parent = nconf.key.apply(null, base.concat(['keys'])); - - self.redis.sadd(parent, partial, next); - }; - - // - // Iterate over the entire key path and add each key to the - // parent key-set if it doesn't exist already. - // - async.forEach(path, addKey, function (err) { + this._addKeys(key, function (err) { if (err) { return callback(err); } @@ -159,6 +147,7 @@ Redis.prototype.set = function (key, value, callback) { // (i.e. If you set and Object then wish to later retrieve only a // member of that Object, the entire Object need not be retrieved). // + self.cache.set(key, value); self._setObject(fullKey, value, callback); } else { @@ -166,13 +155,57 @@ Redis.prototype.set = function (key, value, callback) { // If the value is a simple literal (or an `Array`) then JSON // stringify it and put it into Redis. // - value = JSON.stringify(value); self.cache.set(key, value); + value = JSON.stringify(value); self.redis.set(fullKey, value, callback); } }); }; +Redis.prototype.merge = function (key, value, callback) { + // + // If the key is not an `Object` or is an `Array`, + // then simply set it. Merging is for Objects. + // + if (typeof value !== 'object' || Array.isArray(value)) { + return this.set(key, value, callback); + } + + var self = this, + path = nconf.path(key), + fullKey = nconf.key(this.namespace, key); + + // Set the callback if not provided for "fire and forget" + callback = callback || function () { }; + + // + // Get the set of all children keys for the `key` supplied. If the value + // to be returned is an Object, this list will not be empty. + // + this._addKeys(key, function (err) { + self.redis.smembers(nconf.key(fullKey, 'keys'), function (err, keys) { + function nextMerge (nested, next) { + var keyPath = nconf.key.apply(null, path.concat([nested])); + self.merge(keyPath, value[nested], next); + } + + if (keys && keys.length > 0) { + // + // If there are existing keys then we must do a recursive merge + // of the two Objects. + // + return async.forEach(Object.keys(value), nextMerge, callback); + } + + // + // Otherwise, we can simply invoke `set` to override the current + // literal or Array value with our new Object value + // + self.set(key, value, callback); + }); + }); +}; + // // ### function clear (key, callback) // #### @key {string} Key to remove from this instance @@ -325,6 +358,25 @@ Redis.prototype.reset = function (callback) { }); }; +Redis.prototype._addKeys = function (key, callback) { + var self = this, + path = nconf.path(key); + + function addKey (partial, next) { + var index = path.indexOf(partial), + base = [self.namespace].concat(path.slice(0, index)), + parent = nconf.key.apply(null, base.concat(['keys'])); + + self.redis.sadd(parent, partial, next); + }; + + // + // Iterate over the entire key path and add each key to the + // parent key-set if it doesn't exist already. + // + async.forEach(path, addKey, callback); +}; + // // ### @private function _setObject (key, value, callback) // #### @key {string} Key to set in this instance diff --git a/test/fixtures/data.js b/test/fixtures/data.js index 29a1c24..de9f504 100644 --- a/test/fixtures/data.js +++ b/test/fixtures/data.js @@ -17,4 +17,13 @@ exports.data = { password: 'password' } } -} \ No newline at end of file +}; + +exports.merge = { + prop1: 1, + prop2: [1, 2, 3], + prop3: { + foo: 'bar', + bar: 'foo' + } +}; \ No newline at end of file diff --git a/test/memory-store-test.js b/test/memory-store-test.js index 210879a..614b5cd 100644 --- a/test/memory-store-test.js +++ b/test/memory-store-test.js @@ -7,8 +7,9 @@ var vows = require('vows'), assert = require('assert'), - nconf = require('../lib/nconf'); - + nconf = require('../lib/nconf'), + merge = require('./fixtures/data').merge; + vows.describe('nconf/stores/memory').addBatch({ "When using the nconf memory store": { topic: new nconf.stores.Memory(), @@ -46,6 +47,38 @@ vows.describe('nconf/stores/memory').addBatch({ assert.isTrue(store.clear('foo:bar:bazz')); assert.isTrue(typeof store.get('foo:bar:bazz') === 'undefined'); } + }, + "the merge() method": { + "when overriding an existing literal value": function (store) { + store.set('merge:literal', 'string-value'); + store.merge('merge:literal', merge); + assert.deepEqual(store.get('merge:literal'), merge); + }, + "when overriding an existing Array value": function (store) { + store.set('merge:array', [1,2,3,4]); + store.merge('merge:array', merge); + assert.deepEqual(store.get('merge:literal'), merge); + }, + "when merging into an existing Object value": function (store) { + store.set('merge:object', { + prop1: 2, + prop2: 'prop2', + prop3: { + bazz: 'bazz' + }, + prop4: ['foo', 'bar'] + }); + store.merge('merge:object', merge); + + assert.equal(store.get('merge:object:prop1'), 1); + assert.equal(store.get('merge:object:prop2').length, 3); + assert.deepEqual(store.get('merge:object:prop3'), { + foo: 'bar', + bar: 'foo', + bazz: 'bazz' + }); + assert.equal(store.get('merge:object:prop4').length, 2); + } } } }).export(module); \ No newline at end of file diff --git a/test/redis-store-test.js b/test/redis-store-test.js index 2c8cf2b..514bbfe 100644 --- a/test/redis-store-test.js +++ b/test/redis-store-test.js @@ -8,7 +8,8 @@ var vows = require('vows'), assert = require('assert'), nconf = require('../lib/nconf'), - data = require('./fixtures/data').data; + data = require('./fixtures/data').data, + merge = require('./fixtures/data').merge; vows.describe('nconf/stores/redis').addBatch({ "When using the nconf redis store": { @@ -147,6 +148,67 @@ vows.describe('nconf/stores/redis').addBatch({ } } } +}).addBatch({ + "when using the nconf redis store": { + topic: new nconf.stores.Redis(), + "the merge() method": { + "when overriding an existing literal value": { + topic: function (store) { + var that = this; + store.set('merge:literal', 'string-value', function () { + store.merge('merge:literal', merge, function () { + store.get('merge:literal', that.callback); + }); + }); + }, + "should merge correctly": function (err, data) { + assert.deepEqual(data, merge); + } + }, + "when overriding an existing Array value": { + topic: function (store) { + var that = this; + store.set('merge:array', [1, 2, 3, 4], function () { + store.merge('merge:array', merge, function () { + store.get('merge:array', that.callback); + }); + }); + }, + "should merge correctly": function (err, data) { + assert.deepEqual(data, merge); + } + }, + "when merging into an existing Object value": { + topic: function (store) { + var that = this, current; + current = { + prop1: 2, + prop2: 'prop2', + prop3: { + bazz: 'bazz' + }, + prop4: ['foo', 'bar'] + }; + + store.set('merge:object', current, function () { + store.merge('merge:object', merge, function () { + store.get('merge:object', that.callback); + }); + }); + }, + "should merge correctly": function (err, data) { + assert.equal(data['prop1'], 1); + assert.equal(data['prop2'].length, 3); + assert.deepEqual(data['prop3'], { + foo: 'bar', + bar: 'foo', + bazz: 'bazz' + }); + assert.equal(data['prop4'].length, 2); + } + } + } + } }).addBatch({ "When using the nconf redis store": { topic: new nconf.stores.Redis(),