Refactored Beau's Plugin System

It now uses a plugin registry. Plugins are loaded when the configuration
file is first parsed. When a request is made it is passed over to the
available modifiers before and after it's execution and applies
whichever changes are made. It now passes a copy instead of a reference
to provide a nicer interface.

Dynamic values have been added as a plugin type. These plugins are
javascript functions that can be called from within the beau file and
whose results are used as a replacement.

These are added along with variables to the runtime execution flow. The
current order for their execution is:

Request composition -> Dynamic Values -> Pre-Request Modifiers ->
Post-Request Modifiers.
This commit is contained in:
David Diaz 2018-04-04 14:27:29 -06:00
parent 293c3883e9
commit 1fb45da5de
22 changed files with 3588 additions and 4753 deletions

4590
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@
}, },
"repository": "git@github.com:Seich/Beau.git", "repository": "git@github.com:Seich/Beau.git",
"devDependencies": { "devDependencies": {
"jest": "22.0.4" "jest": "^22.4.0"
}, },
"jest": { "jest": {
"testEnvironment": "node", "testEnvironment": "node",

View File

@ -0,0 +1,11 @@
class DynamicValues {
constructor(registry, settings = {}) {
registry.defineDynamicValue('add', this.add);
}
add(x, y) {
return x + y;
}
}
module.exports = DynamicValues;

View File

@ -0,0 +1,19 @@
class Modifiers {
constructor(registry, settings = {}) {
registry.addPreRequestModifier(this.preRequest);
registry.addPostRequestModifier(this.postRequest);
}
preRequest(request, orig) {
request.headers.preRequestModifier = true;
return request;
}
postRequest(response, orig) {
response.body = 'Hello World';
response.response.body = 'Hello World';
return response;
}
}
module.exports = Modifiers;

View File

@ -1,19 +1,3 @@
module.exports = function(name) { module.exports = function(name) {
return function(settings) { return require(name);
let response = {
name: `${name}`,
preRequest(request) {
request.wasModified = true;
return request;
},
postResponse(response) {
response.changed = true;
return response;
}
};
return response;
};
}; };

View File

@ -12,7 +12,33 @@ Beau {
"ENDPOINT": "http://jsonplaceholder.typicode.com", "ENDPOINT": "http://jsonplaceholder.typicode.com",
"ENVIRONMENT": Object {}, "ENVIRONMENT": Object {},
"HOSTS": Array [], "HOSTS": Array [],
"PLUGINS": Array [], "PLUGINS": Plugins {
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
},
"REQUESTS": Array [
Object {
"ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
],
"VERSION": 1, "VERSION": 1,
"configKeys": Array [ "configKeys": Array [
"VERSION", "VERSION",
@ -48,7 +74,9 @@ Beau {
"endpoint": "http://jsonplaceholder.typicode.com", "endpoint": "http://jsonplaceholder.typicode.com",
"version": 1, "version": 1,
}, },
"requests": Array [ },
"requests": RequestList {
"REQUESTS": Array [
Object { Object {
"ALIAS": "get-post", "ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com", "ENDPOINT": "http://jsonplaceholder.typicode.com",
@ -67,8 +95,6 @@ Beau {
"REQUEST": "GET /user", "REQUEST": "GET /user",
}, },
], ],
},
"requests": RequestList {
"cache": RequestCache { "cache": RequestCache {
"$cache": Object { "$cache": Object {
"$env": Object {}, "$env": Object {},
@ -84,7 +110,33 @@ Beau {
"ENDPOINT": "http://jsonplaceholder.typicode.com", "ENDPOINT": "http://jsonplaceholder.typicode.com",
"ENVIRONMENT": Object {}, "ENVIRONMENT": Object {},
"HOSTS": Array [], "HOSTS": Array [],
"PLUGINS": Array [], "PLUGINS": Plugins {
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
},
"REQUESTS": Array [
Object {
"ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
],
"VERSION": 1, "VERSION": 1,
"configKeys": Array [ "configKeys": Array [
"VERSION", "VERSION",
@ -120,25 +172,6 @@ Beau {
"endpoint": "http://jsonplaceholder.typicode.com", "endpoint": "http://jsonplaceholder.typicode.com",
"version": 1, "version": 1,
}, },
"requests": Array [
Object {
"ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
],
}, },
"list": Array [ "list": Array [
Request { Request {
@ -159,6 +192,14 @@ Beau {
}, },
"REQUEST": "GET /posts/1", "REQUEST": "GET /posts/1",
}, },
"plugins": Plugins {
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
},
}, },
Request { Request {
"ALIAS": "user", "ALIAS": "user",
@ -180,26 +221,14 @@ Beau {
}, },
"REQUEST": "GET /user", "REQUEST": "GET /user",
}, },
"plugins": Plugins {
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
}, },
],
"modifiers": Array [],
"requests": Array [
Object {
"ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
}, },
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
}, },
], ],
}, },

View File

@ -41,7 +41,68 @@ Config {
"host": "info", "host": "info",
}, },
], ],
"PLUGINS": Array [], "PLUGINS": Plugins {
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
},
"REQUESTS": Array [
Object {
"ALIAS": "e1",
"ENDPOINT": "http://example.org",
"HEADERS": Object {
"hello": "mars",
},
"REQUEST": "GET /e1",
},
Object {
"ALIAS": "com:e2",
"ENDPOINT": "http://example.com",
"HEADERS": Object {
"hello": "world",
"world": "hello",
},
"REQUEST": "GET /e2",
},
Object {
"ALIAS": "com:posts",
"ENDPOINT": "http://example.com",
"HEADERS": Object {
"hello": "world",
"world": "hello",
},
"REQUEST": "GET /posts",
},
Object {
"ALIAS": "net:e3",
"ENDPOINT": "http://example.net",
"HEADERS": Object {
"hello": "world",
"world": "bye",
},
"REQUEST": "GET /e3",
},
Object {
"ALIAS": "net:posts",
"ENDPOINT": "http://example.net",
"HEADERS": Object {
"hello": "world",
"world": "bye",
},
"REQUEST": "GET /posts",
},
Object {
"ALIAS": "info:posts",
"ENDPOINT": "http://example.info",
"HEADERS": Object {
"hello": "mars",
},
"REQUEST": "GET /posts",
},
],
"VERSION": 1, "VERSION": 1,
"configKeys": Array [ "configKeys": Array [
"VERSION", "VERSION",
@ -101,60 +162,6 @@ Config {
}, },
], ],
}, },
"requests": Array [
Object {
"ALIAS": "e1",
"ENDPOINT": "http://example.org",
"HEADERS": Object {
"hello": "mars",
},
"REQUEST": "GET /e1",
},
Object {
"ALIAS": "com:e2",
"ENDPOINT": "http://example.com",
"HEADERS": Object {
"hello": "world",
"world": "hello",
},
"REQUEST": "GET /e2",
},
Object {
"ALIAS": "com:posts",
"ENDPOINT": "http://example.com",
"HEADERS": Object {
"hello": "world",
"world": "hello",
},
"REQUEST": "GET /posts",
},
Object {
"ALIAS": "net:e3",
"ENDPOINT": "http://example.net",
"HEADERS": Object {
"hello": "world",
"world": "bye",
},
"REQUEST": "GET /e3",
},
Object {
"ALIAS": "net:posts",
"ENDPOINT": "http://example.net",
"HEADERS": Object {
"hello": "world",
"world": "bye",
},
"REQUEST": "GET /posts",
},
Object {
"ALIAS": "info:posts",
"ENDPOINT": "http://example.info",
"HEADERS": Object {
"hello": "mars",
},
"REQUEST": "GET /posts",
},
],
} }
`; `;
@ -169,7 +176,33 @@ Config {
"ENDPOINT": "http://jsonplaceholder.typicode.com", "ENDPOINT": "http://jsonplaceholder.typicode.com",
"ENVIRONMENT": Object {}, "ENVIRONMENT": Object {},
"HOSTS": Array [], "HOSTS": Array [],
"PLUGINS": Array [], "PLUGINS": Plugins {
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
},
"REQUESTS": Array [
Object {
"ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
],
"VERSION": 1, "VERSION": 1,
"configKeys": Array [ "configKeys": Array [
"VERSION", "VERSION",
@ -205,24 +238,5 @@ Config {
"endpoint": "http://jsonplaceholder.typicode.com", "endpoint": "http://jsonplaceholder.typicode.com",
"version": 1, "version": 1,
}, },
"requests": Array [
Object {
"ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
],
} }
`; `;

View File

@ -0,0 +1,49 @@
// 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`] = `
Object {
"body": "Hello World",
"request": Object {
"body": undefined,
"endpoint": "http://example.com/user",
"headers": Object {
"preRequestModifier": true,
},
},
"response": Object {
"body": "Hello World",
"headers": Array [],
"status": 200,
},
}
`;
exports[`Beau's plugin system shouldn't do anything when given an empty array. 1`] = `
Plugins {
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
}
`;

View File

@ -35,22 +35,3 @@ Object {
}, },
} }
`; `;
exports[`Request should use modifiers 1`] = `
Array [
Array [
Object {
"endpoint": "http://martianwabbit.com/user",
"headers": Object {},
"method": "GET",
"payload": Object {},
"query": Object {},
},
Object {
"alias": "show",
"endpoint": "http://martianwabbit.com",
"request": "GET /user",
},
],
]
`;

View File

@ -3,7 +3,6 @@
exports[`RequestList should execute requests by alias. 1`] = ` exports[`RequestList should execute requests by alias. 1`] = `
Object { Object {
"body": "{\\"hello\\": \\"world\\"}", "body": "{\\"hello\\": \\"world\\"}",
"changed": true,
"request": Object { "request": Object {
"body": Object { "body": Object {
"lastname": "Diaz", "lastname": "Diaz",

View File

@ -31,7 +31,7 @@ describe('Config', () => {
`); `);
const config = new Config(doc); const config = new Config(doc);
expect(Object.keys(config.requests).length).toBe(4); expect(Object.keys(config.REQUESTS).length).toBe(4);
}); });
it('should set up defaults for all requests', () => { it('should set up defaults for all requests', () => {
@ -53,7 +53,7 @@ describe('Config', () => {
const config = new Config(doc); const config = new Config(doc);
expect(config).toMatchSnapshot(); expect(config).toMatchSnapshot();
Object.values(config.requests).forEach(r => { Object.values(config.REQUESTS).forEach(r => {
expect(r.HEADERS.authentication).toMatch('hello'); expect(r.HEADERS.authentication).toMatch('hello');
}); });
}); });
@ -115,8 +115,8 @@ describe('Config', () => {
let config = new Config(doc); let config = new Config(doc);
expect(config.requests[0].ALIAS).toBe('test1:posts'); expect(config.REQUESTS[0].ALIAS).toBe('test1:posts');
expect(config.requests[1].ALIAS).toBe('test2:posts'); expect(config.REQUESTS[1].ALIAS).toBe('test2:posts');
}); });
it(`should throw if host doesn't have a host key`, () => { it(`should throw if host doesn't have a host key`, () => {
@ -153,6 +153,6 @@ describe('Config', () => {
`); `);
let config = new Config(doc); let config = new Config(doc);
expect(config.requests[0].HEADERS.hello).toBe(1); expect(config.REQUESTS[0].HEADERS.hello).toBe(1);
}); });
}); });

View File

@ -0,0 +1,94 @@
const yaml = require('js-yaml');
const Config = require('../config');
const Plugins = require('../plugins');
const Request = require('../request');
const RequestCache = require('../requestCache');
describe(`Beau's plugin system`, () => {
let config;
let request;
let plugins;
beforeEach(() => {
const doc = yaml.safeLoad(`
version: 1
endpoint: 'http://example.com'
plugins:
- Modifiers:
data: hi
- DynamicValues
GET /posts/$[add(1, 1)]: get-post
`);
config = new Config(doc);
plugins = config.PLUGINS;
});
it('should load all plugins', () => {
expect(plugins.registry.preRequestModifiers.length).toBe(1);
expect(plugins.registry.postRequestModifiers.length).toBe(1);
expect(plugins.registry.dynamicValues.length).toBe(1);
});
it(`should throw if given an invalid configuration`, () => {
expect(() => new Plugins([{ test1: true, test2: true }])).toThrow();
});
it(`shouldn't do anything when given an empty array.`, () => {
expect(new Plugins()).toMatchSnapshot();
});
describe(`Request Modifiers`, () => {
beforeEach(() => {
request = new Request(
{
request: 'POST /user',
endpoint: 'http://example.com',
alias: 'update'
},
plugins
);
});
it(`should modify the request and response using modifiers.`, async () => {
await expect(request.exec()).resolves.toMatchSnapshot();
});
});
describe(`Dynamic Values`, () => {
beforeEach(() => {
request = new Request(
{
request: 'PATCH /hello/$[add(1, 2)]',
endpoint: 'http://example.com',
alias: 'say-hello',
headers: {
count: '$[add(1, $value2)]'
}
},
plugins
);
});
it(`should look for dynamic values executing and replacing them`, async () => {
let cache = new RequestCache();
cache.add('$value2', '2');
let req = await request.exec(cache);
expect(req).toHaveProperty('request.headers.count', '3');
expect(req).toMatchSnapshot();
});
it(`should throw when calling an undefined dynamic value`, async () => {
request = new Request({
request: 'POST /hello/$[notAvailable(1, 2)]',
alias: 'say-hello'
});
await expect(request.exec()).rejects.toThrow();
});
});
});

