Merge pull request #1128 from serverless/add-plugin-management-class

Add PluginManagement class
This commit is contained in:
Florian Motlik 2016-05-19 09:47:27 +02:00
commit 57948f00cd
8 changed files with 585 additions and 1 deletions

View File

@ -0,0 +1,118 @@
'use strict';
const Utils = require('./Utils');
const path = require('path');
const has = require('lodash').has;
const forEach = require('lodash').forEach;
class PluginManager {
constructor(serverless) {
this.serverless = serverless;
this.plugins = [];
this.commandsList = [];
this.commands = {};
}
loadAllPlugins(servicePlugins) {
this.loadCorePlugins();
this.loadServicePlugins(servicePlugins);
}
runCommand(commandsArray) {
const events = this.getEvents(commandsArray, this.commands);
// collect all relevant hooks
let hooks = [];
events.forEach((event) => {
const hooksForEvent = [];
this.plugins.forEach((pluginInstance) => {
forEach(pluginInstance.hooks, (hook, hookKey) => {
if (hookKey === event) {
hooksForEvent.push(hook);
}
});
});
hooks = hooks.concat(hooksForEvent);
});
if (hooks.length === 0) {
throw new Error('The command you entered was not found. Did you spell it correctly?');
}
// run all relevant hooks one after another
hooks.forEach((hook) => {
const returnValue = hook();
// check if a Promise is returned
if (returnValue && returnValue.then instanceof Function) {
returnValue.then((value) => {
return value;
});
}
});
}
addPlugin(Plugin) {
this.plugins.push(new Plugin());
this.loadCommands(Plugin);
}
loadCorePlugins() {
const utils = new Utils();
const pluginsDirectoryPath = path.join(__dirname, '../plugins');
const corePlugins = utils.readFileSync(path.join(pluginsDirectoryPath, 'Plugins.json')).plugins;
corePlugins.forEach((corePlugin) => {
const Plugin = require(path.join(pluginsDirectoryPath, corePlugin));
this.addPlugin(Plugin);
});
}
loadServicePlugins(servicePlugins) {
servicePlugins = (typeof servicePlugins !== 'undefined' ? servicePlugins : []);
servicePlugins.forEach((servicePlugins) => {
this.addPlugin(servicePlugins);
});
}
loadCommands(Plugin) {
this.commandsList.push((new Plugin()).commands);
// TODO: refactor ASAP as it slows down overall performance
// rebuild the commands
forEach(this.commandsList, (commands) => {
forEach(commands, (commandDetails, command) => {
this.commands[command] = commandDetails;
});
});
}
getEvents(commandsArray, availableCommands, prefix) {
prefix = (typeof prefix !== 'undefined' ? prefix : '');
const commandPart = commandsArray[0];
if (has(availableCommands, commandPart)) {
const commandDetails = availableCommands[commandPart];
if (commandsArray.length === 1) {
const events = [];
commandDetails.lifeCycleEvents.forEach((event) => {
events.push(`before:${prefix}${commandPart}:${event}`);
events.push(`${prefix}${commandPart}:${event}`);
events.push(`after:${prefix}${commandPart}:${event}`);
});
return events;
}
if (has(commandDetails, 'commands')) {
return this.getEvents(commandsArray.slice(1, commandsArray.length),
commandDetails.commands, `${commandPart}:`);
}
}
return [];
}
}
module.exports = PluginManager;

View File

@ -0,0 +1,42 @@
'use strict';
class HelloWorld {
constructor() {
this.commands = {
greet: {
usage: 'Run this command to get greeted.',
lifeCycleEvents: [
'printGoodMorning',
'printHello',
'printGoodEvening'
]
},
};
this.hooks = {
'before:greet:printHello': this.printGoodMorning,
'greet:printHello': this.printHello,
'after:greet:printHello': this.printGoodEvening,
};
}
printGoodMorning() {
const message = 'Good morning';
console.log(message);
return message;
}
printHello() {
const message = 'Hello';
console.log(message);
return message;
}
printGoodEvening() {
const message = 'Good evening';
console.log(message);
return message;
}
}
module.exports = HelloWorld;

