var ProtoList = require('proto-list') , path = require('path') , fs = require('fs') , ini = require('ini') , EE = require('events').EventEmitter , url = require('url') , http = require('http') var exports = module.exports = function () { var args = [].slice.call(arguments) , conf = new ConfigChain() while(args.length) { var a = args.shift() if(a) conf.push ( 'string' === typeof a ? json(a) : a ) } return conf } //recursively find a file... var find = exports.find = function () { var rel = path.join.apply(null, [].slice.call(arguments)) function find(start, rel) { var file = path.join(start, rel) try { fs.statSync(file) return file } catch (err) { if(path.dirname(start) !== start) // root return find(path.dirname(start), rel) } } return find(__dirname, rel) } var parse = exports.parse = function (content, file, type) { content = '' + content // if we don't know what it is, try json and fall back to ini // if we know what it is, then it must be that. if (!type) { try { return JSON.parse(content) } catch (er) { return ini.parse(content) } } else if (type === 'json') { if (this.emit) { try { return JSON.parse(content) } catch (er) { this.emit('error', er) } } else { return JSON.parse(content) } } else { return ini.parse(content) } } var json = exports.json = function () { var args = [].slice.call(arguments).filter(function (arg) { return arg != null }) var file = path.join.apply(null, args) var content try { content = fs.readFileSync(file,'utf-8') } catch (err) { return } return parse(content, file, 'json') } var env = exports.env = function (prefix, env) { env = env || process.env var obj = {} var l = prefix.length for(var k in env) { if(k.indexOf(prefix) === 0) obj[k.substring(l)] = env[k] } return obj } exports.ConfigChain = ConfigChain function ConfigChain () { EE.apply(this) ProtoList.apply(this, arguments) this._awaiting = 0 this._saving = 0 this.sources = {} } // multi-inheritance-ish var extras = { constructor: { value: ConfigChain } } Object.keys(EE.prototype).forEach(function (k) { extras[k] = Object.getOwnPropertyDescriptor(EE.prototype, k) }) ConfigChain.prototype = Object.create(ProtoList.prototype, extras) ConfigChain.prototype.del = function (key, where) { // if not specified where, then delete from the whole chain, scorched // earth style if (where) { var target = this.sources[where] target = target && target.data if (!target) { return this.emit('error', new Error('not found '+where)) } delete target[key] } else { for (var i = 0, l = this.list.length; i < l; i ++) { delete this.list[i][key] } } return this } ConfigChain.prototype.set = function (key, value, where) { var target if (where) { target = this.sources[where] target = target && target.data if (!target) { return this.emit('error', new Error('not found '+where)) } } else { target = this.list[0] if (!target) { return this.emit('error', new Error('cannot set, no confs!')) } } target[key] = value return this } ConfigChain.prototype.get = function (key, where) { if (where) { where = this.sources[where] if (where) where = where.data if (where && Object.hasOwnProperty.call(where, key)) return where[key] return undefined } return this.list[0][key] } ConfigChain.prototype.save = function (where, type, cb) { if (typeof type === 'function') cb = type, type = null var target = this.sources[where] if (!target || !(target.path || target.source) || !target.data) { // TODO: maybe save() to a url target could be a PUT or something? // would be easy to swap out with a reddis type thing, too return this.emit('error', new Error('bad save target: '+where)) } if (target.source) { var pref = target.prefix || '' Object.keys(target.data).forEach(function (k) { target.source[pref + k] = target.data[k] }) return this } var type = type || target.type var data = target.data if (target.type === 'json') { data = JSON.stringify(data) } else { data = ini.stringify(data) } this._saving ++ fs.writeFile(target.path, data, 'utf8', function (er) { this._saving -- if (er) { if (cb) return cb(er) else return this.emit('error', er) } if (this._saving === 0) { if (cb) cb() this.emit('save') } }.bind(this)) return this } ConfigChain.prototype.addFile = function (file, type, name) { name = name || file var marker = {__source__:name} this.sources[name] = { path: file, type: type } this.push(marker) this._await() fs.readFile(file, 'utf8', function (er, data) { if (er) this.emit('error', er) this.addString(data, file, type, marker) }.bind(this)) return this } ConfigChain.prototype.addEnv = function (prefix, env, name) { name = name || 'env' var data = exports.env(prefix, env) this.sources[name] = { data: data, source: env, prefix: prefix } return this.add(data, name) } ConfigChain.prototype.addUrl = function (req, type, name) { this._await() var href = url.format(req) name = name || href var marker = {__source__:name} this.sources[name] = { href: href, type: type } this.push(marker) http.request(req, function (res) { var c = [] var ct = res.headers['content-type'] if (!type) { type = ct.indexOf('json') !== -1 ? 'json' : ct.indexOf('ini') !== -1 ? 'ini' : href.match(/\.json$/) ? 'json' : href.match(/\.ini$/) ? 'ini' : null marker.type = type } res.on('data', c.push.bind(c)) .on('end', function () { this.addString(Buffer.concat(c), href, type, marker) }.bind(this)) .on('error', this.emit.bind(this, 'error')) }.bind(this)) .on('error', this.emit.bind(this, 'error')) .end() return this } ConfigChain.prototype.addString = function (data, file, type, marker) { data = this.parse(data, file, type) this.add(data, marker) return this } ConfigChain.prototype.add = function (data, marker) { if (marker && typeof marker === 'object') { var i = this.list.indexOf(marker) if (i === -1) { return this.emit('error', new Error('bad marker')) } this.splice(i, 1, data) marker = marker.__source__ this.sources[marker] = this.sources[marker] || {} this.sources[marker].data = data // we were waiting for this. maybe emit 'load' this._resolve() } else { if (typeof marker === 'string') { this.sources[marker] = this.sources[marker] || {} this.sources[marker].data = data } // trigger the load event if nothing was already going to do so. this._await() this.push(data) process.nextTick(this._resolve.bind(this)) } return this } ConfigChain.prototype.parse = exports.parse ConfigChain.prototype._await = function () { this._awaiting++ } ConfigChain.prototype._resolve = function () { this._awaiting-- if (this._awaiting === 0) this.emit('load', this) }