diff --git a/lib/nconf.js b/lib/nconf.js index 34daed5..f00e966 100644 --- a/lib/nconf.js +++ b/lib/nconf.js @@ -7,6 +7,7 @@ var fs = require('fs'), async = require('async'), + common = require('./nconf/common'), Provider = require('./nconf/provider').Provider, nconf = module.exports = Object.create(Provider.prototype); @@ -37,40 +38,10 @@ nconf.key = function () { return Array.prototype.slice.call(arguments).join(':'); }; -// -// ### function loadFiles (files) -// #### @files {Array} List of files to load. -// Loads all the data in the specified `files`. -// -nconf.loadFiles = function (files, callback) { - if (!files) { - return callback(null, {}); - } - - var allData = {}; - - function loadFile (file, next) { - fs.readFile(file, function (err, data) { - if (err) { - return next(err); - } - - data = JSON.parse(data.toString()); - Object.keys(data).forEach(function (key) { - allData[key] = data[key]; - }); - - next(); - }); - } - - async.forEach(files, loadFile, function (err) { - return err ? callback(err) : callback(null, allData); - }); -}; - // // Expose the various components included with nconf // -nconf.stores = require('./nconf/stores'); -nconf.Provider = Provider; \ No newline at end of file +nconf.loadFiles = common.loadFiles; +nconf.formats = require('./nconf/formats'); +nconf.stores = require('./nconf/stores'); +nconf.Provider = Provider; \ No newline at end of file diff --git a/lib/nconf/common.js b/lib/nconf/common.js new file mode 100644 index 0000000..1ba183a --- /dev/null +++ b/lib/nconf/common.js @@ -0,0 +1,53 @@ +/* + * utils.js: Utility functions for the nconf module. + * + * (C) 2011, Charlie Robbins + * + */ + +var fs = require('fs'), + async = require('async'), + formats = require('./formats'), + stores = require('./stores'); + +var common = exports; + +// +// ### function loadFiles (files) +// #### @files {Object|Array} List of files (or settings object) to load. +// #### @callback {function} Continuation to respond to when complete. +// Loads all the data in the specified `files`. +// +common.loadFiles = function (files, callback) { + if (!files) { + return callback(null, {}); + } + + var options = Array.isArray(files) ? { files: files } : files, + store = new stores.Memory(); + + // + // Set the default JSON format if not already + // specified + // + options.format = options.format || formats.json; + + function loadFile (file, next) { + fs.readFile(file, function (err, data) { + if (err) { + return next(err); + } + + data = options.format.parse(data.toString()); + Object.keys(data).forEach(function (key) { + store.merge(key, data[key]); + }); + + next(); + }); + } + + async.forEach(files, loadFile, function (err) { + return err ? callback(err) : callback(null, store.store); + }); +}; \ No newline at end of file diff --git a/lib/nconf/formats.js b/lib/nconf/formats.js new file mode 100644 index 0000000..465abe3 --- /dev/null +++ b/lib/nconf/formats.js @@ -0,0 +1,28 @@ +/* + * formats.js: Default formats supported by nconf + * + * (C) 2011, Charlie Robbins + * + */ + +var ini = require('ini'); + +var formats = exports; + +// +// ### @json +// Standard JSON format which pretty prints `.stringify()`. +// +formats.json = { + stringify: function (obj) { + return JSON.stringify(obj, null, 2) + }, + parse: JSON.parse +}; + +// +// ### @ini +// Standard INI format supplied from the `ini` module +// http://en.wikipedia.org/wiki/INI_file +// +formats.ini = ini; \ No newline at end of file diff --git a/lib/nconf/provider.js b/lib/nconf/provider.js index 75480e3..b58c782 100644 --- a/lib/nconf/provider.js +++ b/lib/nconf/provider.js @@ -5,7 +5,9 @@ * */ -var optimist = require('optimist'), +var async = require('async'), + optimist = require('optimist'), + common = require('./common'), stores = require('./stores'); // @@ -62,7 +64,7 @@ Provider.prototype.get = function (key, callback) { return this.overrides[key]; } - return this.store.get(key, callback); + return this._execute('get', 1, key, callback); }; // @@ -73,19 +75,56 @@ Provider.prototype.get = function (key, callback) { // Sets the `value` for the specified `key` in this instance. // Provider.prototype.set = function (key, value, callback) { - return this.store.set(key, value, callback); + return this._execute('set', 2, key, value, callback); }; // -// ### function merge (key, value) +// ### function merge ([key,] value [, callback]) // #### @key {string} Key to merge the value into // #### @value {literal|Object} Value to merge into the key -// Merges the properties in `value` into the existing object value -// at `key`. If the existing value `key` is not an Object, it will be -// completely overwritten. +// #### @callback {function} **Optional** Continuation to respond to when complete. +// Merges the properties in `value` into the existing object value at `key`. // -Provider.prototype.merge = function (key, value, callback) { - return this.store.merge(key, value, callback); +// 1. If the existing value `key` is not an Object, it will be completely overwritten. +// 2. If `key` is not supplied, then the `value` will be merged into the root. +// +Provider.prototype.merge = function () { + var self = this, + args = Array.prototype.slice.call(arguments), + callback = typeof args[args.length - 1] === 'function' && args.pop(), + value = args.pop(), + key = args.pop(); + + function mergeProperty (prop, next) { + return self._execute('merge', 2, prop, value[prop], next); + } + + if (!key) { + if (Array.isArray(value) || typeof value !== 'object') { + return onError(new Error('Cannot merge non-Object into top-level.'), callback); + } + + return async.forEach(Object.keys(value), mergeProperty, callback || function () { }) + } + + return this._execute('merge', 2, key, value, callback); +}; + +// +// ### function mergeFiles (files, callback) +// #### @files {Object|Array} List of files (or settings object) to load. +// #### @callback {function} Continuation to respond to when complete. +// Merges all `key:value` pairs in the `files` supplied into the +// store that is managed by this provider instance. +// +Provider.prototype.mergeFiles = function (files, callback) { + var self = this; + + common.loadFiles(files, function (err, merged) { + return !err + ? self.merge(merged, callback) + : onError(err); + }); }; // @@ -95,7 +134,7 @@ Provider.prototype.merge = function (key, value, callback) { // Removes the value for the specified `key` from this instance. // Provider.prototype.clear = function (key, callback) { - return this.store.clear(key, callback); + return this._execute('clear', 1, key, callback); }; // @@ -113,16 +152,9 @@ Provider.prototype.load = function (callback) { return this.store.loadSync(); } - if (!this.store.load) { - var error = new Error('nconf store ' + this.store.type + ' has no load() method'); - if (callback) { - return callback (error); - } - - throw error; - } - - return this.store.load(callback); + return !this.store.load + ? onError(new Error('nconf store ' + this.store.type + ' has no load() method'), callback) + : this.store.load(callback); }; // @@ -147,16 +179,9 @@ Provider.prototype.save = function (value, callback) { } } - if (!this.store.save) { - var error = new Error('nconf store ' + this.store.type + ' has no save() method'); - if (callback) { - return callback (error); - } - - throw error; - } - - return this.store.save(value, callback); + return !this.store.save + ? onError(new Error('nconf store ' + this.store.type + ' has no save() method'), callback) + : this.store.save(value, callback); }; // @@ -165,9 +190,36 @@ Provider.prototype.save = function (value, callback) { // Clears all keys associated with this instance. // Provider.prototype.reset = function (callback) { - return this.store.reset(callback); + return this._execute('reset', 0, callback); }; +// +// ### @private function _execute (action, syncLength, [arguments]) +// #### @action {string} Action to execute on `this.store`. +// #### @syncLength {number} Function length of the sync version. +// #### @arguments {Array} Arguments array to apply to the action +// Executes the specified `action` on `this.store`, ensuring a callback supplied +// to a synchronous store function is still invoked. +// +Provider.prototype._execute = function (action, syncLength /* [arguments] */) { + var args = Array.prototype.slice.call(arguments, 2), + callback, + response; + + if (this.store[action].length > syncLength) { + return this.store[action].apply(this.store, args); + } + + callback = typeof args[args.length - 1] === 'function' && args.pop(); + response = this.store[action].apply(this.store, args); + + if (callback) { + callback(null, response); + } + + return response; +} + // // ### getter @useArgv {boolean} // Gets a property indicating if we should wrap calls to `.get` @@ -185,4 +237,15 @@ Provider.prototype.__defineGetter__('useArgv', function () { Provider.prototype.__defineSetter__('useArgv', function (val) { this._useArgv = val || false; this.overrides = this.overrides || optimist.argv; -}); \ No newline at end of file +}); + +// +// Throw the `err` if a callback is not supplied +// +function onError(err, callback) { + if (callback) { + return callback(err); + } + + throw err; +} \ No newline at end of file diff --git a/lib/nconf/stores/file.js b/lib/nconf/stores/file.js index 913a26e..bd12f1d 100644 --- a/lib/nconf/stores/file.js +++ b/lib/nconf/stores/file.js @@ -5,9 +5,10 @@ * */ -var fs = require('fs'), - path = require('path'), - util = require('util'), +var fs = require('fs'), + path = require('path'), + util = require('util'), + formats = require('../formats'), Memory = require('./memory').Memory; // @@ -26,12 +27,7 @@ var File = exports.File = function (options) { this.type = 'file'; this.file = options.file; this.dir = options.dir || process.cwd(); - this.format = options.format || { - stringify: function (obj) { - return JSON.stringify(obj, null, 2) - }, - parse: JSON.parse - }; + this.format = options.format || formats.json; }; // Inherit from the Memory store diff --git a/lib/nconf/stores/memory.js b/lib/nconf/stores/memory.js index 2ef3f67..94e234e 100644 --- a/lib/nconf/stores/memory.js +++ b/lib/nconf/stores/memory.js @@ -156,7 +156,7 @@ Memory.prototype.merge = function (key, value) { // // If the current value at the key target is not an `Object`, - // of is an `Array` then simply override it because the new value + // or is an `Array` then simply override it because the new value // is an Object. // if (typeof target[key] !== 'object' || Array.isArray(target[key])) { @@ -177,4 +177,4 @@ Memory.prototype.reset = function () { this.mtimes = {}; this.store = {}; return true; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/package.json b/package.json index 7a7c523..1794e31 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "keywords": ["configuration", "key value store", "plugabble"], "dependencies": { "async": "0.1.x", + "ini": "1.x.x", "optimist": "0.2.x", "pkginfo": "0.2.x" }, diff --git a/test/fixtures/merge/file1.json b/test/fixtures/merge/file1.json new file mode 100644 index 0000000..b5bf573 --- /dev/null +++ b/test/fixtures/merge/file1.json @@ -0,0 +1,8 @@ +{ + "apples": true, + "bananas": true, + "candy": { + "something1": true, + "something2": true + } +} \ No newline at end of file diff --git a/test/fixtures/merge/file2.json b/test/fixtures/merge/file2.json new file mode 100644 index 0000000..1214bf1 --- /dev/null +++ b/test/fixtures/merge/file2.json @@ -0,0 +1,8 @@ +{ + "candy": { + "something3": true, + "something4": true + }, + "dates": true, + "elderberries": true +} \ No newline at end of file diff --git a/test/provider-test.js b/test/provider-test.js index 4c32c51..3f85837 100644 --- a/test/provider-test.js +++ b/test/provider-test.js @@ -10,9 +10,12 @@ var assert = require('assert'), path = require('path'), spawn = require('child_process').spawn, vows = require('vows'), - nconf = require('../lib/nconf') - -var first = '/path/to/file1', + nconf = require('../lib/nconf'); + +var mergeFixtures = path.join(__dirname, 'fixtures', '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'; function assertDefaults (script) { @@ -27,6 +30,18 @@ function assertDefaults (script) { } } +function assertMerged (provider) { + var store = provider.store.store; + assert.isTrue(store.apples); + assert.isTrue(store.bananas); + assert.isTrue(store.candy.something1); + assert.isTrue(store.candy.something2); + assert.isTrue(store.candy.something3); + assert.isTrue(store.candy.something4); + assert.isTrue(store.dates); + assert.isTrue(store.elderberries); +} + vows.describe('nconf/provider').addBatch({ "When using nconf": { "an instance of 'nconf.Provider'": { @@ -48,4 +63,26 @@ vows.describe('nconf/provider').addBatch({ "when 'useArgv' is true": assertDefaults(path.join(__dirname, 'fixtures', 'scripts', 'default-override.js')) } } +}).addBatch({ + "When using nconf": { + "an instance of 'nconf.Provider'": { + "the merge() method": { + topic: new nconf.Provider().use('file', { file: files[1] }), + "should have the result merged in": function (provider) { + provider.load(); + provider.merge(override); + assertMerged(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) { + assertMerged(provider); + } + } + } + } }).export(module); \ No newline at end of file