View File

@ -0,0 +1,8 @@
# Hello world
This plugin is a simple "Hello World" plugin which shows how the plugin system works.
## Setup
Run `npm install` to install all dependencies.
## Tests
Tests live inside the `tests` directory. Run `npm test` inside the root of this plugin to run the tests.

View File

@ -0,0 +1,13 @@
{
"name": "serverless-plugins-hello-world",
"version": "0.0.1",
"description": "",
"license": "MIT",
"scripts": {
"test": "node_modules/.bin/mocha ./tests/HelloWorld.js"
},
"devDependencies": {
"chai": "^3.5.0",
"mocha": "^2.4.5"
}
}

View File

@ -0,0 +1,51 @@
'use strict';
/**
* Test: HelloWorld Plugin
*/
const expect = require('chai').expect;
const HelloWorld = require('../HelloWorld');
describe('HelloWorld', () => {
let helloWorld;
beforeEach(() => {
helloWorld = new HelloWorld();
});
describe('#constructor()', () => {
it('should have commands', () => {
expect(helloWorld.commands).to.be.not.empty;
});
it('should have hooks', () => {
expect(helloWorld.hooks).to.be.not.empty;
});
});
describe('#printGoodMorning()', () => {
it('should print "Good morning"', () => {
const greeting = helloWorld.printGoodMorning();
expect(greeting).to.equal('Good morning');
});
});
describe('#printHello()', () => {
it('should print "Hello"', () => {
const greeting = helloWorld.printHello();
expect(greeting).to.equal('Hello');
});
});
describe('#printGoodEvening()', () => {
it('should print "Good evening"', () => {
const greeting = helloWorld.printGoodEvening();
expect(greeting).to.equal('Good evening');
});
});
});

5
lib/plugins/Plugins.json Normal file
View File

@ -0,0 +1,5 @@
{
"plugins": [
"./HelloWorld/HelloWorld.js"
]
}

View File

