Skip to content
This repository was archived by the owner on Sep 25, 2020. It is now read-only.

Commit af9b7f6

Browse files
committed
Merge pull request #4 from uber/set-remote
Implement `setRemote`, `multiSet()` and `freeze()`
2 parents 178dc5a + a270759 commit af9b7f6

File tree

7 files changed

+423
-188
lines changed

7 files changed

+423
-188
lines changed

README.md

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,22 @@ You can also call the process with
5353
### `var config = fetchConfig(dirname, opts)`
5454

5555
```ocaml
56-
playdoh-server/config := (dirname: String, opts?: {
56+
type Keypath : String | Array<String>
57+
58+
zero-config := (dirname: String, opts?: {
5759
argv?: Array<String>,
5860
dc?: String,
5961
blackList?: Array<String>,
6062
env?: Object<String, String>,
6163
seed?: Object<String, Any>
6264
}) => {
63-
get: (keypath?: String) => Any,
64-
set: (keypath: String, value: Any) => void,
65-
__state: Object<String, Any>
65+
get: (keypath?: Keypath) => Any,
66+
set: ((keypath: Keypath, value: Any) => void) &
67+
(value: Any) => void,
68+
freeze: () => void,
69+
getRemote: (keypath?: Keypath) => Any,
70+
setRemote: ((keypath: Keypath, value: Any) => void) &
71+
(value: Any) => void
6672
}
6773
```
6874

@@ -214,6 +220,46 @@ You can call `config.set("port", 9001)` to set the port value.
214220
You can call `config.set("playdoh-logger.kafka.port", 9001)` to
215221
set then nested kafka port config option.
216222

223+
Note you can also call `config.set(entireObject)` to merge an
224+
entire object into the `config` instance. This will use
225+
deep extend to set all the key / value pairs in `entireObject`
226+
onto the config instance.
227+
228+
#### `config.freeze()`
229+
230+
Since the `config` object is supposed to represent a set of
231+
static, immutable configuration that's loaded at process
232+
startup time it would be useful to enforce this.
233+
234+
Once you are ready to stop mutating `config` you can call
235+
`.freeze()`. Any future calls to `.set()` will throw a
236+
config frozen exception.
237+
238+
Note that you can always call `config.setRemote()` as that is
239+
not effected by `.freeze()`
240+
241+
#### `var value = config.getRemote(keypath)`
242+
243+
The same as `config.get()` but gets from a different in memory
244+
object then `config.get()`.
245+
246+
It's recommended that you use `config.get()` and `config.set()`
247+
for any local configuration that is static and effectively
248+
immutable after process startup.
249+
250+
You can use `config.getRemote()` and `config.setRemote()` for
251+
any dynamic configuration that is effectively controlled
252+
remotely outside your program.
253+
254+
#### `config.setRemote(keypath, value)`
255+
256+
The same as `config.set()` but sets to a different in memory
257+
objec then `config.set()`.
258+
259+
You can use `config.getRemote()` and `config.setRemote()` for
260+
any dynamic configuration that is effectively controlled
261+
remotely outside your program.
262+
217263
## Installation
218264

219265
`npm install zero-config`

config-wrapper.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
module.exports = ConfigWrapper;
8+
9+
function ConfigWrapper(configObject) {
10+
var frozen = false;
11+
12+
return {
13+
get: getKey,
14+
set: setKey,
15+
freeze: freeze
16+
};
17+
18+
function getKey(keyPath) {
19+
if (!keyPath) {
20+
return configObject;
21+
}
22+
23+
return getPath(configObject, keyPath);
24+
}
25+
26+
function setKey(keyPath, value) {
27+
if (frozen) {
28+
throw errors.SetFrozenObject({
29+
keyPath: keyPath,
30+
valueStr: JSON.stringify(value),
31+
value: value
32+
});
33+
}
34+
35+
if (arguments.length === 1) {
36+
return multiSet(keyPath);
37+
}
38+
39+
if (!isValidKeyPath(keyPath)) {
40+
throw errors.InvalidKeyPath({
41+
keyPath: keyPath
42+
});
43+
}
44+
45+
var v = getKey(keyPath);
46+
47+
if (typeof v === 'object' && v !== null) {
48+
v = deepExtend({}, v, value);
49+
} else {
50+
v = value;
51+
}
52+
53+
return putPath(configObject, keyPath, v);
54+
}
55+
56+
function freeze() {
57+
frozen = true;
58+
}
59+
60+
function multiSet(obj) {
61+
if (obj === null || typeof obj !== 'object') {
62+
throw errors.InvalidMultiSetArgument({
63+
objStr: JSON.stringify(obj),
64+
obj: obj
65+
});
66+
}
67+
68+
Object.keys(obj).forEach(setEachKey);
69+
70+
function setEachKey(key) {
71+
setKey([key], obj[key]);
72+
}
73+
}
74+
}
75+
76+
function isValidKeyPath(keyPath) {
77+
return typeof keyPath === 'string' ||
78+
Array.isArray(keyPath);
79+
}

