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
66 changes: 52 additions & 14 deletions src/server/config/config.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,56 @@
import Promise from 'bluebird';
import Joi from 'joi';
import _ from 'lodash';
import toPath from 'lodash/internal/toPath';
import override from './override';
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');

function unset(object, rawPath) {
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);
delete parent[leaf];
if (!_.size(parent)) {
unset(object, parentPath);
}
break;
}
}

module.exports = class Config {
static withDefaultSchema(settings = {}) {
return new Config(createDefaultSchema(), settings);
}

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,26 +64,26 @@ 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);
unset(this[schemaExts], key);
unset(this[pendingSets], key);
delete this[vals][key];
}

Expand Down Expand Up @@ -138,7 +161,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 +186,23 @@ 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 objKeys = Object.keys(children);
let schema = Joi.object().keys(objKeys).default();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passing objKeys here is unnecessary. Joi is itterating the own properties of objKeys and adding them as keys, but since it's an array it has no own properties and does nothing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I read the Joi docs wrong before.

The call to .keys() is required, otherwise the validation fails (and the tests do as a result), but it just needs to be given an empty object, since the actual keys are appended after the fact.


for (const key of objKeys) {
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
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