CatalystX::Routes, Sugar and Power

I’ve just released a new module, CatalystX::Routes, which adds a layer of sugar for declaring Catalyst actions (aka URI mapping, routes, etc).

Route declarations work together with the Catalyst::Action::REST distribution to make it trivial to declare RESTful actions in your controllers.

I’m very excited about this module for several reasons. First, Catalyst’s sub attribute-based action declaration is hideous. I cringe every time I look at it. The syntax is also un-Perlishly picky. For example, these two things are not the same:

1
2
sub foo : Chained ('/') : Args (0) { }
sub foo : Chained('/') : Args(0) { }

The difference? Well, the first one is a syntax error. Yes, that’s right, attributes don’t allow spaces before their parameters.

So here’s what some code looks like when converted from “plain old Catalyst controller” to CatalystX::Routes:

1
2
sub _set_contact : Chained('/account/_set_account')
                 : PathPart('contact') : CaptureArgs(1) { ... }

becomes …

1
2
3
4
5
chain_point _set_contact
    => chained '/account/_set_account'
    => path_part 'contact'
    => capture_args 1
    => sub { ... };

RESTful end points become even cleaner. Now we can get rid of the ugly combination of foo, foo_GET, and foo_POST subs.

1
2
3
sub contact : Chained('_set_contact') : PathPart('')
            : Args(0) : ActionClass('+R2::Action::REST') { }
sub contact_GET : Private { ... }

becomes …

1
2
3
4
get q{}
    => chained '_set_contact'
    => args 0
    => sub { ... };

We are able to entirely eliminate the do-nothing sub that was needed just to declare a RESTful URI. When you declare a method for an HTTP action, CatalystX::Routes makes sure all the necessary bits are declared behing the scene.

(That get q{} is used to declare a chain end point with the same URI as the mid point it chains from.)

CatalystX::Routes also provides special sugar for providing HTML responses to browsers along with Catalyst::Action::REST::ForBrowsers, so we can write:

1
2
3
4
get_html q{}
    => chained '_set_contact'
    => args 0
    => sub { ... };

Now when a browser makes a GET request for this URI, we will dispatch to the get_html action.

The real power of CatalystX::Routes goes well beyond making things prettier. Subroutine attributes are parsed by Perl at compile time, and are an entirely separate piece of syntax from other Perl code. In other words, you can’t write this:

1
2
3
4
BEGIN {
    my $chain = '_set_contact';
    sub contact : Chained($chain) : Args(0) { ... }
}

Well, you can write this, but what happens is that Perl simply parses the Chained attribute as containing the string '$chain'. You cannot use variables in attributes.

With CatalystX::Routes, you declare actions using regular Perl code, which means you can use variables, loops, and so on to make it easy to generate actions.

For example, in one of my controllers, I had several RESTful entities with a very similar set of actions (view a collection, view an individual entity, POST a new entitiy, PUT an update, etc.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
for my $type ( qw( donation note ) ) {
    my $plural = PL_N($type);
    my $entity_chain_point = "_set_$type";
    chain_point $entity_chain_point
        => chained '_set_contact'
        => path_part $type
        => capture_args 1
        => sub {
            my $self = shift;
            my $c    = shift;
            my $id   = shift;
            my $entity = $class->new( $id_col => $id );
            $c->stash()->{$type} = $entity;
        };
    put q{}
        => chained $entity_chain_point
        => args 0
        => sub {
            my $self = shift;
            my $c    = shift;
            my $contact = $c->stash()->{contact};
            $self->_check_authz( ... );
            my $entity = $c->stash()->{$type};
            eval { $entity->update( ... ); };
            if ( my $e = $@ ) { ... }
            $c->redirect_and_detach( ... );
        };
}

That’s a mouthful, but there are a few key takeaways. First, I was able to define the mid-point of my chain in a variable named $entity_chain_point, and then use that variable to declare actions:

1
2
3
chain_point $entity_chain_point ...
    put q{}
        => chained $entity_chain_point ...

I was also able to do the same thing with the path part for the chain mid-point:

1
2
3
chain_point $entity_chain_point
    => chained '_set_contact'
    => path_part $type ...

And because the subroutines for each action are closures, I’m able to reuse the same subroutine bodies for different entities.

Generating actions programmatically is an incredibly powerful tool for code reuse. I’ve just been using CatalystX::Routes for a day or so, so I’ve really only scratched the surface, but I’m quite excited about the possibilities.

Let me end with a caveat. This is new code, and I’ve only made one release. The API may change without warning, at least for now. And for all I know, this will all turn out to have been a horrible idea, and three months from now I’ll be using subroutine attributes again.

But I doubt it.