Added a schema validator. (#13)

* Added a schema validator.

This allows you to check if the given beau config is valid. Will be used
to improve the CLI and remove schema validation-type errors from the
actual code.

* Added a validate command to the CLI.

This command is a way to test the config file. Eventually it'll probably
be removed and the schema validation will run on every other command. I
have to determine how useful this is and how performance might be
affected.
This commit is contained in:
Sergio Díaz 2018-05-01 22:20:14 -06:00 committed by GitHub
parent 4fba235bad
commit 90761acaa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 188 additions and 19 deletions

View File

@ -6,13 +6,17 @@ const { Command, flags } = require('@oclif/command');
const Beau = require('../../src/beau');
class Base extends Command {
loadConfig(configFile) {
openConfigFile(configFile) {
if (!fs.existsSync(configFile)) {
this.error(`The config file, ${configFile} was not found.`);
this.exit(1);
}
const config = yaml.safeLoad(fs.readFileSync(configFile, 'utf-8'));
return yaml.safeLoad(fs.readFileSync(configFile, 'utf-8'));
}
loadConfig(configFile) {
const config = this.openConfigFile(configFile);
const env = dotenv.config().parsed || {};
return new Beau(config, env);

View File

@ -0,0 +1,34 @@
const clc = require('cli-color');
const fs = require('fs');
const yaml = require('js-yaml');
const { flags } = require('@oclif/command');
const Base = require('../base');
const { validate } = require('../../../src/schema.js');
class ValidateCommand extends Base {
async run() {
const { flags, args } = this.parse(ValidateCommand);
const configFile = args.alias || flags.config;
const config = this.openConfigFile(configFile);
let result = await validate(config);
if (result.valid) {
this.log(`${configFile} is valid.`);
} else {
this.error(result.message);
}
}
}
ValidateCommand.description = `Validates the given configuration file against Beau's configuration schema.`;
ValidateCommand.flags = { ...Base.flags };
ValidateCommand.args = [
{
name: 'alias',
required: false,
description: `The configuration file to validate.`
}
];
module.exports = ValidateCommand;

19
examples/httpbin.yml Normal file
View File

@ -0,0 +1,19 @@
version: 1
endpoint: https://httpbin.org/
cookiejar: true
GET /anything:
alias: anything
form:
name: David
params:
hello: World
GET /cookies/set:
alias: set-cookies
params:
hello: World
GET /status/418:
alias: teapot

View File

@ -1,7 +1,7 @@
endpoint: http://localhost:10080
plugins:
- beau-jwt:
- jwt:
data:
userId: 12
name: Sergio

47
package-lock.json generated
View File

@ -3934,6 +3934,21 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isemail": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/isemail/-/isemail-3.1.2.tgz",
"integrity": "sha512-zfRhJn9rFSGhzU5tGZqepRSAj3+g6oTOHxMGGriWNJZzyLPUK8H7VHpqKntegnW8KLyGA9zwuNaCoopl40LTpg==",
"requires": {
"punycode": "2.x.x"
},
"dependencies": {
"punycode": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz",
"integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0="
}
}
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -4787,6 +4802,23 @@
"merge-stream": "^1.0.1"
}
},
"joi": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/joi/-/joi-13.2.0.tgz",
"integrity": "sha512-VUzQwyCrmT2lIpxBCYq26dcK9veCQzDh84gQnCtaxCa8ePohX8JZVVsIb+E66kCUUcIvzeIpifa6eZuzqTZ3NA==",
"requires": {
"hoek": "5.x.x",
"isemail": "3.x.x",
"topo": "3.x.x"
},
"dependencies": {
"hoek": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.3.tgz",
"integrity": "sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw=="
}
}
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@ -7181,6 +7213,21 @@
}
}
},
"topo": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz",
"integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==",
"requires": {
"hoek": "5.x.x"
},
"dependencies": {
"hoek": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.3.tgz",
"integrity": "sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw=="
}
}
},
"tough-cookie": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",

View File

@ -19,6 +19,7 @@
"clui": "^0.3.1",
"deepmerge": "^2.1.0",
"dotenv": "^5.0.1",
"joi": "^13.2.0",
"globby": "^8.0.1",
"is-plain-object": "^2.0.4",
"js-yaml": "^3.11.0",

View File

