Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 70: Document array behavior #94

Merged
merged 4 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,14 +530,23 @@ If you have values that you would like to share between various configuration ob
{
"$filter": "env",
"$base": {
"logLocation": "/logs"
"logLocation": "/logs",
"flags": ["a", "b"],
"tags": {
"$value": ["DEBUG"],
"$replace": true
}
},
"production": {
"logLevel": "error"
"logLevel": "error",
"flags": ["c", "d"],
"tags": ["INFO", "ERROR"]
},
"qa": {
"logLevel": "info",
"logLocation": "/qa/logs"
"logLocation": "/qa/logs",
"flags": ["e", "f"],
"tags": ["DEBUG"]
},
"staging": {
"logLevel": "debug"
Expand All @@ -553,7 +562,9 @@ When requesting the **key** `/` with:
```json
{
"logLevel": "error",
"logLocation": "/logs"
"logLocation": "/logs",
"flags": ["a", "b", "c", "d"],
"tags": ["INFO", "ERROR"]
}
```

Expand All @@ -565,11 +576,15 @@ However when requesting the **key** `/` with:
```json
{
"logLevel": "debug",
"logLocation": "/logs"
"logLocation": "/logs",
"flags": ["a", "b"],
"tags": ["DEBUG"],
}
```

If the same key occurs in `$base` and the `filtered value` then value in `$base` will be overridden.
If the same key occurs in `$base` and the `filtered value`:
- for objects, the value in `$base` will be overridden.
- for arrays, the arrays are merged unless the `$base` array is specified with the `$value` key and the `$replace` flag as shown above.

In the above sample, when requesting the **key** `/` with:

Expand Down
5 changes: 3 additions & 2 deletions bin/confidence
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// Load modules

const Fs = require('fs');
const Path = require('path');
const Yargs = require('yargs');
const Alce = require('alce');
const Confidence = require('../');
Expand All @@ -28,7 +29,7 @@ internals.argv = Yargs.usage('Usage: $0 -c config.json [--filter.criterion=value

internals.getConfig = function () {

const configPath = !internals.argv.c.startsWith('/') ? process.cwd() + '/' + internals.argv.c : internals.argv.c;
const configPath = Path.resolve(internals.argv.c);

return new Promise((resolve, reject) => {

Expand Down Expand Up @@ -64,7 +65,7 @@ internals.generate = async () => {
process.stdout.write(JSON.stringify(set, null, indentation));
}
catch (err) {
console.log('Failed loading configuration file: ' + internals.argv.c + ' (' + err.isJoi ? err.annotate() : err.message + ')');
process.stderr.write('Failed loading configuration file: ' + internals.argv.c + ' (' + err + ')');
process.exit(1);
}
};
Expand Down
28 changes: 26 additions & 2 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ internals.Joi = Joi.extend([
base: Joi.object(),
language: {
withPattern: 'fails to match the {{name}} pattern',
notInstanceOf: 'cannot be an instance of {{name}}'
notInstanceOf: 'cannot be an instance of {{name}}',
replaceBaseArrayFlag: '{{desc}}'
},
rules: [
{
Expand Down Expand Up @@ -57,6 +58,28 @@ internals.Joi = Joi.extend([
return this.createError('object.notInstanceOf', { v: value, name: params.fn.name }, state, options);
}

return value;
}
},
{
name: 'replaceBaseArrayFlag',
params: {},
validate(_params, value, state, options) {

if (Object.keys(value).includes('$replace')) {
if (!state.path.includes('$base')) {
return this.createError('object.replaceBaseArrayFlag', { desc: '$replace only allowed under path $base' }, state, options);
}

if (!Object.keys(value).includes('$value')) {
return this.createError('object.replaceBaseArrayFlag', { desc: '$replace missing required peer $value' }, state, options);
}

if (!Array.isArray(value.$value)) {
return this.createError('object.replaceBaseArrayFlag', { desc: '$replace requires $value to be an array' }, state, options);
}
}

return value;
}
}
Expand Down Expand Up @@ -107,6 +130,7 @@ internals.alternatives = internals.Joi.lazy(() => {
exports.store = internals.store = internals.Joi.object().keys({
$param: internals.Joi.string().regex(/^\w+(?:\.\w+)*$/, { name: 'Alphanumeric Characters and "_"' }),
$value: internals.alternatives,
$replace: internals.Joi.boolean().invalid(false),
$env: internals.Joi.string().regex(/^\w+$/, { name: 'Alphanumeric Characters and "_"' }),
$coerce: internals.Joi.string().valid('number'),
$filter: internals.Joi.alternatives([
Expand Down Expand Up @@ -143,5 +167,5 @@ exports.store = internals.store = internals.Joi.object().keys({
.with('$coerce', '$env')
.withPattern('$filter', /^((\$range)|([^\$].*))$/, { inverse: true, name: '$filter with a valid value OR $range' })
.withPattern('$range', /^([^\$].*)$/, { name: '$range with non-ranged values' })
.replaceBaseArrayFlag()
.allow(null);

5 changes: 3 additions & 2 deletions lib/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ internals.defaults = function (node, base) {

base = base || {};

if (typeof node === 'object' && (Array.isArray(base) === Array.isArray(node))) {
return Hoek.merge(Hoek.clone(base), Hoek.clone(node));
const filteredBase = internals.filter(base);
if (typeof node === 'object' && (Array.isArray(filteredBase) === Array.isArray(node)) && !base.$replace) {
return Hoek.merge(Hoek.clone(filteredBase), Hoek.clone(node));
}

return node;
Expand Down
29 changes: 29 additions & 0 deletions test/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ describe('bin', () => {

return new Promise((resolve) => {

const errors = [];
const confidence = ChildProcess.spawn('node', [confidencePath, '-c', configPath, '--filter.env', 'production', '-i', 'someString']);

confidence.stdout.on('data', (data) => {
Expand All @@ -186,10 +187,38 @@ describe('bin', () => {
confidence.stderr.on('data', (data) => {

expect(data.toString()).to.exist();
errors.push(data.toString());
});

confidence.on('close', () => {

expect(errors.join('')).to.match(/Argument check failed[\s\S]*indentation/);
resolve();
});
});
});

it('fails when configuration file cannot be found', () => {

return new Promise((resolve) => {

const errors = [];
const confidence = ChildProcess.spawn('node', [confidencePath, '-c', 'doesNotExist', '--filter.env', 'production', '-i', 2]);

confidence.stdout.on('data', (data) => {

expect(data.toString()).to.not.exist();
});

confidence.stderr.on('data', (data) => {

expect(data.toString()).to.exist();
errors.push(data.toString());
});

confidence.on('close', () => {

expect(errors.join('')).to.match(/Failed loading configuration file: doesNotExist/);
resolve();
});
});
Expand Down
49 changes: 47 additions & 2 deletions test/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ const tree = {
],
$default: 6
},

arrayReplace1: { $filter: 'env', $base: { $value: ['a'], $replace: true }, $default: { $value: ['b'] }, dev: ['c'] },
arrayReplace2: { $filter: 'env', $base: { $value: ['a'], $replace: true }, $default: ['b'], dev: [] },
arrayMerge1: { $filter: 'env', $base: { $value: ['a'] }, $default: { $value: ['b'] }, dev: ['c'] },
arrayMerge2: { $filter: 'env', $base: { $value: ['a'] }, $default: ['b'], dev: [] },
arrayMerge3: { $filter: 'env', $base: ['a'], $default: { $value: ['b'] }, dev: {} },
arrayMerge4: { $filter: 'env', $base: ['a'], $default: ['b'], dev: {} },

noProto: Object.create(null),
$meta: {
something: 'else'
Expand Down Expand Up @@ -194,8 +202,27 @@ describe('get()', () => {
get('/key11', { a: 'env', b: 'abc', port: 3000 }, {}, [], { KEY1: 'env' });
get('/key11', { a: 'env', b: '3000', port: 4000 }, {}, [], { KEY1: 'env', KEY2: 3000, PORT: '4000' });
get('/key11', { a: 'env', b: '3000', port: 3000 }, {}, [], { KEY1: 'env', KEY2: 3000, PORT: 'abc' });
get('/', { key1: 'abc', key10: { b: 123 }, key11: { b: 'abc', port: 3000 }, key2: 2, key3: { sub1: 0 }, key4: [12, 13, 14], key5: {}, noProto: {}, ab: 6 });
get('/', { key1: 'abc', key10: { b: 123 }, key11: { b: 'abc', port: 3000 }, key2: 2, key3: { sub1: 0, sub2: '' }, key4: [12, 13, 14], key5: {}, noProto: {}, ab: 6 }, { xfactor: 'yes' });

const slashResult = {
key1: 'abc',
key10: { b: 123 },
key11: { b: 'abc', port: 3000 },
key2: 2,
key3: { sub1: 0 },
key4: [12, 13, 14],
key5: {},
noProto: {},
ab: 6,
arrayReplace1: ['b'],
arrayReplace2: ['b'],
arrayMerge1: ['a', 'b'],
arrayMerge2: ['a', 'b'],
arrayMerge3: ['a', 'b'],
arrayMerge4: ['a', 'b']
};
get('/', slashResult);
get('/', Object.assign({}, slashResult, { key3: { sub1: 0, sub2: '' }, ab: 6 }), { xfactor: 'yes' });

get('/ab', 2, { random: { 1: 2 } }, [{ filter: 'random.1', valueId: '[object]', filterId: 'random_ab_test' }]);
get('/ab', { a: 5 }, { random: { 1: 3 } }, [{ filter: 'random.1', valueId: '3', filterId: 'random_ab_test' }]);
get('/ab', 4, { random: { 1: 9 } });
Expand All @@ -204,6 +231,20 @@ describe('get()', () => {
get('/ab', 5, { random: { 1: 19 } });
get('/ab', 6, { random: { 1: 29 } });

get('/arrayReplace1', ['b']);
get('/arrayReplace2', ['b']);
get('/arrayMerge1', ['a', 'b']);
get('/arrayMerge2', ['a', 'b']);
get('/arrayMerge3', ['a', 'b']);
get('/arrayMerge4', ['a', 'b']);

get('/arrayReplace1', ['c'], { env: 'dev' });
get('/arrayReplace2', [], { env: 'dev' });
get('/arrayMerge1', ['a', 'c'], { env: 'dev' });
get('/arrayMerge2', ['a'], { env: 'dev' });
get('/arrayMerge3', {}, { env: 'dev' });
get('/arrayMerge4', {}, { env: 'dev' });

it('fails on invalid key', () => {

const value = store.get('key');
Expand Down Expand Up @@ -311,6 +352,10 @@ describe('validate()', () => {
validate('invalid id', { key: 5, $id: 4 });
validate('empty id', { key: 5, $id: null });

validate('$replace with no $value', { $base: { $replace: true } });
validate('$replace with non-array $value', { $base: { $value: 'a', $replace: true } });
validate('$replace not under $base', { $default: { $value: ['a'], $replace: true } });

it('returns null with null as the node', () => {

const err = Confidence.Store.validate(null);
Expand Down