View File

@ -64,7 +64,7 @@ describe('Request', () => {
}); });
it('should execute a request', async () => { it('should execute a request', async () => {
await expect(request.exec([], cache)).resolves.toMatchSnapshot(); await expect(request.exec(cache)).resolves.toMatchSnapshot();
await expect( await expect(
requestWithoutDependencies.exec() requestWithoutDependencies.exec()
).resolves.toMatchSnapshot(); ).resolves.toMatchSnapshot();
@ -74,21 +74,4 @@ describe('Request', () => {
requestPromiseNativeMock.fail = true; requestPromiseNativeMock.fail = true;
await expect(requestWithoutDependencies.exec()).rejects.toThrow(Error); await expect(requestWithoutDependencies.exec()).rejects.toThrow(Error);
}); });
it('should use modifiers', async () => {
const preRequest = jest.fn();
const withPreRequest = [{ preRequest }];
const notCalled = jest.fn();
const nonModifiers = [{ notCalled }];
await requestWithoutDependencies.exec(withPreRequest);
expect(preRequest).toHaveBeenCalled();
expect(preRequest.mock.calls).toMatchSnapshot();
await requestWithoutDependencies.exec(nonModifiers);
expect(notCalled).not.toHaveBeenCalled();
});
}); });

View File

@ -12,17 +12,6 @@ describe('RequestList', () => {
const doc = { const doc = {
ENDPOINT: endpoint, ENDPOINT: endpoint,
ENVIRONMENT: env, ENVIRONMENT: env,
PLUGINS: [
{
'beau-jwt': {
data: {
secret: 'shhh.',
userId: 412
}
}
},
'beau-document'
],
'GET /post': { alias: 'get-posts' }, 'GET /post': { alias: 'get-posts' },
'POST /user': { 'POST /user': {
alias: 'user', alias: 'user',
@ -38,7 +27,12 @@ describe('RequestList', () => {
requestPromiseNativeMock.fail = false; requestPromiseNativeMock.fail = false;
let config = new Config(doc); let config = new Config(doc);
requests = new RequestList(config.requests, config); requests = new RequestList(config);
});
it('should allow an empty request list', () => {
requests = new RequestList();
expect(requests.list.length).toBe(0);
}); });
it('should load valid requests', () => { it('should load valid requests', () => {
@ -49,12 +43,6 @@ describe('RequestList', () => {
requests.fetchDependencies(['get-posts']); requests.fetchDependencies(['get-posts']);
}); });
it('should load plugins', () => {
const pluginLessList = new RequestList();
expect(requests.modifiers.length).toBe(2);
expect(pluginLessList.modifiers.length).toBe(0);
});
it('should execute requests by alias.', async () => { it('should execute requests by alias.', async () => {
await expect(requests.execByAlias('user')).resolves.toMatchSnapshot(); await expect(requests.execByAlias('user')).resolves.toMatchSnapshot();
}); });
@ -83,6 +71,6 @@ describe('RequestList', () => {
} }
}); });
expect(() => new RequestList(config.requests, config)).toThrow(); expect(() => new RequestList(config, config)).toThrow();
}); });
}); });

