1var ProtoList = require('proto-list') 2 , path = require('path') 3 , fs = require('fs') 4 , ini = require('ini') 5 , EE = require('events').EventEmitter 6 , url = require('url') 7 , http = require('http') 8 9var exports = module.exports = function () { 10 var args = [].slice.call(arguments) 11 , conf = new ConfigChain() 12 13 while(args.length) { 14 var a = args.shift() 15 if(a) conf.push 16 ( 'string' === typeof a 17 ? json(a) 18 : a ) 19 } 20 21 return conf 22} 23 24//recursively find a file... 25 26var find = exports.find = function () { 27 var rel = path.join.apply(null, [].slice.call(arguments)) 28 29 function find(start, rel) { 30 var file = path.join(start, rel) 31 try { 32 fs.statSync(file) 33 return file 34 } catch (err) { 35 if(path.dirname(start) !== start) // root 36 return find(path.dirname(start), rel) 37 } 38 } 39 return find(__dirname, rel) 40} 41 42var parse = exports.parse = function (content, file, type) { 43 content = '' + content 44 // if we don't know what it is, try json and fall back to ini 45 // if we know what it is, then it must be that. 46 if (!type) { 47 try { return JSON.parse(content) } 48 catch (er) { return ini.parse(content) } 49 } else if (type === 'json') { 50 if (this.emit) { 51 try { return JSON.parse(content) } 52 catch (er) { this.emit('error', er) } 53 } else { 54 return JSON.parse(content) 55 } 56 } else { 57 return ini.parse(content) 58 } 59} 60 61var json = exports.json = function () { 62 var args = [].slice.call(arguments).filter(function (arg) { return arg != null }) 63 var file = path.join.apply(null, args) 64 var content 65 try { 66 content = fs.readFileSync(file,'utf-8') 67 } catch (err) { 68 return 69 } 70 return parse(content, file, 'json') 71} 72 73var env = exports.env = function (prefix, env) { 74 env = env || process.env 75 var obj = {} 76 var l = prefix.length 77 for(var k in env) { 78 if(k.indexOf(prefix) === 0) 79 obj[k.substring(l)] = env[k] 80 } 81 82 return obj 83} 84 85exports.ConfigChain = ConfigChain 86function ConfigChain () { 87 EE.apply(this) 88 ProtoList.apply(this, arguments) 89 this._awaiting = 0 90 this._saving = 0 91 this.sources = {} 92} 93 94// multi-inheritance-ish 95var extras = { 96 constructor: { value: ConfigChain } 97} 98Object.keys(EE.prototype).forEach(function (k) { 99 extras[k] = Object.getOwnPropertyDescriptor(EE.prototype, k) 100}) 101ConfigChain.prototype = Object.create(ProtoList.prototype, extras) 102 103ConfigChain.prototype.del = function (key, where) { 104 // if not specified where, then delete from the whole chain, scorched 105 // earth style 106 if (where) { 107 var target = this.sources[where] 108 target = target && target.data 109 if (!target) { 110 return this.emit('error', new Error('not found '+where)) 111 } 112 delete target[key] 113 } else { 114 for (var i = 0, l = this.list.length; i < l; i ++) { 115 delete this.list[i][key] 116 } 117 } 118 return this 119} 120 121ConfigChain.prototype.set = function (key, value, where) { 122 var target 123 124 if (where) { 125 target = this.sources[where] 126 target = target && target.data 127 if (!target) { 128 return this.emit('error', new Error('not found '+where)) 129 } 130 } else { 131 target = this.list[0] 132 if (!target) { 133 return this.emit('error', new Error('cannot set, no confs!')) 134 } 135 } 136 target[key] = value 137 return this 138} 139 140ConfigChain.prototype.get = function (key, where) { 141 if (where) { 142 where = this.sources[where] 143 if (where) where = where.data 144 if (where && Object.hasOwnProperty.call(where, key)) return where[key] 145 return undefined 146 } 147 return this.list[0][key] 148} 149 150ConfigChain.prototype.save = function (where, type, cb) { 151 if (typeof type === 'function') cb = type, type = null 152 var target = this.sources[where] 153 if (!target || !(target.path || target.source) || !target.data) { 154 // TODO: maybe save() to a url target could be a PUT or something? 155 // would be easy to swap out with a reddis type thing, too 156 return this.emit('error', new Error('bad save target: '+where)) 157 } 158 159 if (target.source) { 160 var pref = target.prefix || '' 161 Object.keys(target.data).forEach(function (k) { 162 target.source[pref + k] = target.data[k] 163 }) 164 return this 165 } 166 167 var type = type || target.type 168 var data = target.data 169 if (target.type === 'json') { 170 data = JSON.stringify(data) 171 } else { 172 data = ini.stringify(data) 173 } 174 175 this._saving ++ 176 fs.writeFile(target.path, data, 'utf8', function (er) { 177 this._saving -- 178 if (er) { 179 if (cb) return cb(er) 180 else return this.emit('error', er) 181 } 182 if (this._saving === 0) { 183 if (cb) cb() 184 this.emit('save') 185 } 186 }.bind(this)) 187 return this 188} 189 190ConfigChain.prototype.addFile = function (file, type, name) { 191 name = name || file 192 var marker = {__source__:name} 193 this.sources[name] = { path: file, type: type } 194 this.push(marker) 195 this._await() 196 fs.readFile(file, 'utf8', function (er, data) { 197 if (er) this.emit('error', er) 198 this.addString(data, file, type, marker) 199 }.bind(this)) 200 return this 201} 202 203ConfigChain.prototype.addEnv = function (prefix, env, name) { 204 name = name || 'env' 205 var data = exports.env(prefix, env) 206 this.sources[name] = { data: data, source: env, prefix: prefix } 207 return this.add(data, name) 208} 209 210ConfigChain.prototype.addUrl = function (req, type, name) { 211 this._await() 212 var href = url.format(req) 213 name = name || href 214 var marker = {__source__:name} 215 this.sources[name] = { href: href, type: type } 216 this.push(marker) 217 http.request(req, function (res) { 218 var c = [] 219 var ct = res.headers['content-type'] 220 if (!type) { 221 type = ct.indexOf('json') !== -1 ? 'json' 222 : ct.indexOf('ini') !== -1 ? 'ini' 223 : href.match(/\.json$/) ? 'json' 224 : href.match(/\.ini$/) ? 'ini' 225 : null 226 marker.type = type 227 } 228 229 res.on('data', c.push.bind(c)) 230 .on('end', function () { 231 this.addString(Buffer.concat(c), href, type, marker) 232 }.bind(this)) 233 .on('error', this.emit.bind(this, 'error')) 234 235 }.bind(this)) 236 .on('error', this.emit.bind(this, 'error')) 237 .end() 238 239 return this 240} 241 242ConfigChain.prototype.addString = function (data, file, type, marker) { 243 data = this.parse(data, file, type) 244 this.add(data, marker) 245 return this 246} 247 248ConfigChain.prototype.add = function (data, marker) { 249 if (marker && typeof marker === 'object') { 250 var i = this.list.indexOf(marker) 251 if (i === -1) { 252 return this.emit('error', new Error('bad marker')) 253 } 254 this.splice(i, 1, data) 255 marker = marker.__source__ 256 this.sources[marker] = this.sources[marker] || {} 257 this.sources[marker].data = data 258 // we were waiting for this. maybe emit 'load' 259 this._resolve() 260 } else { 261 if (typeof marker === 'string') { 262 this.sources[marker] = this.sources[marker] || {} 263 this.sources[marker].data = data 264 } 265 // trigger the load event if nothing was already going to do so. 266 this._await() 267 this.push(data) 268 process.nextTick(this._resolve.bind(this)) 269 } 270 return this 271} 272 273ConfigChain.prototype.parse = exports.parse 274 275ConfigChain.prototype._await = function () { 276 this._awaiting++ 277} 278 279ConfigChain.prototype._resolve = function () { 280 this._awaiting-- 281 if (this._awaiting === 0) this.emit('load', this) 282} 283