index.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. 'use strict'
  2. class FiggyPudding {
  3. constructor (specs, opts, providers) {
  4. this.__specs = specs || {}
  5. Object.keys(this.__specs).forEach(alias => {
  6. if (typeof this.__specs[alias] === 'string') {
  7. const key = this.__specs[alias]
  8. const realSpec = this.__specs[key]
  9. if (realSpec) {
  10. const aliasArr = realSpec.aliases || []
  11. aliasArr.push(alias, key)
  12. realSpec.aliases = [...(new Set(aliasArr))]
  13. this.__specs[alias] = realSpec
  14. } else {
  15. throw new Error(`Alias refers to invalid key: ${key} -> ${alias}`)
  16. }
  17. }
  18. })
  19. this.__opts = opts || {}
  20. this.__providers = reverse((providers).filter(
  21. x => x != null && typeof x === 'object'
  22. ))
  23. this.__isFiggyPudding = true
  24. }
  25. get (key) {
  26. return pudGet(this, key, true)
  27. }
  28. get [Symbol.toStringTag] () { return 'FiggyPudding' }
  29. forEach (fn, thisArg = this) {
  30. for (let [key, value] of this.entries()) {
  31. fn.call(thisArg, value, key, this)
  32. }
  33. }
  34. toJSON () {
  35. const obj = {}
  36. this.forEach((val, key) => {
  37. obj[key] = val
  38. })
  39. return obj
  40. }
  41. * entries (_matcher) {
  42. for (let key of Object.keys(this.__specs)) {
  43. yield [key, this.get(key)]
  44. }
  45. const matcher = _matcher || this.__opts.other
  46. if (matcher) {
  47. const seen = new Set()
  48. for (let p of this.__providers) {
  49. const iter = p.entries ? p.entries(matcher) : entries(p)
  50. for (let [key, val] of iter) {
  51. if (matcher(key) && !seen.has(key)) {
  52. seen.add(key)
  53. yield [key, val]
  54. }
  55. }
  56. }
  57. }
  58. }
  59. * [Symbol.iterator] () {
  60. for (let [key, value] of this.entries()) {
  61. yield [key, value]
  62. }
  63. }
  64. * keys () {
  65. for (let [key] of this.entries()) {
  66. yield key
  67. }
  68. }
  69. * values () {
  70. for (let [, value] of this.entries()) {
  71. yield value
  72. }
  73. }
  74. concat (...moreConfig) {
  75. return new Proxy(new FiggyPudding(
  76. this.__specs,
  77. this.__opts,
  78. reverse(this.__providers).concat(moreConfig)
  79. ), proxyHandler)
  80. }
  81. }
  82. try {
  83. const util = require('util')
  84. FiggyPudding.prototype[util.inspect.custom] = function (depth, opts) {
  85. return (
  86. this[Symbol.toStringTag] + ' '
  87. ) + util.inspect(this.toJSON(), opts)
  88. }
  89. } catch (e) {}
  90. function BadKeyError (key) {
  91. throw Object.assign(new Error(
  92. `invalid config key requested: ${key}`
  93. ), {code: 'EBADKEY'})
  94. }
  95. function pudGet (pud, key, validate) {
  96. let spec = pud.__specs[key]
  97. if (validate && !spec && (!pud.__opts.other || !pud.__opts.other(key))) {
  98. BadKeyError(key)
  99. } else {
  100. if (!spec) { spec = {} }
  101. let ret
  102. for (let p of pud.__providers) {
  103. ret = tryGet(key, p)
  104. if (ret === undefined && spec.aliases && spec.aliases.length) {
  105. for (let alias of spec.aliases) {
  106. if (alias === key) { continue }
  107. ret = tryGet(alias, p)
  108. if (ret !== undefined) {
  109. break
  110. }
  111. }
  112. }
  113. if (ret !== undefined) {
  114. break
  115. }
  116. }
  117. if (ret === undefined && spec.default !== undefined) {
  118. if (typeof spec.default === 'function') {
  119. return spec.default(pud)
  120. } else {
  121. return spec.default
  122. }
  123. } else {
  124. return ret
  125. }
  126. }
  127. }
  128. function tryGet (key, p) {
  129. let ret
  130. if (p.__isFiggyPudding) {
  131. ret = pudGet(p, key, false)
  132. } else if (typeof p.get === 'function') {
  133. ret = p.get(key)
  134. } else {
  135. ret = p[key]
  136. }
  137. return ret
  138. }
  139. const proxyHandler = {
  140. has (obj, prop) {
  141. return prop in obj.__specs && pudGet(obj, prop, false) !== undefined
  142. },
  143. ownKeys (obj) {
  144. return Object.keys(obj.__specs)
  145. },
  146. get (obj, prop) {
  147. if (
  148. typeof prop === 'symbol' ||
  149. prop.slice(0, 2) === '__' ||
  150. prop in FiggyPudding.prototype
  151. ) {
  152. return obj[prop]
  153. }
  154. return obj.get(prop)
  155. },
  156. set (obj, prop, value) {
  157. if (
  158. typeof prop === 'symbol' ||
  159. prop.slice(0, 2) === '__'
  160. ) {
  161. obj[prop] = value
  162. return true
  163. } else {
  164. throw new Error('figgyPudding options cannot be modified. Use .concat() instead.')
  165. }
  166. },
  167. deleteProperty () {
  168. throw new Error('figgyPudding options cannot be deleted. Use .concat() and shadow them instead.')
  169. }
  170. }
  171. module.exports = figgyPudding
  172. function figgyPudding (specs, opts) {
  173. function factory (...providers) {
  174. return new Proxy(new FiggyPudding(
  175. specs,
  176. opts,
  177. providers
  178. ), proxyHandler)
  179. }
  180. return factory
  181. }
  182. function reverse (arr) {
  183. const ret = []
  184. arr.forEach(x => ret.unshift(x))
  185. return ret
  186. }
  187. function entries (obj) {
  188. return Object.keys(obj).map(k => [k, obj[k]])
  189. }