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.