[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);
}
options = options || {};
options.type = options.type || name;
options = options || {};
var type = options.type || name;
function sameOptions (store) {
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);
}
options = options || {};
options.type = options.type || name;
options = options || {};
var type = options.type || name;
if (Object.keys(stores).indexOf(common.capitalize(options.type)) === -1) {
throw new Error('Cannot add store with unknown type: ' + options.type);
if (Object.keys(stores).indexOf(common.capitalize(type)) === -1) {
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);
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).
//
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) {
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'));
}
@ -357,10 +399,8 @@ Provider.prototype._execute = function (action, syncLength /* [arguments] */) {
// Gets or sets a property representing overrides which supercede all
// other values for this instance.
//
Provider.prototype.__defineSetter__('overrides', updateSystem.bind(this, 'overrides'));
Provider.prototype.__defineGetter__('overrides', function () {
return this._argv;
});
Provider.prototype.__defineSetter__('overrides', function (val) { updateSystem.call(this, 'overrides', val) });
Provider.prototype.__defineGetter__('overrides', function () { return this._argv });
//
// ### @argv {boolean}
@ -368,10 +408,8 @@ Provider.prototype.__defineGetter__('overrides', function () {
// by checking `optimist.argv`. Can be a boolean or the pass-thru
// options for `optimist`.
//
Provider.prototype.__defineSetter__('argv', updateSystem.bind(this, 'argv'));
Provider.prototype.__defineGetter__('argv', function () {
return this._argv;
});
Provider.prototype.__defineSetter__('argv', function (val) { updateSystem.call(this, 'argv', val) });
Provider.prototype.__defineGetter__('argv', function () { return this._argv });
//
// ### @env {boolean}
@ -379,10 +417,8 @@ Provider.prototype.__defineGetter__('argv', function () {
// by checking `process.env`. Can be a boolean or an Array of
// environment variables to extract.
//
Provider.prototype.__defineSetter__('env', updateSystem.bind(this, 'env'));
Provider.prototype.__defineGetter__('env', function () {
return this._env;
});
Provider.prototype.__defineSetter__('env', function (val) { updateSystem.call(this, 'env', val) });
Provider.prototype.__defineGetter__('env', function () { return this._env });
//
// Throw the `err` if a callback is not supplied

View file

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

View file

@ -17,10 +17,10 @@ var util = require('util'),
// and command-line arguments.
//
var System = exports.System = function (options) {
options = options || {};
Memory.call(this, options);
this.type = 'system';
this.readOnly = true;
this.overrides = options.overrides || null;
this.env = options.env || false;
this.argv = options.argv || false;

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);
};
exports.assertDefaults = function (script) {
exports.assertSystemConf = function (options) {
return {
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));
},
"should respond with the value passed into the script": function (_, data) {

View file

@ -51,10 +51,25 @@ vows.describe('nconf').addBatch({
}
},
"the get() method": {
"should respond with the correct value": function () {
assert.equal(nconf.get('foo:bar:bazz'), 'buzz');
"without a callback": {
"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": {
"should respond with the true": function () {
assert.equal(nconf.get('foo:bar:bazz'), 'buzz');
@ -65,9 +80,7 @@ vows.describe('nconf').addBatch({
"the load() method": {
"without a callback": {
"should throw an exception": function () {
assert.throws(function () {
nconf.load();
})
assert.throws(function () { nconf.load() });
}
},
"with a callback": {
@ -82,9 +95,7 @@ vows.describe('nconf').addBatch({
"the save() method": {
"without a callback": {
"should throw an exception": function () {
assert.throws(function () {
nconf.save();
})
assert.throws(function () { nconf.save() });
}
},
"with a callback": {

View file

@ -16,32 +16,45 @@ var assert = require('assert'),
var fixturesDir = path.join(__dirname, 'fixtures'),
mergeFixtures = path.join(fixturesDir, 'merge'),
files = [path.join(mergeFixtures, 'file1.json'), path.join(mergeFixtures, 'file2.json')],
override = JSON.parse(fs.readFileSync(files[0]), 'utf8'),
first = '/path/to/file1',
second = '/path/to/file2';
override = JSON.parse(fs.readFileSync(files[0]), 'utf8');
vows.describe('nconf/provider').addBatch({
"When using nconf": {
"an instance of 'nconf.Provider'": {
"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) {
var old = provider.file;
assert.equal(provider.file.file, first);
provider.use('file', { file: second });
assert.equal(provider.file.file, files[0]);
provider.use('file', { file: files[1] });
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": {
"when 'useArgv' is true": helpers.assertDefaults(path.join(fixturesDir, 'scripts', 'default-override.js'))
}*/
"the default nconf provider": {
"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": {
"an instance of 'nconf.Provider'": {
"the merge() method": {
@ -49,18 +62,9 @@ vows.describe('nconf/provider').addBatch({
"should have the result merged in": function (provider) {
provider.load();
provider.merge(override);
helpers.assertMerged(null, provider);
}
},
"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);
helpers.assertMerged(null, provider.file.store);
}
}
}
}
})*/.export(module);
}).export(module);

View file

@ -9,14 +9,14 @@ var fs = require('fs'),
path = require('path'),
vows = require('vows'),
assert = require('assert'),
nconf = require('../lib/nconf'),
data = require('./fixtures/data').data,
nconf = require('../../lib/nconf'),
data = require('../fixtures/data').data,
store;
vows.describe('nconf/stores/file').addBatch({
"When using the nconf file store": {
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));
store = new nconf.stores.File({ file: filePath });
return null;
@ -33,7 +33,7 @@ vows.describe('nconf/stores/file').addBatch({
},
"When using the nconf file store": {
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 });
return null;
},
@ -49,7 +49,7 @@ vows.describe('nconf/stores/file').addBatch({
}).addBatch({
"When using the nconf file store": {
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 });
return tmpStore;
},
@ -120,7 +120,7 @@ vows.describe('nconf/stores/file').addBatch({
},
"when the target file doesn't exist higher in the directory tree": {
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)({
dir: path.dirname(filePath),
file: 'search-store.json'

View file

@ -7,8 +7,8 @@
var vows = require('vows'),
assert = require('assert'),
nconf = require('../lib/nconf'),
merge = require('./fixtures/data').merge;
nconf = require('../../lib/nconf'),
merge = require('../fixtures/data').merge;
vows.describe('nconf/stores/memory').addBatch({
"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);