Posts Tagged ‘Devel::Declare’

Modules I like : Devel::Declare

Monday, November 9th, 2009

For $work, I’ve been working on a job queue system, using Moose, Catalyst (for a REST API) and DBIx::Class to store the jobs and some meta (yeah I know, there is not enough job queue system already, the world really needs a new one …).

Basicaly, I’ve got a XXX::Worker class that all the workers extends. This class provide methods for fetching job, add a new job, mark a job as fail, retry, …

The main loop in the XXX::Worker class look like this:

# $context is a hashref with some info the job or method may need
while(1) {
    my @jobs = $self->fetch_jobs();
    foreach my $job (@jobs) {
        my $method = $job->{funcname};
        $self->$method($context, $job);
    }
    $self->wait;
}

and the worker look like this

package MyWorker;
use Moose;
extends 'XXX::Worker';
 
sub foo {
    my ($self, $context, $job) = @_;
    # do something
    $self->job_success();
}

But as I’m using Moose, I want to add more sugar to the syntax, so writing a new worker would be really more easy.

Here comes Devel::Declare.

The syntax I want for my worker is this one:

work foo {
    $self->logger->info("start to work on job");
    # do something with $job
}
 
work bar {
    # do something with $job
}
 
success foo {
    $self->logger->info("woot job success");
}
 
fail bar {
    $self->logger->info("ho noez this one failed");
}

Where with ‘work‘ I write the code the writer will execute on a task, ‘success‘, a specific code that will be executed after a job is marked as successfull, and ‘fail‘ for when the job fail.

I will show how to add the ‘work‘ keyword. I start by writing a new package :

XXX::Meta:
 
package XXX::Meta;
 
use Moose;
use Moose::Exporter;
use Moose::Util::MetaRole;
 
use Devel::Declare;
 
use XXX::Meta::Class;
use XXX::Keyword::Work;
 
Moose::Exporter->setup_import_methods();
 
sub init_meta {
    my ( $me, %options ) = @_;
 
    my $for = $options{for_class};
 
    XXX::Keyword::Work->install_methodhandler( into => $for, );
 
    Moose::Util::MetaRole::apply_metaclass_roles(
        for_class       => $for,
        metaclass_roles => ['XXX::Meta::Class'],
    );
 
}
 
1;

The init_meta method is provided by Moose: (from the POD)

The init_meta method sets up the metaclass object for the class specified by for_class. This method injects a a meta accessor into the class so you can get at this object. It also sets the class’s superclass to base_class, with Moose::Object as the default.

So I inject into the class that will use XXX::Meta a new metaclass, XXX::Meta::Class.

Let’s take a look to XXX::Meta::Class:

package XXX::Meta::Class;
 
use Moose::Role;
use Moose::Meta::Class;
use MooseX::Types::Moose qw(Str ArrayRef ClassName Object);
 
has work_metaclass  => (
    is      => 'ro',
    isa     => Object,
    builder => '_build_metaclass',
    lazy    => 1,
);
 
has 'local_work' => (
    traits     => ['Array'],
    is         => 'ro',
    isa        => ArrayRef [Str],
    required   => 1,
    default    => sub { [] },
    auto_deref => 1,
    handles    => { '_add_work' => 'push', }
);
 
sub _build_metaclass {
    my $self = shift;
    return Moose::Meta::Class->create_anon_class(
        superclasses => [ $self->method_metaclass ],
        cache        => 1,
    );
}
 
sub add_local_method {
    my ( $self, $method, $name, $code ) = @_;
 
    my $method_name = $method . "_" . $name;
    my $body        = $self->work_metaclass->name->wrap(
        $code,
        original_body => $code,
        name          => $method_name,
        package_name  => $self->name,
    );
 
    my $method_add = "_add_" . $method;
    $self->add_method( $method_name, $body );
    $self->$method_add($method_name);
}
 
1;

Here I add to the ->meta provided by Moose ‘local_work‘, which is an array that contains all my ‘work‘ methods. So each time I do something like

work foo {
}
 
work bar {
}

in my worker, I add this method to ->meta->local_work.

And the class for our keyword work:

package XXX::Keyword::Work;
 
use strict;
use warnings;
 
use Devel::Declare ();
use Sub::Name;
 
use base 'Devel::Declare::Context::Simple';
 
sub install_methodhandler {
    my $class = shift;
    my %args  = @_;
    {
        no strict 'refs';
        *{ $args{into} . '::work' } = sub (&) { };
    }
 
    my $ctx = $class->new(%args);
    Devel::Declare->setup_for(
        $args{into},
        {
            work => {
                const => sub { $ctx->parser(@_) }
            },
        }
    );
}
 
sub parser {
    my $self = shift;
    $self->init(@_);
 
    $self->skip_declarator;
    my $name = $self->strip_name;
    $self->strip_proto;
    $self->strip_attrs;
 
    my $inject = $self->scope_injector_call();
    $self->inject_if_block(
        $inject . " my (\$self, \$content, \$job) = \@_; " );
 
    my $pack = Devel::Declare::get_curstash_name;
    Devel::Declare::shadow_sub(
        "${pack}::work",
        sub (&) {
            my $work_method = shift;
            $pack->meta->add_local_method( 'work', $name, $work_method );
        }
    );
    return;
}
 
1;

The install_methodhandler add the work keyword, with a block of code. This code is sent to the parser, that will add more sugar. With the inject_if_block, I inject the following line

my ($self, $context, $job) = @_;

as this will always be my 3 arguments for a work method.

Now, for each new worker, I write something like this:

package MyWorker;
use Moose;
extends 'XXX::Worker';
use XXX::Meta;
 
work foo {
}

The next step is too find the best way to reduce the first four lines to two.

(some of this code is ripped from other modules that use Devel::Declare. The best way to learn what you can do with this module is to read code from other modules that use it)