[api test] Finished API and tests for hierarchical configuration storage.

This commit is contained in:
indexzero 2011-09-18 21:37:01 -04:00
parent 7ef9b11d33
commit 1ef5797e83
13 changed files with 206 additions and 70 deletions

View file

@ -70,8 +70,8 @@ Provider.prototype.use = function (name, options) {
throw new Error('Cannot use reserved name: ' + name); throw new Error('Cannot use reserved name: ' + name);
} }
options = options || {}; options = options || {};
options.type = options.type || name; var type = options.type || name;
function sameOptions (store) { function sameOptions (store) {
return Object.keys(options).every(function (key) { return Object.keys(options).every(function (key) {
@ -108,15 +108,19 @@ Provider.prototype.add = function (name, options) {
throw new Error('Cannot use reserved name: ' + name); throw new Error('Cannot use reserved name: ' + name);
} }
options = options || {}; options = options || {};
options.type = options.type || name; var type = options.type || name;
if (Object.keys(stores).indexOf(common.capitalize(options.type)) === -1) { if (Object.keys(stores).indexOf(common.capitalize(type)) === -1) {
throw new Error('Cannot add store with unknown type: ' + options.type); throw new Error('Cannot add store with unknown type: ' + type);
} }
this[name] = this.create(options.type, options); this[name] = this.create(type, options);
this._stores.push(name); this._stores.push(name);
if (this[name].loadSync) {
this[name].loadSync();
}
}; };
// //
@ -156,7 +160,45 @@ Provider.prototype.create = function (type, options) {
// Retrieves the value for the specified key (if any). // Retrieves the value for the specified key (if any).
// //
Provider.prototype.get = function (key, callback) { Provider.prototype.get = function (key, callback) {
return this._execute('get', 1, key, callback); //
// If there is no callback we can short-circuit into the default
// logic for traversing stores.
//
if (!callback) {
return this._execute('get', 1, key, callback);
}
//
// Otherwise the asynchronous, hierarchical `get` is
// slightly more complicated because we do not need to traverse
// the entire set of stores, but up until there is a defined value.
//
var current = 0,
self = this,
response;
async.whilst(function () {
return typeof response === 'undefined' && current < self._stores.length;
}, function (next) {
var store = self[self._stores[current]];
current++;
if (store.get.length >= 2) {
return store.get(key, function (err, value) {
if (err) {
return next(err);
}
response = value;
next();
});
}
response = store.get(key);
next();
}, function (err) {
return err ? callback(err) : callback(null, response);
});
}; };
// //
@ -293,7 +335,7 @@ Provider.prototype.save = function (value, callback) {
function saveStore(name, next) { function saveStore(name, next) {
var store = self[name]; var store = self[name];
if (!store.load && !store.saveSync) { if (!store.save && !store.saveSync) {
return next(new Error('nconf store ' + store.type + ' has no save() method')); return next(new Error('nconf store ' + store.type + ' has no save() method'));
} }
@ -357,10 +399,8 @@ Provider.prototype._execute = function (action, syncLength /* [arguments] */) {
// Gets or sets a property representing overrides which supercede all // Gets or sets a property representing overrides which supercede all
// other values for this instance. // other values for this instance.
// //
Provider.prototype.__defineSetter__('overrides', updateSystem.bind(this, 'overrides')); Provider.prototype.__defineSetter__('overrides', function (val) { updateSystem.call(this, 'overrides', val) });
Provider.prototype.__defineGetter__('overrides', function () { Provider.prototype.__defineGetter__('overrides', function () { return this._argv });
return this._argv;
});
// //
// ### @argv {boolean} // ### @argv {boolean}
@ -368,10 +408,8 @@ Provider.prototype.__defineGetter__('overrides', function () {
// by checking `optimist.argv`. Can be a boolean or the pass-thru // by checking `optimist.argv`. Can be a boolean or the pass-thru
// options for `optimist`. // options for `optimist`.
// //
Provider.prototype.__defineSetter__('argv', updateSystem.bind(this, 'argv')); Provider.prototype.__defineSetter__('argv', function (val) { updateSystem.call(this, 'argv', val) });
Provider.prototype.__defineGetter__('argv', function () { Provider.prototype.__defineGetter__('argv', function () { return this._argv });
return this._argv;
});
// //
// ### @env {boolean} // ### @env {boolean}
@ -379,10 +417,8 @@ Provider.prototype.__defineGetter__('argv', function () {
// by checking `process.env`. Can be a boolean or an Array of // by checking `process.env`. Can be a boolean or an Array of
// environment variables to extract. // environment variables to extract.
// //
Provider.prototype.__defineSetter__('env', updateSystem.bind(this, 'env')); Provider.prototype.__defineSetter__('env', function (val) { updateSystem.call(this, 'env', val) });
Provider.prototype.__defineGetter__('env', function () { Provider.prototype.__defineGetter__('env', function () { return this._env });
return this._env;
});
// //
// Throw the `err` if a callback is not supplied // Throw the `err` if a callback is not supplied

View file

@ -18,9 +18,9 @@ var fs = require('fs'),
// around the Memory store that can persist configuration to disk. // around the Memory store that can persist configuration to disk.
// //
var File = exports.File = function (options) { var File = exports.File = function (options) {
if (!options.file) { if (!options || !options.file) {
throw new Error ('Missing required option `files`'); throw new Error ('Missing required option `files`');
} }
Memory.call(this, options); Memory.call(this, options);
@ -128,8 +128,8 @@ File.prototype.loadSync = function () {
// Else, the path exists, read it from disk // Else, the path exists, read it from disk
// //
try { try {
data = fs.readFileSync(this.file, 'utf8'); data = this.format.parse(fs.readFileSync(this.file, 'utf8'));
this.store = this.format.parse(data); this.store = data;
} }
catch (ex) { catch (ex) {
throw new Error("Error parsing your JSON configuration file.") throw new Error("Error parsing your JSON configuration file.")

View file

@ -17,13 +17,13 @@ var util = require('util'),
// and command-line arguments. // and command-line arguments.
// //
var System = exports.System = function (options) { var System = exports.System = function (options) {
options = options || {};
Memory.call(this, options); Memory.call(this, options);
this.type = 'system'; this.type = 'system';
this.readOnly = true;
this.overrides = options.overrides || null; this.overrides = options.overrides || null;
this.env = options.env || false; this.env = options.env || false;
this.argv = options.argv || false; this.argv = options.argv || false;
}; };
// Inherit from the Memory store // Inherit from the Memory store
@ -92,7 +92,7 @@ System.prototype.loadArgv = function () {
Object.keys(argv).forEach(function (key) { Object.keys(argv).forEach(function (key) {
self.set(key, argv[key]); self.set(key, argv[key]);
}); });
return this.store; return this.store;
}; };

11
test/fixtures/scripts/nconf-argv.js vendored Normal file
View file

@ -0,0 +1,11 @@
/*
* default-argv.js: Test fixture for using optimist defaults with nconf.
*
* (C) 2011, Charlie Robbins
*
*/
var nconf = require('../../../lib/nconf');
nconf.argv = true;
process.stdout.write(nconf.get('something'));

11
test/fixtures/scripts/nconf-env.js vendored Normal file
View file

@ -0,0 +1,11 @@
/*
* nconf-env.js: Test fixture for using process.env defaults with nconf.
*
* (C) 2011, Charlie Robbins
*
*/
var nconf = require('../../../lib/nconf');
nconf.env = true;
process.stdout.write(nconf.get('SOMETHING'));

12
test/fixtures/scripts/provider-argv.js vendored Normal file
View file

@ -0,0 +1,12 @@
/*
* provider-argv.js: Test fixture for using optimist defaults with nconf.
*
* (C) 2011, Charlie Robbins
*
*/
var nconf = require('../../../lib/nconf');
var provider = new (nconf.Provider)({ argv: true });
process.stdout.write(provider.get('something'));

12
test/fixtures/scripts/provider-env.js vendored Normal file
View file

@ -0,0 +1,12 @@
/*
* provider-argv.js: Test fixture for using process.env defaults with nconf.
*
* (C) 2011, Charlie Robbins
*
*/
var nconf = require('../../../lib/nconf');
var provider = new (nconf.Provider)({ env: true });
process.stdout.write(provider.get('SOMETHING'));

View file

@ -27,10 +27,23 @@ exports.assertMerged = function (err, merged) {
assert.isTrue(merged.elderberries); assert.isTrue(merged.elderberries);
}; };
exports.assertDefaults = function (script) { exports.assertSystemConf = function (options) {
return { return {
topic: function () { topic: function () {
spawn('node', [script, '--something', 'foobar']) var env = null;
if (options.env) {
env = {}
Object.keys(process.env).forEach(function (key) {
env[key] = process.env[key];
});
Object.keys(options.env).forEach(function (key) {
env[key] = options.env[key];
});
}
spawn('node', [options.script].concat(options.argv), { env: env })
.stdout.once('data', this.callback.bind(this, null)); .stdout.once('data', this.callback.bind(this, null));
}, },
"should respond with the value passed into the script": function (_, data) { "should respond with the value passed into the script": function (_, data) {

View file

@ -51,10 +51,25 @@ vows.describe('nconf').addBatch({
} }
}, },
"the get() method": { "the get() method": {
"should respond with the correct value": function () { "without a callback": {
assert.equal(nconf.get('foo:bar:bazz'), 'buzz'); "should respond with the correct value": function () {
assert.equal(nconf.get('foo:bar:bazz'), 'buzz');
}
},
"with a callback": {
topic: function () {
nconf.get('foo:bar:bazz', this.callback);
},
"should respond with the correct value": function (err, value) {
assert.equal(value, 'buzz');
}
} }
}, }
}
}
}).addBatch({
"When using nconf": {
"with the memory store": {
"the clear() method": { "the clear() method": {
"should respond with the true": function () { "should respond with the true": function () {
assert.equal(nconf.get('foo:bar:bazz'), 'buzz'); assert.equal(nconf.get('foo:bar:bazz'), 'buzz');
@ -65,9 +80,7 @@ vows.describe('nconf').addBatch({
"the load() method": { "the load() method": {
"without a callback": { "without a callback": {
"should throw an exception": function () { "should throw an exception": function () {
assert.throws(function () { assert.throws(function () { nconf.load() });
nconf.load();
})
} }
}, },
"with a callback": { "with a callback": {
@ -82,9 +95,7 @@ vows.describe('nconf').addBatch({
"the save() method": { "the save() method": {
"without a callback": { "without a callback": {
"should throw an exception": function () { "should throw an exception": function () {
assert.throws(function () { assert.throws(function () { nconf.save() });
nconf.save();
})
} }
}, },
"with a callback": { "with a callback": {

View file

@ -16,32 +16,45 @@ var assert = require('assert'),
var fixturesDir = path.join(__dirname, 'fixtures'), var fixturesDir = path.join(__dirname, 'fixtures'),
mergeFixtures = path.join(fixturesDir, 'merge'), mergeFixtures = path.join(fixturesDir, 'merge'),
files = [path.join(mergeFixtures, 'file1.json'), path.join(mergeFixtures, 'file2.json')], files = [path.join(mergeFixtures, 'file1.json'), path.join(mergeFixtures, 'file2.json')],
override = JSON.parse(fs.readFileSync(files[0]), 'utf8'), override = JSON.parse(fs.readFileSync(files[0]), 'utf8');
first = '/path/to/file1',
second = '/path/to/file2';
vows.describe('nconf/provider').addBatch({ vows.describe('nconf/provider').addBatch({
"When using nconf": { "When using nconf": {
"an instance of 'nconf.Provider'": { "an instance of 'nconf.Provider'": {
"calling the use() method with the same store type and different options": { "calling the use() method with the same store type and different options": {
topic: new nconf.Provider().use('file', { file: first }), topic: new nconf.Provider().use('file', { file: files[0] }),
"should use a new instance of the store type": function (provider) { "should use a new instance of the store type": function (provider) {
var old = provider.file; var old = provider.file;
assert.equal(provider.file.file, first); assert.equal(provider.file.file, files[0]);
provider.use('file', { file: second }); provider.use('file', { file: files[1] });
assert.notStrictEqual(old, provider.file); assert.notStrictEqual(old, provider.file);
assert.equal(provider.file.file, second); assert.equal(provider.file.file, files[1]);
} }
}, },
//"when 'useArgv' is true": helpers.assertDefaults(path.join(fixturesDir, 'scripts', 'nconf-override.js')) "when 'argv' is true": helpers.assertSystemConf({
script: path.join(fixturesDir, 'scripts', 'provider-argv.js'),
argv: ['--something', 'foobar']
}),
"when 'env' is true": helpers.assertSystemConf({
script: path.join(fixturesDir, 'scripts', 'provider-env.js'),
env: { SOMETHING: 'foobar' }
}),
}, },
/*"the default nconf provider": { "the default nconf provider": {
"when 'useArgv' is true": helpers.assertDefaults(path.join(fixturesDir, 'scripts', 'default-override.js')) "when 'argv' is set to true": helpers.assertSystemConf({
}*/ script: path.join(fixturesDir, 'scripts', 'nconf-argv.js'),
argv: ['--something', 'foobar'],
env: { SOMETHING: true }
}),
"when 'env' is set to true": helpers.assertSystemConf({
script: path.join(fixturesDir, 'scripts', 'nconf-env.js'),
env: { SOMETHING: 'foobar' }
})
}
} }
})/*.addBatch({ }).addBatch({
"When using nconf": { "When using nconf": {
"an instance of 'nconf.Provider'": { "an instance of 'nconf.Provider'": {
"the merge() method": { "the merge() method": {
@ -49,18 +62,9 @@ vows.describe('nconf/provider').addBatch({
"should have the result merged in": function (provider) { "should have the result merged in": function (provider) {
provider.load(); provider.load();
provider.merge(override); provider.merge(override);
helpers.assertMerged(null, provider); helpers.assertMerged(null, provider.file.store);
}
},
"the mergeFiles() method": {
topic: function () {
var provider = new nconf.Provider().use('memory');
provider.mergeFiles(files, this.callback.bind(this, null, provider))
},
"should have the result merged in": function (_, provider) {
helpers.assertMerged(null, provider);
} }
} }
} }
} }
})*/.export(module); }).export(module);

View file

@ -9,14 +9,14 @@ var fs = require('fs'),
path = require('path'), path = require('path'),
vows = require('vows'), vows = require('vows'),
assert = require('assert'), assert = require('assert'),
nconf = require('../lib/nconf'), nconf = require('../../lib/nconf'),
data = require('./fixtures/data').data, data = require('../fixtures/data').data,
store; store;
vows.describe('nconf/stores/file').addBatch({ vows.describe('nconf/stores/file').addBatch({
"When using the nconf file store": { "When using the nconf file store": {
topic: function () { topic: function () {
var filePath = path.join(__dirname, 'fixtures', 'store.json'); var filePath = path.join(__dirname, '..', 'fixtures', 'store.json');
fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
store = new nconf.stores.File({ file: filePath }); store = new nconf.stores.File({ file: filePath });
return null; return null;
@ -33,7 +33,7 @@ vows.describe('nconf/stores/file').addBatch({
}, },
"When using the nconf file store": { "When using the nconf file store": {
topic: function () { topic: function () {
var filePath = path.join(__dirname, 'fixtures', 'malformed.json'); var filePath = path.join(__dirname, '..', 'fixtures', 'malformed.json');
store = new nconf.stores.File({ file: filePath }); store = new nconf.stores.File({ file: filePath });
return null; return null;
}, },
@ -49,7 +49,7 @@ vows.describe('nconf/stores/file').addBatch({
}).addBatch({ }).addBatch({
"When using the nconf file store": { "When using the nconf file store": {
topic: function () { topic: function () {
var tmpPath = path.join(__dirname, 'fixtures', 'tmp.json'), var tmpPath = path.join(__dirname, '..', 'fixtures', 'tmp.json'),
tmpStore = new nconf.stores.File({ file: tmpPath }); tmpStore = new nconf.stores.File({ file: tmpPath });
return tmpStore; return tmpStore;
}, },
@ -120,7 +120,7 @@ vows.describe('nconf/stores/file').addBatch({
}, },
"when the target file doesn't exist higher in the directory tree": { "when the target file doesn't exist higher in the directory tree": {
topic: function () { topic: function () {
var filePath = this.filePath = path.join(__dirname, 'fixtures', 'search-store.json'); var filePath = this.filePath = path.join(__dirname, '..', 'fixtures', 'search-store.json');
return new (nconf.stores.File)({ return new (nconf.stores.File)({
dir: path.dirname(filePath), dir: path.dirname(filePath),
file: 'search-store.json' file: 'search-store.json'

View file

@ -7,8 +7,8 @@
var vows = require('vows'), var vows = require('vows'),
assert = require('assert'), assert = require('assert'),
nconf = require('../lib/nconf'), nconf = require('../../lib/nconf'),
merge = require('./fixtures/data').merge; merge = require('../fixtures/data').merge;
vows.describe('nconf/stores/memory').addBatch({ vows.describe('nconf/stores/memory').addBatch({
"When using the nconf memory store": { "When using the nconf memory store": {

View file

@ -0,0 +1,26 @@
/*
* system-test.js: Tests for the nconf system store.
*
* (C) 2011, Charlie Robbins
*
*/
var vows = require('vows'),
assert = require('assert'),
helpers = require('../helpers'),
nconf = require('../../lib/nconf');
vows.describe('nconf/stores/system').addBatch({
"An instance of nconf.stores.System": {
topic: new nconf.stores.System(),
"should have the correct methods defined": function (system) {
assert.isFunction(system.loadSync);
assert.isFunction(system.loadOverrides);
assert.isFunction(system.loadArgv);
assert.isFunction(system.loadEnv);
assert.isFalse(system.argv);
assert.isFalse(system.env);
assert.isNull(system.overrides);
}
}
}).export(module);