Modules I like : Devel::Declare
Monday, November 9th, 2009For $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)