@ -7,7 +7,8 @@ describe('All Tests', function() {
before(function() {});
after(function() {});
require('./tests/classes/Serverless');
// require('./tests/classes/Serverless');
require('./tests/classes/PluginManager');
// require('./tests/classes/Utils');
// require('./tests/classes/Plugin');
// require('./tests/classes/YamlParser');

View File

@ -0,0 +1,346 @@
'use strict';
/**
* Test: PluginManager Class
*/
const expect = require('chai').expect;
const PluginManager = require('../../../lib/classes/PluginManager');
const Serverless = require('../../../lib/Serverless');
const HelloWorld = require('../../../lib/plugins/HelloWorld/HelloWorld');
describe('PluginManager', () => {
let pluginManager;
let serverless;
let helloWorld;
class ServicePluginMock1 {}
class ServicePluginMock2 {}
class PromisePluginMock {
constructor() {
this.commands = {
deploy: {
usage: 'Deploy to the default infrastructure',
lifeCycleEvents: [
'resources',
'functions'
],
commands: {
onpremises: {
usage: 'Deploy to your On-Premises infrastructure',
lifeCycleEvents: [
'resources',
'functions'
],
},
},
},
};
this.hooks = {
'deploy:functions': this.functions.bind(this),
'before:deploy:onpremises:functions': this.resources.bind(this)
};
// used to test if the function was executed correctly
this.deployedFunctions = 0;
this.deployedResources = 0;
}
functions() {
return new Promise((resolve, reject) => {
this.deployedFunctions = this.deployedFunctions + 1;
return resolve();
});
}
resources() {
return new Promise((resolve, reject) => {
this.deployedResources = this.deployedResources + 1;
return resolve();
});
}
}
class SynchronousPluginMock {
constructor() {
this.commands = {
deploy: {
usage: 'Deploy to the default infrastructure',
lifeCycleEvents: [
'resources',
'functions'
],
commands: {
onpremises: {
usage: 'Deploy to your On-Premises infrastructure',
lifeCycleEvents: [
'resources',
'functions'
],
},
},
},
};
this.hooks = {
'deploy:functions': this.functions.bind(this),
'before:deploy:onpremises:functions': this.resources.bind(this),
};
// used to test if the function was executed correctly
this.deployedFunctions = 0;
this.deployedResources = 0;
}
functions() {
this.deployedFunctions = this.deployedFunctions + 1;
}
resources() {
this.deployedResources = this.deployedResources + 1;
}
}
beforeEach(() => {
serverless = new Serverless({});
pluginManager = new PluginManager(serverless);
helloWorld = new HelloWorld();
});
describe('#constructor()', () => {
it('should set the serverless instance', () => {
expect(pluginManager.serverless).to.deep.equal(serverless);
});
it('should create an empty plugins array', () => {
expect(pluginManager.plugins.length).to.equal(0);
});
it('should create an empty commandsList array', () => {
expect(pluginManager.commandsList.length).to.equal(0);
});
it('should create an empty commands object', () => {
expect(pluginManager.commands).to.deep.equal({});
});
});
describe('#addPlugin()', () => {
it('should add a plugin instance to the plugins array', () => {
pluginManager.addPlugin(SynchronousPluginMock);
expect(pluginManager.plugins[0]).to.be.an.instanceof(SynchronousPluginMock);
});
it('should load the plugin commands', () => {
pluginManager.addPlugin(SynchronousPluginMock);
expect(pluginManager.commandsList[0]).to.have.property('deploy');
});
});
describe('#loadAllPlugins()', () => {
it('should load only core plugins when no service plugins are given', () => {
// Note: We need the HelloWorld plugin for this test to pass
pluginManager.loadAllPlugins();
expect(pluginManager.plugins).to.include(helloWorld);
});
it('should load all plugins when service plugins are given', () => {
const servicePlugins = [ServicePluginMock1, ServicePluginMock2];
pluginManager.loadAllPlugins(servicePlugins);
const servicePluginMock1 = new ServicePluginMock1();
const servicePluginMock2 = new ServicePluginMock2();
expect(pluginManager.plugins).to.contain(servicePluginMock1);
expect(pluginManager.plugins).to.contain(servicePluginMock2);
expect(pluginManager.plugins).to.contain(helloWorld);
});
it('should load all plugins in the correct order', () => {
const servicePlugins = [ServicePluginMock1, ServicePluginMock2];
// we need to mock it so that tests won't break when more core plugins are added later on
// because we access the plugins array with an index which will change every time a new core
// plugin will be added
const loadCorePluginsMock = () => {
pluginManager.addPlugin(HelloWorld);
};
// This is the exact same functionality like loadCorePlugins()
loadCorePluginsMock();
pluginManager.loadServicePlugins(servicePlugins);
expect(pluginManager.plugins[0]).to.be.instanceof(HelloWorld);
expect(pluginManager.plugins[1]).to.be.instanceof(ServicePluginMock1);
expect(pluginManager.plugins[2]).to.be.instanceof(ServicePluginMock2);
});
});
describe('#loadCorePlugins()', () => {
it('should load the Serverless core plugins', () => {
pluginManager.loadCorePlugins();
expect(pluginManager.plugins).to.contain(helloWorld);
});
});
describe('#loadServicePlugins()', () => {
it('should load the service plugins', () => {
const servicePlugins = [ServicePluginMock1, ServicePluginMock2];
pluginManager.loadServicePlugins(servicePlugins);
const servicePluginMock1 = new ServicePluginMock1();
const servicePluginMock2 = new ServicePluginMock2();
expect(pluginManager.plugins).to.contain(servicePluginMock1);
expect(pluginManager.plugins).to.contain(servicePluginMock2);
});
});
describe('#loadCommands()', () => {
it('should load the plugin commands', () => {
pluginManager.loadCommands(SynchronousPluginMock);
expect(pluginManager.commandsList[0]).to.have.property('deploy');
});
});
describe('#getEvents()', () => {
beforeEach(() => {
pluginManager.loadCommands(SynchronousPluginMock);
});
it('should get all the matching events for a root level command in the correct order', () => {
const commandsArray = ['deploy'];
const events = pluginManager.getEvents(commandsArray, pluginManager.commands);
expect(events[0]).to.equal('before:deploy:resources');
expect(events[1]).to.equal('deploy:resources');
expect(events[2]).to.equal('after:deploy:resources');
expect(events[3]).to.equal('before:deploy:functions');
expect(events[4]).to.equal('deploy:functions');
expect(events[5]).to.equal('after:deploy:functions');
});
it('should get all the matching events for a nested level command in the correct order', () => {
const commandsArray = ['deploy', 'onpremises'];
const events = pluginManager.getEvents(commandsArray, pluginManager.commands);
expect(events[0]).to.equal('before:deploy:onpremises:resources');
expect(events[1]).to.equal('deploy:onpremises:resources');
expect(events[2]).to.equal('after:deploy:onpremises:resources');
expect(events[3]).to.equal('before:deploy:onpremises:functions');
expect(events[4]).to.equal('deploy:onpremises:functions');
expect(events[5]).to.equal('after:deploy:onpremises:functions');
});
it('should return an empty events array when the command is not defined', () => {
const commandsArray = ['foo'];
const events = pluginManager.getEvents(commandsArray, pluginManager.commands);
expect(events.length).to.equal(0);
});
});
describe('#runCommand()', () => {
it('should throw an error when the given command is not available', () => {
pluginManager.addPlugin(SynchronousPluginMock);
const commandsArray = ['foo'];
expect(() => { pluginManager.runCommand(commandsArray); }).to.throw(Error);
});
it('should run the hooks in the correct order', () => {
class CorrectHookOrderPluginMock {
constructor() {
this.commands = {
run: {
usage: 'Pushes the current hook status on the hookStatus array',
lifeCycleEvents: [
'beforeHookStatus',
'midHookStatus',
'afterHookStatus'
],
},
};
this.hooks = {
'before:run:beforeHookStatus': this.beforeHookStatus.bind(this),
'run:midHookStatus': this.midHookStatus.bind(this),
'after:run:afterHookStatus': this.afterHookStatus.bind(this),
};
// used to test if the hooks were run in the correct order
this.hookStatus = [];
}
beforeHookStatus() {
this.hookStatus.push('before');
}
midHookStatus() {
this.hookStatus.push('mid');
}
afterHookStatus() {
this.hookStatus.push('after');
}
}
pluginManager.addPlugin(CorrectHookOrderPluginMock);
const commandsArray = ['run'];
pluginManager.runCommand(commandsArray);
expect(pluginManager.plugins[0].hookStatus[0]).to.equal('before');
expect(pluginManager.plugins[0].hookStatus[1]).to.equal('mid');
expect(pluginManager.plugins[0].hookStatus[2]).to.equal('after');
});
describe('when using a synchronous hook function', () => {
beforeEach(() => {
pluginManager.addPlugin(SynchronousPluginMock);
});
it('should run a simple command', () => {
const commandsArray = ['deploy'];
pluginManager.runCommand(commandsArray);
expect(pluginManager.plugins[0].deployedFunctions).to.equal(1);
});
it('should run a nested command', () => {
const commandsArray = ['deploy', 'onpremises'];
pluginManager.runCommand(commandsArray);
expect(pluginManager.plugins[0].deployedResources).to.equal(1);
});
});
describe('when using a promise based hook function', () => {
beforeEach(() => {
pluginManager.addPlugin(PromisePluginMock);
});
it('should run a simple command', () => {
const commandsArray = ['deploy'];
pluginManager.runCommand(commandsArray);
expect(pluginManager.plugins[0].deployedFunctions).to.equal(1);
});
it('should run a nested command', () => {
const commandsArray = ['deploy', 'onpremises'];
pluginManager.runCommand(commandsArray);
expect(pluginManager.plugins[0].deployedResources).to.equal(1);
});
});
});
});