elixir

0.1.2 • Public • Published

Elixir

Build Status

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 potions routing table. By logging the segments of potions 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.

Dependents (4)

Package Sidebar

Install

npm i elixir

Weekly Downloads

45

Version

0.1.2

License

none

Last publish

Collaborators

  • psev