Telephone +44(0)1524 64544
Email: info@shadowcat.co.uk

Oh Subdispatch, Oh Subdispatch

App::IdiotBox and Web::Simple

Mon Mar 1 22:00:00 2010

Digg! submit to reddit Delicious RSS Feed Readers

Born out of a traditionally innaccurate "I'm sure we could knock that up quickly" comment at the arse end of Perl Oasis 2010, App::IdiotBox, a very very bare bones video site, is now live on Presenting Perl. A more coherent announcement of the Presenting Perl site can can be found on Mark's blog.

Since Mark is the man behind the purpose of the site, I thought I'd talk a little bit about the tech - there'll hopefully be more posts like this once I get the various components cleaned up and CPAN'ed. However, one of them already is on CPAN - Web::Simple, the mini (micro?) framework I've used for the site.

The thing I really want to talk about is subdispatch, which I'm very happy to say is in the category of "features I didn't realise would be as useful as they turned out to be". Idiotbox's dispatch is relatively simple:

- On a request to /, display the front page

- On a request beginning /some-event, find the relevant conference/event

- If the path is just /some-event/, show the video list for the event

- If the path is /some-event/some-video/, lookup and display the video

In IdiotBox, this becomes:

dispatch {
  sub (/) { $self->show_front_page },
  subdispatch sub (/*/...) {
    my $bucket = $self->buckets->get({ slug => $_[1] });
    [
      sub (/) {
        $self->show_bucket($bucket)
      },
      sub (/*/) {
        $self->show_video($bucket->videos->get({ slug => $_[1] }));
      }
    ]
  }
};

(noting that Web::Simple arranges for $self to exist, saving us unpacking @_)

To take that apart:

  sub (/) { $self->show_front_page },

matches requests to the root of the application and dispatches them to a routine to render the front page. Next:

  subdispatch sub (/*/...) {

says that for any path with at least one part, we want the first part - the '*' - and to modify the request to make the path inside the subdispatch the remainder after grabbing that (the '...' requests this). That means that:

  /                            -> request not matched
  /opw2010/                    -> matches 'opw2010', remainder '/'
  /opw2010/troll-god-mountain/ -> matches 'opw2010', remainder '/troll-god-mountain/'

Then, inside the dispatch, we match on the remainder:

    [
      sub (/) { ... },  # matches /opw2010/
      sub (/*/) { ... } # matches /opw2010/troll-god-mountain/
    ]

since a Web::Simple dispatcher wrapped in subdispatch is expected to return a new set of dispatchers to dispatch what's left of the request against.

Interestingly, the same mechanism also provides for very simple RESTful dispatch:

  subdispatch (/user/*) {
    my $user = $self->users->get($_[1]);
    [
      sub (OPTIONS) { $self->methods_ok(qw(GET PUT DELETE)) },
      sub (GET) { $self->show_user($user) },
      sub (PUT %:name=&:email=) { $self->update_or_create_user($user, $_[1]) },
      sub (PUT) { $self->required_params_error(qw(name email)) },
      sub (DELETE) { $self->delete_user($user) },
      sub () { [ 405, [ 'Content-type', 'text/plain' ], [ 'Method not allowed' ] ] }
    ]
  }

where the '%:name=&:email=' says "from the post body" (%), "as a named rather than positional parameter" (:), "give me name" (name), "which is required" (=), "and" (&) "named email, also required" (:email=). That means that $_[1] in the above code is guaranteed to contain two keys, 'name' and 'email', both with a scalar value. If either parameter is not present, dispatch will fall through to the default PUT handler which can provide an error.

The Web::Simple parameter unpacking system is, admittedly, somewhat dense, but it's also extremely powerful without eating up huge amounts of screen real estate in your editor. I'll come back to this with more examples as IdiotBox grows to need it.

And one last note entirely unrelated to subdispatch - earlier today there was a brief problem with CGI serving on the system Presenting Perl is running on - but since I already had the directory structure for videos, I was able to run:

  $ ./idiotbox.cgi / >/index.html
  $ for i in lpw2009/ lpw2009/*/ opw2010/ opw2010/*/ nwe.pm/ nwe.pm/*/;
    do
      ./idiotbox.cgi /$i >$i/index.html
    done

and then added the following to the deployment .htaccess:

RewriteCond %{REQUEST_FILENAME}/index.html -f
RewriteRule (.*) $1/index.html [L]

The L stops further rules (like the one pointing things at idiotbox.cgi) from running, and when the request comes back round (mod_rewrite redispatches after a URL-path-based rather than file based rewrite) it's then caught by our static file serving stanza further up the file:

RewriteCond %{REQUEST_FILENAME} -f
RewriteRule .* - [L]

and the pre-cached output from our shell loop is served, so idiotbox.cgi isn't even required.

I'll probably hack the pre-caching into idiotbox itself as a command at some point - it'd be nice for sites like this where most things are static most of the time to be able to write everything out to disk easily, and reserve the generate-on-every-request for development only.

Latest Web::Simple master (git://git.shadowcat.co.uk/catagits/Web-Simple.git) is now using Plack, the perl web server directly for CGI and FastCGI and I hope do do another release shortly with notes on deploying as both on shared hosts (more specifically Dreamhost but with an invitation to bitch if they don't work on other budget hosts).

We're discussing IdiotBox and associated things on #web-simple on irc.perl.org (whose site just got a facelift) so come join in if you're interested.

The new Iron Man planet codebase is nearly here too - if you want to see your badges updating sooner rather than later, go help the wonderful men of #northwestengland.pm.org as they do final tweaks before putting it live.

Happy Hacking.

-- mst, out