├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── MIGRATION.md ├── README.md ├── config-wrapper.js ├── errors.js ├── get-config-state.js ├── index.js ├── package.json ├── read-datacenter.js └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.a 8 | *.o 9 | *.so 10 | *.node 11 | bin/* 12 | 13 | # Node Waf Byproducts # 14 | ####################### 15 | .lock-wscript 16 | build/ 17 | autom4te.cache/ 18 | 19 | # Node Modules # 20 | ################ 21 | # Better to let npm install these from the package.json defintion 22 | # rather than maintain this manually 23 | node_modules/ 24 | 25 | # Packages # 26 | ############ 27 | # it's better to unpack these files and commit the raw source 28 | # git has its own built in compression methods 29 | *.7z 30 | *.dmg 31 | *.gz 32 | *.iso 33 | *.jar 34 | *.rar 35 | *.tar 36 | *.zip 37 | 38 | # Logs and databases # 39 | ###################### 40 | *.log 41 | dump.rdb 42 | *.tap 43 | *.xml 44 | 45 | # OS generated files # 46 | ###################### 47 | .DS_Store? 48 | .DS_Store 49 | ehthumbs.db 50 | Icon? 51 | Thumbs.db 52 | coverage 53 | 54 | # Text Editor Byproducts # 55 | ########################## 56 | *.sw? 57 | .idea/ 58 | 59 | # Python object code 60 | ########################## 61 | *.py[oc] 62 | 63 | # All translation files # 64 | ######################### 65 | static/translations-s3/ 66 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "camelcase": true, 4 | "curly": false, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "immed": true, 8 | "indent": 4, 9 | "latedef": "nofunc", 10 | "newcap": false, 11 | "noarg": true, 12 | "nonew": true, 13 | "plusplus": false, 14 | "quotmark": false, 15 | "regexp": false, 16 | "undef": true, 17 | "unused": true, 18 | "strict": false, 19 | "trailing": true, 20 | "noempty": true, 21 | "maxdepth": 4, 22 | "maxparams": 4, 23 | "globals": { 24 | "console": true, 25 | "Buffer": true, 26 | "setTimeout": true, 27 | "clearTimeout": true, 28 | "setInterval": true, 29 | "clearInterval": true, 30 | "require": false, 31 | "module": false, 32 | "exports": true, 33 | "global": false, 34 | "process": true, 35 | "__dirname": false, 36 | "__filename": false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | - "0.10" 5 | - "0.11" 6 | before_install: npm i npm@~1.4.6 -g 7 | script: npm run travis 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Uber Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | ## Migration from v4 to v5 2 | 3 | The secrets loading algorithm has changed. We previously always loaded 4 | `config/secrets/secrets.json` and loaded `config/secrets-ENV.json`. 5 | 6 | The new loading algorithm is to treat `config/secrets/secrets.json` 7 | as production only. This means it only gets loaded when `NODE_ENV` is 8 | the string `"production"` 9 | 10 | The `config/secrets-ENV.json` files moved to `config/secrets/secrets-ENV.json` 11 | and will only be loaded when `NODE_ENV` is not `"production"`. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zero-config 2 | 3 | [![build status][build-svg]][build] 4 | [![Coverage Status][cover-svg]][cover] 5 | [![Davis Dependency status][dep-svg]][dep] 6 | 7 | 8 | 9 | 10 | 11 | A zero configuration configuration loader 12 | 13 | ## Example 14 | 15 | ```js 16 | // config/common.json 17 | { 18 | "port": 9001 19 | } 20 | ``` 21 | 22 | ```js 23 | // config/production.json 24 | { 25 | "redis": { 26 | "host": "localhost", 27 | "port": 6379 28 | } 29 | } 30 | ``` 31 | 32 | ```js 33 | // server.js 34 | var fs = require('fs') 35 | var fetchConfig = require('zero-config') 36 | 37 | var config = fetchConfig(__dirname, { 38 | dcValue: fs.existsSync('/etc/zero-config/datacenter') ? 39 | fs.readFileSync('/etc/zero-config/datacenter', 'utf8') : 40 | null 41 | }) 42 | 43 | var port = config.get("port") 44 | var redisConf = config.get("redis") 45 | var redisPort = config.get("redis.port") 46 | ``` 47 | 48 | You can also call the process with 49 | `node server.js --port 10253` to change the config 50 | information from the command line 51 | 52 | ## Docs 53 | 54 | ### `var config = fetchConfig(dirname, opts)` 55 | 56 | ```ocaml 57 | type Keypath : String | Array 58 | 59 | type Config : { 60 | get: (keypath?: Keypath) => Any, 61 | set: ((keypath: Keypath, value: Any) => void) & 62 | (value: Any) => void, 63 | freeze: () => void, 64 | deepFreeze: () => void, 65 | clone: () => Config 66 | getRemote: (keypath?: Keypath) => Any, 67 | setRemote: ((keypath: Keypath, value: Any) => void) & 68 | (value: Any) => void 69 | } 70 | 71 | zero-config := (dirname: String, opts?: { 72 | argv?: Array, 73 | dcValue?: String, 74 | blackList?: Array, 75 | env?: Object, 76 | isStaging?: Boolean, 77 | seed?: Object, 78 | defaults?: Object 79 | }) => Config 80 | ``` 81 | 82 | `fetchConfig` takes the current __dirname as an argument, it 83 | assumes that there exists a config folder at `./config` in 84 | your project and it assumes there exists a `common.json` and a 85 | `NODE_ENV.json` for each environment. 86 | 87 | It returns you a `config` object with a `get(keypath)` method 88 | to fetch properties out of config. `get()` takes a keypath, 89 | i.e. `"prop.nested.someKey"`to get direct or nested properties 90 | in the config object. 91 | 92 | It's recommended you use `.get()` as in the future we will 93 | enable dynamic config properties through flipr support. 94 | 95 | 96 | ### The config lookup algorithm 97 | 98 | The `fetchConfig()` function tries to fetch config from multiple 99 | locations and then deep merges the objects it finds together 100 | into a single object. 101 | 102 | Below are the sources it reads in order of least precendence. 103 | i.e. the later sources in the list overwrite the earlier ones 104 | 105 | - a defaults object that populates values that have 106 | not been set by any other means. 107 | - a `config/common.json` JSON file in your project 108 | - a `config/NODE_ENV.json` JSON file in your project 109 | - a `config/secrets/secrets-NODE_ENV.json` JSON file in your 110 | project containing secrets per NODE_ENV but not production 111 | - a `config/secrets/secrets.json` JSON file in your project 112 | containing secrets (API keys, OAuth tokens, etc) only for production 113 | - a `config/NODE_ENV.{datacenter}.json` JSON file in your 114 | project if you specificed a datacenter. 115 | - a `config/staging.json` JSON file in your project if isStaging 116 | option is true 117 | - a `config/staging.{datacenter}.json` JSON file in your project 118 | if isStaging option is true and you specificed a datacenter. 119 | - a `{ datacenter: '{datacenter}' }` literal if you 120 | specified a datacenter. 121 | - a `--config=/var/config/some-file.json` JSON file if you 122 | passed a command line argument called `--config` to the 123 | process. 124 | - a object literal based on command line arguments. i.e. if 125 | you pass `--foo='bar' --bar.baz='bob'` you will get 126 | `{ "foo": "bar", "bar": { "baz": "bob" } }` 127 | - a seed object of manual overwrites for testing purposes. 128 | 129 | The config loader also uses `config-chain` for the actual 130 | loading logic so you can read [their docs][config-chain] 131 | 132 | #### `dirname` 133 | 134 | `dirname` is the directory that is the parent of the `config` 135 | directly. If you call `fetchConfig` in a file located in the 136 | root directory you can just pass `__dirname` as config lives 137 | at `./config`. 138 | 139 | If you require `fetchConfig` anywhere else like `./api/server.js` 140 | you will have to pass `path.join(__dirname, '..')` 141 | 142 | #### `opts` 143 | 144 | `opts` is an optional object, that contains the following 145 | properties. 146 | 147 | **Note** that `opts` is only optional in environments other then 148 | `"production`". If your `process.env.NODE_ENV` is set to 149 | `"production"` then you **MUST** specifiy `opts` and specify 150 | the `opts.dcValue` parameter. 151 | 152 | Running a production service without knowing how to load 153 | datacenter specific configuration is a bug. 154 | 155 | #### `opts.dcValue` 156 | 157 | `opts.dcValue` is either `null` or a datacenter name. 158 | 159 | Say you have two datacenters, EC2-west and EC2-east. It's 160 | recommended that you have a file called `/etc/datacenter` 161 | that contains either the string `EC2-west` or `EC2-east`. 162 | 163 | This way any service can know what datacenter it is running 164 | in with a simple `cat /etc/datacenter`. 165 | 166 | You can then call `fetchConfig(...)` with the datacenter value 167 | by calling `fs.readFileSync('/etc/datacenter')` 168 | 169 | Note that if you pass the dc config to `fetchConfig` then the 170 | config object will contain the `"datacenter"` key whose value 171 | is either `EC2-west` or `EC2-east` or whatever your datacenter 172 | names are. 173 | 174 | We will also load the file `config/production.EC2-west.json` 175 | and merge that into the config tree. 176 | 177 | #### `opts.argv` 178 | 179 | `opts.argv` is optional and probably not needed 180 | 181 | `fetchConfig` will read your process argv information using 182 | the [`minimist`][minimist] module. 183 | 184 | If you do not want `fetchConfig` to read global argv for you, 185 | you can pass in an `argv` object with keys like `'foo'` and 186 | `'bar.baz''` and values that are strings / numbers / booleans 187 | 188 | #### `opts.isStaging` 189 | 190 | `opts.isStaging` is an optional boolean value to indicate it is 191 | a staging deployment, if set true. 192 | 193 | `fetchConfig` will read `staging.json` for a staging deployment, 194 | followed by `staging.{datacenter}.json` if datacenter is specified. 195 | 196 | #### `opts.blackList` 197 | 198 | `opts.blackList` is an optional array of argv keys to blacklist. 199 | 200 | `fetchConfig` by default converts all command line arguments to 201 | configuration keys. If you want to pass a non config key 202 | command line argument like `--debug` or `--restart-fast`, etc. 203 | then you might want to add them to the `blackList` 204 | 205 | If your `opts.blackList` is `['debug']` then `config.get('debug')` 206 | will not resolve to the `--debug` command line argument. 207 | 208 | #### `opts.env` 209 | 210 | `opts.env` is optional and probably not needed. 211 | 212 | `fetchConfig` will read the env using `process.env`. The only 213 | property it reads is an environment variable called `NODE_ENV`. 214 | 215 | If you prefer to not have this variable configured through 216 | the environment or want to call it something else then you 217 | can pass in `{ NODE_ENV: whatever }` as `opts.env` 218 | 219 | #### `opts.loose` 220 | 221 | should a value be requested from the config using get() and the 222 | key does not exist an error will be thrown. By setting 223 | `opts.loose` to `true` this feature is disabled and a value of 224 | undefined is returned should this key not be preset in the 225 | config. 226 | 227 | #### `opts.seed` 228 | 229 | `opts.seed` is optional, it can be set to an object 230 | 231 | If it exists we will merge the seed object into the config 232 | data we have fetched. seed overwrites all the other sources 233 | of configuration. 234 | 235 | The `seed` option is very useful for testing purposes, it allows 236 | you to overwrite the configuration that your application would 237 | load with test specific properties. 238 | 239 | This is an alternative to the `NODE_ENV=test `pattern, we highly 240 | recommend that you do not have a `test.json` file at all. 241 | 242 | #### `opts.defaults` 243 | 244 | `opts.defaults` is optional, it can be set to an object. 245 | 246 | If it exists, it will populate all the values that are unset 247 | (but not undefined) in the loaded config with those in 248 | `opts.defaults`. 249 | 250 | The difference between `defaults` and `seed` is that `seed` over- 251 | writes set values, while `defaults` does not. 252 | 253 | #### `var value = config.get(keypath)` 254 | 255 | `config.get(keypath)` will return the value at a keypath. The 256 | `keypath` must be a string. 257 | 258 | You can call `config.get('port')` to get the port value. You 259 | can call `config.get('playdoh-logger.kafka.port')` to get 260 | the nested kafka port config option. 261 | 262 | #### `config.set(keypath, value)` 263 | 264 | `config.set(keypath, value)` will set a value at the keypath. 265 | 266 | You can call `config.set("port", 9001)` to set the port value. 267 | You can call `config.set("playdoh-logger.kafka.port", 9001)` to 268 | set then nested kafka port config option. 269 | 270 | Note you can also call `config.set(entireObject)` to merge an 271 | entire object into the `config` instance. This will use 272 | deep extend to set all the key / value pairs in `entireObject` 273 | onto the config instance. 274 | 275 | #### `config.freeze()` 276 | 277 | Since the `config` object is supposed to represent a set of 278 | static, immutable configuration that's loaded at process 279 | startup time it would be useful to enforce this. 280 | 281 | Once you are ready to stop mutating `config` you can call 282 | `.freeze()`. Any future calls to `.set()` will throw a 283 | config frozen exception. 284 | 285 | Note that you can always call `config.setRemote()` as that is 286 | not effected by `.freeze()` 287 | 288 | #### `config.deepFreeze()` 289 | 290 | A stricter from of freeze which actually recursively calls 291 | Object.freeze() on the config object rendering it immutable. 292 | 293 | In strict mode this will throw an error if calling code attempts 294 | to mutate the returned config object. A side benefit of this is 295 | that it enables config.get() to return the actual object instead 296 | of a deep-copy, greatly reducing allocation pressure if your 297 | application is fetching large objects out of the config repeatedly. 298 | 299 | #### `config.clone()` 300 | 301 | To get a deep clone of the config object, use `config.clone()`. 302 | A cloned config object will have the same underlying data but 303 | none of the other properties. For example, if you clone a frozen 304 | config object, you are able to make changes to the clone but not 305 | the original object. 306 | 307 | #### `var value = config.getRemote(keypath)` 308 | 309 | The same as `config.get()` but gets from a different in memory 310 | object then `config.get()`. 311 | 312 | It's recommended that you use `config.get()` and `config.set()` 313 | for any local configuration that is static and effectively 314 | immutable after process startup. 315 | 316 | You can use `config.getRemote()` and `config.setRemote()` for 317 | any dynamic configuration that is effectively controlled 318 | remotely outside your program. 319 | 320 | #### `config.setRemote(keypath, value)` 321 | 322 | The same as `config.set()` but sets to a different in memory 323 | objec then `config.set()`. 324 | 325 | You can use `config.getRemote()` and `config.setRemote()` for 326 | any dynamic configuration that is effectively controlled 327 | remotely outside your program. 328 | 329 | ## Installation 330 | 331 | `npm install zero-config` 332 | 333 | ## Tests 334 | 335 | `npm test` 336 | 337 | ## Best Practices 338 | 339 | Zero-config is designed to help you structure your config 340 | files to support a number of production concerns. These best 341 | practices reflect our approach and some of the reasons we 342 | designed Zero-config as we did. 343 | 344 | - Configuration should live in a single file 345 | - Only put configuration in more specific configuration 346 | files when you really have to. Dev and test configs should 347 | only contain changes to support development 348 | (e.g. turning off caching). 349 | - Put your secrets in a `secrets.json` so that they are 350 | easier to manage safely. Ideally never commit these files 351 | to your source control repository. This is why we keep secrets 352 | in a folder that is easy to symlink 353 | - If you must have development secrets in source control 354 | for developer convenience then try to scrub them from 355 | builds of your projects. We call these `secrets-ENV.json` to 356 | make that easy. 357 | 358 | ## Contributors 359 | 360 | - Raynos 361 | - sh1mmer 362 | 363 | ## MIT Licenced 364 | 365 | [build-svg]: https://secure.travis-ci.org/uber/zero-config.svg 366 | [build]: https://travis-ci.org/uber/zero-config 367 | [cover-svg]: https://coveralls.io/repos/uber/zero-config/badge.svg 368 | [cover]: https://coveralls.io/r/uber/zero-config 369 | [dep-svg]: https://david-dm.org/uber/zero-config.svg 370 | [dep]: https://david-dm.org/uber/zero-config 371 | [test-svg]: https://ci.testling.com/uber/zero-config.svg 372 | [tes]: https://ci.testling.com/uber/zero-config 373 | [npm-svg]: https://nodei.co/npm/zero-config.svg?stars&downloads 374 | [npm]: https://nodei.co/npm/zero-config 375 | -------------------------------------------------------------------------------- /config-wrapper.js: -------------------------------------------------------------------------------- 1 | var getPath = require('dotty').get; 2 | var putPath = require('dotty').put; 3 | var deepExtend = require('deep-extend'); 4 | 5 | var errors = require('./errors.js'); 6 | 7 | var safeCloneInto = {value: null}; 8 | var safeCloneFrom = {value: null}; 9 | 10 | module.exports = ConfigWrapper; 11 | 12 | function ConfigWrapper(configObject, loose) { 13 | var frozen = false; 14 | var deepFrozen = false; 15 | // default `loose` to true. 16 | loose = typeof loose === 'boolean' ? loose : true; 17 | 18 | return { 19 | get: configuredGet, 20 | set: setKey, 21 | freeze: freeze, 22 | deepFreeze: deepFreeze 23 | }; 24 | 25 | function getKey(keyPath) { 26 | if (!keyPath) { 27 | return safe(configObject); 28 | } 29 | 30 | return safe(getPath(configObject, keyPath)); 31 | } 32 | 33 | function configuredGet(keyPath) { 34 | if (!keyPath) { 35 | return getKey(); 36 | } 37 | 38 | var value = getKey(keyPath); 39 | var strictMode = !loose; 40 | 41 | if (value === undefined && strictMode) { 42 | throw errors.NonexistantKeyPath(({ 43 | keyPath: keyPath 44 | })); 45 | } 46 | 47 | return value; 48 | } 49 | 50 | function setKey(keyPath, value) { 51 | if (frozen) { 52 | throw errors.SetFrozenObject({ 53 | keyPath: keyPath, 54 | valueStr: JSON.stringify(value), 55 | value: value 56 | }); 57 | } 58 | 59 | if (arguments.length === 1) { 60 | return multiSet(keyPath); 61 | } 62 | 63 | if (!isValidKeyPath(keyPath)) { 64 | throw errors.InvalidKeyPath({ 65 | keyPath: keyPath 66 | }); 67 | } 68 | 69 | var v = getKey(keyPath); 70 | v = deepExtend({keyPath: v}, {keyPath: value}); 71 | return putPath(configObject, keyPath, v.keyPath); 72 | } 73 | 74 | function freeze() { 75 | frozen = true; 76 | } 77 | 78 | function deepFreeze() { 79 | frozen = true; 80 | deepFrozen = true; 81 | deepFreezeObject(configObject); 82 | } 83 | 84 | function multiSet(obj) { 85 | if (obj === null || typeof obj !== 'object') { 86 | throw errors.InvalidMultiSetArgument({ 87 | objStr: JSON.stringify(obj), 88 | obj: obj 89 | }); 90 | } 91 | 92 | Object.keys(obj).forEach(setEachKey); 93 | 94 | function setEachKey(key) { 95 | setKey([key], obj[key]); 96 | } 97 | } 98 | 99 | function safe(value) { 100 | if (deepFrozen === true) { 101 | return value; 102 | } 103 | safeCloneInto.value = null; 104 | safeCloneFrom.value = value; 105 | return deepExtend(safeCloneInto, safeCloneFrom).value; 106 | } 107 | } 108 | 109 | function isValidKeyPath(keyPath) { 110 | return typeof keyPath === 'string' || 111 | Array.isArray(keyPath); 112 | } 113 | 114 | function deepFreezeObject(o) { 115 | Object.freeze(o); 116 | 117 | Object.keys(o).forEach(function eachProp(prop) { 118 | if (Object.hasOwnProperty.call(o, prop) && 119 | o[prop] !== null && 120 | (typeof o[prop] === 'object' || typeof o[prop] === 'function') && 121 | !Object.isFrozen(o[prop])) { 122 | 123 | deepFreezeObject(o[prop]); 124 | } 125 | }); 126 | return o; 127 | } 128 | -------------------------------------------------------------------------------- /errors.js: -------------------------------------------------------------------------------- 1 | var TypedError = require('error/typed'); 2 | 3 | var InvalidDirname = TypedError({ 4 | type: 'missing.dirname.argument', 5 | message: 'invalid __dirname argument.\n' + 6 | 'Must call fetchConfig(__dirname).\n' + 7 | '__dirname should be a string and is non-optional.\n' + 8 | 'instead I got {strDirname}.\n' + 9 | 'SUGGESTED FIX: update the `fetchConfig()` callsite.\n' 10 | }); 11 | 12 | var MissingDatacenter = TypedError({ 13 | type: 'missing.datacenter.file', 14 | warning: true, 15 | message: 'no such file or directory \'{path}\'.\n' + 16 | 'expected to find datacenter configuration at {path}.\n' 17 | }); 18 | 19 | var DatacenterRequired = TypedError({ 20 | type: 'datacenter.option.required', 21 | message: 'expected `opts.dcValue` to be passed to fetchConfig.\n' + 22 | 'must call `fetchConfig(__dirname, { dcValue: "..." }).\n' + 23 | 'instead I got opts: {strOpts}.\n' + 24 | '`opts.dcValue` is not optional when NODE_ENV is "production".\n' + 25 | 'SUGGESTED FIX: update the `fetchConfig()` callsite.\n' 26 | }); 27 | 28 | var NonexistantKeyPath = TypedError({ 29 | type: 'nonexistant.key.path', 30 | message: 'attempting to get a nonexistant keyPath.\n' + 31 | 'Expected the key: {keyPath} to be found.\n' + 32 | 'SUGGESTED FIX: add {keyPath} and value to config' 33 | }); 34 | 35 | var DatacenterFileRequired = TypedError({ 36 | type: 'datacenter.file.required', 37 | message: 'no such file or directory \'{path}\'.\n' + 38 | 'expected to find datacenter configuration at {path}.\n' + 39 | 'when NODE_ENV is "production" the datacenter file must exist.\n' + 40 | 'SUGGESTED FIX: configure your system so it has a datacenter file.\n' 41 | }); 42 | 43 | var InvalidKeyPath = TypedError({ 44 | type: 'invalid.keypath', 45 | message: 'specified an invalid keypath to `config.set()`.\n' + 46 | 'expected a string but instead got {keyPath}.\n' + 47 | 'SUGGESTED FIX: update the `config.set()` callsite.\n' 48 | }); 49 | 50 | var InvalidMultiSetArgument = TypedError({ 51 | type: 'invalid.multi.set', 52 | message: 'Invalid `config.set(obj)` argument.\n' + 53 | 'expected an object but instead got {objStr}.\n' + 54 | 'SUGGESTED FIX: update the `config.set()` callsite to ' + 55 | 'be a valid object.\n', 56 | objStr: null, 57 | obj: null 58 | }); 59 | 60 | var SetFrozenObject = TypedError({ 61 | type: 'set.frozen.object', 62 | message: 'Cannot `config.set(key, value)`. Config is ' + 63 | 'frozen.\n' + 64 | 'expected `config.set()` not to be called. Instead ' + 65 | 'it was called with {keyPath} and {valueStr}.\n' + 66 | 'SUGGESTED FIX: Do not call `config.set()` it was ' + 67 | 'frozen by someone else.\n', 68 | keyPath: null, 69 | valueStr: null, 70 | value: null 71 | }); 72 | 73 | module.exports = { 74 | InvalidDirname: InvalidDirname, 75 | MissingDatacenter: MissingDatacenter, 76 | DatacenterRequired: DatacenterRequired, 77 | DatacenterFileRequired: DatacenterFileRequired, 78 | InvalidKeyPath: InvalidKeyPath, 79 | NonexistantKeyPath: NonexistantKeyPath, 80 | InvalidMultiSetArgument: InvalidMultiSetArgument, 81 | SetFrozenObject: SetFrozenObject 82 | }; 83 | -------------------------------------------------------------------------------- /get-config-state.js: -------------------------------------------------------------------------------- 1 | var flatten = require('flatten-prototypes'); 2 | var configChain = require('config-chain'); 3 | var parseArgs = require('minimist'); 4 | var join = require('path').join; 5 | var putPath = require('dotty').put; 6 | 7 | module.exports = getConfigState; 8 | 9 | function getConfigState(dirname, opts) { 10 | var cliArgs = parseArgs(opts.argv || process.argv.slice(2)); 11 | var env = opts.env || process.env; 12 | var NODE_ENV = (env.NODE_ENV) ? env.NODE_ENV.toLowerCase() : null; 13 | var dc = opts.datacenterValue; 14 | var blackList = opts.blackList || ['_']; 15 | var isStaging = opts.isStaging; 16 | 17 | // hardcoded to read from `./config` by convention 18 | var configFolder = join(dirname, 'config'); 19 | 20 | // blackList allows you to ensure certain keys from argv 21 | // do not get set on the config object 22 | blackList.forEach(function (key) { 23 | if (cliArgs[key]) { 24 | delete cliArgs[key]; 25 | } 26 | }); 27 | 28 | /* use config-chain module as it contains a set of 29 | "transports" for loading configuration from disk 30 | */ 31 | var configTree = configChain( 32 | // the seed option overwrites everything 33 | opts.seed || null, 34 | // include all CLI arguments 35 | makeDeep(cliArgs), 36 | // load file from --config someFilePath 37 | cliArgs.config || null, 38 | // get datacenter from opts.dc file 39 | dc ? dc : null, 40 | // load ./config/staging.DATACENTER.json 41 | dc && isStaging ? 42 | join(configFolder, 'staging' + '.' + dc.datacenter + '.json') : 43 | null, 44 | // load ./config/staging.json 45 | isStaging ? join(configFolder, 'staging' + '.json') : null, 46 | // load ./config/NODE_ENV.DATACENTER.json 47 | dc && NODE_ENV ? 48 | join(configFolder, NODE_ENV + '.' + dc.datacenter + '.json') : 49 | null, 50 | // load ./config/secrets/secrets.json only in production 51 | NODE_ENV === 'production' ? 52 | join(configFolder, 'secrets', 'secrets.json') : 53 | null, 54 | // load ./config/secrets/secrets-NODE_ENV.json except in production 55 | NODE_ENV !== 'production' ? 56 | join(configFolder, 'secrets', 'secrets' + '-' + NODE_ENV + '.json') : 57 | null, 58 | // load ./config/NODE_ENV.json 59 | NODE_ENV ? join(configFolder, NODE_ENV + '.json') : null, 60 | // load ./config/common.json 61 | join(configFolder, 'common.json'), 62 | // load defaults 63 | opts.defaults || null 64 | ); 65 | 66 | // there is a "bug" in config-chain where it doesn't 67 | // support deep extension. So we flatten deeply 68 | // https://github.com/dominictarr/config-chain/issues/14 69 | var configState = flatten(configTree.store); 70 | 71 | // flattenPrototypes grabs `valueOf` prop from the root prototype 72 | // remove it here. 73 | delete configState.valueOf; 74 | 75 | return configState; 76 | } 77 | 78 | // given a shallow object where keys are key paths like: 79 | // { 'foo.bar': 'baz', 'foo.baz': 'foo' } 80 | // it returns a deep object with the key paths expanded like: 81 | // { 'foo': { 'bar': 'baz', 'baz': 'foo' } } 82 | function makeDeep(obj) { 83 | var deepObj = {}; 84 | 85 | Object.keys(obj).forEach(function (key) { 86 | putPath(deepObj, key, obj[key]); 87 | }); 88 | 89 | return deepObj; 90 | } 91 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var process = require('process'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var Result = require('raynos-rust-result'); 4 | var clone = require('clone'); 5 | 6 | var errors = require('./errors.js'); 7 | var readDatacenter = require('./read-datacenter.js'); 8 | var getConfigState = require('./get-config-state.js'); 9 | var ConfigWrapper = require('./config-wrapper.js'); 10 | 11 | module.exports = fetchConfigSync; 12 | 13 | function fetchConfigSync(dirname, opts) { 14 | if (typeof dirname !== 'string' || dirname === '') { 15 | throw errors.InvalidDirname({ 16 | dirname: dirname, 17 | strDirname: JSON.stringify(dirname) 18 | }); 19 | } 20 | 21 | opts = opts || {}; 22 | 23 | // config is EventEmitter purely for `.emit('error', err)` 24 | var config = new EventEmitter(); 25 | 26 | var result = readDatacenter(opts); 27 | 28 | if (Result.isErr(result)) { 29 | var err = Result.Err(result); 30 | // throw error async. this allows for breaking a 31 | // circular dependency between config & logger. 32 | process.nextTick(function () { 33 | config.emit('error', err); 34 | }); 35 | } else { 36 | opts.datacenterValue = Result.Ok(result); 37 | } 38 | 39 | var configState = getConfigState(dirname, opts); 40 | var localConfigWrapper = ConfigWrapper(configState, opts.loose); 41 | var remoteConfigWrapper = ConfigWrapper({}, opts.loose); 42 | 43 | config.get = localConfigWrapper.get; 44 | config.set = localConfigWrapper.set; 45 | config.freeze = localConfigWrapper.freeze; 46 | config.deepFreeze = localConfigWrapper.deepFreeze; 47 | config.clone = function(){ 48 | return ConfigWrapper(clone(configState)); 49 | }; 50 | config.getRemote = remoteConfigWrapper.get; 51 | config.setRemote = remoteConfigWrapper.set; 52 | 53 | return config; 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zero-config", 3 | "version": "7.1.1", 4 | "description": "A zero configuration configuration loader", 5 | "keywords": [], 6 | "author": "Raynos ", 7 | "repository": "git://github.com/uber/zero-config.git", 8 | "main": "index", 9 | "homepage": "https://github.com/uber/zero-config", 10 | "bugs": { 11 | "url": "https://github.com/uber/zero-config/issues", 12 | "email": "raynos2@gmail.com" 13 | }, 14 | "dependencies": { 15 | "clone": "~0.2.0", 16 | "config-chain": "^1.1.8", 17 | "deep-extend": "^0.4.0", 18 | "dotty": "0.0.2", 19 | "error": "^4.1.1", 20 | "flatten-prototypes": "^4.0.0", 21 | "minimist": "^1.1.0", 22 | "process": "^0.7.0", 23 | "raynos-rust-result": "0.1.0-improvement3" 24 | }, 25 | "devDependencies": { 26 | "coveralls": "^2.10.0", 27 | "fixtures-fs": "^1.0.2", 28 | "istanbul": "^0.2.7", 29 | "jshint": "^2.5.0", 30 | "opn": "^0.1.2", 31 | "pre-commit": "0.0.5", 32 | "tap-spec": "^0.2.0", 33 | "tape": "^2.14.0" 34 | }, 35 | "licenses": [ 36 | { 37 | "type": "MIT", 38 | "url": "http://github.com/uber/zero-config/raw/master/LICENSE" 39 | } 40 | ], 41 | "scripts": { 42 | "test": "npm run jshint -s && node test/index.js | tap-spec", 43 | "unit-test": "node test/index.js | tap-spec", 44 | "jshint-pre-commit": "jshint --verbose $(git diff --cached --name-only | grep '\\.js$')", 45 | "jshint": "jshint --verbose $(git ls-files | grep '\\.js$')", 46 | "cover": "istanbul cover --report none --print detail test/index.js", 47 | "view-cover": "istanbul report html && opn ./coverage/index.html", 48 | "travis": "npm run cover -s && istanbul report lcov && ((cat coverage/lcov.info | coveralls) || exit 0)" 49 | }, 50 | "engine": { 51 | "node": ">= 0.8.x" 52 | }, 53 | "pre-commit": [ 54 | "jshint-pre-commit", 55 | "unit-test" 56 | ], 57 | "playdoh-version": "2.5.0" 58 | } 59 | -------------------------------------------------------------------------------- /read-datacenter.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var Result = require('raynos-rust-result'); 3 | 4 | var errors = require('./errors.js'); 5 | 6 | module.exports = readDatacenter; 7 | 8 | function readDatacenter(opts) { 9 | var env = opts.env || process.env; 10 | var NODE_ENV = env.NODE_ENV; 11 | 12 | // specifying a datacenter is optional in dev but required 13 | // in production. 14 | if (NODE_ENV === 'production' && !opts.dc && !opts.dcValue) { 15 | throw errors.DatacenterRequired({ 16 | strOpts: JSON.stringify(opts) 17 | }); 18 | } 19 | 20 | var result; 21 | 22 | if (opts.dcValue) { 23 | result = Result.Ok({ 24 | 'datacenter': opts.dcValue.replace(/\s/g, '') 25 | }); 26 | } else if (opts.dc) { 27 | var fileResult = readFileOrError(opts.dc); 28 | if (Result.isErr(fileResult)) { 29 | var err = Result.Err(fileResult); 30 | // create error synchronously for correct stack trace 31 | if (NODE_ENV === 'production') { 32 | result = Result.Err(errors.DatacenterFileRequired({ 33 | path: err.path, 34 | errno: err.errno, 35 | code: err.code, 36 | syscall: err.syscall 37 | })); 38 | } else { 39 | result = Result.Err(errors.MissingDatacenter({ 40 | path: err.path, 41 | errno: err.errno, 42 | code: err.code, 43 | syscall: err.syscall 44 | })); 45 | } 46 | } else { 47 | result = Result.Ok({ 48 | 'datacenter': Result.Ok(fileResult) 49 | .replace(/\s/g, '') 50 | }); 51 | } 52 | } else { 53 | result = Result.Ok(null); 54 | } 55 | 56 | return result; 57 | } 58 | 59 | // break try catch into small function to avoid v8 de-optimization 60 | function readFileOrError(uri) { 61 | var content; 62 | try { 63 | content = fs.readFileSync(uri, 'utf8'); 64 | } catch (err) { 65 | return Result.Err(err); 66 | } 67 | 68 | return Result.Ok(content); 69 | } 70 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var path = require('path'); 3 | var withFixtures = require('fixtures-fs'); 4 | 5 | var fetchConfig = require('../index.js'); 6 | 7 | function catchFn(fn) { 8 | var err; 9 | try { 10 | fn(); 11 | } catch (e) { 12 | err = e; 13 | } 14 | 15 | return err; 16 | } 17 | 18 | test('fetchConfig can be called as a function', function (assert) { 19 | var c = fetchConfig(__dirname); 20 | 21 | assert.equal(typeof c, 'object'); 22 | assert.equal(typeof c.get, 'function'); 23 | assert.equal(typeof c.getRemote, 'function'); 24 | assert.equal(typeof c.on, 'function'); 25 | assert.equal(typeof c.once, 'function'); 26 | 27 | assert.end(); 28 | }); 29 | 30 | test('fetchConfig throws without dirname', function (assert) { 31 | assert.throws(function () { 32 | fetchConfig(); 33 | }, /invalid __dirname/); 34 | 35 | assert.end(); 36 | }); 37 | 38 | test('fetchConfig creates a config object', function (assert) { 39 | var config = fetchConfig(__dirname); 40 | 41 | assert.equal(typeof config, 'object'); 42 | assert.equal(typeof config.get, 'function'); 43 | assert.end(); 44 | }); 45 | 46 | test('fetchConfig.get() does not have valueOf key', function (assert) { 47 | var config = fetchConfig(__dirname); 48 | 49 | assert.notOk(Object.hasOwnProperty.call(config.get(), 'valueOf')); 50 | assert.end(); 51 | }); 52 | 53 | test('mutating fetchConfig.get() does not mutate state', function (assert) { 54 | var config = fetchConfig(__dirname); 55 | var object = { foo: 'bar' }; 56 | var array = ['foo']; 57 | 58 | config.set('shouldNotChangeObject', object); 59 | config.get('shouldNotChangeObject').anotherValue = 'shouldNotSet'; 60 | 61 | config.set('shouldNotChangeArray', array); 62 | config.get('shouldNotChangeArray')[0] = 'shouldNotSet'; 63 | 64 | assert.notOk(Object.hasOwnProperty.call( 65 | config.get('shouldNotChangeObject'), 'anotherValue')); 66 | 67 | assert.notEqual(config.get('shouldNotChangeArray')[0], 'shouldNotSet'); 68 | 69 | assert.end(); 70 | }); 71 | 72 | test('fetchConfig reads from argv', function (assert) { 73 | var argv = ['--foo', 'bar', '--baz.lulz', 'some value']; 74 | 75 | var config = fetchConfig(__dirname, { argv: argv }); 76 | 77 | assert.equal(config.get('foo'), 'bar'); 78 | assert.equal(config.get('baz.lulz'), 'some value'); 79 | assert.end(); 80 | }); 81 | 82 | test('config loads config files', withFixtures(__dirname, { 83 | config: { 84 | 'common.json': JSON.stringify({ 85 | port: 3000, 86 | nested: { 87 | key: true, 88 | shadowed: ':(' 89 | }, 90 | freeKey: 'nice' 91 | }), 92 | 'test.json': JSON.stringify({ 93 | port: 4000, 94 | someKey: 'ok', 95 | nested: { 96 | extra: 40, 97 | shadowed: ':)' 98 | } 99 | }), 100 | secrets: { 101 | 'secrets.json': JSON.stringify({ 102 | awsKey: 'ABC123DEF' 103 | }), 104 | }, 105 | } 106 | }, function (assert) { 107 | var env = { 108 | 'NODE_ENV': 'test' 109 | }; 110 | 111 | var config = fetchConfig(__dirname, { 112 | env: env, 113 | loose: true 114 | }); 115 | assert.equal(config.get('port'), 4000); 116 | assert.equal(config.get('nested.key'), true); 117 | assert.equal(config.get('nested.shadowed'), ':)'); 118 | assert.equal(config.get('nested.extra'), 40); 119 | assert.equal(config.get('someKey'), 'ok'); 120 | assert.equal(config.get('freeKey'), 'nice'); 121 | assert.notEqual(config.get('awsKey'), 'ABC123DEF'); 122 | assert.equal(config.get('freeKey'), 'nice'); 123 | assert.equal(config.get('nested.shadowed'), ':)'); 124 | assert.equal(config.get('fakeKey', undefined)); 125 | 126 | 127 | var conf = config.get(); 128 | assert.equal(conf.someKey, 'ok'); 129 | assert.equal(conf.freeKey, 'nice'); 130 | assert.equal(conf.port, 4000); 131 | assert.notEqual(conf.awsKey, 'ABC123DEF'); 132 | assert.deepEqual(conf.nested, { 133 | key: true, 134 | shadowed: ':)', 135 | extra: 40 136 | }); 137 | 138 | 139 | 140 | assert.end(); 141 | })); 142 | 143 | test('env case gets normalized', withFixtures(__dirname, { 144 | config: { 145 | secrets: { 146 | 'secrets.json': JSON.stringify({ 147 | awsKey: 'abc123' 148 | }), 149 | 'secrets-TEST': JSON.stringify({ 150 | awsKey: 'def456' 151 | }) 152 | } 153 | } 154 | }, function (assert) { 155 | var env = { 156 | 'NODE_ENV': 'PRODUCTION' 157 | }; 158 | 159 | var config = fetchConfig(__dirname, { env: env , dcValue: 'peak1'}); 160 | 161 | assert.equal(config.get('awsKey'), 'abc123'); 162 | 163 | var conf = config.get(); 164 | assert.equal(conf.awsKey, 'abc123'); 165 | 166 | env = { 167 | 'NODE_ENV': 'production' 168 | }; 169 | 170 | config = fetchConfig(__dirname, { env: env , dcValue: 'peak1'}); 171 | 172 | assert.equal(config.get('awsKey'), 'abc123'); 173 | 174 | conf = config.get(); 175 | assert.equal(conf.awsKey, 'abc123'); 176 | 177 | assert.end(); 178 | })); 179 | 180 | test('error thrown when not in loose mode', withFixtures(__dirname, { 181 | config: { 182 | 'common.json': JSON.stringify({ 183 | port: 3000, 184 | nested: { 185 | key: true, 186 | shadowed: ':(' 187 | }, 188 | freeKey: 'nice' 189 | }), 190 | 'test.json': JSON.stringify({ 191 | port: 4000, 192 | someKey: 'ok', 193 | nested: { 194 | extra: 40, 195 | shadowed: ':)' 196 | } 197 | }) 198 | } 199 | }, function (assert) { 200 | var env = { 201 | 'NODE_ENV': 'test' 202 | }; 203 | 204 | var config = fetchConfig(__dirname, { 205 | env: env, 206 | loose: false 207 | }); 208 | 209 | assert.equal(config.get('freeKey'), 'nice'); 210 | assert.throws(function() { 211 | config.get('fakeKey'); 212 | }, /nonexistant keyPath/); 213 | 214 | var conf = config.get(); 215 | assert.equal(conf.someKey, 'ok'); 216 | assert.equal(conf.freeKey, 'nice'); 217 | assert.equal(conf.port, 4000); 218 | assert.deepEqual(conf.nested, { 219 | key: true, 220 | shadowed: ':)', 221 | extra: 40 222 | }); 223 | 224 | assert.end(); 225 | })); 226 | 227 | 228 | test('env config files take presidence', withFixtures(__dirname, { 229 | config: { 230 | 'common.json': JSON.stringify({ 231 | port: 3000, 232 | nested: { 233 | key: true, 234 | shadowed: ':(' 235 | }, 236 | freeKey: 'nice' 237 | }), 238 | 'test.json': JSON.stringify({ 239 | port: 4000, 240 | someKey: 'ok', 241 | nested: { 242 | extra: 40, 243 | shadowed: ':)' 244 | } 245 | }), 246 | secrets : { 247 | 'secrets.json': JSON.stringify({ 248 | awsKey: 'ABC123DEF' 249 | }), 250 | 'secrets-test.json': JSON.stringify({ 251 | awsKey: 'ZYX098WVU' 252 | }) 253 | }, 254 | } 255 | }, function (assert) { 256 | var env = { 257 | 'NODE_ENV': 'test' 258 | }; 259 | 260 | var config = fetchConfig(__dirname, { env: env }); 261 | assert.equal(config.get('awsKey'), 'ZYX098WVU'); 262 | 263 | var conf = config.get(); 264 | assert.equal(conf.awsKey, 'ZYX098WVU'); 265 | 266 | //reset to production 267 | env = { 268 | 'NODE_ENV': 'production' 269 | }; 270 | 271 | config = fetchConfig(__dirname, { env: env, dcValue: 'peak1'}); 272 | assert.equal(config.get('awsKey'), 'ABC123DEF'); 273 | conf = config.get(); 274 | assert.equal(conf.awsKey, 'ABC123DEF'); 275 | 276 | assert.end(); 277 | })); 278 | 279 | test('config loads from datacenter file', withFixtures(__dirname, { 280 | 'config': { 281 | 'common.json': JSON.stringify({ 282 | a: 'a', 283 | b: { 284 | c: 'c', 285 | d: 'd' 286 | } 287 | }), 288 | 'production.json': JSON.stringify({ 289 | b: { 290 | c: 'c2' 291 | } 292 | }), 293 | 'production.peak1.json': JSON.stringify({ 294 | a: 'a3' 295 | }) 296 | }, 297 | 'datacenter': 'peak1' 298 | }, function (assert) { 299 | var env = { 300 | 'NODE_ENV': 'production' 301 | }; 302 | 303 | var config = fetchConfig(__dirname, { 304 | env: env, 305 | dc: path.join(__dirname, 'datacenter') 306 | }); 307 | 308 | assert.equal(config.get('datacenter'), 'peak1'); 309 | assert.equal(config.get('a'), 'a3'); 310 | assert.equal(config.get('b.c'), 'c2'); 311 | assert.equal(config.get('b.d'), 'd'); 312 | assert.deepEqual(config.get('b'), { c: 'c2', d: 'd' }); 313 | 314 | assert.end(); 315 | })); 316 | 317 | test('config loads from dcValue', withFixtures(__dirname, { 318 | 'config': { 319 | 'common.json': JSON.stringify({ 320 | a: 'a', 321 | b: { 322 | c: 'c', 323 | d: 'd' 324 | } 325 | }), 326 | 'production.json': JSON.stringify({ 327 | b: { 328 | c: 'c2' 329 | } 330 | }), 331 | 'production.peak1.json': JSON.stringify({ 332 | a: 'a3' 333 | }) 334 | } 335 | }, function (assert) { 336 | var env = { 337 | 'NODE_ENV': 'production' 338 | }; 339 | 340 | var config = fetchConfig(__dirname, { 341 | env: env, 342 | dcValue: 'peak1' 343 | }); 344 | 345 | assert.equal(config.get('datacenter'), 'peak1'); 346 | assert.equal(config.get('a'), 'a3'); 347 | assert.equal(config.get('b.c'), 'c2'); 348 | assert.equal(config.get('b.d'), 'd'); 349 | assert.deepEqual(config.get('b'), { c: 'c2', d: 'd' }); 350 | 351 | assert.end(); 352 | })); 353 | 354 | test('config reads a datacenter file', withFixtures(__dirname, { 355 | datacenter: 'peak1' 356 | }, function (assert) { 357 | var config = fetchConfig(__dirname, { 358 | dc: path.join(__dirname, 'datacenter') 359 | }); 360 | 361 | assert.equal(config.get('datacenter'), 'peak1'); 362 | 363 | assert.end(); 364 | })); 365 | 366 | test('non existent datacenter file', function (assert) { 367 | var config = fetchConfig(__dirname, { 368 | dc: path.join(__dirname, 'datacenter') 369 | }); 370 | 371 | config.once('error', function (err) { 372 | assert.equal(err.type, 'missing.datacenter.file'); 373 | assert.equal(err.warning, true); 374 | assert.equal(err.code, 'ENOENT'); 375 | assert.ok(/no such file/.test(err.message)); 376 | 377 | assert.end(); 378 | }); 379 | }); 380 | 381 | test('non existant datacenter file in production', function (assert) { 382 | var config = fetchConfig(__dirname, { 383 | dc: path.join(__dirname, 'datacenter'), 384 | env: { NODE_ENV: 'production' } 385 | }); 386 | 387 | config.once('error', function (err) { 388 | assert.equal(err.type, 'datacenter.file.required'); 389 | assert.equal(err.code, 'ENOENT'); 390 | assert.ok(/no such file/.test(err.message)); 391 | assert.ok(/datacenter file must exist/.test(err.message)); 392 | 393 | assert.end(); 394 | }); 395 | }); 396 | 397 | test('will load from --config', withFixtures(__dirname, { 398 | 'systemConfig': { 399 | 'config.json': JSON.stringify({ a: 42 }) 400 | }, 401 | 'config': { 402 | 'common.json': JSON.stringify({ a: 50, b: 20 }) 403 | } 404 | }, function (assert) { 405 | var configPath = path.join(__dirname, 406 | 'systemConfig', 'config.json'); 407 | 408 | var config = fetchConfig(__dirname, { 409 | argv: ['--config', configPath] 410 | }); 411 | 412 | assert.equal(config.get('b'), 20); 413 | assert.equal(config.get('a'), 42); 414 | 415 | assert.end(); 416 | })); 417 | 418 | test('no opts.dcValue in production', function (assert) { 419 | var err = catchFn(function () { 420 | fetchConfig(__dirname, { 421 | env: { NODE_ENV: 'production' } 422 | }); 423 | }); 424 | 425 | assert.ok(err); 426 | assert.equal(err.type, 'datacenter.option.required'); 427 | assert.ok(/expected `opts.dcValue`/.test(err.message)); 428 | assert.ok(/`opts.dcValue` is not optional/.test(err.message)); 429 | 430 | assert.end(); 431 | }); 432 | 433 | test('config loads from staging file', withFixtures(__dirname, { 434 | 'config': { 435 | 'common.json': JSON.stringify({ 436 | a: 'a', 437 | b: { 438 | c: 'c', 439 | d: 'd' 440 | } 441 | }), 442 | 'production.json': JSON.stringify({ 443 | b: { 444 | c: 'c2' 445 | } 446 | }), 447 | 'production.peak1.json': JSON.stringify({ 448 | a: 'a3' 449 | }), 450 | 'staging.json': JSON.stringify({ 451 | b: { 452 | d: 'd2' 453 | } 454 | }), 455 | 'staging.peak1.json': JSON.stringify({ 456 | b: { 457 | e: 'e' 458 | } 459 | }) 460 | }, 461 | 'datacenter': 'peak1' 462 | }, function (assert) { 463 | var env = { 464 | 'NODE_ENV': 'production' 465 | }; 466 | 467 | var config = fetchConfig(__dirname, { 468 | env: env, 469 | dc: path.join(__dirname, 'datacenter'), 470 | isStaging: true 471 | }); 472 | 473 | assert.equal(config.get('datacenter'), 'peak1'); 474 | assert.equal(config.get('a'), 'a3'); 475 | assert.equal(config.get('b.c'), 'c2'); 476 | assert.equal(config.get('b.d'), 'd2'); 477 | assert.equal(config.get('b.e'), 'e'); 478 | assert.deepEqual(config.get('b'), { c: 'c2', d: 'd2', e: 'e' }); 479 | 480 | assert.end(); 481 | })); 482 | 483 | test('blackList feature', function (assert) { 484 | var config = fetchConfig(__dirname, { 485 | blackList: ['foo', 'bar'], 486 | argv: ['--foo', 'foo', '--bar', 'bar', '--baz', 'baz'], 487 | loose: true 488 | }); 489 | 490 | assert.equal(config.get('foo'), undefined); 491 | assert.equal(config.get('bar'), undefined); 492 | assert.equal(config.get('baz'), 'baz'); 493 | 494 | assert.end(); 495 | }); 496 | 497 | test('blackList unset keys do not break', function (assert) { 498 | var config = fetchConfig(__dirname, { 499 | blackList: ['foo', 'bar'], 500 | argv: ['--baz', 'baz'], 501 | loose: true 502 | }); 503 | 504 | assert.equal(config.get('foo'), undefined); 505 | assert.equal(config.get('bar'), undefined); 506 | assert.equal(config.get('baz'), 'baz'); 507 | 508 | assert.end(); 509 | }); 510 | 511 | test('config.set()', function (assert) { 512 | var config = fetchConfig(__dirname); 513 | 514 | config.set('key', 'value'); 515 | config.set('nested.key', 'value2'); 516 | config.set('nested.key3', 'value3'); 517 | config.set(['nested', 'key4'], 'value4'); 518 | config.set(['nested', 'key.with.dots5'], 'value5'); 519 | 520 | assert.equal(config.get('key'), 'value', 'flat key'); 521 | assert.equal(config.get('nested.key'), 'value2', 'nested key'); 522 | assert.equal(config.get('nested.key3'), 'value3', 'child nested key'); 523 | assert.equal(config.get('nested.key4'), 'value4', 'array key'); 524 | assert.equal(config.get(['nested', 'key.with.dots5']), 525 | 'value5', 'array key with dots'); 526 | 527 | assert.end(); 528 | }); 529 | 530 | test('config.set(array)', function (assert) { 531 | var config = fetchConfig(__dirname); 532 | 533 | config.set('key', {'foo': 'bar'}); 534 | config.set('key', [1, 2, 3]); 535 | 536 | var val = config.get('key'); 537 | assert.ok(Array.isArray(val)); 538 | assert.deepEqual(val, [1, 2, 3]); 539 | assert.end(); 540 | }); 541 | 542 | test('config.set() deep', function (assert) { 543 | var config = fetchConfig(__dirname); 544 | 545 | config.set('key', { 546 | foo: 'bar', 547 | deep: { 548 | 'thingy': 'thongy' 549 | } 550 | }); 551 | 552 | config.set('key', { 553 | newKey: 'woh', 554 | deep: { 555 | 'other': 'yeah' 556 | } 557 | }); 558 | 559 | var k = config.get('key'); 560 | assert.deepEqual(k, { 561 | foo: 'bar', 562 | newKey: 'woh', 563 | deep: { 564 | 'thingy': 'thongy', 565 | 'other': 'yeah' 566 | } 567 | }); 568 | 569 | assert.end(); 570 | }); 571 | 572 | test('config.set(undefined) throws', function (assert) { 573 | var config = fetchConfig(__dirname); 574 | 575 | assert.throws(function () { 576 | config.set(undefined, 42); 577 | }, /invalid keypath/); 578 | 579 | assert.end(); 580 | 581 | }); 582 | 583 | test('config({ seed: seed })', function (assert) { 584 | var argv = [ 585 | '--foo', 'bar', 586 | '--baz.lulz', 'some value', 587 | '--baz.foob', 'thingy' 588 | ]; 589 | 590 | var config = fetchConfig(__dirname, { 591 | argv: argv, 592 | seed: { 593 | baz: { 594 | lulz: 42 595 | }, 596 | bar: 'foo' 597 | } 598 | }); 599 | 600 | assert.equal(config.get('foo'), 'bar'); 601 | assert.equal(config.get('baz.lulz'), 42); 602 | assert.equal(config.get('baz.foob'), 'thingy'); 603 | assert.equal(config.get('bar'), 'foo'); 604 | 605 | assert.end(); 606 | }); 607 | 608 | test('config({ defaults: defaults })', function (assert) { 609 | var argv = [ 610 | '--foo', 'bar', 611 | '--baz.lulz', 'some value', 612 | '--baz.foob', 'thingy' 613 | ]; 614 | 615 | var config = fetchConfig(__dirname, { 616 | argv: argv, 617 | seed: { 618 | baz: { 619 | lulz: 42 620 | }, 621 | bar: 'foo', 622 | shouldBeUndefined: undefined, 623 | shouldBeOverwritten: 'overwrittenValue', 624 | shouldBeMerged: { 625 | deep: { 626 | foo: 'bar' 627 | } 628 | } 629 | }, 630 | defaults: { 631 | shouldBeUndefined: 'defined', 632 | shouldBeFalse: false, 633 | shouldBeTrue: true, 634 | shouldBeNested: { 635 | key: 'value' 636 | }, 637 | shouldBeOverwritten: 'value', 638 | shouldBeMerged: { 639 | deep: { 640 | bar: 'baz' 641 | } 642 | } 643 | }, 644 | loose: true 645 | }); 646 | 647 | assert.equal(config.get('foo'), 'bar'); 648 | assert.equal(config.get('baz.lulz'), 42); 649 | assert.equal(config.get('baz.foob'), 'thingy'); 650 | assert.equal(config.get('shouldBeUndefined'), undefined); 651 | assert.equal(config.get('shouldBeFalse'), false); 652 | assert.equal(config.get('shouldBeTrue'), true); 653 | assert.equal(config.get('shouldBeNested.key'), 'value'); 654 | assert.equal(config.get('shouldBeOverwritten'), 'overwrittenValue'); 655 | assert.equal(config.get('shouldBeMerged.deep.foo'), 'bar'); 656 | assert.equal(config.get('shouldBeMerged.deep.bar'), 'baz'); 657 | 658 | assert.end(); 659 | }); 660 | 661 | test('config.set(entireObj)', function t(assert) { 662 | var config = fetchConfig(__dirname); 663 | 664 | config.set('foo', 'bar'); 665 | config.set('baz', { 42: 42 }); 666 | 667 | assert.equal(config.get('foo'), 'bar'); 668 | assert.deepEqual(config.get('baz'), { '42': 42 }); 669 | 670 | config.set({ 671 | foo: 'new-key', 672 | baz: { 50: 50 }, 673 | other: 'thing' 674 | }); 675 | 676 | assert.equal(config.get('foo'), 'new-key'); 677 | assert.deepEqual(config.get('baz'), { '42': 42, '50': 50 }); 678 | assert.equal(config.get('other'), 'thing'); 679 | 680 | assert.end(); 681 | }); 682 | 683 | test('config.set(weirdValue)', function t(assert) { 684 | var config = fetchConfig(__dirname); 685 | 686 | assert.throws(function () { 687 | config.set(42); 688 | }, /Invalid `config\.set\(obj\)` argument/); 689 | 690 | assert.end(); 691 | }); 692 | 693 | test('config.setRemote()', function t(assert) { 694 | var config = fetchConfig(__dirname); 695 | 696 | config.setRemote('foo', 'bar'); 697 | 698 | assert.deepEqual(config.getRemote(), { 699 | 'foo': 'bar' 700 | }); 701 | 702 | assert.end(); 703 | }); 704 | 705 | test('config.freeze()', function t(assert) { 706 | var config = fetchConfig(__dirname); 707 | 708 | config.set('foo', 'bar'); 709 | 710 | assert.equal(config.get('foo'), 'bar'); 711 | 712 | config.freeze(); 713 | 714 | assert.throws(function () { 715 | config.set('foo', 'baz'); 716 | }, /Config is frozen/); 717 | 718 | assert.throws(function () { 719 | config.set('bar', 'baz'); 720 | }, /Config is frozen/); 721 | 722 | assert.end(); 723 | }); 724 | 725 | test('config.deepFreeze()', function t(assert) { 726 | var config = fetchConfig(__dirname); 727 | 728 | var deep = {bar: {baz: true}}; 729 | 730 | config.set('foo', deep); 731 | 732 | assert.deepEqual(config.get('foo'), deep); 733 | 734 | config.deepFreeze(); 735 | 736 | assert.throws(function () { 737 | config.set('foo', 'notBaz'); 738 | }, /Config is frozen/); 739 | 740 | assert.throws(function () { 741 | config.set('bar', 'baz'); 742 | }, /Config is frozen/); 743 | 744 | var immutable = config.get('foo'); 745 | immutable.bar.baz = false; 746 | assert.equal(immutable.bar.baz, true); 747 | 748 | assert.throws(function() { 749 | 'use strict'; 750 | immutable.bar.baz = false; 751 | }, /Cannot assign to read only property/); 752 | 753 | assert.end(); 754 | }); 755 | 756 | test('config.deepFreeze() function strict-mode', function t(assert) { 757 | 'use strict'; 758 | var config = fetchConfig(__dirname); 759 | 760 | function bar() { 761 | return 'bar'; 762 | } 763 | 764 | bar.biz = 'biz'; 765 | 766 | config.set('foo', bar); 767 | 768 | assert.deepEqual(config.get('foo'), bar); 769 | 770 | config.deepFreeze(); 771 | 772 | assert.throws(function a() { 773 | config.get('foo').biz = 'should throw'; 774 | }, /Cannot assign to read only property/); 775 | 776 | assert.end(); 777 | }); 778 | 779 | test('config.clone()', function t(assert) { 780 | var config = fetchConfig(__dirname); 781 | 782 | config.set('foo', 'bar'); 783 | 784 | var clonedConfig = config.clone(); 785 | clonedConfig.set('foo', 'baz'); 786 | 787 | assert.equal(config.get('foo'), 'bar'); 788 | assert.equal(clonedConfig.get('foo'), 'baz'); 789 | assert.notEqual(config, clonedConfig); 790 | 791 | assert.end(); 792 | }); 793 | --------------------------------------------------------------------------------