Skip to content

Commit

Permalink
Full support of periods
Browse files Browse the repository at this point in the history
  • Loading branch information
A-312 committed Jan 5, 2020
1 parent e5a7264 commit 8cadd2a
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 43 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"dependencies": {
"lodash.clonedeep": "4.5.0",
"moment": "2.24.0",
"objectpath": "1.2.2",
"validator": "11.1.0",
"yargs-parser": "13.0.0"
},
Expand Down
7 changes: 6 additions & 1 deletion packages/convict/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,9 +563,14 @@ config = convict('/some/path/to/a/config-schema.json');
Returns the current value of the `name` property. `name` can use dot notation to reference nested values. E.g.:
```javascript
config.get('db.host');

// or
config.get('db').host;
// also
config.get('db[0]');
// with dot:
config.get('db["www.airbus.com"]'); { 'db': { 'www.airbus.com': 'air company'} }
// in the first level
config.get("['foo.bar']"); // { 'foo.bar': 'baz' }
```

### config.getOrigin(name)
Expand Down
107 changes: 73 additions & 34 deletions packages/convict/lib/convict.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
const fs = require('fs');
const parseArgs = require('yargs-parser');
const cloneDeep = require('lodash.clonedeep');
const parsePath = require('objectpath').parse;
const cvtError = require('./convicterror.js');

// 1
Expand Down Expand Up @@ -95,43 +96,47 @@ const ALLOWED_OPTION_STRICT = 'strict';
const ALLOWED_OPTION_WARN = 'warn';

