summaryrefslogblamecommitdiff
path: root/posts/2009-11-09-modules-i-like-devel-declare.org
blob: facdc811ee5e165451b062ce754f33c2a586e2fa (plain) (tree)








































































































































































































































































                                                                           
For [[http://linkfluence.net/][$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:

#+BEGIN_SRC perl
    # $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;
    }
#+END_SRC

and the worker look like this

#+BEGIN_SRC perl
    package MyWorker;
    use Moose;
    extends 'XXX::Worker';

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

        # do something
        $self->job_success();
    }
#+END_SRC

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
[[http://search.cpan.org/perldoc?Devel::Declare][Devel::Declare]].

The syntax I want for my worker is this one:

#+BEGIN_SRC perl
    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");
    };
#+END_SRC

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:

#+BEGIN_SRC perl
    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;
#+END_SRC

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

#+BEGIN_QUOTE
  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.
#+END_QUOTE

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:

#+BEGIN_SRC perl
    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;
#+END_SRC

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

#+BEGIN_SRC perl
    work foo {};

    work bar {};
#+END_SRC

in my worker, I add this method to *->meta->local\_work*.

And the class for our keyword work:

#+BEGIN_SRC perl
    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;
#+END_SRC

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

#+BEGIN_SRC perl
    my ($self, $context, $job) = @_;
#+END_SRC

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

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

#+BEGIN_SRC perl
    package MyWorker;
    use Moose;
    extends 'XXX::Worker';
    use XXX::Meta;

    work foo {};
#+END_SRC

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)