[api doc test] Finalize API. Add more test coverage

master
indexzero 2011-04-02 03:03:16 -04:00
parent 772de110bb
commit 73bf78339f
7 changed files with 472 additions and 46 deletions

View File

@ -9,25 +9,55 @@ require.paths.unshift(__dirname);
var nconf = exports;
nconf.utils = require('nconf/utils');
nconf.stores = require('nconf/stores');
//
// ### function use (type, options)
// #### @type {string} Type of the nconf store to use.
// #### @options {Object} Options for the store instance.
// Sets the active `nconf.store` to a new instance of the
// specified `type`.
//
nconf.use = function (type, options) {
nconf.store = new nconf.stores[type](options);
};
//
// ### function get (key, callback)
// #### @key {string} Key to retrieve for this instance.
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Retrieves the value for the specified key (if any).
//
nconf.get = function (key, callback) {
return nconf.store.get(key, callback);
};
//
// ### function set (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {literal|Object} Value for the specified key
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Sets the `value` for the specified `key` in this instance.
//
nconf.set = function (key, value, callback) {
return nconf.store.set(key, value, callback);
};
//
// ### function clear (key, callback)
// #### @key {string} Key to remove from this instance
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Removes the value for the specified `key` from this instance.
//
nconf.clear = function (key, callback) {
return nconf.store.clear(key, callback);
};
//
// ### function load (callback)
// #### @callback {function} Continuation to respond to when complete.
// Responds with an Object representing all keys associated in this instance.
//
nconf.load = function (callback) {
if (!nconf.store.load) {
var error = new Error('nconf store ' + nconf.store.type + ' has no load() method');
@ -41,7 +71,19 @@ nconf.load = function (callback) {
return nconf.store.load(callback);
};
nconf.save = function (callback) {
//
// ### function save (value, callback)
// #### @value {Object} **Optional** Config object to set for this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes any existing configuration settings that may exist in this
// instance and then adds all key-value pairs in `value`.
//
nconf.save = function (value, callback) {
if (!callback) {
callback = value;
value = null;
}
if (!nconf.store.save) {
var error = new Error('nconf store ' + nconf.store.type + ' has no save() method');
if (callback) {
@ -51,5 +93,31 @@ nconf.save = function (callback) {
throw error;
}
return nconf.store.save(callback);
};
return nconf.store.save(value, callback);
};
//
// ### function reset (callback)
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Clears all keys associated with this instance.
//
nconf.reset = function (callback) {
return nconf.store.reset(callback);
};
//
// ### function key (arguments)
// Returns a `:` joined string from the `arguments`.
//
nconf.key = function () {
return Array.prototype.slice.call(arguments).join(':');
};
//
// ### function path (key)
// #### @key {string} The ':' delimited key to split
// Returns a fully-qualified path to a nested nconf key.
//
nconf.path = function (key) {
return key.split(':');
};

View File

@ -9,6 +9,12 @@ var fs = require('fs'),
util = require('util'),
Memory = require('./memory').Memory;
//
// ### function File (options)
// #### @options {Object} Options for this instance
// Constructor function for the File nconf store, a simple abstraction
// around the Memory store that can persist configuration to disk.
//
var File = exports.File = function (options) {
if (!options.file) {
throw new Error ('Missing required option `files`');
@ -20,14 +26,32 @@ var File = exports.File = function (options) {
this.format = options.format || JSON;
};
// Inherit from the Memory store
util.inherits(File, Memory);
File.prototype.save = function (callback) {
//
// ### function save (value, callback)
// #### @value {Object} _Ignored_ Left here for consistency
// #### @callback {function} Continuation to respond to when complete.
// Saves the current configuration object to disk at `this.file`
// using the format specified by `this.format`.
//
File.prototype.save = function (value, callback) {
if (!callback) {
callback = value;
value = null;
}
fs.save(this.file, this.format.stringify(this.store), function (err) {
return err ? callback(err) : callback();
});
};
//
// ### function load (callback)
// #### @callback {function} Continuation to respond to when complete.
// Responds with an Object representing all keys associated in this instance.
//
File.prototype.load = function (callback) {
var self = this;
fs.load(this.file, function (err, data) {

View File

@ -7,15 +7,32 @@
var nconf = require('nconf');
//
// ### function Memory (options)
// #### @options {Object} Options for this instance
// Constructor function for the Memory nconf store which maintains
// a nested json structure based on key delimiters `:`.
//
// e.g. `my:nested:key` ==> `{ my: { nested: { key: } } }`
//
var Memory = exports.Memory = function (options) {
options = options || {};
this.store = {};
options = options || {};
this.store = {};
this.mtimes = {};
};
//
// ### function get (key)
// #### @key {string} Key to retrieve for this instance.
// Retrieves the value for the specified key (if any).
//
Memory.prototype.get = function (key) {
var target = this.store,
path = nconf.utils.path(key);
path = nconf.path(key);
//
// Scope into the object to get the appropriate nested context
//
while (path.length > 0) {
key = path.shift();
if (!target[key]) {
@ -29,10 +46,24 @@ Memory.prototype.get = function (key) {
}
};
//
// ### function set (key, value)
// #### @key {string} Key to set in this instance
// #### @value {literal|Object} Value for the specified key
// Sets the `value` for the specified `key` in this instance.
//
Memory.prototype.set = function (key, value) {
var target = this.store,
path = nconf.utils.path(key);
path = nconf.path(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]) {
@ -42,15 +73,29 @@ Memory.prototype.set = function (key, value) {
target = target[key];
}
// Set the specified value in the nested JSON structure
key = path.shift();
target[key] = value;
return true;
};
//
// ### function clear (key)
// #### @key {string} Key to remove from this instance
// Removes the value for the specified `key` from this instance.
//
Memory.prototype.clear = function (key) {
var target = this.store,
path = nconf.utils.path(key);
path = nconf.path(key);
//
// Remove the key from the set of `mtimes` (modified times)
//
delete this.mtimes[key];
//
// Scope into the object to get the appropriate nested context
//
while (path.length > 1) {
key = path.shift();
if (!target[key]) {
@ -60,7 +105,18 @@ Memory.prototype.clear = function (key) {
target = target[key];
}
// Delete the key from the nested JSON structure
key = path.shift();
delete target[key];
return true;
};
};
//
// ### function reset (callback)
// Clears all keys associated with this instance.
//
Memory.prototype.reset = function () {
this.mtimes = {};
this.store = {};
return true;
}

View File

@ -11,23 +11,56 @@ var async = require('async'),
nconf = require('nconf'),
Memory = require('./Memory').Memory;
//
// ### function Redis (options)
// #### @options {Object} Options for this instance
// Constructor function for the Redis nconf store which maintains
// a nested Redis key structure based on key delimiters `:`.
//
// e.g.
// my:nested:key, 'value'
// namespace:keys ==> ['my']
// namespace:nested:keys ==> ['key']
// namespace:nested:key ==> 'value'
//
var Redis = exports.Redis = function (options) {
options = options || {}
this.namespace = options.namespace || 'nconf';
this.host = options.host || 'localhost';
this.port = options.port || 6379;
this.ttl = options.ttl || 60 * 60 * 1000;
this.cache = new Memory();
this.redis = redis.createClient(options.port, options.host);
};
//
// ### function get (key, callback)
// #### @key {string} Key to retrieve for this instance.
// #### @callback {function} Continuation to respond to when complete.
// Retrieves the value for the specified key (if any).
//
Redis.prototype.get = function (key, callback) {
var self = this,
result = {},
fullKey = nconf.utils.key(this.namespace, key);
now = Date.now(),
mtime = this.cache.mtimes[key],
fullKey = nconf.key(this.namespace, key);
this.redis.smembers(nconf.utils.key(fullKey, 'keys'), function (err, keys) {
//
// If the key exists in the cache and the ttl is less than
// the value set for this instance, return from the cache.
//
if (mtime && now - mtime < this.ttl) {
return callback(null, this.cache.get(key));
}
//
// 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.redis.smembers(nconf.key(fullKey, 'keys'), function (err, keys) {
function addValue (source, next) {
self.get(nconf.utils.key(key, source), function (err, value) {
self.get(nconf.key(key, source), function (err, value) {
if (err) {
return next(err);
}
@ -38,62 +71,253 @@ Redis.prototype.get = function (key, callback) {
}
if (keys && keys.length > 0) {
//
// If the value to be retrieved is an Object, recursively attempt
// to get the value from redis. Here we use a recursive call to `this.get`
// to support nested Object keys.
//
async.forEach(keys, addValue, function (err) {
return err ? callback(err) : callback(null, result);
if (err) {
return callback(err);
}
self.cache.set(key, result);
callback(null, result);
})
}
else {
//
// If there are no keys, then the value to be retrieved is a literal
// and we can simply return the value from redis directly.
//
self.redis.get(fullKey, function (err, value) {
return err ? callback(err) : callback(null, JSON.parse(value));
if (err) {
return callback(err);
}
result = JSON.parse(value);
if (result) {
self.cache.set(key, result);
}
callback(null, result);
});
}
});
};
//
// ### function set (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {literal|Object} Value for the specified key
// #### @callback {function} Continuation to respond to when complete.
// Sets the `value` for the specified `key` in this instance.
//
Redis.prototype.set = function (key, value, callback) {
var self = this,
path = nconf.utils.path(key);
path = nconf.path(key);
function addKey (partial, next) {
var index = path.indexOf(partial),
base = [self.namespace].concat(path.slice(0, index)),
parent = nconf.utils.key(base.concat(['keys']));
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) {
if (err) {
return callback(err);
}
var fullKey = nconf.utils.key(self.namespace, key);
var fullKey = nconf.key(self.namespace, key);
if (!Array.isArray(value) && typeof value === 'object') {
//
// If the value is an `Object` (and not an `Array`) then
// nest into the value and set the child keys appropriately.
// This is done for efficient lookup when setting Object keys.
// (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._setObject(fullKey, value, callback);
}
else {
//
// 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);
self.redis.set(fullKey, value, callback);
}
});
};
//
// ### function clear (key, callback)
// #### @key {string} Key to remove from this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes the value for the specified `key` from this instance.
//
Redis.prototype.clear = function (key, callback) {
var self = this,
result = {},
path = [this.namespace].concat(nconf.path(key)),
last = path.pop(),
fullKey = nconf.key(this.namespace, key);
//
// Clear the key from the cache for this instance
//
this.cache.clear(key);
//
// Remove the `key` from the parent set of keys.
//
this.redis.srem(nconf.key.apply(null, path.concat(['keys'])), last, function (err) {
//
// Remove the value from redis by iterating over the set of keys (if any)
// and deleting each value. If no keys, then just delete the simple literal.
//
self.redis.smembers(nconf.key(fullKey, 'keys'), function (err, keys) {
function removeValue (child, next) {
//
// Recursively call `self.clear` here to ensure we remove any
// nested Objects completely from this instance.
//
self.clear(nconf.key(key, child), next);
}
if (keys && keys.length > 0) {
//
// If there are child keys then iterate over them,
// removing each child along the way.
//
async.forEach(keys, removeValue, callback);
}
else {
//
// Otherwise if this is just a simple literal, then
// simply remove it from Redis directly.
//
self.redis.del(fullKey, callback);
}
});
});
};
//
// ### function save (value, callback)
// #### @value {Object} Config object to set for this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes any existing configuration settings that may exist in this
// instance and then adds all key-value pairs in `value`.
//
Redis.prototype.save = function (value, callback) {
if (Array.isArray(value) || typeof value !== 'object') {
return callback(new Error('`value` to be saved must be an object.'));
}
var self = this,
keys = Object.keys(value);
//
// Clear all existing keys associated with this instance.
//
this.reset(function (err) {
if (err) {
return callback(err);
}
//
// Iterate over the keys in the new value, setting each of them.
//
async.forEach(keys, function (key, next) {
self.set(key, value[key], next);
}, callback);
});
};
//
// ### function load (callback)
// #### @callback {function} Continuation to respond to when complete.
// Responds with an Object representing all keys associated in this instance.
//
Redis.prototype.load = function (callback) {
var self = this,
result = {};
this.redis.smembers(nconf.key(this.namespace, 'keys'), function (err, keys) {
if (err) {
return callback(err);
}
function addValue (key, next) {
self.get(key, function (err, value) {
if (err) {
return next(err);
}
result[key] = value;
next();
});
}
async.forEach(keys, addValue, function (err) {
return err ? callback(err) : callback(null, result);
});
});
};
//
// ### function reset (callback)
// #### @callback {function} Continuation to respond to when complete.
// Clears all keys associated with this instance.
//
Redis.prototype.reset = function (callback) {
var self = this;
//
// Get the list of of top-level keys, then clear each of them
//
this.redis.smembers(nconf.key(this.namespace, 'keys'), function (err, existing) {
if (err) {
return callback(err);
}
async.forEach(existing, function (key, next) {
self.clear(key, next);
}, callback);
});
};
//
// ### @private function _setObject (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {Object} Value for the specified key
// #### @callback {function} Continuation to respond to when complete.
// Internal helper function for setting all keys of a nested object.
//
Redis.prototype._setObject = function (key, value, callback) {
var self = this,
keys = Object.keys(value);
function addValue (child, next) {
self.redis.sadd(nconf.utils.key(key, 'keys'), child, function (err) {
//
// Add the child key to the parent key-set, then set the value.
// Recursively call `_setObject` in the event of nested Object(s).
//
self.redis.sadd(nconf.key(key, 'keys'), child, function (err) {
if (err) {
return next(err);
}
var fullKey = nconf.utils.key(key, child),
var fullKey = nconf.key(key, child),
childValue = value[child];
if (!Array.isArray(childValue) && typeof childValue === 'object') {
@ -106,6 +330,9 @@ Redis.prototype._setObject = function (key, value, callback) {
});
}
//
// Iterate over the keys of the Object and set the appropriate values.
//
async.forEach(keys, addValue, function (err) {
return err ? callback(err) : callback();
});

View File

@ -1,16 +0,0 @@
/*
* utils.js: Utils for the nconf module.
*
* (C) 2011, Charlie Robbins
*
*/
var utils = exports;
utils.key = function () {
return Array.prototype.slice.call(arguments).join(':');
};
utils.path = function (key) {
return key.split(':');
};

View File

@ -5,7 +5,7 @@
*
*/
require.paths.unshift(require('path').join(__dirname, '..', '..', 'lib'));
require.paths.unshift(require('path').join(__dirname, '..', 'lib'));
var vows = require('vows'),
assert = require('assert'),

View File

@ -5,7 +5,7 @@
*
*/
require.paths.unshift(require('path').join(__dirname, '..', '..', 'lib'));
require.paths.unshift(require('path').join(__dirname, '..', 'lib'));
var vows = require('vows'),
assert = require('assert'),
@ -94,12 +94,79 @@ vows.describe('nconf/stores/redis').addBatch({
}
}
}).addBatch({
/*,
"the clear() method": {
"should respond with the true": function (store) {
assert.equal(store.get('foo:bar:bazz'), 'buzz');
assert.isTrue(store.clear('foo:bar:bazz'));
assert.isTrue(typeof store.get('foo:bar:bazz') === 'undefined');
"When using the nconf redis store": {
topic: new nconf.stores.Redis(),
"the clear() method": {
topic: function (store) {
var that = this;
store.clear('foo', function (err) {
if (err) {
return that.callback(err);
}
store.get('foo', that.callback);
});
},
"should actually remove the value from Redis": function (err, value) {
assert.isNull(err);
assert.isNull(value);
}
}
}*/
}
}).addBatch({
"When using the nconf redis store": {
topic: new nconf.stores.Redis(),
"the save() method": {
topic: function (store) {
var that = this;
store.save(data, function (err) {
if (err) {
return that.callback(err);
}
store.get('obj', that.callback);
});
},
"should set all values correctly": function (err, value) {
assert.isNull(err);
assert.deepEqual(value, data.obj);
}
}
}
}).addBatch({
"When using the nconf redis store": {
topic: new nconf.stores.Redis(),
"the load() method": {
topic: function (store) {
store.load(this.callback);
},
"should respond with the correct object": function (err, value) {
assert.isNull(err);
assert.deepEqual(value, data);
}
}
}
}).addBatch({
"When using the nconf redis store": {
topic: new nconf.stores.Redis(),
"the reset() method": {
topic: function (store) {
var that = this;
this.store = store;
store.reset(function (err) {
if (err) {
return that.callback(err);
}
store.get('obj', that.callback);
});
},
"should remove all keys from redis": function (err, value) {
assert.isNull(err);
assert.isNull(value);
assert.length(Object.keys(this.store.cache.store), 0);
}
}
}
}).export(module);