NAME
Trait::Attribute::Derived - trait for lazy-built Moose attributes that
are derived from another attribute
SYNOPSIS
use strict;
use warnings;
use Test::More;
{
package Person;
use Moose;
use Trait::Attribute::Derived Split => {
fields => { segment => 'Num' },
processor => sub { (split)[$_{segment}] },
};
has full_name => (
is => 'ro',
isa => 'Str',
required => 1,
);
has first_name => (
traits => [ Split ],
source => 'full_name',
segment => 0,
);
has last_name => (
traits => [ Split ],
source => 'full_name',
segment => -1,
);
has initial => (
traits => [ Split ],
source => 'full_name',
segment => 0,
postprocessor => sub { substr $_, 0, 1 },
);
}
my $bob = Person->new(full_name => 'Robert Redford');
is($bob->first_name, 'Robert');
is($bob->initial, 'R');
is($bob->last_name, 'Redford');
done_testing;
DESCRIPTION
It is quite common in Moose to have one attribute derived from another
via lazy builders. Often you will have several which are very similar:
has first_name => (
is => 'ro',
lazy => 1,
builder => '_build_first_name',
);
sub _build_first_name {
my $self = shift;
(split /\s/, $self->full_name)[0];
}
has last_name => (
is => 'ro',
lazy => 1,
builder => '_build_last_name',
);
sub _build_last_name {
my $self = shift;
(split /\s/, $self->full_name)[-1];
}
Other examples might be an attribute holding an XML DOM tree where
several attributes are lazily built using XPath queries; or an attribute
holding a DBI database handle where several attribues are lazily built
by querying the database; or where one attribute holds the binary
contents of a file, and others are fields extracted using "unpack".
Trait::Attribute::Derived allows you to automate some of this, reducing
duplicated code.
Trait::Attribute::Derived is a trait for Moose attributes; it a
parameterized role. The first step when using it is to create a variant
of the role with the parameters filled in.
use Trait::Attribute::Derived Split => {
fields => { segment => 'Num' },
processor => sub { (split)[$_{segment}] },
};
This defines a variant called "Split". The "processor" coderef is the
template for deriving a lazily built attribute from a source attribute.
Within this coderef, the special global $_ is set to the value of the
source attribute, and the special global %_ hash contains a set of other
fields useful in deriving the lazily built attributes.
Using our example from the SYNOPSIS, $_ will be the string "Robert
Redford" and %_ will be a hash "(segment => 0)" when building the
"first_name" or "(segment => -1)" when building the "last_name".
If you'd rather not use magic global variables, the coderef is also
passed as arguments (@_): $self, the source attribute value, and a
refernce to that hash.
The "fields" hashref defines which fields will be available in %_ plus a
type constraint for each.
Then when we define the attribute itself:
has first_name => (
traits => [ Split ],
source => 'full_name',
segment => 0,
);
First of all we reference the "Split" trait variant; secondly we tell it
what source attribute to derive the first name from ("full_name");
lastly we tell it what segment of the name we want. This corresponds to
the "segment" field we defined when creating the trait variant.
Here's another example:
{
package Text;
use Moose;
use Trait::Attribute::Derived FindReplace => {
fields => {
find => 'RegexpRef',
replace => 'Str',
},
processor => sub {
my ($self, $value, $fields) = @_;
$value =~ s/$fields->{find}/$fields->{replace}/g;
return $value;
},
};
has plain => (
is => 'ro',
isa => 'Str',
);
has vowels_only => (
traits => [ FindReplace ],
source => 'plain',
find => qr{[^AEIOU]}i,
replace => '',
);
has no_vowels => (
traits => [ FindReplace ],
source => 'plain',
find => qr{[AEIOU]}i,
replace => '',
);
}
An alternative to setting "source" on each derived attribute is to set
it once when creating the trait variant:
use Trait::Attribute::Derived FindReplace => {
source => 'plain',
fields => { ... },
processor => sub { ... },
};
One last detail from the SYNOPSIS is postprocessing. An attribute can
define a "postprocessor" coderef that executes after the "processor"
coderef. This takes the same parameters as the "processor" coderef (and
has access to $_ and %_) but rather than operating on the source
attribute, operates on the output of the "processor".
has first_three_vowels_only => (
traits => [ FindReplace ],
source => 'plain',
find => qr{[^AEIOU]}i,
replace => '',
postprocessor => sub { substr($_, 0, 3) },
);
Introspection
use 5.010;
# say "full_name"
say Person->meta->get_attribute('first_name')->derived_from;
# say "0"
say Person->meta->get_attribute('first_name')->segment;
# say "1"
say Person->meta->get_attribute('initial')->has_postprocessor;
BUGS
Please report any bugs to
<http://rt.cpan.org/Dist/Display.html?Queue=Trait-Attribute-Derived>.
SEE ALSO
Moose::Cookbook::Meta::WhyMeta,
Moose::Cookbook::Meta::Labeled_AttributeTrait, Moose::Meta::Attribute.
AUTHOR
Toby Inkster <tobyink@cpan.org>.
COPYRIGHT AND LICENCE
This software is copyright (c) 2013 by Toby Inkster.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
DISCLAIMER OF WARRANTIES
THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.