Dec 14

In Mason 1, extending Mason’s behavior means either using the limited plugin API (allowing you to take action before and after a request or component call), or else writing subclasses, which is problematic when you try to use more than one together.

In Mason 2, plugins are based on Moose roles, allowing you to modify behavior in each of Mason’s main subclasses — Interp, Request, Compiler, Compilation, and Component — in a composable way.

Here’s my first Mason 2 plugin, to perltidy object files after components are compiled – useful in development. First, a nominal base class:

package Mason::Plugin::TidyObjectFiles;
use Moose;
extends 'Mason::Plugin';

1;

(This has to exist for Mason to recognize the plugin, but there isn’t much to do there right now.)

Then, a role for each of Mason’s subclasses that we want to modify. In this case we only need to modify Mason::Compiler:

package Mason::Plugin::TidyObjectFiles::Compiler;
use Moose::Role;
use Perl::Tidy;
use strict;
use warnings;

has 'tidy_options' => ( is => 'ro' );

around 'write_object_file' => sub {
    my ( $orig, $self, $object_file, $object_contents ) = @_;

    my $argv = $self->tidy_options || '';
    my $source = $object_contents;
    Perl::Tidy::perltidy(
        'perltidyrc' => '/dev/null',
        source       => $source,
        destination  => $object_contents,
        argv         => $argv
    );
    $self->$orig( $object_file, $object_contents );
};

1;

To use this, I simply list ‘TidyObjectFiles’ in my plugins:

my $mason = Mason->new(..., plugins=>[ 'TidyObjectFiles' ]);

or, if I want to specify perltidy options:

my $mason = Mason->new(..., plugins=>[ 'TidyObjectFiles' ], tidy_options => '-noll -l=100');

The Mason::Plugin:: prefix is automatically added to plugin names. You can also specify a full path with a leading ‘+’, e.g.

plugins => ['+MyApp::Plugin::MyPlugin']

Now my object files go from looking like this:

no warnings 'redefine';
sub _comp_info { return {comp_dir_path => '/',comp_is_external => 1,comp_path => '/hi.m'} }
sub main {
my $self = shift;
my $m = $self->m;

my $_buffer = $m->current_buffer;
 #line 1 "/Users/swartz/git/mason.git/tmp/comps/hi.m"
$$_buffer .= 'Hi there! The time is ';
 #line 1 "/Users/swartz/git/mason.git/tmp/comps/hi.m"
{ $$_buffer .=  scalar(localtime)  if defined( scalar(localtime) ) }
 #line 1 "/Users/swartz/git/mason.git/tmp/comps/hi.m"
$$_buffer .= '.
';


;return;
}

to this:

no warnings 'redefine';

sub _comp_info {
    return {
        comp_dir_path    => '/',
        comp_is_external => 1,
        comp_path        => '/hi.m'
    };
}

sub main {
    my $self = shift;
    my $m    = $self->m;

    my $_buffer = $m->current_buffer;

    #line 1 "/Users/swartz/git/mason.git/tmp/comps/hi.m"
    $$_buffer .= 'Hi there! The time is ';

    #line 1 "/Users/swartz/git/mason.git/tmp/comps/hi.m"
    { $$_buffer .= scalar(localtime) if defined( scalar(localtime) ) }

    #line 1 "/Users/swartz/git/mason.git/tmp/comps/hi.m"
    $$_buffer .= '.
';

    return;
}

5 Responses to “Mason 2: Plugins”

  1. Shawn M Moore Says:

    I recommend looking very closely at Dist::Zilla’s plugin architecture. I’m certain Mason 2 would benefit a lot from it!

  2. Ricardo Signes Says:

    How does the code get from Mason::Plugin::TidyObjectFiles (passed to Mason->new) to Mason::Plugin::TidyObjectFiles::Compiler? Is it using Module::Pluggable or something? How does it know how to map roles to delegates of Mason?

    In:

    my $mason = Mason->new(…, plugins=>[ 'TidyObjectFiles' ], tidy_options => ‘-noll -l=100′);

    How is tidy_options mapped to the right place? It reminds me uncomfortably of Class::Container.

  3. Jonathan Swartz Says:

    Ricardo: Take a look at Mason::find_subclass -

    https://github.com/jonswar/perl-mason/blob/master/lib/Mason.pm#L62-78

    When Mason goes to create a compiler, it calls Mason->find_subclass(‘Compiler’), which will search through all the plugins for a Compiler.pm role and then create a subclass of Mason::Compiler with those roles. Similarly for when Mason creates a request, etc.

    tidy_options is just an accessor in the role, so it ends up in the constructed Compiler subclass.

    HTH. This is all very new (last night) and open to suggestions. I haven’t had a chance to look at Dist::Zilla’s plugin architecture but will definitely do so. But I wanted to get something initial implemented because I had a few plugins to write. :)

  4. Jonathan Swartz Says:

    Ricardo: Incidentally, there is still some Class::Container like logic, in that parameters can be passed to Interp and will automatically make their way to Request and Compiler as appropriate. It’s easier now with Class::MOP introspection.

    https://github.com/jonswar/perl-mason/blob/master/lib/Mason/Interp.pm#L60-77
    https://gist.github.com/740807

    Sounds you don’t like Class::Container…is it the implementation or the general concept?

  5. Mason 2: Filters Says:

    [...] A set of standard filters are automatically available in components, and other filter packages can be loaded via plugins. [...]

Leave a Reply

preload preload preload