Hacking Thy Fearful Symmetry

Hacker, hacker coding bright
Powered by a Gamboling Beluga

Dance For Me, Caribou, Dance!

created: December 3, 2012

This is a work in progress, but it's so much on the far side of wicked, I have to share.

So, I'm working on the rewrite of Galuga. Which is itself a big fat excuse to play with my other pet projects, like DBIx::NoSQL::Store::Manager and Template::Caribou. Tonight, I was playing with the latter, and I thought "It's kinda a pain to define all the templates within the class file. Wouldn't it be nice to define them in individual files instead?". Fair point, and easily to do with the help of a role:

package Template::Caribou::Files;

use strict;
use warnings;

use Path::Class;
use Method::Signatures;
use List::Pairwise qw/ mapp /;

use MooseX::SemiAffordanceAccessor;
use Moose::Role;

has template_dirs => (
    is => 'ro',
    isa => 'ArrayRef',
    default => sub { [] },
    trigger => \&load_template_dirs,
);

method load_template_dirs {
    for my $dir ( map { dir($_) }  @{ $self->template_dirs } ) {
        $dir->recurse( callback => sub{ 
             return unless $_[0] =~ /\.bou$/;
             my $f = $_[0]->relative($dir)->stringify;
             $f =~ s/\.bou$//;
             $self->import_template_file( $f => $_[0] );
        });
    }
}

has template_files => (
    traits => [ 'Hash' ],
    is => 'ro',
    isa => 'HashRef',
    default => sub { {} },
    handles => {
        set_template_file => 'set',
        template_files_mapping => 'elements',
    },
);

sub BUILD { $_[0]->load_template_dirs; }


sub import_template_file {
    my $self = shift;

    my( $name, $file ) = @_ == 2 ? @_ : ( undef, @_ );

    $file = file($file) unless ref $file;

    ( $name = $file->basename ) =~ s/\..*?$// unless $name;

    my $class = ref( $self ) || $self;

    my $sub = eval <<"END_EVAL";
package $class;
use Method::Signatures;
method {
# line 1 "@{[ $file->absolute ]}"
    @{[ $file->slurp ]}
}
END_EVAL

    die $@ if $@;

    $self->set_template( $name => $sub );
    $self->set_template_file( $name => [ $file => $file->stat->mtime ] );

    return $name;
}

method refresh_template_dirs {

    my %seen = mapp { $b->[0] => $b->[1] } $self->template_files_mapping;

    for my $dir ( map { dir($_) }  @{ $self->template_dirs } ) {
        $dir->recurse( callback => sub{ 
             return unless $_[0] =~ /\.bou$/;

             return if $seen{"$_[0]"} >= $_[0]->stat->mtime;

             my $f = $_[0]->relative($dir)->stringify;

             $f =~ s/\.bou$//;
             $self->import_template_file( $f => $_[0] );
        });
    }
}

method refresh_template_files {
    $self->refresh_template_dirs;
}

1;

Then I thought, "Instead of having to run a script over and over again as I create those templates, wouldn't it be awesome to generate an ad-hoc web app that would refresh in real time?". So I went ahead and created a second role:

package Template::Caribou::DevServer;

use strict;
use warnings;

use Dancer;

use Moose::Role;

sub dev_server {
    my $self = shift;

    hook before => sub { 
        $self->refresh_template_files;

        for my $t ( grep { !$seen{$_} } $self->all_templates ) {
            warn "adding template $t\n";
            get "/$t" => sub { $self->render( $t => %{params()} ) };
            $seen{$t}++;
        }
    };

    my %seen;
    for my $t ( grep { !$seen{$_} } $self->all_templates ) {
        warn "adding template $t\n";
        get "/$t" => sub { $self->render( $t => %{params()} ) };
        $seen{$t}++;
    }


    dance;
}

1;

Let's create a template class to go with the season (by the by, you are following all the Perl Advent calendars, right?).

package Wishlist;

use strict;
use warnings;

use Moose;

use Template::Caribou;
use Template::Caribou::Tags::HTML qw/ :all /;

with 'Template::Caribou', 
     'Template::Caribou::Files',
     'Template::Caribou::DevServer';

has '+template_dirs' => (
    default => sub { [ 'templates' ] },
);

has wishlist => (
    is => 'ro',
    isa => 'ArrayRef',
    required => 1,
);

1;

And now, the cool stuff happens. First, we create an object and fire up its very own web application:


$ perl -MWishlist -e'Wishlist->new( wishlist => [ "tiger socks", "alpaca wool", "a pony" ])->dev_server'
HTTP::Server::Simple::PSGI: You can connect to your server at http://localhost:3000/

And with that...


$ echo 'html{ body { p{ "hello there" } } }' > templates/main.bou

$ curl localhost:3000/main
<html><body><p>hello there</p></body></html>

$ echo 'ul { li { $_ } for @{$self->wishlist} }' > templates/list.bou

$  curl localhost:3000/list
<ul><li>tiger socks</li><li>alpaca wool</li><li>a pony</li></ul>

$ echo 'body { show("list") }' > templates/main.bou 

$  curl localhost:3000/main
<body><ul><li>tiger socks</li><li>alpaca wool</li><li>a pony</li></ul></body>

Okay, I confess, that display is slightly rigged: an already existing template has to be hit so that the new ones are registered. But still, you have to admit, it's all still pretty darn cool.

In all case, all that magic haven't made it yet to the master branch of Template::Caribou, but if you want to experiment with the code, it's all there in its own branch.

comments powered by Disqus