@ -3,7 +3,6 @@
exports[`Beau's config Loader. should create a request list 1`] = `
Beau {
"config": Config {
"CACHE": false,
"COOKIEJAR": false,
"DEFAULTS": Object {
"headers": Object {
@ -45,7 +44,6 @@ Beau {
"VERSION": 1,
"configKeys": Array [
"VERSION",
"CACHE",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
@ -54,7 +52,6 @@ Beau {
"COOKIEJAR",
],
"defaultConfigValues": Object {
"CACHE": false,
"COOKIEJAR": false,
"DEFAULTS": Object {},
"ENDPOINT": "",
@ -108,7 +105,6 @@ Beau {
},
},
"config": Config {
"CACHE": false,
"COOKIEJAR": false,
"DEFAULTS": Object {
"headers": Object {
@ -150,7 +146,6 @@ Beau {
"VERSION": 1,
"configKeys": Array [
"VERSION",
"CACHE",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
@ -159,7 +154,6 @@ Beau {
"COOKIEJAR",
],
"defaultConfigValues": Object {
"CACHE": false,
"COOKIEJAR": false,
"DEFAULTS": Object {},
"ENDPOINT": "",

View File

@ -2,7 +2,6 @@
exports[`Config should load multiple hosts 1`] = `
Config {
"CACHE": false,
"COOKIEJAR": false,
"DEFAULTS": Object {
"HEADERS": Object {
@ -113,7 +112,6 @@ Config {
"VERSION": 1,
"configKeys": Array [
"VERSION",
"CACHE",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
@ -122,7 +120,6 @@ Config {
"COOKIEJAR",
],
"defaultConfigValues": Object {
"CACHE": false,
"COOKIEJAR": false,
"DEFAULTS": Object {},
"ENDPOINT": "",
@ -176,7 +173,6 @@ Config {
exports[`Config should set up defaults for all requests 1`] = `
Config {
"CACHE": false,
"COOKIEJAR": false,
"DEFAULTS": Object {
"HEADERS": Object {
@ -218,7 +214,6 @@ Config {
"VERSION": 1,
"configKeys": Array [
"VERSION",
"CACHE",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
@ -227,7 +222,6 @@ Config {
"COOKIEJAR",
],
"defaultConfigValues": Object {
"CACHE": false,
"COOKIEJAR": false,
"DEFAULTS": Object {},
"ENDPOINT": "",

View File

@ -6,13 +6,11 @@ describe('Config', () => {
const doc = yaml.safeLoad(`
version: 1
endpoint: http://martianwabbit.com
cache: false
shouldntBeAdded: true
`);
const config = new Config(doc);
expect(config.ENDPOINT).toBe(doc.endpoint);
expect(config.CACHE).toBe(doc.cache);
expect(config.VERSION).toBe(doc.version);
expect(config.shouldntBeAdded).toBeUndefined();
});

View File

@ -6,7 +6,6 @@ class Config {
constructor(doc, env = {}) {
this.defaultConfigValues = {
VERSION: 1,
CACHE: false,
ENDPOINT: '',
PLUGINS: [],
DEFAULTS: {},

View File

@ -17,7 +17,7 @@ class RequestCache {
let result = this.$cache;
path.split('.').forEach(part => {
if (result[part] === undefined) {
throw new Error(`${path} not found in cache: `, path);
throw new Error(`${path} not found in cache.`);
}
result = result[part];

79
src/schema.js Normal file
View File

@ -0,0 +1,79 @@
const Joi = require('joi');
const { requestRegex } = require('./shared.js');
const pluginSchema = [
Joi.string(),
Joi.object()
.keys(null)
.max(1)
];
const requestSchema = [
Joi.object()
.keys({
HEADERS: Joi.object().keys(null),
PAYLOAD: [Joi.object().keys(null), Joi.string()],
PARAMS: Joi.object().keys(null),
FORM: Joi.object().keys(null),
ALIAS: Joi.string().required(),
FORMDATA: Joi.object().keys(null)
})
.without('FORM', ['PAYLOAD', 'FORMDATA'])
.without('PAYLOAD', ['FORM', 'FORMDATA'])
.without('FORMDATA', ['FORM', 'PAYLOAD'])
.rename(/headers/i, 'HEADERS', { override: true })
.rename(/payload/i, 'PAYLOAD', { override: true })
.rename(/params/i, 'PARAMS', { override: true })
.rename(/form/i, 'FORM', { override: true })
.rename(/alias/i, 'ALIAS', { override: true }),
Joi.string()
];
const hostSchema = Joi.object()
.keys({
HOST: Joi.string().required(),
ENDPOINT: Joi.string(),
DEFAULTS: Joi.object().keys(null)
})
.pattern(requestRegex, requestSchema)
.rename(/host/i, 'HOST', { override: true })
.rename(/defaults/i, 'DEFAULTS', { override: true })
.rename(/endpoint/i, 'ENDPOINT', { override: true });
const schema = Joi.object()
.keys({
VERSION: Joi.number().integer(),
ENDPOINT: Joi.string().uri(),
PLUGINS: Joi.array().items(pluginSchema),
DEFAULTS: Joi.object(),
ENVIRONMENT: Joi.object(),
HOSTS: Joi.array().items(hostSchema),
COOKIEJAR: Joi.boolean()
})
.pattern(requestRegex, requestSchema)
.rename(/version/i, 'VERSION', { override: true })
.rename(/endpoint/i, 'ENDPOINT', { override: true })
.rename(/hosts/i, 'HOSTS', { override: true })
.rename(/plugins/i, 'PLUGINS', { override: true })
.rename(/defaults/i, 'DEFAULTS', { override: true })
.rename(/environment/i, 'ENVIRONMENT', { override: true })
.rename(/cookiejar/i, 'COOKIEJAR', { override: true });
const validate = async function(config) {
try {
let results = await Joi.validate(config, schema, {
allowUnknown: true
});
return { valid: true };
} catch ({ name, details }) {
return {
valid: false,
message: `${name}: \n ${details
.map(d => d.message + ' @ ' + d.path)
.join(' \n ')}`
};
}
};
module.exports = { schema, validate };