Elixir
Preface
Elixir is based on a rewrite of Director by Flatiron. It depends solely on RaptorJuice, which is a lightweight (around 4.5k), self contained iterator and utility library. RaptorJuice originated as the general utility functions for Elixer.
At this point, Elixer is not intended to act as a drop-in replacement for Director, though it could easily be extended to do so. Rather, it should be viewed as refinement of the original logic which exists in Director.
Purpose
Elixir is a flexible router which is:
- Based on regular expressions.
- Segmented by a customizable delimiter.
- Entirely asynchronous.
Development
-
Ensure that you have Node.js on your system.
-
Prepare your environment by executing the following commands:
git clone https://github.com/junglefresh/Elixir.js elixir cd elixir npm install sudo npm install -g grunt-cli mocha
Testing
To automatically run tests upon saving changes to the source or tests run the following:
mocha --reporter spec ./spec.js -w
-
Upon execution of the following command, after each write:
- The unit tests will be confirmed.
Building
To begin automatically building the project, run:
grunt watch
-
Please note that a Java Runtime is required due to the jsdoc dependency.
-
By executing the following command, upon each write:
- The unit tests will be confirmed.
- Documentation will be regenerated if all tests pass.
Demonstration
Introduction
Elixer is a router which treats any segmented string, such as the path
portion of a URL, as a trajectory into a tree structure. This
trajectory is defined by the segments of the path. A segment is
any portion of a path which lies between two delimiting sequence.
For a URL, the common delimiting sequence is a /
, but with Elixir
it can be defined to suit your needs.
In summary, Elixir projects a segmented string into its routing table.
Adding and Removing
Elixir allows us to attach functions to any part of a route's tree structure. A route is defined as a segmented string, so it is similar to a path, yet its segments are regular expressions.
var potion = new Elixir();
potion.add('/test', 'on', function() {
Elixir.R.log('Hello world!');
});
Elixir.R.log(potion.table.segments);
First, we create a new Elixir object and call it potion
. Then, we
add an on
state handler to the /test
route in potion
s routing
table. By logging the segments of potion
s routing table in the last
line of the example, you can see that the segments of the route were
created, and our handler was added to the on
state.
Trigger and Context
So what now? We've added an event handler to potion
, lets trigger it
with a path that matches its route.
- Context...
potion.trigger('/test', null, { }, function() {
if(this.error) {
Elixir.R.log('Fout:' + this.error.message);
}
Elixir.R.log('Tot ziens, wereld!');
});
Well, it kinda works, but why isn't the callback with our goodbye message being executed?
Asynchronous Functionality
The answer lies in the fact that Elixir has been built entirely
asynchronously. Every handler function in the routing table must call
the next
function supplied with its context.
var potion = new Elixir();
potion.add('/test', 'on', function() {
Elixir.R.log('Hello world!');
this.next(); // The fix.
});
potion.trigger('/test', null, { }, function() {
if(this.error) {
Elixir.R.log('Fout:' + this.error.message);
}
Elixir.R.log('Tot ziens, wereld!');
});
By calling next
without any arguments, we ensure that the proceeding
function on our trajectory is invoked. On the other hand, we can
diverge from our or trajectory and invoke the callback supplied to
trigger
immediately, by supplying any truthy value to next
.
var potion = new Elixir();
potion.add('/test', 'on', function() {
Elixir.R.log('Hello world!');
this.next({ message : 'An error!' }); // The fix.
});
potion.trigger('/test', null, { }, function() {
if(this.error) {
Elixir.R.log('Fout: ' + this.error.message);
}
Elixir.R.log('Tot ziens, wereld!');
});
In this example, by passing an object to next
, the error
property
of the triggered event's context is set to the argument supplied to
next
. Any subsequent handlers on our current trajectory (in this
case, there are none) will by bypassed.
Tree Structure
To understand the structure of a routing table, we need to take a step back and look at a path for what it really is. The segments of a path (as described in the introduction) project onto a series of nodes in a tree. This series of nodes, or projection, can then be traversed in any method to achieve the desired order of event handlers.
var potion = new Elixir();
potion.add('/test', 'on', function() {
Elixir.R.log('I am test!');
});
potion.add('/test/ing', 'on', function() {
Elixir.R.log('I am ing!');
});
potion.trigger('/test');
potion.trigger('/test/ing');
The cool part about routes is that they are actually regular expressions. What this means is that we can create generic nodes or segments to handle a range of inputs.
potion.add('/test/ing/(\\d+)', 'on', function(numbers) {
Elixir.R.log('I am ' + numbers);
});
potion.trigger('/test/ing/123');
As an artifact of a route's basis an regular expressions, we can create create segments which contain the delimiting sequence.
potion.add('/(test/ing/(\\d+))', 'on', function(match) {
Elixir.R.log('I am test-ing-' + match);
});
potion.trigger('/test/ing/123');
Notice two things. First, the route containing two occurances of the delimiting sequence took precidence over the routes with fewer occurances. Second, the entire match was presented as the parameter to the handler, not the interion match.
States
The magic really starts to happen when we throw in some states. The idea of states allow us to create structured routes and repeat very littly code.
The default states are before
, on
and after
, in that order.
var potion = new Elixir();
potion.add('/', 'before', function() {
this.output = 'I ';
this.next();
});
potion.add('/Jamaican', 'before', function() {
this.output += 'am ';
this.next();
});
potion.add('/Jamaican', 'on', function(match) {
this.output += match;
this.next();
});
potion.add('/Jamaican', 'after', function() {
this.output += ', maaaannn.';
this.next();
});
potion.add('/', 'after', function() {
Elixir.R.log(this.output);
this.next();
});
potion.trigger('/Jamaican');
We can add more routes and have them easily printed. Note that the root segment is assumed in any route.
potion.add('/(fly/high)', 'before', function() {
this.output += '... Uhhh';
this.next();
});
potion.add('/(fly/high)', 'on', function() {
this.output += '... ';
this.next();
});
potion.add('/(fly/high)', 'after', function() {
this.output += 'What was I gonna say?';
this.next();
});
potion.trigger('fly/high');
Methods
Methods allow us to to perform more specific actions on a trajectory.
var potion = new Elixir({ methods : [ 'talk', 'speak' ] });
potion.add('/samson', 'before', function() {
this.output = 'I ';
this.next();
})
potion.add('/samson', 'before', function() {
this.output += 'wanna ';
this.next();
})
potion.add('/samson', 'talk', 'on', function() {
this.output += 'talk ';
this.next();
})
potion.add('/samson', 'speak', 'on', function() {
this.output += 'speak ';
this.next();
})
potion.add('/samson', 'after', function() {
this.output += 'to ';
this.next();
})
potion.add('/samson', 'after', function() {
this.output += 'Samson.';
Elixir.R.log(this.output);
this.next();
})
potion.trigger('/samson', 'talk');
potion.trigger('/samson', 'speak');
Stacked Handlers
The previous example could have been more consisely written by grouping our stacked handlers.
var potion = new Elixir({ methods : [ 'talk', 'speak' ] });
potion.add('/samson', 'before', [
function() {
this.output = 'I ';
this.next();
},
function() {
this.output += 'wanna ';
this.next();
}
]);
potion.add('/samson', 'talk', 'on', function() {
this.output += 'talk ';
this.next();
})
potion.add('/samson', 'speak', 'on', function() {
this.output += 'speak ';
this.next();
})
potion.add('/samson', 'after', [
function() {
this.output += 'to ';
this.next();
},
function() {
this.output += 'Samson.';
Elixir.R.log(this.output);
this.next();
}
])
potion.trigger('/samson', 'talk');
potion.trigger('/samson', 'speak');
Customization
Both methods and states can be customized on a per-router basis. Each router has three state categories. We can change the states a router uses upon initialization.
var potion = new Elixir({
methods : [ 'eightfold', 'path' ],
before : [ 'pre' ],
on : [ 'event' ],
after : [ 'post' ]
});
Elixir.R.log(potion.states);
Elixir.R.log(potion.methods);
As you might imagine, each state category can actually contain multiple states and order is preserved.
var potion = new Elixir({
methods : [ 'eightfold', 'path' ],
before : [ 'pre', 'setup' ],
on : [ 'event', 'on' ],
after : [ 'destroy', 'post' ]
});
Elixir.R.log(potion.states);
Elixir.R.log(potion.methods);
Conclusion
Elixir is a flexible, asynchronous, structured event handler. By combining states and methods with routes and paths we can drastically reduce code repitition.