2016-01-13
(Programming)
A bit about how my site works, and a super-minimal-really-I-mean-it framework I made to run it.
I am a Perl developer by trade, and wanting to quickly build a site with minimal fuss, I turned to Perl to help me. What could be less fuss than building my own webapp for building my site...
I used to build things like this in Catalyst, with fancy DBIx::Class models and stacking handlers with semantic URLs YadaYada. After getting really sick of the heavyness of such things I turned to Dancer and Dancer2. I was happy in Dancer land for a while, but eventually also got a bit sick of the amount of copy-pasting I was doing, even with such a supposedly light framework. Eventually I threw my toys and decided I needed something even lighter than Dancer. What I really wanted was this:
Essentially a web framework is really a fairly loose collection of tools, paired with some form of dispatcher. By dispatcher I mean that incoming requests have a path part (ie: /some/path/to/some/thing), and you want to map that path to a sub/function/method. Basically you want to be able to run some code in response to a request, without worrying about the boilerplate required to get there.
One of the things I found with Dancer (and Catalyst more so) is that dispatch is completely handled by the framework. Catalyst has the "View" namespace and Dancer has the route creation functions (DSL), which have pretty similar results I think. Since you want your real application logic to be in code outside these View modules, they end up being pretty light weight things that you start to wonder why they are there. I wanted to get rid of this concept, and have full control over my dispatcher. For me it is a small amount of code to dispatch a request, and yet it is also the thing that I want the right amount of control over. So my framework looks like this:
This is the file run by plackup:
#!/usr/bin/perl
use strict;
use warnings;
use lib qw( lib );
use HTTP::SSPA;
use Personal::Site;
use constant APP_ROOT => '/some/where/over/the/rainbow';
my $site = Personal::Site->new( {
data_root => APP_ROOT . '/data',
publish_root => '/birds/fly/over/the/rainbow/',
} );
my $app = HTTP::SSPA->new( { app_root => APP_ROOT, site => $site } );
my $plack = sub {
my ( $env ) = @_;
my $response = $app->process( $env );
$response;
};
I have the generic request handling code in HTTP::SSPA (Super Simple Plack App), and my application code in Personal::Site. I create the site object with the data it needs (location of my post, tag and category data). I create the SSPA object with the location where it can find templates (they happen to be in the same place here), and the site object. The SSPA module will dispatch to the site, and requires there to be a dispatch() imethod on the object. The meat of the HTTP::SSPA is how it uses this dispatcher provided by the site:
sub process {
my ( $self, $env ) = @_;
my $req = Plack::Request->new( $env );
my $res = Plack::Response->new( 404 );
$self->req( $req );
my $params = $req->parameters->mixed;
$self->params( $params );
my %DISPATCH = $self->site->dispatch;
if ( $DISPATCH{BEFORE} ) {
$DISPATCH{BEFORE}->( $self );
}
my $path = $req->path;
my $method = $req->method;
my $handler_group;
my $handler;
if ( $DISPATCH{$method} ) {
$handler_group = $DISPATCH{$method};
}
elsif ( $DISPATCH{ANY} ) {
$handler_group = $DISPATCH{ANY};
}
elsif ( exists $DISPATCH{404} ) {
$handler_group = $DISPATCH{404};
die "here";
}
else {
$handler_group = $HANDLER_STATIC;
}
if ( ref $handler_group eq 'CODE' ) {
$handler = $handler_group;
}
else {
if ( $handler_group->{$path} ) {
$handler = $handler_group->{$path};
}
elsif ( $handler_group->{'*'} ) {
$handler = $handler_group->{'*'};
}
else {
$handler = $HANDLER_STATIC;
}
}
my $response = $handler->( $self );
if ( $self->template ) {
$response = $self->render( $self->template, $response );
$self->template( undef );
}
elsif ( $response && ref $response ) {
$response = JSON::encode_json( $response );
}
if ( $response ) {
$res->status( 200 );
$res->body( $response );
}
else {
$res->status( 404 );
$res->body( $response );
}
$res->finalize;
}
The "site" object that you pass in the constructor to SSPA must have this "dispatch" method, which returns the dispatch table. In my case that dispatch looks like this:
sub dispatch {
my ( $self ) = @_;
return (
'BEFORE' => sub { return $self->_before( $_[0] ) },
'POST' => {
'/api' => sub { return $self->api( $_[0] ) },
'*' => sub { return $self->_publish( $_[0] ) },
},
'GET' => {
'/' => sub { return $self->_index( $_[0] ) },
'*' => sub { return $self->_catchall( $_[0] ) },
},
);
}
The rules for what you return from your dispatch code are as follows: If you return a reference, and you have set the template() attribute on the SSPA object, that data will be rendered using that template. If you return a reference and have not set the template, SSPA will assume you want the data to be JSON encoded and returned for AJAX requests. Otherwise SSPA assumes you have generated the content yourself and returns that, or 404 if you return false.
It's pretty simple and allows for a fair amount of flexibility. The "BEFORE" gets called before every request, then I have two method level handlers for GET and POST. I have an index override for "/" and everything else goes into my _catchall() sub. Likewise I have an api endpoint for AJAX calls, and everything else goes to _publish().
In the _catchall() sub I have application code for getting the path parts extracted and dispatching to various pages. I even think this might be too much, and I might remove the entire dispatch table concept from SSPA and have it just all a single function there, like handle() or something. Because having the framework handle diapatching is something you want full control over in your app, and splitting a URL into path parts or applying regexes to them to dispatch is such a trivial thing that it can just be done in your application code.