function flatten(obj, useProperties) {
const stack = Object.keys(obj);
const stack = Object.keys(obj).map((path) => [ path ]);
const entries = [];

while (stack.length) {
let key = stack.shift();
let val = walk(obj, key);
if (typeof val === 'object' && !Array.isArray(val) && val != null) {
let path = stack.shift();
let node = walk(obj, path);
if (typeof node === 'object' && !Array.isArray(node) && node != null) {
if (useProperties) {
if ('_cvtProperties' in val) {
val = val._cvtProperties;
key = key + '._cvtProperties';
if ('_cvtProperties' in node) {
node = node._cvtProperties;
path.push('_cvtProperties');
} else {
entries.push([key, val]);
entries.push([path, node]);
continue;
}
}
const subkeys = Object.keys(val);
const children = Object.keys(node);

// Don't filter out empty objects
if (subkeys.length > 0) {
subkeys.forEach(function(subkey) {
stack.push(key + '.' + subkey);
if (children.length > 0) {
children.forEach(function(child) {
stack.push(path.concat(child));
});
continue;
}
}
entries.push([key, val]);
entries.push([path, node]);
}

const flattened = {};
entries.forEach(function(entry) {
let key = entry[0];
let path = entry[0];
const val = entry[1];

if (Array.isArray(path) === false) throw new Error('errror : ' + path);

if (useProperties) {
key = key.replace(/\._cvtProperties/g, '');
path = path.filter((property) => property !== '_cvtProperties');
}
const val = entry[1];
flattened[key] = val;

flattened[stringifyPath(path)] = val;
});

return flattened;
Expand Down Expand Up @@ -411,7 +416,7 @@ function applyValues(from, to, schema) {
}

function traverseSchema(schema, path) {
const ar = path.split('.');
const ar = parsePath(path);
let o = schema;
while (ar.length > 0) {
const k = ar.shift();
Expand Down Expand Up @@ -469,18 +474,52 @@ function loadFile(path) {
return parse(fs.readFileSync(path, 'utf-8'));
}

function pathToSchemaPath(path, addKey) {
const schemaPath = [];

path = parsePath(path);
path.forEach((property) => {
schemaPath.push(property);
schemaPath.push('_cvtProperties');
});
schemaPath.splice(-1);

if (addKey)
schemaPath.push(addKey);

return schemaPath;
}

function stringifyPath(arr, quote, forceQuote) {
quote = (quote === '"') ? '"' : "'";
const regexp = new RegExp('(\\\\|' + quote + ')', 'g'); // regex => /(\\|')/g

return arr.map(function(value, key) {
let property = value.toString();
if (!forceQuote && /^[A-z_]\w*$/.exec(property)) { // str with only A-z0-9_ chars will display `foo.bar`
return (key !== 0) ? '.' + property : property;
} else if (!forceQuote && /^\d+$/.exec(property)) { // str with only numbers will display `foo[0]`
return '[' + property + ']';
} else {
property = property.replace(regexp, '\\$1');
return '[' + quote + property + quote + ']';
}
}).join('');
}

function walk(obj, path, initializeMissing) {
if (path) {
const ar = path.split('.');
while (ar.length) {
const k = ar.shift();
if (initializeMissing && obj[k] == null) {
obj[k] = {};
obj = obj[k];
} else if (k in obj) {
obj = obj[k];
path = Array.isArray(path) ? path : parsePath(path);
const sibling = path.slice(0);
while (sibling.length) {
const key = sibling.shift();
if (initializeMissing && obj[key] == null) {
obj[key] = {};
obj = obj[key];
} else if (key in obj) {
obj = obj[key];
} else {
throw new PATH_INVALID('cannot find configuration param: ' + path);
throw new PATH_INVALID('cannot find configuration param: ' + stringifyPath(path));
}
}
}
Expand Down Expand Up @@ -526,10 +565,10 @@ const convict = function convict(def, opts) {
*/
toString: function() {
const clone = cloneDeep(this._instance);
this._sensitive.forEach(function(key) {
const path = key.split('.');
this._sensitive.forEach(function(fullpath) {
const path = parsePath(fullpath);
const childKey = path.pop();
const parentKey = path.join('.');
const parentKey = stringifyPath(path);
const parent = walk(clone, parentKey);
parent[childKey] = '[Sensitive]';
});
Expand Down Expand Up @@ -564,7 +603,7 @@ const convict = function convict(def, opts) {
* notation to reference nested values
*/
getOrigin: function(path) {
path = (path.split('.').join('._cvtProperties.')) + '._cvtGetOrigin';
path = pathToSchemaPath(path, '_cvtGetOrigin');
const o = walk(this._schema._cvtProperties, path);
return o ? o() : null;
},
Expand Down Expand Up @@ -600,7 +639,7 @@ const convict = function convict(def, opts) {
default: function(path) {
// The default value for FOO.BAR.BAZ is stored in `_schema._cvtProperties` at:
// FOO._cvtProperties.BAR._cvtProperties.BAZ.default
path = (path.split('.').join('._cvtProperties.')) + '.default';
path = pathToSchemaPath(path, 'default');
const o = walk(this._schema._cvtProperties, path);
return cloneDeep(o);
},
Expand Down Expand Up @@ -648,9 +687,9 @@ const convict = function convict(def, opts) {
const coerce = (mySchema && mySchema._cvtCoerce) ? mySchema._cvtCoerce : (v) => v;
value = coerce(value);
// walk to the value
const path = fullpath.split('.');
const path = parsePath(fullpath);
const childKey = path.pop();
const parentKey = path.join('.');
const parentKey = stringifyPath(path);
const parent = walk(this._instance, parentKey, true);

// respect priority
Expand Down
11 changes: 8 additions & 3 deletions packages/convict/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/convict/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"main": "lib/convict.js",
"dependencies": {
"lodash.clonedeep": "4.5.0",
"objectpath": "1.2.2",
"yargs-parser": "13.0.0"
},
"devDependencies": {
Expand Down
18 changes: 15 additions & 3 deletions packages/convict/test/get-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ describe('convict get function', function() {
name_with_underscores: true
}
}
},
'foo.bar': {
format: 'String',
default: 'air'
},
'foo.baz': {
bing: {
format: 'String',
default: 'bus'
}
}
});
});
Expand All @@ -39,13 +49,15 @@ describe('convict get function', function() {

describe('.get()', function() {
it('must find a nested value', function() {
let val = conf.get('foo.bar');

expect(val).to.equal(7);
expect(conf.get('foo.bar')).to.equal(7);
expect(conf.get('["foo.bar"]')).to.equal('air');
});

it('must handle three levels of nesting', function() {
expect(conf.get('foo.baz.bing')).to.equal('foo');
expect(conf.get('["foo.baz"].bing')).to.equal('bus');
conf.set('["foo.baz"].bing', 'plane');
expect(conf.get('["foo.baz"].bing')).to.equal('plane');
});

it('must handle names with spaces and underscores', function() {
Expand Down
4 changes: 2 additions & 2 deletions packages/convict/test/validation-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ describe('configuration files contain properties not declared in the schema', fu
});

it('must use the user output function when it was declared', function() {
const expected = "\u001b[33;1mWarning:\u001b[0m configuration param '0' not declared in the schema";
const expected = "\u001b[33;1mWarning:\u001b[0m configuration param '[0]' not declared in the schema";

expect(message).to.equal(expected);
});
Expand Down Expand Up @@ -245,7 +245,7 @@ describe('schema contains an object property with a custom format', function() {
).to.not.throw();
});

it.skip("must not throw if an object's default value property name contains a period", function() {
it("must not throw if an object's default value property name contains a period", function() {
const schema = {
object: {
doc: 'default value contains property name that contains a period',
Expand Down

0 comments on commit 8cadd2a

Please sign in to comment.