Merge branch 'master' into next-schema-validation

This commit is contained in:
David Diaz 2018-05-01 11:17:38 -06:00
commit 0a7fbc90ff
11 changed files with 2461 additions and 2163 deletions

189
bin/beau
View File

@ -1,188 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
const program = require('commander');
const process = require('process');
const Beau = require('../src/beau');
const yaml = require('js-yaml');
const fs = require('fs');
const { Line, Spinner } = require('clui');
const clc = require('cli-color');
const jsome = require('jsome');
const dotenv = require('dotenv');
const updateNotifier = require('update-notifier');
const package = require('../package.json'); require('@oclif/command')
.run()
updateNotifier({ pkg: package }).notify(); .catch(require('@oclif/errors/handle'));
program.version(package.version);
program
.command('request <alias>')
.option(
'-c --config <config>',
'Specify your request config file. Defaults to beau.yml in the current directory.',
'beau.yml'
)
.option(
'--verbose',
'Show all the information available on the current request.',
false
)
.option('--no-format', 'Return the text without any special formatting.')
.action(async (alias, { config, format, verbose }) => {
const beau = loadConfig(config);
let spinner;
if (format) {
spinner = new Spinner(clc.yellow(`Requesting: ${alias}`));
spinner.start();
}
try {
let res = await beau.requests.execByAlias(alias);
let { status, headers, body } = res.response;
let { endpoint } = res.request;
if (format) {
spinner.stop();
status = status.toString().startsWith(2)
? clc.green(status)
: clc.red(status);
new Line()
.padding(2)
.column('Status', 20, [clc.cyan])
.column('Endpoint', 20, [clc.cyan])
.output();
new Line()
.padding(2)
.column(status, 20)
.column(endpoint)
.output();
new Line().output();
if (verbose) {
jsome(res);
} else {
jsome(body);
}
} else {
console.log(status);
console.log(endpoint);
console.log(JSON.stringify(headers));
console.log(JSON.stringify(body));
}
process.exit(0);
} catch (err) {
new Line().output();
console.error(err.message);
process.exit(1);
}
});
program
.command('list')
.option(
'-c --config <config>',
'Specify your request config file. Defaults to beau.yml in the current directory.',
'beau.yml'
)
.option('--no-format', 'Return the text without any special formatting.')
.action(({ config, format }) => {
const beau = loadConfig(config);
if (format) {
new Line()
.padding(2)
.column('HTTP Verb', 20, [clc.cyan])
.column('Alias', 30, [clc.cyan])
.column('Endpoint', 20, [clc.cyan])
.output();
beau.requests.list.forEach(({ VERB, ALIAS, ENDPOINT, PATH }) =>
new Line()
.padding(2)
.column(VERB, 20, [clc.yellow])
.column(ALIAS, 30, [clc.yellow])
.column(ENDPOINT.replace(/\/$/, '') + '/' + PATH.replace(/^\//, ''))
.output()
);
new Line().output();
} else {
beau.requests.list.forEach(({ VERB, ALIAS, ENDPOINT, PATH }) => {
console.log(`${VERB}\t${ALIAS}\t${ENDPOINT.replace(/\/$/, '')}/${PATH.replace(/^\//, '')}`);
});
}
});
program
.command('init')
.option(
'-e --endpoint <endpoint>',
'Allows you to set the default endpoint',
null
)
.action(({ endpoint }) => {
const newFile = `# Beau.yml
version: 1${
endpoint === null
? `
# endpoint: http://example.com
`
: `
endpoint: ${endpoint}
`
}
# defaults:
# params:
# userId: 25
# GET /profile: profile
# GET /posts:
# alias: posts
# params:
# order: ASC
# POST /profile:
# alias: save-profile
# headers:
# authentication: Bearer token
# payload:
# name: David
# lastname: Diaz
`;
if (!fs.existsSync('beau.yml')) {
fs.writeFileSync('beau.yml', newFile);
console.info('beau.yml created!');
} else {
console.error('beau.yml already exists.');
}
});
program.parse(process.argv);
if (!program.args.length) {
program.help();
}
function loadConfig(configFile) {
if (!fs.existsSync(configFile)) {
console.error(`The config file, ${configFile} was not found.`);
process.exit(1);
}
const config = yaml.safeLoad(fs.readFileSync(configFile, 'utf-8'));
const env = dotenv.config().parsed || {};
return new Beau(config, env);
}

37
bin/cli/base.js Normal file
View File

@ -0,0 +1,37 @@
const yaml = require('js-yaml');
const fs = require('fs');
const dotenv = require('dotenv');
const { Command, flags } = require('@oclif/command');
const Beau = require('../../src/beau');
class Base extends Command {
loadConfig(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'));
const env = dotenv.config().parsed || {};
return new Beau(config, env);
}
}
Base.flags = {
config: flags.string({
char: 'c',
description: 'The configuration file to be used.',
default: 'beau.yml'
}),
verbose: flags.boolean({
char: 'V',
description: 'Show all additional information available for a command.'
}),
'no-format': flags.boolean({
description: `Disables color formatting for usage on external tools.`
})
};
module.exports = Base;

52
bin/cli/commands/list.js Normal file
View File

@ -0,0 +1,52 @@
const clc = require('cli-color');
const { Line } = require('clui');
const { flags } = require('@oclif/command');
const Base = require('../base');
class ListCommand extends Base {
async run() {
const { flags } = this.parse(ListCommand);
const Beau = this.loadConfig(flags.config);
if (flags['no-format']) {
return Beau.requests.list.forEach(
({ VERB, ALIAS, ENDPOINT, PATH }) =>
this.log(
VERB +
`\t` +
ALIAS +
`\t` +
ENDPOINT.replace(/\/$/, '') +
`/` +
PATH.replace(/^\//, '')
)
);
}
new Line()
.padding(2)
.column('HTTP Verb', 20, [clc.cyan])
.column('Alias', 30, [clc.cyan])
.column('Endpoint', 20, [clc.cyan])
.output();
Beau.requests.list.forEach(({ VERB, ALIAS, ENDPOINT, PATH }) =>
new Line()
.padding(2)
.column(VERB, 20, [clc.yellow])
.column(ALIAS, 30, [clc.yellow])
.column(
ENDPOINT.replace(/\/$/, '') + '/' + PATH.replace(/^\//, '')
)
.output()
);
new Line().output();
}
}
ListCommand.description = `Lists all available requests in the config file.`;
ListCommand.flags = { ...Base.flags };
module.exports = ListCommand;

View File

@ -0,0 +1,83 @@
const clc = require('cli-color');
const jsome = require('jsome');
const { Line, Spinner } = require('clui');
const { flags } = require('@oclif/command');
const Base = require('../base');
class RequestCommand extends Base {
prettyOutput(res, verbose = false) {
let { status, body } = res.response;
this.spinner.stop();
status = status.toString().startsWith(2)
? clc.green(status)
: clc.red(status);
new Line()
.padding(2)
.column('Status', 20, [clc.cyan])
.column('Endpoint', 20, [clc.cyan])
.output();
new Line()
.padding(2)
.column(status, 20)
.column(res.request.endpoint)
.output();
new Line().output();
jsome((verbose ? res : body) || null);
}
async run() {
const { flags, args } = this.parse(RequestCommand);
const Beau = this.loadConfig(flags.config);
this.spinner = new Spinner(clc.yellow(`Requesting: ${args.alias}`), [
'⣾',
'⣽',
'⣻',
'⢿',
'⡿',
'⣟',
'⣯',
'⣷'
]);
try {
if (!flags['no-format']) {
this.spinner.start();
}
let res = await Beau.requests.execByAlias(args.alias);
if (flags['no-format']) {
this.log(res.response.status);
this.log(res.request.endpoint);
this.log(JSON.stringify(res.response.headers));
this.log(JSON.stringify(res.response.body));
} else {
this.prettyOutput(res, flags.verbose);
}
} catch (err) {
new Line().output();
this.spinner.stop();
this.error(err.message);
}
}
}
RequestCommand.description = `Executes a request by name.`;
RequestCommand.flags = { ...Base.flags };
RequestCommand.args = [
{
name: 'alias',
required: true,
description: `The alias of the request to execute.`
}
];
module.exports = RequestCommand;

4175
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,23 +11,32 @@
"test:coverage": "jest --coverage" "test:coverage": "jest --coverage"
}, },
"dependencies": { "dependencies": {
"@oclif/command": "^1.4.16",
"@oclif/config": "^1.6.13",
"@oclif/plugin-help": "^1.2.5",
"@oclif/plugin-warn-if-update-available": "^1.3.6",
"cli-color": "^1.1.0", "cli-color": "^1.1.0",
"clui": "^0.3.1", "clui": "^0.3.1",
"commander": "^2.15.1",
"deepmerge": "^2.1.0", "deepmerge": "^2.1.0",
"dotenv": "^5.0.1", "dotenv": "^5.0.1",
"joi": "^13.2.0", "joi": "^13.2.0",
"globby": "^8.0.1",
"is-plain-object": "^2.0.4",
"js-yaml": "^3.11.0", "js-yaml": "^3.11.0",
"jsome": "^2.5.0", "jsome": "^2.5.0",
"request": "^2.85.0", "request": "^2.85.0",
"request-promise-native": "^1.0.5", "request-promise-native": "^1.0.5",
"requireg": "^0.1.6", "requireg": "^0.1.6"
"update-notifier": "^2.5.0"
}, },
"repository": "git@github.com:Seich/Beau.git", "repository": "git@github.com:Seich/Beau.git",
"devDependencies": { "devDependencies": {
"jest": "^22.4.0" "jest": "^22.4.0"
}, },
"oclif": {
"commands": "./bin/cli/commands",
"bin": "beau",
"plugins": ["@oclif/plugin-help", "@oclif/plugin-warn-if-update-available"]
},
"jest": { "jest": {
"testEnvironment": "node", "testEnvironment": "node",
"notify": true "notify": true

View File

@ -186,6 +186,7 @@ Beau {
"DEPENDENCIES": Set {}, "DEPENDENCIES": Set {},
"ENDPOINT": "http://jsonplaceholder.typicode.com", "ENDPOINT": "http://jsonplaceholder.typicode.com",
"FORM": undefined, "FORM": undefined,
"FORMDATA": undefined,
"HEADERS": Object { "HEADERS": Object {
"authentication": "hello", "authentication": "hello",
}, },
@ -218,6 +219,7 @@ Beau {
"DEPENDENCIES": Set {}, "DEPENDENCIES": Set {},
"ENDPOINT": "http://jsonplaceholder.typicode.com", "ENDPOINT": "http://jsonplaceholder.typicode.com",
"FORM": undefined, "FORM": undefined,
"FORMDATA": undefined,
"HEADERS": Object { "HEADERS": Object {
"authentication": "hello", "authentication": "hello",
"hello": "world", "hello": "world",

View File

@ -1,24 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Beau's plugin system Dynamic Values should look for dynamic values executing and replacing them 1`] = `
Object {
"body": "Hello World",
"request": Object {
"body": undefined,
"endpoint": "http://example.com/hello/3",
"headers": Object {
"count": "3",
"preRequestModifier": true,
},
},
"response": Object {
"body": "Hello World",
"headers": Array [],
"status": 200,
},
}
`;
exports[`Beau's plugin system Request Modifiers should modify the request and response using modifiers. 1`] = ` exports[`Beau's plugin system Request Modifiers should modify the request and response using modifiers. 1`] = `
Object { Object {
"body": "Hello World", "body": "Hello World",

View File

@ -65,21 +65,31 @@ describe(`Beau's plugin system`, () => {
endpoint: 'http://example.com', endpoint: 'http://example.com',
alias: 'say-hello', alias: 'say-hello',
headers: { headers: {
count: '$[add(1, $value2)]' count: '$[add(1, $value2)]',
} empty: ''
},
payload: 'counted $[add(1, $value2)] so far.'
}, },
plugins plugins
); );
}); });
it(`should look for dynamic values executing and replacing them`, async () => {
let cache = new RequestCache(); let cache = new RequestCache();
cache.add('value2', '2'); cache.add('value2', '2');
it(`should look for dynamic values executing and replacing them`, async () => {
let req = await request.exec(cache); let req = await request.exec(cache);
expect(req).toHaveProperty('request.body', 'counted 3 so far.');
});
expect(req).toHaveProperty('request.headers.count', '3'); it(`should change the internal datatype if the only thing in the value is the dynamic value`, async () => {
expect(req).toMatchSnapshot(); let req = await request.exec(cache);
expect(req).toHaveProperty('request.headers.count', 3);
});
it(`should return empty values as empty`, async () => {
let req = await request.exec(cache);
expect(req).toHaveProperty('request.headers.empty', '');
}); });
it(`should throw when calling an undefined dynamic value`, async () => { it(`should throw when calling an undefined dynamic value`, async () => {

View File

@ -2,6 +2,7 @@ const vm = require('vm');
const requireg = require('requireg'); const requireg = require('requireg');
const deepmerge = require('deepmerge'); const deepmerge = require('deepmerge');
const { toKebabCase, dynamicValueRegex, replaceInObject } = require('./shared'); const { toKebabCase, dynamicValueRegex, replaceInObject } = require('./shared');
const isPlainObject = require('is-plain-object');
class Plugins { class Plugins {
constructor(plugins = []) { constructor(plugins = []) {
@ -36,7 +37,7 @@ class Plugins {
} }
executeModifier(modifier, obj, orig) { executeModifier(modifier, obj, orig) {
let result = deepmerge({}, obj); let result = deepmerge({}, obj, { isMergeableObject: isPlainObject });
this.registry[modifier].forEach( this.registry[modifier].forEach(
modifier => (result = modifier(result, orig)) modifier => (result = modifier(result, orig))
@ -47,7 +48,25 @@ class Plugins {
replaceDynamicValues(obj) { replaceDynamicValues(obj) {
return replaceInObject(obj, val => { return replaceInObject(obj, val => {
let valIsEmpty = val.trim().length === 0;
if (valIsEmpty) {
return val;
}
try { try {
let onlyHasDynamic =
val.replace(dynamicValueRegex, '').trim() === '';
if (onlyHasDynamic) {
let call;
val.replace(dynamicValueRegex, (match, c) => {
call = c;
});
return vm.runInContext(call, this.context);
}
return val.replace(dynamicValueRegex, (match, call) => { return val.replace(dynamicValueRegex, (match, call) => {
return vm.runInContext(call, this.context); return vm.runInContext(call, this.context);
}); });

View File

@ -25,7 +25,8 @@ class Request {
'PARAMS', 'PARAMS',
'FORM', 'FORM',
'ALIAS', 'ALIAS',
'COOKIEJAR' 'COOKIEJAR',
'FORMDATA'
], ],
req req
); );
@ -93,6 +94,7 @@ class Request {
qs: this.PARAMS, qs: this.PARAMS,
body: this.PAYLOAD, body: this.PAYLOAD,
form: this.FORM, form: this.FORM,
formData: this.FORMDATA,
json: true, json: true,
simple: false, simple: false,
@ -103,7 +105,8 @@ class Request {
'headers', 'headers',
'qs', 'qs',
'body', 'body',
'form' 'form',
'formData'
]); ]);
settings = this.plugins.replaceDynamicValues(settings); settings = this.plugins.replaceDynamicValues(settings);