View File

@ -4,8 +4,7 @@ const Config = require('./config');
class Beau { class Beau {
constructor(doc, env = {}) { constructor(doc, env = {}) {
this.config = new Config(doc, env); this.config = new Config(doc, env);
this.requests = new RequestList(this.config);
this.requests = new RequestList(this.config.requests, this.config);
} }
} }

View File

@ -1,5 +1,6 @@
const deepMerge = require('deepmerge'); const deepMerge = require('deepmerge');
const { requestRegex, UpperCaseKeys } = require('./shared'); const { requestRegex, UpperCaseKeys } = require('./shared');
const Plugins = require('./plugins');
class Config { class Config {
constructor(doc, env = {}) { constructor(doc, env = {}) {
@ -23,7 +24,7 @@ class Config {
this.ENVIRONMENT = deepMerge(this.ENVIRONMENT, env); this.ENVIRONMENT = deepMerge(this.ENVIRONMENT, env);
this.requests = []; this.REQUESTS = [];
this.loadRequests(doc, { this.loadRequests(doc, {
DEFAULTS: this.DEFAULTS, DEFAULTS: this.DEFAULTS,
@ -31,6 +32,8 @@ class Config {
}); });
this.loadHosts(this.HOSTS, config); this.loadHosts(this.HOSTS, config);
this.PLUGINS = new Plugins(this.PLUGINS);
} }
loadHosts(hosts, rootConfig) { loadHosts(hosts, rootConfig) {
@ -77,7 +80,7 @@ class Config {
return deepMerge(defaults, request); return deepMerge(defaults, request);
}); });
this.requests = this.requests.concat(requests); this.REQUESTS = this.REQUESTS.concat(requests);
} }
loadConfig(host) { loadConfig(host) {

92
src/plugins.js Normal file
View File

@ -0,0 +1,92 @@
const vm = require('vm');
const requireg = require('requireg');
const deepmerge = require('deepmerge');
const { toKebabCase, dynamicValueRegex, replaceInObject } = require('./shared');
class Plugins {
constructor(plugins = []) {
this.registry = {
preRequestModifiers: [],
postRequestModifiers: [],
dynamicValues: []
};
this.context = {};
plugins.forEach(plugin => this.loadPlugin(plugin));
}
loadPlugin(plugin) {
let name = plugin;
let settings = {};
if (typeof plugin === 'object') {
let keys = Object.keys(plugin);
if (keys.length !== 1) {
throw new Error(`Plugin items should contain only one key.`);
}
name = Object.keys(plugin)[0];
settings = plugin[name];
}
plugin = requireg(`./beau-${toKebabCase(name)}`);
new plugin(this, settings);
}
executeModifier(modifier, obj, orig) {
let result = deepmerge({}, obj);
this.registry[modifier].forEach(
modifier => (result = modifier(result, orig))
);
return result;
}
execPreRequestModifiers(request, originalRequest) {
return this.executeModifier(
'preRequestModifiers',
request,
originalRequest
);
}
execPostRequestModifiers(response, originalRequest) {
return this.executeModifier(
'postRequestModifiers',
response,
originalRequest
);
}
replaceDynamicValues(obj) {
return replaceInObject(obj, val => {
try {
return val.replace(dynamicValueRegex, (match, call) => {
return vm.runInContext(call, this.context);
});
} catch (e) {
throw new Error(`DynamicValue: ` + e);
}
});
}
addPreRequestModifier(modifier) {
this.registry.preRequestModifiers.push(modifier);
}
addPostRequestModifier(modifier) {
this.registry.postRequestModifiers.push(modifier);
}
defineDynamicValue(name, fn) {
this.registry.dynamicValues.push({ name, fn });
this.context[name] = fn;
vm.createContext(this.context);
}
}
module.exports = Plugins;

View File

@ -1,4 +1,8 @@
const request = require('request-promise-native'); const request = require('request-promise-native');
const RequestList = require('./requestList');
const RequestCache = require('./requestCache');
const Plugins = require('./plugins');
const { const {
httpVerbs, httpVerbs,
requestRegex, requestRegex,
@ -6,12 +10,11 @@ const {
UpperCaseKeys, UpperCaseKeys,
removeOptionalKeys removeOptionalKeys
} = require('./shared'); } = require('./shared');
const RequestList = require('./requestList');
const RequestCache = require('./requestCache');
class Request { class Request {
constructor(req) { constructor(req, plugins = new Plugins()) {
this.originalRequest = req; this.originalRequest = req;
this.plugins = plugins;
const { const {
REQUEST, REQUEST,
@ -50,13 +53,15 @@ class Request {
} }
findDependencies(request, set = new Set()) { findDependencies(request, set = new Set()) {
if (typeof request === 'object') { let type = typeof request;
const keys = Object.keys(request).filter(key => key !== 'ALIAS');
keys.forEach(key => { if (type === 'object') {
Object.keys(request)
.filter(key => key !== 'ALIAS')
.forEach(key => {
set = this.findDependencies(request[key], set); set = this.findDependencies(request[key], set);
}); });
} else if (typeof request === 'string') { } else if (type === 'string') {
const matches = request.match(replacementRegex) || []; const matches = request.match(replacementRegex) || [];
const deps = matches.map(m => m.split('.')[0].substring(1)); const deps = matches.map(m => m.split('.')[0].substring(1));
@ -66,21 +71,22 @@ class Request {
return set; return set;
} }
async exec(modifiers = [], cache = new RequestCache()) { async exec(cache = new RequestCache()) {
const settings = { let settings = cache.parse({
endpoint: cache.parse(this.ENDPOINT), endpoint: this.ENDPOINT,
method: this.VERB, method: this.VERB,
headers: cache.parse(this.HEADERS), headers: this.HEADERS,
query: cache.parse(this.PARAMS), query: this.PARAMS,
payload: cache.parse(this.PAYLOAD) payload: this.PAYLOAD
};
modifiers.forEach(mod => {
if (typeof mod.preRequest !== 'undefined') {
mod.preRequest(settings, this.originalRequest);
}
}); });
settings = this.plugins.replaceDynamicValues(settings);
settings = this.plugins.execPreRequestModifiers(
settings,
this.originalRequest
);
try { try {
const response = await request( const response = await request(
removeOptionalKeys( removeOptionalKeys(
@ -100,7 +106,7 @@ class Request {
) )
); );
const results = { let results = {
request: { request: {
headers: response.request.headers, headers: response.request.headers,
body: response.request.body, body: response.request.body,
@ -114,11 +120,16 @@ class Request {
body: response.body body: response.body
}; };
results = this.plugins.execPostRequestModifiers(
results,
this.originalRequest
);
cache.add(`$${this.ALIAS}`, results); cache.add(`$${this.ALIAS}`, results);
return results; return results;
} catch ({ error }) { } catch ({ error }) {
throw new Error(error); throw new Error(`Request Error: ` + error);
} }
} }
} }

View File

@ -1,4 +1,4 @@
const { replacementRegex } = require('./shared'); const { replacementRegex, replaceInObject } = require('./shared');
class RequestCache { class RequestCache {
constructor() { constructor() {
@ -27,26 +27,13 @@ class RequestCache {
} }
parse(item) { parse(item) {
let type = typeof item;
if (type === 'undefined') {
return {};
}
if (item === null) { if (item === null) {
return null; return null;
} }
if (type === 'string') { return replaceInObject(item, item =>
return item.replace(replacementRegex, key => this.get(key)); item.replace(replacementRegex, key => this.get(key))
} );
if (type === 'object') {
Object.keys(item).forEach(k => (item[k] = this.parse(item[k])));
return item;
}
return item;
} }
} }

View File

@ -1,14 +1,12 @@
const Request = require('./request'); const Request = require('./request');
const RequestCache = require('./requestCache'); const RequestCache = require('./requestCache');
const httpVerbs = require('./shared').httpVerbs; const httpVerbs = require('./shared').httpVerbs;
const requireg = require('requireg');
class RequestList { class RequestList {
constructor(requests = [], config = {}) { constructor(config = { REQUESTS: [] }) {
this.config = config; this.config = config;
this.requests = requests; this.REQUESTS = config.REQUESTS;
this.modifiers = this.loadPlugins();
this.list = this.loadRequests(); this.list = this.loadRequests();
this.cache = new RequestCache(); this.cache = new RequestCache();
@ -17,7 +15,7 @@ class RequestList {
async execByAlias(alias) { async execByAlias(alias) {
if (this.cache.exists(`$${alias}`)) { if (this.cache.exists(`$${alias}`)) {
return this.applyPostResponseModifiers(this.cache.get(`$${alias}`)); return this.cache.get(`$${alias}`);
} }
const request = this.list.find(r => r.ALIAS === alias); const request = this.list.find(r => r.ALIAS === alias);
@ -28,9 +26,8 @@ class RequestList {
try { try {
await this.fetchDependencies(Array.from(request.DEPENDENCIES)); await this.fetchDependencies(Array.from(request.DEPENDENCIES));
const response = await request.exec(this.modifiers, this.cache); const response = await request.exec(this.cache);
return response;
return this.applyPostResponseModifiers(response);
} catch (reason) { } catch (reason) {
throw new Error( throw new Error(
`Request: ${request.VERB} ${ `Request: ${request.VERB} ${
@ -49,10 +46,9 @@ class RequestList {
loadRequests() { loadRequests() {
let requests = []; let requests = [];
this.requests.forEach(request => { this.REQUESTS.forEach(request => {
try { try {
let r = new Request(request); requests.push(new Request(request, this.config.PLUGINS));
requests.push(r);
} catch (e) { } catch (e) {
throw new Error(`${request.request} was ignored: ${e}`); throw new Error(`${request.request} was ignored: ${e}`);
} }
@ -60,34 +56,6 @@ class RequestList {
return requests; return requests;
} }
loadPlugins() {
if (typeof this.config.PLUGINS === 'undefined') {
return [];
}
return this.config.PLUGINS.map(plugin => {
let name = plugin;
let settings = null;
if (typeof plugin === 'object') {
name = Object.keys(plugin)[0];
settings = plugin[name];
}
return new (requireg(name))(settings);
});
}
applyPostResponseModifiers(response) {
this.modifiers.forEach(mod => {
if (typeof mod.postResponse !== 'undefined') {
mod.postResponse(response);
}
});
return response;
}
} }
module.exports = RequestList; module.exports = RequestList;

View File

@ -11,7 +11,8 @@ const httpVerbs = [
]; ];
const requestRegex = new RegExp(`(${httpVerbs.join('|')})\\s(.*)`, 'i'); const requestRegex = new RegExp(`(${httpVerbs.join('|')})\\s(.*)`, 'i');
const replacementRegex = /\$([a-zA-Z\.\d\-\_\/\\\:]*)/g; const replacementRegex = /\$([a-zA-Z\.\d\-\_\/\\\:]+)/g;
const dynamicValueRegex = /\$\[(\w+\((?:.|[\n\r])+?\))\]/g;
const UpperCaseKeys = function(obj) { const UpperCaseKeys = function(obj) {
let result = {}; let result = {};
@ -37,10 +38,43 @@ const removeOptionalKeys = function(obj, optionalValues) {
return result; return result;
}; };
const toKebabCase = function(str) {
return str
.trim()
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/\s+/g, '-')
.toLowerCase();
};
const replaceInObject = function(obj, fn) {
if (obj === null) {
return null;
}
let type = typeof obj;
if (type === 'undefined') {
return {};
}
if (type === 'string') {
return fn(obj);
}
if (type === 'object') {
Object.keys(obj).forEach(k => (obj[k] = replaceInObject(obj[k], fn)));
}
return obj;
};
module.exports = { module.exports = {
httpVerbs, httpVerbs,
requestRegex, requestRegex,
replacementRegex, replacementRegex,
dynamicValueRegex,
UpperCaseKeys, UpperCaseKeys,
removeOptionalKeys removeOptionalKeys,
toKebabCase,
replaceInObject
}; };

2942
yarn.lock

File diff suppressed because it is too large Load Diff