errors.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.dc` to be passed to fetchConfig.\n' +
22+
'must call `fetchConfig(__dirname, { dc: "..." }).\n' +
23+
'instead I got opts: {strOpts}.\n' +
24+
'`opts.dc` is not optional when NODE_ENV is "production".\n' +
25+
'SUGGESTED FIX: update the `fetchConfig()` callsite.\n'
26+
});
27+
28+
var DatacenterFileRequired = TypedError({
29+
type: 'datacenter.file.required',
30+
message: 'no such file or directory \'{path}\'.\n' +
31+
'expected to find datacenter configuration at {path}.\n' +
32+
'when NODE_ENV is "production" the datacenter file must exist.\n' +
33+
'SUGGESTED FIX: configure your system so it has a datacenter file.\n'
34+
});
35+
36+
var InvalidKeyPath = TypedError({
37+
type: 'invalid.keypath',
38+
message: 'specified an invalid keypath to `config.set()`.\n' +
39+
'expected a string but instead got {keyPath}.\n' +
40+
'SUGGESTED FIX: update the `config.set()` callsite.\n'
41+
});
42+
43+
var InvalidMultiSetArgument = TypedError({
44+
type: 'invalid.multi.set',
45+
message: 'Invalid `config.set(obj)` argument.\n' +
46+
'expected an object but instead got {objStr}.\n' +
47+
'SUGGESTED FIX: update the `config.set()` callsite to ' +
48+
'be a valid object.\n',
49+
objStr: null,
50+
obj: null
51+
});
52+
53+
var SetFrozenObject = TypedError({
54+
type: 'set.frozen.object',
55+
message: 'Cannot `config.set(key, value)`. Config is ' +
56+
'frozen.\n' +
57+
'expected `config.set()` not to be called. Instead ' +
58+
'it was called with {keyPath} and {valueStr}.\n' +
59+
'SUGGESTED FIX: Do not call `config.set()` it was ' +
60+
'frozen by someone else.\n',
61+
keyPath: null,
62+
valueStr: null,
63+
value: null
64+
});
65+
66+
module.exports = {
67+
InvalidDirname: InvalidDirname,
68+
MissingDatacenter: MissingDatacenter,
69+
DatacenterRequired: DatacenterRequired,
70+
DatacenterFileRequired: DatacenterFileRequired,
71+
InvalidKeyPath: InvalidKeyPath,
72+
InvalidMultiSetArgument: InvalidMultiSetArgument,
73+
SetFrozenObject: SetFrozenObject
74+
};

get-config-state.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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;
13+
var dc = opts.datacenterValue;
14+
var blackList = opts.blackList || ['_'];
15+
16+
// hardcoded to read from `./config` by convention
17+
var configFolder = join(dirname, 'config');
18+
19+
// blackList allows you to ensure certain keys from argv
20+
// do not get set on the config object
21+
blackList.forEach(function (key) {
22+
if (cliArgs[key]) {
23+
delete cliArgs[key];
24+
}
25+
});
26+
27+
/* use config-chain module as it contains a set of
28+
"transports" for loading configuration from disk
29+
*/
30+
var configTree = configChain(
31+
// the seed option overwrites everything
32+
opts.seed || null,
33+
// include all CLI arguments
34+
makeDeep(cliArgs),
35+
// load file from --config someFilePath
36+
cliArgs.config || null,
37+
// get datacenter from opts.dc file
38+
dc ? dc : null,
39+
// load ./config/NODE_ENV.DATACENTER.json
40+
dc && NODE_ENV ?
41+
join(configFolder, NODE_ENV + '.' + dc.datacenter + '.json') :
42+
null,
43+
// load ./config/NODE_ENV.json
44+
NODE_ENV ? join(configFolder, NODE_ENV + '.json') : null,
45+
// load ./config/common.json
46+
join(configFolder, 'common.json')
47+
);
48+
49+
// there is a "bug" in config-chain where it doesn't
50+
// support deep extension. So we flatten deeply
51+
// https://github.com/dominictarr/config-chain/issues/14
52+
var configState = flatten(configTree.store);
53+
54+
return configState;
55+
}
56+
57+
// given a shallow object where keys are key paths like:
58+
// { 'foo.bar': 'baz', 'foo.baz': 'foo' }
59+
// it returns a deep object with the key paths expanded like:
60+
// { 'foo': { 'bar': 'baz', 'baz': 'foo' } }
61+
function makeDeep(obj) {
62+
var deepObj = {};
63+
64+
Object.keys(obj).forEach(function (key) {
65+
putPath(deepObj, key, obj[key]);
66+
});
67+
68+
return deepObj;
69+
}

0 commit comments

Comments
 (0)