• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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