Web Dispatcher

In any website, it is necessary to be able to transform a given URL (such as /user/view) and GET/POST parameters (such as userId=10&edit=1) into the corresponding function call that will process the request and its parameters.

A typical website application will do (for each request) :

  • connect to the database, fetch user/session credentials, initialize everything
  • resolve the class/method which we need to call based on the URL
  • dispatch the request (call the method)
  • print result
  • cleanup

In order to ease URL resolving and dispatching, you can use the Dispatch class.

Dispatch Example

import haxe.web.Dispatch;
// ....
var api = { doUser : function() trace("CALLED") };
Dispatch.run("/user",new Hash(),api);

In that case, the Dispatch class will call the api.doUser() method because it was used with the /user URL (and an empty Hash of GET/POST parameters).

If you are using the Neko platform, you could also write directly :

// use runtime URI and GET/POST parameters
Dispatch.run(neko.Web.getURI(),neko.Web.getParams(),api);

(Please note that if you are using the handy Neko Development WebServer, you need to start it using the -rewrite parameter - for example nekotools server -p 2000 -h localhost -d c:\your\web\root\ -rewrite - to make use of "pretty urls".)

You can of course use a full class instead of a structure object :

class Api {
    public function new() {
    }
    function doUser() {
        trace("CALLED");
    }
}
//.....
Dispatch.run("/user",new Hash(),new Api());

In case the corresponding method doXXXX is not found on the api object, or if the URL is /, the action doDefault is used instead. An exception DispatchError.DENotFound("XXXX") is thrown if there is no default action (XXXX here being the placeholder for the URL part name).

URL Parts

You can add typed function parameters to your api methods, they will be extracted from the URL :

class Api {
    // ....
    function doAdd( x : Int, y : Int ) {
        trace( x + y );
    }
}
// ....
Dispatch.run("/add/5/6", new Hash(), new Api());

The following basic types are recognized :

  • Int
  • Float
  • String
  • Bool (0/null/false, true otherwise)

In case the required part is missing, an exception DispatchError.DEMissing is thrown. In case the required part does not have the valid format, an exception DispatchError.DEInvalidValue is thrown.

GET/POST Parameters

In order to handle parameters, you can add an args variable to your api methods :

class Api {
    // ...
    function doMultiply( args : { x : Int, y : Int } ) {
        trace(args.x*args.y);
    }
}
// ...
var params = new Hash();
params.set("x","5");
params.set("y","6");
Dispatch.run("/multiply", params, new Api());

In case one of the required arguments is missing, an exception
DispatchError.DEMissingParam("argname") is thrown.

You can make one or several parameters optional by specifying that its type is Null :

class Api {
    // ...
    function doMultiply( args : { x : Null<Int>, y : Int } ) {
        if( args.x == null )
            trace("NULL");
        else
            trace(args.x*args.y);
    }
}
// ...
var params = new Hash();
//params.set("x","5");
params.set("y","6");
Dispatch.run("/multiply", params, new Api());

Finally, if you want your function to be either called with good arguments or with null if one of the required argument is missing, you can simply make args an optional argument :

class Api {
    // ...
    function doMultiply( ?args : { x : Int, y : Int } ) {
        if( args == null )
            trace("NULL");
        else
            trace(args.x*args.y);
    }
}
// ...
var params = new Hash();
Dispatch.run("/multiply", params, new Api());

Multiple GET/POST Arguments

In case your page is more complex and can take several different sets of arguments, you can use the .getParams() dispatcher method :

function doMultiply( d : Dispatch ) {
    var p = d.getParams();
    if( p != null )
        trace("RESULT = "+(p.a+p.b));
}

The getParams() method will use the compiler type-inference to build the list of arguments (in the previous example { a : Int, b : Int }) and will return null if one of the arguments is missing.

Sub Dispatching

If you want to have a first dispatch layer that will call a second one, you can simply use the Dispatch type as an api method parameter :

