Glenlivet is a hook and plugin system that allows you to create flexible, reusable processing workflows.
npm install glenlivet
Glenlivet's job is to make it as easy as possible to write and implement plugins. Similar to Grunt, but for writing apps.
- Configure processing workflows declaratively with "bottles"
- Bundle bottles together using "barrels"
- Plugins automatically load based on configuration keys
- Use hooks to attach processing steps to bottles
- Add your own hooks to control processing flow
- Plugins are simple to create, and can be applied to barrels or bottles
Glenlivet is meant to be modular and flexible. Its goal is to allow a bunch of plugins to work together in a loosely coupled way.
So far, it's been used to create the mobile API for the Threadless iPhone App that is mostly backed by scraped data from their website. Plugins are used to layer in user sessions, caching, and HTML to JSON payload mapping.
Barrels create a logical grouping of bottles. Their purpose is to provide a way to create functionality on top of bottles. For example, you might want to create a web service that interfaces with a group of bottles.
var glenlivet = require('glenlivet');
var myBarrel = glenlivet.createBarrel({});
var testBottle = myBarrel.createBottle('test', {
fetch: { //Loads fetch plugin
uri: 'http://www.prolificinteractive.com:page'
}
});
testBottle.fetch({
fetch: {
page: '/about'
}
}, function (result) {
console.log(result.fetch.error || result.fetch.body);
});
Plugins typically attach processing steps via bottle hooks. They're loaded using they configuration keys on barrels and bottles.
Plugins are defined with named functions:
function myPlugin (context) {
context.is(glenlivet.Bottle, function (bottle, myConfig) {
console.log('I am in a bottle');
});
context.is(glenlivet.Barrel, function (bottle, myConfig) {
console.log('I am in a barrel');
});
}
Note: Plugins must be defined as named functions:
function correctWay () {}
var incorrectWay = function () {}
var thisWorksToo = function thisWorksToo () {}
Plugins are passed a context
object when they're called, which has several methods:
A single constructor or an array of constructors can be passed in as the first argument.
Tests if the plugin is currently called against an instance of the constructor, and runs the callback with two arguments:
instance
: An object the plugin is running against.pluginConfig
: The config corresponding to the plugin.
As a convenience, tests if another plugin is defined in the current context, and runs the callback if so with one argument:
otherPluginConfig
: The config corresponding to the other plugin.
You can register a plugin at multiple scopes: glenlivet, barrels, and bottles.
glenlivet.plugins.register(myPlugin); //At the glenlivet scope
barrel.plugins.register(myPlugin); //At the barrel scope
bottle.plugins.register(myPlugin); //At the bottle scope
Hooks allow plugins to get along with each other by inserting themselves at different parts of the processing workflow. In Glenlivet, hooks are defined as hierarchies, often by plugins.
Hooks are added using bottle.hooks.add(hierarchy)
, and implemented with three methods:
bottle.hooks.before(colonSeparatedPath, callback)
bottle.hooks.when(colonSeparatedPath, callback)
bottle.hooks.after(colonSeparatedPath, callback)
The callback receives:
result
: The object that gets decorated by plugins to yield a resultnext
: Used with asynchronous processes. Tells Glenlivet to advance to the next step.done
: Completes this step and prevents the processing of any further hooks.
If the callback includes only one argument in its signature, it will be run synchronously.
function myPlugin (context) {
context.is(glenlivet.Bottle, function (bottle, myConfig) {
bottle.hooks.add({
myPlugin: {
setup: {}
}
});
//Synchronous
bottle.hooks.after('myPlugin:setup', function (result) {
result.myPlugin.helloWorld = 'hello'
});
//Asynchronous
bottle.hooks.when('myPlugin', function (result, next) {
setTimeout(function () {
result.myPlugin.helloWorld += 'world'
next();
}, 10);
});
//Using the done function
bottle.hooks.before('myPlugin', function (result, next, done) {
if (result.foo === 'bar') {
done();
}
});
});
}
To trigger the hook cascade, use the .trigger() method.
bottle.hooks.trigger('hook:subhook:etc', { foo: 'bar' }, callback);
.trigger() takes 3 arguments:
hook
: the path to the hookdecorator
(optional): an object passed through each hook and finally to the callbackcallback
(optional): run after all hooks are triggered
Glenlivet also adds convenience methods to bottles that trigger top-level hooks, like so:
function addition (context) {
context.is(glenlivet.Bottle, function (bottle, myConfig) {
bottle.hooks.add({
addition: {}
});
bottle.hooks.when('addition', function (result) {
result.addition.sum = result.addition.a + result.addition.b;
});
});
}
bottle.addition({
addition: {
a: 100,
b: 50
}
}, function (result) {
console.log(result.addPlugin.sum); //Should output "150"
});
Hooks can be joined together using the .join() method. As a convenience, it can also map decorator values so that, for example, data from one plugin can be seamlessly piped into another.
bottle.plugins.join('hookA', 'hookB', {
'hookA:x': 'hookB:y'
});
.join() takes 3 arguments
hookA
: the path to the connecting hookhookB
: the path to the hook that will be triggered after hookAmap
(optional): maps values from one part of the decorator object to another, and creates any part of the path that does not exist