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

Implement plugin config prefix #6554

Merged
merged 12 commits into from
Mar 23, 2016
35 changes: 33 additions & 2 deletions src/server/config/__tests__/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,36 @@ describe('lib/config/config', function () {

describe('constructor', function () {

it('should not allow any config if the schema is not passed', function (done) {
it('should not allow any config if the schema is not passed', function () {
var config = new Config();
var run = function () {
config.set('something.enable', true);
};
expect(run).to.throwException();
done();
});

it('should allow keys in the schema', function () {
var config = new Config(schema);
var run = function () {
config.set('test.client.host', 'http://0.0.0.0');
};
expect(run).to.not.throwException();
});

it('should not allow keys not in the schema', function () {
var config = new Config(schema);
var run = function () {
config.set('paramNotDefinedInTheSchema', true);
};
expect(run).to.throwException();
});

it('should not allow child keys not in the schema', function () {
var config = new Config(schema);
var run = function () {
config.set('test.client.paramNotDefinedInTheSchema', true);
};
expect(run).to.throwException();
});

it('should set defaults', function () {
Expand Down Expand Up @@ -198,6 +221,14 @@ describe('lib/config/config', function () {
expect(config.get('myTest.test')).to.be(true);
});

it('should allow you to extend the schema with a prefix', function () {
var newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
config.extendSchema('prefix.myTest', newSchema);
expect(config.get('prefix')).to.eql({ myTest: { test: true }});
expect(config.get('prefix.myTest')).to.eql({ test: true });
expect(config.get('prefix.myTest.test')).to.be(true);
});

it('should NOT allow you to extend the schema if somethign else is there', function () {
var newSchema = Joi.object({ test: Joi.boolean().default(true) }).default();
var run = function () {
Expand Down
83 changes: 83 additions & 0 deletions src/server/config/__tests__/unset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import unset from '../unset';
import expect from 'expect.js';

describe('unset(obj, key)', function () {
describe('invalid input', function () {
it('should do nothing if not given an object', function () {
const obj = 'hello';
unset(obj, 'e');
expect(obj).to.equal('hello');
});

it('should do nothing if not given a key', function () {
const obj = { one: 1 };
unset(obj);
expect(obj).to.eql({ one: 1 });
});

it('should do nothing if given an empty string as a key', function () {
const obj = { one: 1 };
unset(obj, '');
expect(obj).to.eql({ one: 1 });
});
});

describe('shallow removal', function () {
let obj;

beforeEach(function () {
obj = { one: 1, two: 2, deep: { three: 3, four: 4 } };
});

it('should remove the param using a string key', function () {
unset(obj, 'two');
expect(obj).to.eql({ one: 1, deep: { three: 3, four: 4 } });
});

it('should remove the param using an array key', function () {
unset(obj, ['two']);
expect(obj).to.eql({ one: 1, deep: { three: 3, four: 4 } });
});
});

describe('deep removal', function () {
let obj;

beforeEach(function () {
obj = { one: 1, two: 2, deep: { three: 3, four: 4 } };
});

it('should remove the param using a string key', function () {
unset(obj, 'deep.three');
expect(obj).to.eql({ one: 1, two: 2, deep: { four: 4 } });
});

it('should remove the param using an array key', function () {
unset(obj, ['deep', 'three']);
expect(obj).to.eql({ one: 1, two: 2, deep: { four: 4 } });
});
});

describe('recursive removal', function () {
it('should clear object if only value is removed', function () {
const obj = { one: { two: { three: 3 } } };
unset(obj, 'one.two.three');
expect(obj).to.eql({});
});

it('should clear object if no props are left', function () {
const obj = { one: { two: { three: 3 } } };
unset(obj, 'one.two');
expect(obj).to.eql({});
});

it('should remove deep property, then clear the object', function () {
const obj = { one: { two: { three: 3, four: 4 } } };
unset(obj, 'one.two.three');
expect(obj).to.eql({ one: { two: { four: 4 } } });

unset(obj, 'one.two.four');
expect(obj).to.eql({});
});
});
});
45 changes: 29 additions & 16 deletions src/server/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import Promise from 'bluebird';
import Joi from 'joi';
import _ from 'lodash';
import override from './override';
import unset from './unset';
import createDefaultSchema from './schema';
import pkg from '../../utils/package_json';
import clone from './deep_clone_with_buffers';
import { zipObject } from 'lodash';

const schema = Symbol('Joi Schema');
const schemaKeys = Symbol('Schema Extensions');
const schemaExts = Symbol('Schema Extensions');
const vals = Symbol('config values');
const pendingSets = Symbol('Pending Settings');

Expand All @@ -18,16 +18,15 @@ module.exports = class Config {
}

constructor(initialSchema, initialSettings) {
this[schemaKeys] = new Map();

this[schemaExts] = Object.create(null);
this[vals] = Object.create(null);
this[pendingSets] = new Map(_.pairs(clone(initialSettings || {})));
this[pendingSets] = _.merge(Object.create(null), initialSettings || {});

if (initialSchema) this.extendSchema(initialSchema);
}

getPendingSets() {
return this[pendingSets];
return new Map(_.pairs(this[pendingSets]));
}

extendSchema(key, extension) {
Expand All @@ -41,27 +40,27 @@ module.exports = class Config {
throw new Error(`Config schema already has key: ${key}`);
}

this[schemaKeys].set(key, extension);
_.set(this[schemaExts], key, extension);
this[schema] = null;

let initialVals = this[pendingSets].get(key);
let initialVals = _.get(this[pendingSets], key);
if (initialVals) {
this.set(key, initialVals);
this[pendingSets].delete(key);
unset(this[pendingSets], key);
} else {
this._commit(this[vals]);
}
}

removeSchema(key) {
if (!this[schemaKeys].has(key)) {
if (!_.has(this[schemaExts], key)) {
throw new TypeError(`Unknown schema key: ${key}`);
}

this[schema] = null;
this[schemaKeys].delete(key);
this[pendingSets].delete(key);
delete this[vals][key];
unset(this[schemaExts], key);
unset(this[pendingSets], key);
unset(this[vals], key);
}

resetTo(obj) {
Expand Down Expand Up @@ -138,7 +137,7 @@ module.exports = class Config {
// Catch the partial paths
if (path.join('.') === key) return true;
// Only go deep on inner objects with children
if (schema._inner.children.length) {
if (_.size(schema._inner.children)) {
for (let i = 0; i < schema._inner.children.length; i++) {
let child = schema._inner.children[i];
// If the child is an object recurse through it's children and return
Expand All @@ -163,8 +162,22 @@ module.exports = class Config {

getSchema() {
if (!this[schema]) {
let objKeys = zipObject([...this[schemaKeys]]);
this[schema] = Joi.object().keys(objKeys).default();
this[schema] = (function convertToSchema(children) {
let schema = Joi.object().keys({}).default();

for (const key of Object.keys(children)) {
const child = children[key];
const childSchema = _.isPlainObject(child) ? convertToSchema(child) : child;

if (!childSchema || !childSchema.isJoi) {
throw new TypeError('Unable to convert configuration definition value to Joi schema: ' + childSchema);
}

schema = schema.keys({ [key]: childSchema });
}

return schema;
}(this[schemaExts]));
}

return this[schema];
Expand Down
26 changes: 26 additions & 0 deletions src/server/config/unset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import _ from 'lodash';
import toPath from 'lodash/internal/toPath';

module.exports = function unset(object, rawPath) {
if (!object) return;
const path = toPath(rawPath);

switch (path.length) {
case 0:
return;

case 1:
delete object[rawPath];
break;

default:
const leaf = path.pop();
const parentPath = path.slice();
const parent = _.get(object, parentPath);
unset(parent, leaf);
if (!_.size(parent)) {
unset(object, parentPath);
}
break;
}
};
14 changes: 9 additions & 5 deletions src/server/plugins/plugin.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _ from 'lodash';
import toPath from 'lodash/internal/toPath';
import Joi from 'joi';
import { attempt, fromNode } from 'bluebird';
import { basename, resolve } from 'path';
Expand Down Expand Up @@ -35,6 +36,8 @@ const defaultConfigSchema = Joi.object({
* @param {String} [opts.version=pkg.version] - the version of this plugin
* @param {Function} [opts.init] - A function that will be called to initialize
* this plugin at the appropriate time.
* @param {Function} [opts.configPrefix=this.id] - The prefix to use for configuration
* values in the main configuration service
* @param {Function} [opts.config] - A function that produces a configuration
* schema using Joi, which is passed as its
* first argument.
Expand All @@ -55,6 +58,7 @@ module.exports = class Plugin {
this.requiredIds = opts.require || [];
this.version = opts.version || pkg.version;
this.externalInit = opts.init || _.noop;
this.configPrefix = opts.configPrefix || this.id;
this.getConfigSchema = opts.config || _.noop;
this.init = _.once(this.init);

Expand Down Expand Up @@ -83,18 +87,18 @@ module.exports = class Plugin {
async readConfig() {
let schema = await this.getConfigSchema(Joi);
let { config } = this.kbnServer;
config.extendSchema(this.id, schema || defaultConfigSchema);
config.extendSchema(this.configPrefix, schema || defaultConfigSchema);

if (config.get([this.id, 'enabled'])) {
if (config.get([...toPath(this.configPrefix), 'enabled'])) {
return true;
} else {
config.removeSchema(this.id);
config.removeSchema(this.configPrefix);
return false;
}
}

async init() {
let { id, version, kbnServer } = this;
let { id, version, kbnServer, configPrefix } = this;
let { config } = kbnServer;

// setup the hapi register function and get on with it
Expand Down Expand Up @@ -127,7 +131,7 @@ module.exports = class Plugin {
await fromNode(cb => {
kbnServer.server.register({
register: register,
options: config.has(id) ? config.get(id) : null
options: config.has(configPrefix) ? config.get(configPrefix) : null
}, cb);
});

Expand Down