class Api {
    // ..
    function doSub( d : Dispatch ) {
        trace("BEFORE");
        d.dispatch(this); // could be another class
        trace("AFTER");
    }
}
// ...
Dispatch.run("/sub/add/5/6",new Hash(),new Api());

The doSub method can itself consume parts of the URL or ask for GET/POST parameters :

class Api {
    // ..
    function doSub( d : Dispatch, result : Int ) {
        d.dispatch(this);
        trace("EXPECTED "+result);
    }
}
// ...
Dispatch.run("/sub/30/add/5/6",new Hash(),new Api());

Dispatch instance

Instead of calling Dispatch.run you can also create your own Dispatch instance :

var d = new Dispatch("/user",new Hash());
d.dispatch(new Api());

Please note that d.dispatch needs to know the exact type of the Api in order to build the dispatch configuration (see below for technical details).

If you want for instance to define a function :

function setApi( api : Dynamic ) {
    myDispatcher.dispatch(api); // ERROR !
}

Instead, you can use a combination of two methods :

  • Dispatch.make(new Api()) : will create a Dispatch.Config for the given typed api. Please note that this is a static method, not an instance one.
  • myDispatcher.runtimeDispatch(config) : will dispatch using the given configuration

Actually, calling .dispatch or Dispatch.run already produces a combination of make and runtimeDispatch calls.

Metadata Handling

You can add metadata for each of your api methods, and use appropriate handling by redefining the onMeta method on the Dispatch instance :

class Api {
    @adminOnly function doPassword() {
        trace("Your password is 1234");
    }
}
// ...
var d = new Dispatch("/password",new Hash());
d.onMeta = function(m,value) {
    if( m == "adminOnly" && !isAdmin ) throw "Admin Only !";
}
d.dispatch(new Api());

You can use this to perform any kind of checks or setup before the api method get called.

SPOD Support

You can also use SPOD objects types in parameters or arguments, they will be fetched by using their primary key :

class User extends neko.db.Object {
    public static var manager = new neko.db.Manager<User>(User);
    public var id : Int;
    public var name : String;
}
// ...
class Api {
    // ...
    function doView( u : User ) {
        trace(u.name);
    }
}
// will fetch User with id=150
Dispatch.run("/view/150",new Hash(),new Api());

By default, SPOD objects are fetched in unlocked mode. You can use lock mode by specifying Dispatch.Lock<User> type.

Compile-Time Metadata Checking

Advanced Feature : You can register a compile-time metadata checker macro that will enable you to ensure you that metadata is correctly declared. Use a small class such as the following example :

import haxe.macro.Type;
import haxe.macro.Context;
class MetaCheck {
    static function checkMeta( f : ClassField ) {
        for( m in f.meta.get() )
            if( m.name != "adminOnly" )
                Context.error("Unrecognized meta",m.pos);
    }
    public static function setup() {
        Dispatch.checkMeta = checkMeta;
    }
}

Don't forget to add the following line in your compilation parameters :

--macro MetaCheck.setup()

FAQ

Why actions are prefixed with do ?

Your Api might include helper methods that you don't want to be accessible through a Dispatch. Prefixing with do explicitly tells that this match can be called with a dispatcher.

How to have Dynamic URL resolution ?

You can subclass the Dispatch class and either change the name during onMeta or override the resolveName method.

How does Dispatch Work ?

A part of the Dispatch implementation is a macro that does analyse the type of the api at compile-time and build the corresponding dispatch configuration.

This dispatch configuration consists in a simple structure object which fields are of type Dispatch.DispatchRule. It enables the Dispatch instance to know the types of the method arguments at runtime, information which is only accessible at compile-time on most Haxe supported platforms.

For classes instances, the dispatch configuration is serialized in the @dispatchConfig class metadata, it is then only unserialized once (the first time a dispatch call is made) and cached for later usage.

This way the dispatching process only use optimized Haxe operations (such as enums matching) which guarantee a very low overhead compared to more complex systems.

OTOH, it is required to know the exact compile-time type of the API methods, as explained in the previous Dispatch Instance section.

version #15868, modified 2013-01-25 11:26:06 by bubar