From d0d003284bd1bf837253fe83d41fa1ad40b32ac9 Mon Sep 17 00:00:00 2001 From: doug-martin Date: Wed, 11 Apr 2012 20:57:39 -0500 Subject: [PATCH] v0.0.1 --- README => LICENSE | 0 README.md | 399 +++++++++++++ index.js | 1 + lib/index.js | 370 ++++++++++++ package.json | 24 + test/configs/config-env/config.json | 41 ++ test/configs/configs1/config1.json | 13 + test/configs/configs2/config2.json | 10 + test/gofigure.test.js | 849 ++++++++++++++++++++++++++++ 9 files changed, 1707 insertions(+) rename README => LICENSE (100%) create mode 100644 README.md create mode 100644 index.js create mode 100644 lib/index.js create mode 100644 package.json create mode 100644 test/configs/config-env/config.json create mode 100644 test/configs/configs1/config1.json create mode 100644 test/configs/configs2/config2.json create mode 100644 test/gofigure.test.js diff --git a/README b/LICENSE similarity index 100% rename from README rename to LICENSE diff --git a/README.md b/README.md new file mode 100644 index 0000000..3efcea8 --- /dev/null +++ b/README.md @@ -0,0 +1,399 @@ +#gofigure + +Gofigure is a configuration tool for node to help in the gathering and monitoring of configuration files in node. + +# Installation + + npm install gofigure + +#Usage + + + * [Loading A Configuration](#load) + * [Directories](#loadDir) + * [Files](#loadFiles) + * [Monitoring Property Changes](#monitoring) + * [Monitoring All Files](#monitoringAll) + * [Monitoring Certain Files](#monitoringSome) + * [Property Topic Syntax](#monitoringSyntax) + * [Property Change Callback](#monitoringCB) + * [Environments](#environments) + + +##Loading configurations + +Gofigure currently handles the loading of JSON files for configurations. + +To Get an instance of a configuration object use the `gofigure` method. The `gofigure` method takes an object that accepts the following options + + * [locations](#loadDir) : an array of directories that contain your configurations. + * [files](#loadFiles) : an array of files that contain your configurations. + * [monitor](#monitoring) : set to true to monitor changes to configuration files. + * `ignoreMissing` : By default `gofigure` will ignore missing directories. Set this to false to precent the ignoring of missing configuration directories. + * [environment](#environments) : By default will look for `process.env.NODE_ENV` if this is not set then gofigure will read all properties. If you wish to explicity set the environment then set this property. + +```javascript + +var gofigure = require("gofigure"); + +//Loader for directory of configurations +var dirLoader = gofigure({ + locations : [__dirname + "/configs"] +}); + + +//Loader for files of configurations +var dirLoader = gofigure({ + files : [process.env.HOME + "/configs/config1.json", __dirname + "/config1.json"] +}); + + +``` + +You can load configurations asynchronously + +```javascript +loader.load(function(err, config){ + var PORT = config.port, HOST = config.host; +}); +``` + +or synchronously + +```javascript +var gofigure = require("gofigure"); + +var loader = gofigure({locations : [__dirname + "/configs"]}); +var config = loader.loadSync(); +``` + + +###Directories of configurations +To load directories that contain configuration files in the options object provide locations property that is an array of directories than contain your configurations. + +```javascript + +var gofigure = require("gofigure"); + +var loader = gofigure({locations : [__dirname + "/configs"]}); +loader.load(function(err, config){ + var PORT = config.port, HOST = config.host; +}); +``` + +The order of the locations matter as it defines a precedence for files. For example suppose you have a directory of default configuration files, and on production you want to override those configuration with environment specific configurations with out changing your module or source controlled files. + +```javascript +var gofigure = require("gofigure"); + +var loader = gofigure({locations : ["/prod/configs", __dirname + "/configs"]}); +loader.load(function(err, config){ + var PORT = config.port, HOST = config.host; +}); +``` + +Here any production configuration files found in `/prod/configs` will override the properties in `__dirname + "/configs"`. + +Another use case might be in development where you have default properties and instead of altering the source controlled files the developer can override them by putting them in their home directory. + +```javascript +var gofigure = require("gofigure"); +var HOME = process.env.HOME; + +var loader = gofigure({locations : [ HOME + "/yourApp/configs", __dirname + "/configs"]}); +loader.load(function(err, config){ + var PORT = config.port, HOST = config.host; +}); +``` + + +###Files + +You may also load specific files rather than entire directories. + +```javascript +var gofigure = require("gofigure"); + +var loader = gofigure({files : ["/prod/configs/config1.json", __dirname + "/config.json"]}); +loader.load(function(err, config){ + var PORT = config.port, HOST = config.host; +}); +``` + +Again order matters `/prod/configs/config1.json` will override `__dirname + "/config.json"` + + +##Monitoring + +Gofigure supports the monitoring of changes to configuration files. + + +###All files + +To enable monitoring you can specify monitor to true in the options. + +```javascript +var gofigure = require("gofigure"); + +var loader = gofigure({monitor : true, files : ["/prod/configs/config1.json", __dirname + "/config.json"]}); +var config = loader.loadSync(); + +loading.on("my.cool.property", function(newValue){ + //...do something +}); +``` + +###Individual Files + +To monitor certain files you can use the files property and with object that have a `monitor : true` KV pair. + +```javascript +var gofigure = require("gofigure"); + +var loader = gofigure({files : [ + { + file : "/prod/configs/config1.json", + monitor : true + + }, + __dirname + "/config.json" +]}); +var config = loader.loadSync(); + +loading.on("my.cool.property", function(newValue){ + //...do something +}); +``` +Just `config1.json` will be monitored for changes. + + +###Property topic syntax + +To listen to all properties + +```javascript +loading.on(function(config){ + //...do something +}); + +loading.on(function(nameOfPropertyChanged, config){ + //...do something +}); + +loading.on(function(nameOfPropertyChanged, value, config){ + //...do something +}); +``` + +To listen to specific properties + +```javascript +loading.on("my.cool.property", function(newValue){ + //...do something +}); + +loading.on("my.cool.property", function(newValue, config){ + //...do something +}); + +loading.on("my.cool.property", function(nameOfPropertyChanged, value, config){ + //...do something +}); +``` + +Wild cards + +```javascript + +//listen to any property changed on the my.cool object +loading.on("my.cool.*", function(propName, newValue){ + //...do something +}); + + +//listen to the change of a property named 'property' on any object +//that is a member of my +loading.on("my.*.property", function(propName, newValue){ + //...do something +}); + +//listen to the change of a property named 'property' that is +//a member of a property called cool +loading.on("*.cool.property", function(propName, newValue){ + //...do something +}); + +//listen to the change of property or otherProperty on the my.cool object. +loading.on("my.cool.{property|otherProperty}", function(propName, newValue){ + //...do something +}); + +//listen to the change of property or otherProperty on the my cool or +//notCool object. +loading.on("my.{cool|notCool}.{property|otherProperty}", function(propName, newValue){ + //...do something +}); +``` + + +###Callback Arguments + + +The property change callback will pass in the following values depending on the arity of the callback. + +If 1 argument is expected then just the callback invoked with the new value is a. + +```javascript + +loading.on("my.cool.property", function(newValue){ + //...do something +}); + + +``` + +If two arguments are expected then it is invoked with the property name and the new value. + + +```javascript + +loading.on("my.cool.property", function(propName, newValue){ + //...do something +}); + + +``` + +Other wise the callback is invoked with the propertyName, newValue and the configuration object. + +```javascript + +loading.on("my.cool.property", function(propName, newValue, configObject){ + //...do something +}); + + +``` + + +##Environments + +`gofigure` also supports environments, by default it will look for `NODE_ENV` and if it is set then it will use it. + +The following is an example configuration file + +```javascript + +{ + "development": { + "logging":{ + "patio":{ + "level":"DEBUG", + "appenders":[ + { + "type":"RollingFileAppender", + "file":"/var/log/myApp/patio.log" + }, + { + "type":"ConsoleAppender" + } + ] + } + }, + "app" : { + "host" : "localhost", + "port" : "8088" + }, + "MYSQL_DB" : "mysql://test:testpass@localhost:3306/dev_db", + "MONGO_DB" : "mongodb://test:testpass@localhost:27017/dev_db" + }, + "production": { + "logging":{ + "patio":{ + "level":"ERROR", + "appenders":[ + { + "type":"RollingFileAppender", + "file":"/var/log/myApp/patio.log" + } + ] + } + }, + "app" : { + "host" : "prod.mydomain.com", + "port" : "80" + }, + "MYSQL_DB" : "mysql://test:testpass@prod.mydomain.com:3306/prod_db", + "MONGO_DB" : "mongodb://test:testpass@prod.mydomain.com:27017/prd_db" + }, + "test": { + "logging":{ + "patio":{ + "level":"INFO", + "appenders":[ + { + "type":"RollingFileAppender", + "file":"/var/log/myApp/patio.log" + } + ] + } + }, + "app" : { + "host" : "test.mydomain.com", + "port" : "80" + }, + "MYSQL_DB" : "mysql://test:testpass@test.mydomain.com:3306/test_db", + "MONGO_DB" : "mongodb://test:testpass@test.mydomain.com:27017/test_db" + } +} + +``` + +To load just the development properties set the `environment` to development. + +```javascript + +var gofigure = require("gofigure"), + patio = require("patio"), + mongoose = require("mongoose"), + comb = require("comb"), + DB, HOST, PORT; + + +var loader = gofigure({ + files : [__dirname + "/config-env.json"], + environment : "development" +}) + .on("MYSQL_DB", function(uri){ + patio.connect(uri); + }) + .on("MONGO_DB", function(uri){ + mongoose.connect(uri); + }) + .on("logging", function(logging){ + new comb.logging.PropertyConfigurator().configure(logging); + patio.configureLogging(logging); + }) + .on("app", function(app){ + //... + }) + .load(function(){ + //do something + }) + +``` + +License +------- + +MIT + +Meta +---- + +* Code: `git clone git://github.com/pollenware/gofigure.git` +* Website: - Twitter: - 877.465.4045 + + + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..79b36b7 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = exports = require("./lib/index.js"); \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..a66aaaa --- /dev/null +++ b/lib/index.js @@ -0,0 +1,370 @@ +"use strict"; +var comb = require("comb"), + toArray = comb.array.toArray, + hitch = comb.hitch, + partial = comb.partial, + path = require("path"), + fs = require("fs"); + + +var cwd = process.cwd(); +var readFiles = function (files, dir) { + var configs = {}; + files.forEach(function (f) { + try { + var p = path.resolve(dir || cwd, f); + configs[p] = require(p); + } catch (e) { + console.error("Error loading " + f); + console.error(e.stack); + } + }); + return configs; +}; +var getConfigs = function (dir, sync) { + if (sync) { + return readFiles(fs.readdirSync(dir), dir); + } else { + var ret = new comb.Promise(); + fs.readdir(dir, function (err, files) { + if (!err) { + ret.callback(readFiles(files, dir)); + } else { + ret.errback(err); + } + }); + return ret; + } + +}; + +var watchedFiles = {}; +var watchFiles = function (files, cb) { + files.forEach(function (file) { + if (!watchedFiles[file]) { + var listeners = (watchedFiles[file] = []); + fs.watch(file, {}, function (event, filename) { + if (event == "change") { + delete require.cache[file]; + process.nextTick(function () { + listeners.forEach(function (cb) { + process.nextTick(partial(cb, file)); + }); + }); + } + }); + } else { + watchedFiles[file].push(cb); + } + }) +}; + +var Config = comb.define(null, { + instance:{ + + loaded:null, + + locations:null, + + files:null, + + monitor:false, + + environment:process.env.NODE_ENV || null, + + config:null, + + ignoreMissing:true, + + constructor:function (locations, files, opts) { + this.loaded = []; + opts = opts || {}; + this.config = {}; + this.__listeners = {}; + if (comb.isHash(files)) { + opts = files; + files = null; + } + if (locations) { + this.locations = toArray(locations || []).reverse(); + } + if (files) { + this.files = toArray(files || []).reverse(); + } + for (var i in opts) { + this[i] = opts[i]; + } + }, + + _monitor:function (f, force) { + f = f || this.loaded; + if (this.monitor || force) { + watchFiles(f, hitch(this, function (file) { + var config = readFiles(f), newC = {}; + for (var i in config) { + var c = config[i]; + if (this.environment) { + c = c[this.environment] || {}; + } + comb.deepMerge(newC, c); + } + this.__merge("", newC, this.config); + })); + } + }, + + __merge:function (path, o1, o2) { + //check for deleted values + var o1Keys = Object.keys(o1), o2Keys = Object.keys(o2); + o1Keys.sort(); + o2Keys.sort(); + if (!comb.deepEqual(o1Keys, o2Keys)) { + o2Keys.forEach(function (k) { + if (o1Keys.indexOf(k) == -1) { + var newPath = path ? [path, k].join(".") : k; + delete o2[k]; + this.emit(newPath, undefined); + } + }, this); + } + o1Keys.forEach(function (j) { + var fVal = o1[j], tVal = o2[j]; + if (!comb.deepEqual(fVal, tVal)) { + var newPath = path ? [path, j].join(".") : j; + if (comb.isHash(fVal)) { + if (comb.isHash(tVal)) { + this.__merge(newPath, fVal, tVal); + } else if (comb.isUndefinedOrNull(tVal)) { + var tVal = (o2[j] = {}); + this.__merge(newPath, fVal, tVal); + } else { + o2[j] = fVal; + } + } else { + o2[j] = fVal; + } + this.emit(newPath, fVal); + } else { + o2[j] = o1[j]; + } + }, this); + return this; + }, + + + emit:function (name, value) { + var listeners = this.__listeners; + for (var i in listeners) { + var listener = listeners[i] + if (listener.match.test(name)) { + var lls = listener.listeners; + if (lls.length) { + //do this incase they remove a cb while calling listeners + for (var i in lls) { + (function (c) { + process.nextTick(function () { + c(name, value) + }); + })(lls[i]) + } + } + } + } + }, + + __createMatcher:function (topic) { + return new RegExp(["^", + topic.replace(".", "\\.") + .replace(/\*/g, ".+") + .replace(/\{((?:\w+\|?)*)\}/ig, function (str, match) { + return "(?:" + match + ")"; + }), "$"].join("")); + }, + + __mergeConfigs:function (configs) { + var config = this.config, env = this.environment, newConfig = {}; + for (var i in configs) { + this.loaded.push(i); + var c = configs[i]; + if (env) { + c = c[env] || {}; + } + comb.deepMerge(newConfig, c); + } + this.__merge("", newConfig, this.config); + return config; + }, + + __mergeFiles:function () { + var monitor = [], isHash = false, files = this.files.map(function (f) { + if (comb.isHash(f)) { + isHash = true; + if (comb.isBoolean(f.monitor)) { + monitor.push(f.file); + } + return f.file; + + } else { + return f; + } + }); + var ret = this.__mergeConfigs(readFiles(files)); + this._monitor(isHash ? monitor : null, true); + return ret; + }, + + removeListener:function (prop, cb) { + if (comb.isFunction(prop)) { + cb = prop; + prop = ""; + } + var listeners = this.__listeners; + for (var i in listeners) { + var listener = listeners[i] + if (listener.match.test(prop)) { + var lls = listener.listeners; + var l = lls.length - 1; + for (; l >= 0; l--) { + if (lls[l].__cb === cb) { + lls.splice(l, 1); + break; + } + } + } + } + }, + + removeAllListeners:function (prop) { + var listeners = this.__listeners; + for (var i in listeners) { + var listener = listeners[i] + if (listener.match.test(prop)) { + listener.listeners.length = 0; + } + } + }, + + addListener:function (prop, cb) { + if (comb.isFunction(prop)) { + cb = prop; + prop = "*"; + } + var listener = this.__listeners[prop], listeners; + if (!listener) { + listener = (this.__listeners[prop] = {match:this.__createMatcher(prop)}); + listeners = (listener.listeners = []); + } else { + listeners = listener.listeners; + } + var l = cb.length; + var wrapped = function (name, value) { + if (l) { + var tConfig = comb.deepMerge({}, this.config); + if (l == 1) { + if (prop === "*") { + cb(this.config); + } else { + cb(value); + } + } else if (l == 2) { + if (prop == "*") { + cb(name, tConfig); + } else { + cb(name, value); + } + } else { + cb(name, value, tConfig) + } + } else { + cb(); + } + }.bind(this); + wrapped.__cb = cb; + listeners.push(wrapped); + return this; + }, + + on:function () { + return this.addListener.apply(this, arguments); + }, + + once:function (prop, cb) { + if (comb.isFunction(prop)) { + cb = prop; + prop = "*"; + } + //have to keep reference to self to maintain length; + var self = this; + return this.addListener(prop, function wrapped(a, b, c) { + cb.apply(self, arguments); + self.removeListener(prop, wrapped); + }); + }, + + load:function (cb) { + var ret = new comb.Promise(), + config = this.config; + if (this.locations) { + var loaded = this.loaded, + locations = this.locations, + l = locations.length, ignore = this.ignoreMissing, + monitor = this._monitor.bind(this), + mergeConfigs = this.__mergeConfigs.bind(this), + files = {}; + (function _load(index) { + if (index >= l) { + mergeConfigs(files); + monitor(); + ret.callback(config); + } else { + var dir = locations[index]; + getConfigs(dir, false).then(hitch(this, function (configs) { + comb.merge(files, configs); + _load(++index) + }), function (err) { + if (ignore) { + _load(++index) + } else { + console.warn(err.stack); + ret.errback(err); + } + }); + } + }.bind(this))(0); + } else if (this.files) { + ret.callback(this.__mergeFiles()); + } else { + ret.callback({}); + } + return ret.classic(cb); + }, + + loadSync:function () { + if (this.locations) { + var files = {}; + this.locations.forEach(function (dir) { + try { + comb.merge(files, getConfigs(dir, true)) + } catch (e) { + if (!this.ignoreMissing) { + console.warn(e.stack); + throw e; + } + } + }, this); + this.__mergeConfigs(files); + this._monitor(); + } else if (this.files) { + this.__mergeFiles(); + } + return this.config; + } + } +}); + +module.exports = exports = function (options) { + var files = options.files, locations = options.locations; + delete options.files; + delete options.locations; + return new Config(locations, files, options); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..3b32c9d --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name":"gofigure", + "description":"Configuration helper for node", + "version":"0.0.1", + "keywords":["Config", "Configuration", "Configurations", "JSON", "Properties", "Property"], + "repository":{ + "type":"git", + "url":"git@github.com:Pollen/gofigure.git" + }, + "dependencies":{ + "comb":">=0.1.1" + }, + "devVependencies":{ + "it":">=0.0.1" + }, + "author":"Doug Martin (doug.martin@pollenware.com)", + "main":"index.js", + "directories":{ + "lib":"lib" + }, + "engines":{ + "node":">= 0.6.1" + } +} diff --git a/test/configs/config-env/config.json b/test/configs/config-env/config.json new file mode 100644 index 0000000..4ae6bd3 --- /dev/null +++ b/test/configs/config-env/config.json @@ -0,0 +1,41 @@ +{ + "development": { + "a": 1, + "b": { + "c": 2, + "d": 3 + }, + "e": { + "f": 4, + "g": { + "h": 5 + } + } + }, + "production": { + "a": 6, + "b": { + "c": 7, + "d": 8 + }, + "e": { + "f": 9, + "g": { + "h": 10 + } + } + }, + "test": { + "a": 11, + "b": { + "c": 12, + "d": 13 + }, + "e": { + "f": 14, + "g": { + "h": 15 + } + } + } +} \ No newline at end of file diff --git a/test/configs/configs1/config1.json b/test/configs/configs1/config1.json new file mode 100644 index 0000000..22daadd --- /dev/null +++ b/test/configs/configs1/config1.json @@ -0,0 +1,13 @@ +{ + "a": 1, + "b": { + "c": 1, + "d": 2 + }, + "e": { + "f": 3, + "g": { + "h": 4 + } + } +} \ No newline at end of file diff --git a/test/configs/configs2/config2.json b/test/configs/configs2/config2.json new file mode 100644 index 0000000..6e5a8fb --- /dev/null +++ b/test/configs/configs2/config2.json @@ -0,0 +1,10 @@ +{ + "b": { + "c": 2 + }, + "i": { + "j": 5, + "k": 6 + }, + "l": 7 +} \ No newline at end of file diff --git a/test/gofigure.test.js b/test/gofigure.test.js new file mode 100644 index 0000000..fa97e97 --- /dev/null +++ b/test/gofigure.test.js @@ -0,0 +1,849 @@ +var it = require("it"), + assert = require("assert"), + comb = require("comb"), + fs = require("fs"), + goFigure = require("../index.js"); + +var conf1 = { + "a":1, + "b":{ + "c":1, + "d":2 + }, + "e":{ + "f":3, + "g":{ + "h":4 + } + } +}; + +var conf2 = { + "b":{ + "c":2 + }, + "i":{ + "j":5, + "k":6 + }, + "l":7 +}; + +var envConf = { + "development":{ + "a":1, + "b":{ + "c":2, + "d":3 + }, + "e":{ + "f":4, + "g":{ + "h":5 + } + } + }, + "production":{ + "a":6, + "b":{ + "c":7, + "d":8 + }, + "e":{ + "f":9, + "g":{ + "h":10 + } + } + }, + "test":{ + "a":11, + "b":{ + "c":12, + "d":13 + }, + "e":{ + "f":14, + "g":{ + "h":15 + } + } + } +}; + +it.describe("GoFigure", function (it) { + + it.beforeEach(function () { + fs.writeFileSync(__dirname + "/configs/configs1/config1.json", JSON.stringify(conf1, null, 4)); + fs.writeFileSync(__dirname + "/configs/configs2/config2.json", JSON.stringify(conf2, null, 4)); + fs.writeFileSync(__dirname + "/configs/config-env/config.json", JSON.stringify(envConf, null, 4)); + }); + + it.afterEach(function () { + fs.writeFileSync(__dirname + "/configs/configs1/config1.json", JSON.stringify(conf1, null, 4)); + fs.writeFileSync(__dirname + "/configs/configs2/config2.json", JSON.stringify(conf2, null, 4)); + fs.writeFileSync(__dirname + "/configs/config-env/config.json", JSON.stringify(envConf, null, 4)); + }); + + it.describe("#load", function (it) { + it.should("load configuration from directories", function (next) { + var config1 = goFigure({locations:[__dirname + "/configs/configs1"]}); + var config2 = goFigure({locations:[__dirname + "/configs/configs2"]}); + config1.load( + function (err, res) { + assert.deepEqual(res, conf1); + }).then(function () { + config2.load( + function (err, res) { + if (!err) { + assert.deepEqual(res, conf2); + next(null); + } + }).addErrback(next); + }, next); + }); + + it.should("load configuration from files", function (next) { + var config1 = goFigure({files:[__dirname + "/configs/configs1/config1.json"]}); + var config2 = goFigure({files:[__dirname + "/configs/configs2/config2.json"]}); + config1.load( + function (err, res) { + assert.deepEqual(res, conf1); + }).then(function () { + config2.load( + function (err, res) { + assert.deepEqual(res, conf2); + next(null); + }).addErrback(next); + }, next); + }); + + it.describe("with an env", function (it) { + it.should("from directories", function (next) { + var configDev = goFigure({environment:"development", locations:[__dirname + "/configs/config-env"]}); + var configProd = goFigure({environment:"production", locations:[__dirname + "/configs/config-env"]}); + var configTest = goFigure({environment:"test", locations:[__dirname + "/configs/config-env"]}); + configDev.load( + function (err, res) { + assert.deepEqual(res, envConf.development); + }).then(function () { + configProd.load( + function (err, res) { + if (!err) { + assert.deepEqual(res, envConf.production); + next(null); + } + }).then(function () { + configTest.load( + function (err, res) { + if (!err) { + assert.deepEqual(res, envConf.test); + next(null); + } + }).addErrback(next); + }, next); + }, next); + }); + + it.should("from files", function (next) { + var configDev = goFigure({environment:"development", files:[__dirname + "/configs/config-env/config.json"]}); + var configProd = goFigure({environment:"production", files:[__dirname + "/configs/config-env/config.json"]}); + var configTest = goFigure({environment:"test", files:[__dirname + "/configs/config-env/config.json"]}); + configDev.load( + function (err, res) { + assert.deepEqual(res, envConf.development); + }).then(function () { + configProd.load( + function (err, res) { + if (!err) { + assert.deepEqual(res, envConf.production); + next(null); + } + }).then(function () { + configTest.load( + function (err, res) { + if (!err) { + assert.deepEqual(res, envConf.test); + next(null); + } + }).addErrback(next); + }, next); + }, next); + }); + }); + }); + + it.describe("#loadSync", function (it) { + it.should("load configuration from certain directories", function () { + var config1 = goFigure({locations:[__dirname + "/configs/configs1"]}); + var config2 = goFigure({locations:[__dirname + "/configs/configs2"]}); + assert.deepEqual(config1.loadSync(), conf1); + assert.deepEqual(config2.loadSync(), conf2); + }); + + it.should("load configuration from certain directories", function () { + var config1 = goFigure({files:[__dirname + "/configs/configs1/config1.json"]}); + var config2 = goFigure({files:[__dirname + "/configs/configs2/config2.json"]}); + assert.deepEqual(config1.loadSync(), conf1); + assert.deepEqual(config2.loadSync(), conf2); + }); + + it.should("load should call listeners when setting", function (next) { + var config1 = goFigure({files:[__dirname + "/configs/configs1/config1.json"]}); + var res = []; + ["a", "b", "b.c", "b.f", "e", "e.f", "e.g", "e.g.h"].forEach(function (topic) { + config1.on(topic, function (k, v) { + res.push([topic, k, v]); + }); + }); + assert.deepEqual(config1.loadSync(), conf1); + process.nextTick(function () { + assert.deepEqual(res, [ + ["a", "a", 1], + ["b.c", "b.c", 1], + ["b", "b", {"c":1, "d":2}], + ["e.f", "e.f", 3], + ["e.g.h", "e.g.h", 4], + ["e.g", "e.g", {"h":4}], + ["e", "e", {"f":3, "g":{"h":4}}] + ]); + next(); + }); + }); + + it.describe("with an env", function (it) { + it.should("from directories", function () { + var configDev = goFigure({environment:"development", locations:[__dirname + "/configs/config-env"]}); + var configProd = goFigure({environment:"production", locations:[__dirname + "/configs/config-env"]}); + var configTest = goFigure({environment:"test", locations:[__dirname + "/configs/config-env"]}); + assert.deepEqual(configDev.loadSync(), envConf.development); + assert.deepEqual(configProd.loadSync(), envConf.production); + assert.deepEqual(configTest.loadSync(), envConf.test); + }); + + it.should("from files", function () { + var configDev = goFigure({environment:"development", files:[__dirname + "/configs/config-env/config.json"]}); + var configProd = goFigure({environment:"production", files:[__dirname + "/configs/config-env/config.json"]}); + var configTest = goFigure({environment:"test", files:[__dirname + "/configs/config-env/config.json"]}); + assert.deepEqual(configDev.loadSync(), envConf.development); + assert.deepEqual(configProd.loadSync(), envConf.production); + assert.deepEqual(configTest.loadSync(), envConf.test); + }); + }); + }); + + it.describe("#on", function (it) { + it.should("monitor configurations in certain directories", function (next) { + var config1 = goFigure({monitor:true, locations:[__dirname + "/configs/configs1"]}); + config1.loadSync(); + var res = [], called = 0; + ["a", "b.{c|d}", "e", "e.g", "e.g.*"].forEach(function(topic){ + config1.on(topic, function (key, val, config) { + called++; + res.push({key:key, val:comb.isObject(val) ? comb.deepMerge({}, val) : val}); + }); + }) + var changes = [ + [comb.deepMerge({}, conf1, {a:4}), 1], + [comb.deepMerge({}, conf1, {b:{c:5}}), 2], + [comb.deepMerge({}, conf1, {b:{d:7}}), 2], + [comb.deepMerge({}, conf1, {e:{f:8}}), 2], + [comb.deepMerge({}, conf1, {e:{g:{h:9}}}), 3] + ]; + var l = changes.length; + (function _next(index) { + if (index < l) { + fs.writeFile(__dirname + "/configs/configs1/config1.json", JSON.stringify(changes[index][0], null, 4), function (err, res) { + if (err) { + next(err); + } else { + (function to() { + process.nextTick(function () { + if (called >= changes[index][1]) { + called = 0; + _next(++index); + } else { + to(); + } + }); + })(); + } + }); + } else { + assert.deepEqual(res, [ + {key:"a", val:4}, + {key:"a", val:1}, + {key:"b.c", val:5}, + {key:"b.c", val:1}, + {key:"b.d", val:7}, + {key:"b.d", val:2}, + {key:"e", val:{f:8, g:{h:4}}}, + {key:"e.g.h", val:9}, + {key:"e.g", val:{h:9}}, + {key:"e", val:{f:3, g:{h:9}}} + ]); + next(); + } + })(0); + + + }); + + it.should("monitor configurations of files", function (next) { + var config1 = goFigure({monitor:true, files:[__dirname + "/configs/configs1/config1.json"]}); + config1.loadSync(); + var res = [], called = 0; + ["a", "b.{c|d}", "e", "e.g", "e.g.*"].forEach(function(topic){ + config1.on(topic, function (key, val, config) { + called++; + res.push({key:key, val:comb.isObject(val) ? comb.deepMerge({}, val) : val}); + }); + }) + var changes = [ + [comb.deepMerge({}, conf1, {a:4}), 1], + [comb.deepMerge({}, conf1, {b:{c:5}}), 2], + [comb.deepMerge({}, conf1, {b:{d:7}}), 2], + [comb.deepMerge({}, conf1, {e:{f:8}}), 2], + [comb.deepMerge({}, conf1, {e:{g:{h:9}}}), 3] + ]; + var l = changes.length; + (function _next(index) { + if (index < l) { + fs.writeFile(__dirname + "/configs/configs1/config1.json", JSON.stringify(changes[index][0], null, 4), function (err, res) { + if (err) { + next(err); + } else { + (function to() { + process.nextTick(function () { + if (called >= changes[index][1]) { + called = 0; + _next(++index); + } else { + to(); + } + }); + })(); + } + }); + } else { + assert.deepEqual(res, [ + {key:"a", val:4}, + {key:"a", val:1}, + {key:"b.c", val:5}, + {key:"b.c", val:1}, + {key:"b.d", val:7}, + {key:"b.d", val:2}, + {key:"e", val:{f:8, g:{h:4}}}, + {key:"e.g.h", val:9}, + {key:"e.g", val:{h:9}}, + {key:"e", val:{f:3, g:{h:9}}} + ]); + next(); + } + })(0); + + + }); + + it.should("monitor configurations of certain files", function (next) { + var config1 = goFigure({monitor:false, files:[ + {monitor:true, file:__dirname + "/configs/configs1/config1.json"} + ]}); + config1.loadSync(); + var res = [], called = 0; + ["a", "b.{c|d}", "e", "e.g", "e.g.*"].forEach(function(topic){ + config1.on(topic, function (key, val, config) { + called++; + res.push({key:key, val:comb.isObject(val) ? comb.deepMerge({}, val) : val}); + }); + }) + var changes = [ + [comb.deepMerge({}, conf1, {a:4}), 1], + [comb.deepMerge({}, conf1, {b:{c:5}}), 2], + [comb.deepMerge({}, conf1, {b:{d:7}}), 2], + [comb.deepMerge({}, conf1, {e:{f:8}}), 2], + [comb.deepMerge({}, conf1, {e:{g:{h:9}}}), 3] + ]; + var l = changes.length; + (function _next(index) { + if (index < l) { + fs.writeFile(__dirname + "/configs/configs1/config1.json", JSON.stringify(changes[index][0], null, 4), function (err, res) { + if (err) { + next(err); + } else { + (function to() { + process.nextTick(function () { + if (called >= changes[index][1]) { + called = 0; + _next(++index); + } else { + to(); + } + }); + })(); + } + }); + } else { + assert.deepEqual(res, [ + {key:"a", val:4}, + {key:"a", val:1}, + {key:"b.c", val:5}, + {key:"b.c", val:1}, + {key:"b.d", val:7}, + {key:"b.d", val:2}, + {key:"e", val:{f:8, g:{h:4}}}, + {key:"e.g.h", val:9}, + {key:"e.g", val:{h:9}}, + {key:"e", val:{f:3, g:{h:9}}} + ]); + next(); + } + })(0); + + + }); + + it.describe("with an env", function (it) { + it.should("monitor configurations of files", function (next) { + var res = [], called = 0; + ["development", "production", "test"].forEach(function (env) { + var config = goFigure({monitor:true, environment:env, files:[__dirname + "/configs/config-env/config.json"]}); + config.loadSync(); + ["a", "b.{c|d}", "e", "e.g", "e.g.*"].forEach(function(topic){ + config.on(topic, function (key, val, config) { + called++; + res.push({env : env, key:key, val:comb.isObject(val) ? comb.deepMerge({}, val) : val}); + }); + }) + }); + var changes = [ + [comb.deepMerge({}, envConf, {development:{a:6}, production:{a:11}, test:{a:16}}), 3], + [comb.deepMerge({}, envConf, {development:{b:{c:7}}, production:{b:{c:12}}, test:{b:{c:17}}}), 6], + [comb.deepMerge({}, envConf, {development:{b:{d:8}}, production:{b:{d:13}}, test:{b:{d:18}}}), 2], + [comb.deepMerge({}, envConf, {development:{e:{f:9}}, production:{e:{f:14}}, test:{e:{f:19}}}), 2], + [comb.deepMerge({}, envConf, {development:{e:{g:{h:10}}}, production:{e:{g:{h:15}}}, test:{e:{g:{h:20}}}}), 3] + ]; + var l = changes.length; + (function _next(index) { + if (index < l) { + fs.writeFile(__dirname + "/configs/config-env/config.json", JSON.stringify(changes[index][0], null, 4), function (err, res) { + if (err) { + next(err); + } else { + (function to() { + process.nextTick(function () { + if (called >= changes[index][1]) { + called = 0; + _next(++index); + } else { + to(); + } + }); + })(); + } + }); + } else { + assert.deepEqual(res, [ + {"env":"development", "key":"a", "val":6}, + {"env":"production", "key":"a", "val":11}, + {"env":"test", "key":"a", "val":16}, + {"env":"development", "key":"a", "val":1}, + {"env":"development", "key":"b.c", "val":7}, + {"env":"production", "key":"a", "val":6}, + {"env":"production", "key":"b.c", "val":12}, + {"env":"test", "key":"a", "val":11}, + {"env":"test", "key":"b.c", "val":17}, + {"env":"development", "key":"b.c", "val":2}, + {"env":"development", "key":"b.d", "val":8}, + {"env":"production", "key":"b.c", "val":7}, + {"env":"production", "key":"b.d", "val":13}, + {"env":"test", "key":"b.c", "val":12}, + {"env":"test", "key":"b.d", "val":18}, + {"env":"development", "key":"b.d", "val":3}, + {"env":"development", "key":"e", "val":{"f":9, "g":{"h":5}}}, + {"env":"production", "key":"b.d", "val":8}, + {"env":"production", "key":"e", "val":{"f":14, "g":{"h":10}}}, + {"env":"test", "key":"b.d", "val":13}, + {"env":"test", "key":"e", "val":{"f":19, "g":{"h":15}}}, + {"env":"development", "key":"e.g.h", "val":10}, + {"env":"development", "key":"e.g", "val":{"h":10}}, + {"env":"development", "key":"e", "val":{"f":4, "g":{"h":10}}}, + {"env":"production", "key":"e.g.h", "val":15}, + {"env":"production", "key":"e.g", "val":{"h":15}}, + {"env":"production", "key":"e", "val":{"f":9, "g":{"h":15}}}, + {"env":"test", "key":"e.g.h", "val":20}, + {"env":"test", "key":"e.g", "val":{"h":20}}, + {"env":"test", "key":"e", "val":{"f":14, "g":{"h":20}}} + ]); + next(); + } + })(0); + }); + }); + }); + + + it.describe("#addListener", function (it) { + it.should("monitor configurations in certain directories", function (next) { + var config1 = goFigure({monitor:true, locations:[__dirname + "/configs/configs1"]}); + config1.loadSync(); + var res = [], called = 0; + ["a", "b.{c|d}", "e", "e.g", "e.g.*"].forEach(function(topic){ + config1.addListener(topic, function (key, val, config) { + called++; + res.push({key:key, val:comb.isObject(val) ? comb.deepMerge({}, val) : val}); + }); + }) + var changes = [ + [comb.deepMerge({}, conf1, {a:4}), 1], + [comb.deepMerge({}, conf1, {b:{c:5}}), 2], + [comb.deepMerge({}, conf1, {b:{d:7}}), 2], + [comb.deepMerge({}, conf1, {e:{f:8}}), 2], + [comb.deepMerge({}, conf1, {e:{g:{h:9}}}), 3] + ]; + var l = changes.length; + (function _next(index) { + if (index < l) { + fs.writeFile(__dirname + "/configs/configs1/config1.json", JSON.stringify(changes[index][0], null, 4), function (err, res) { + if (err) { + next(err); + } else { + (function to() { + process.nextTick(function () { + if (called >= changes[index][1]) { + called = 0; + _next(++index); + } else { + to(); + } + }); + })(); + } + }); + } else { + assert.deepEqual(res, [ + {key:"a", val:4}, + {key:"a", val:1}, + {key:"b.c", val:5}, + {key:"b.c", val:1}, + {key:"b.d", val:7}, + {key:"b.d", val:2}, + {key:"e", val:{f:8, g:{h:4}}}, + {key:"e.g.h", val:9}, + {key:"e.g", val:{h:9}}, + {key:"e", val:{f:3, g:{h:9}}} + ]); + next(); + } + })(0); + + + }); + + it.should("monitor configurations of files", function (next) { + var config1 = goFigure({monitor:true, files:[__dirname + "/configs/configs1/config1.json"]}); + config1.loadSync(); + var res = [], called = 0; + ["a", "b.{c|d}", "e", "e.g", "e.g.*"].forEach(function(topic){ + config1.addListener(topic, function (key, val, config) { + called++; + res.push({key:key, val:comb.isObject(val) ? comb.deepMerge({}, val) : val}); + }); + }) + var changes = [ + [comb.deepMerge({}, conf1, {a:4}), 1], + [comb.deepMerge({}, conf1, {b:{c:5}}), 2], + [comb.deepMerge({}, conf1, {b:{d:7}}), 2], + [comb.deepMerge({}, conf1, {e:{f:8}}), 2], + [comb.deepMerge({}, conf1, {e:{g:{h:9}}}), 3] + ]; + var l = changes.length; + (function _next(index) { + if (index < l) { + fs.writeFile(__dirname + "/configs/configs1/config1.json", JSON.stringify(changes[index][0], null, 4), function (err, res) { + if (err) { + next(err); + } else { + (function to() { + process.nextTick(function () { + if (called >= changes[index][1]) { + called = 0; + _next(++index); + } else { + to(); + } + }); + })(); + } + }); + } else { + assert.deepEqual(res, [ + {key:"a", val:4}, + {key:"a", val:1}, + {key:"b.c", val:5}, + {key:"b.c", val:1}, + {key:"b.d", val:7}, + {key:"b.d", val:2}, + {key:"e", val:{f:8, g:{h:4}}}, + {key:"e.g.h", val:9}, + {key:"e.g", val:{h:9}}, + {key:"e", val:{f:3, g:{h:9}}} + ]); + next(); + } + })(0); + + + }); + + it.should("monitor configurations of certain files", function (next) { + var config1 = goFigure({monitor:false, files:[ + {monitor:true, file:__dirname + "/configs/configs1/config1.json"} + ]}); + config1.loadSync(); + var res = [], called = 0; + ["a", "b.{c|d}", "e", "e.g", "e.g.*"].forEach(function(topic){ + config1.addListener(topic, function (key, val, config) { + called++; + res.push({key:key, val:comb.isObject(val) ? comb.deepMerge({}, val) : val}); + }); + }) + var changes = [ + [comb.deepMerge({}, conf1, {a:4}), 1], + [comb.deepMerge({}, conf1, {b:{c:5}}), 2], + [comb.deepMerge({}, conf1, {b:{d:7}}), 2], + [comb.deepMerge({}, conf1, {e:{f:8}}), 2], + [comb.deepMerge({}, conf1, {e:{g:{h:9}}}), 3] + ]; + var l = changes.length; + (function _next(index) { + if (index < l) { + fs.writeFile(__dirname + "/configs/configs1/config1.json", JSON.stringify(changes[index][0], null, 4), function (err, res) { + if (err) { + next(err); + } else { + (function to() { + process.nextTick(function () { + if (called >= changes[index][1]) { + called = 0; + _next(++index); + } else { + to(); + } + }); + })(); + } + }); + } else { + assert.deepEqual(res, [ + {key:"a", val:4}, + {key:"a", val:1}, + {key:"b.c", val:5}, + {key:"b.c", val:1}, + {key:"b.d", val:7}, + {key:"b.d", val:2}, + {key:"e", val:{f:8, g:{h:4}}}, + {key:"e.g.h", val:9}, + {key:"e.g", val:{h:9}}, + {key:"e", val:{f:3, g:{h:9}}} + ]); + next(); + } + })(0); + + + }); + + it.describe("with an env", function (it) { + it.should("monitor configurations of files", function (next) { + var res = [], called = 0; + ["development", "production", "test"].forEach(function (env) { + var config = goFigure({monitor:true, environment:env, files:[__dirname + "/configs/config-env/config.json"]}); + config.loadSync(); + ["a", "b.{c|d}", "e", "e.g", "e.g.*"].forEach(function(topic){ + config.addListener(topic, function (key, val, config) { + called++; + res.push({env : env, key:key, val:comb.isObject(val) ? comb.deepMerge({}, val) : val}); + }); + }) + }); + var changes = [ + [comb.deepMerge({}, envConf, {development:{a:6}, production:{a:11}, test:{a:16}}), 3], + [comb.deepMerge({}, envConf, {development:{b:{c:7}}, production:{b:{c:12}}, test:{b:{c:17}}}), 6], + [comb.deepMerge({}, envConf, {development:{b:{d:8}}, production:{b:{d:13}}, test:{b:{d:18}}}), 2], + [comb.deepMerge({}, envConf, {development:{e:{f:9}}, production:{e:{f:14}}, test:{e:{f:19}}}), 2], + [comb.deepMerge({}, envConf, {development:{e:{g:{h:10}}}, production:{e:{g:{h:15}}}, test:{e:{g:{h:20}}}}), 3] + ]; + var l = changes.length; + (function _next(index) { + if (index < l) { + fs.writeFile(__dirname + "/configs/config-env/config.json", JSON.stringify(changes[index][0], null, 4), function (err, res) { + if (err) { + next(err); + } else { + (function to() { + process.nextTick(function () { + if (called >= changes[index][1]) { + called = 0; + _next(++index); + } else { + to(); + } + }); + })(); + } + }); + } else { + assert.deepEqual(res, [ + {"env":"development", "key":"a", "val":6}, + {"env":"production", "key":"a", "val":11}, + {"env":"test", "key":"a", "val":16}, + {"env":"development", "key":"a", "val":1}, + {"env":"development", "key":"b.c", "val":7}, + {"env":"production", "key":"a", "val":6}, + {"env":"production", "key":"b.c", "val":12}, + {"env":"test", "key":"a", "val":11}, + {"env":"test", "key":"b.c", "val":17}, + {"env":"development", "key":"b.c", "val":2}, + {"env":"development", "key":"b.d", "val":8}, + {"env":"production", "key":"b.c", "val":7}, + {"env":"production", "key":"b.d", "val":13}, + {"env":"test", "key":"b.c", "val":12}, + {"env":"test", "key":"b.d", "val":18}, + {"env":"development", "key":"b.d", "val":3}, + {"env":"development", "key":"e", "val":{"f":9, "g":{"h":5}}}, + {"env":"production", "key":"b.d", "val":8}, + {"env":"production", "key":"e", "val":{"f":14, "g":{"h":10}}}, + {"env":"test", "key":"b.d", "val":13}, + {"env":"test", "key":"e", "val":{"f":19, "g":{"h":15}}}, + {"env":"development", "key":"e.g.h", "val":10}, + {"env":"development", "key":"e.g", "val":{"h":10}}, + {"env":"development", "key":"e", "val":{"f":4, "g":{"h":10}}}, + {"env":"production", "key":"e.g.h", "val":15}, + {"env":"production", "key":"e.g", "val":{"h":15}}, + {"env":"production", "key":"e", "val":{"f":9, "g":{"h":15}}}, + {"env":"test", "key":"e.g.h", "val":20}, + {"env":"test", "key":"e.g", "val":{"h":20}}, + {"env":"test", "key":"e", "val":{"f":14, "g":{"h":20}}} + ]); + next(); + } + })(0); + }); + }); + }); + + it.describe("#once", function (it) { + it.should("should stop listening", function (next) { + var config1 = goFigure({monitor:true, locations:[__dirname + "/configs/configs1"]}); + config1.loadSync(); + var res = [], called = 0; + var l1 = function (key, val, config) { + called++; + res.push({key:key, val:val, l:"l1"}) + }; + + var l2 = function (key, val, config) { + called++; + res.push({key:key, val:val, l:"l2"}) + }; + config1.once("a", l1); + config1.on("a", l2); + + var changes = [ + [comb.deepMerge({}, conf1, {a:4}), 2], + [comb.deepMerge({}, conf1, {a:1}), 1] + ]; + var l = changes.length; + (function _next(index) { + if (index < l) { + fs.writeFile(__dirname + "/configs/configs1/config1.json", JSON.stringify(changes[index][0], null, 4) + "\n\r", function (err, res) { + if (err) { + next(err); + } else { + (function to() { + process.nextTick(function () { + if (called >= changes[index][1]) { + called = 0; + _next(++index); + } else { + to(); + } + }); + })(); + } + }); + } else { + assert.deepEqual(res, [ + {key:"a", val:4, l:"l1"}, + {key:"a", val:4, l:"l2"}, + {key:"a", val:1, l:"l2"} + ]); + next(); + } + })(0); + + + }); + + }); + + it.describe("#removeListener", function (it) { + it.should("should stop listening", function (next) { + var config1 = goFigure({monitor:true, locations:[__dirname + "/configs/configs1"]}); + config1.loadSync(); + var res = [], called = 0; + var l1 = function (key, val, config) { + called++; + res.push({key:key, val:val, l:"l1"}) + }; + + var l2 = function (key, val, config) { + called++; + res.push({key:key, val:val, l:"l2"}) + }; + config1.on("a", l1); + config1.on("a", l2); + + var changes = [ + [comb.deepMerge({}, conf1, {a:4}), 2], + [comb.deepMerge({}, conf1, {a:1}), 1] + ]; + var l = changes.length; + (function _next(index) { + if (index < l) { + fs.writeFile(__dirname + "/configs/configs1/config1.json", JSON.stringify(changes[index][0], null, 4), function (err, res) { + if (err) { + next(err); + } else { + (function to() { + process.nextTick(function () { + if (called >= changes[index][1]) { + config1.removeListener("a", l1); + called = 0; + _next(++index); + } else { + to(); + } + }); + })(); + } + }); + } else { + assert.deepEqual(res, [ + {key:"a", val:4, l:"l1"}, + {key:"a", val:4, l:"l2"}, + {key:"a", val:1, l:"l2"} + ]); + next(); + } + })(0); + + + }); + + }); + it.run(); +}) +; +