Building Blocks of Moose

Well for today’s post I am going to continue along with one of the basic building blocks of a good Moose program coercion. We saw in the last post how I started to clean up my interface with coercion and I am going to do that to the next Accessor.pm attribute


has elements => (
isa => 'ArrayRef',
is => 'rw',
);

On one point I ran into while with this attribute was I could make this one into a Hash-Ref of Element objects then I would be able to get rid of the name attribute and just use the key as that attribute. Well I could do this but then I would have use a 'ordered' hash of some form to keep the order that the elements are entered. I have leart over the years that the there are a few dbs out there that really care what order you pull a field out of a DB so best to just keep it an 'ArrayRef' for now.

Now the first thing I have to do is change my attribute a little like this


has elements => (
-- isa => 'ArrayRef',
++ isa => 'Elements',
++ coerce => 1,
is => 'rw',
);

Hmm I do not like that single word 'Elements' it is just not very descriptive so I think I will use this

as elements => (
-- isa => 'ArrayRef',
++ isa => 'ArrayRefofElements',
++ coerce => 1,
is => 'rw',
);

a little more clear IMHO.

Now to get this to work with my Types role I first have to use the class I am interested in and create a simple coercion for it is like this;


use Database::Accessor::View;
++use Database::Accessor::Element;

class_type 'View', { class => 'Database::Accessor::View' };
++class_type 'Element', { class => 'Database::Accessor::Element' };


Now we just have to tell Moose what an 'ArrayRefofElements' is and we do this with the subtype command. So I add in

subtype 'ArrayRefofElements' => as 'ArrayRef[Element]';

and Moose will now see that as a type. Now we are not at the Coercion part yet. If I rerun my base_02_accssor.t again they should all pass but they do not as I get

Failed test 'use Database::Accessor;'
at 02_base_Accessor.t line 41.
Tried to use 'Database::Accessor'.
Error: You cannot coerce an attribute (elements) unless its type (ArrayRefofElements) has a coercion at C:\Dwimperl\perl\site\lib\Moose\Exporter.pm line 419

Note the error here is not that it cannot do a coercion but that there is no coercion for that subtype and my attribute says there is. Well easy fix for now just comment out the coerce like this


as elements => (
isa => 'ArrayRefofElements',
# coerce => 1,
is => 'rw',
);

and get all ok.

So now to to do the coerce for that ArrayRef we have to add the following into my Types role;


coerce 'ArrayRefofElements', from 'ArrayRef', via {
[ map { Database::Accessor::Element->new($_) } @$_ ];
};

So in this coerce command I tell Moose to take the ArrayRef that is coming in, the $_, and iterate over it with map, and at each iteration try to create a new Elelemt class with the passed in values and then an implicit retrurn an Array-Ref.

Now the above is just shorthand I could of done this as well;


oerce 'ArrayRefofElements', from 'ArrayRef', via {
my $new = [];
foreach my $element {@$_}{
push(@{$new}, Database::Accessor::Element->new($element ) )
}
return $new;
};

not a concise of course but it does show you can do all sorts of majick here when coercing.

Now if I was to run my 02_base_accessor.t again I would get all to pass as one would expect and the 04_coerce.t would all pass as well as I am not doing any coersion on the attribute yet. So I need to change my test by first taking out all those Element class tests


-- my $street = Database::Accessor::Element->new( { name => 'street', } );
-- ok( ref($street) eq 'Database::Accessor::Element', "Street is an Element" );
-- my $country = Database::Accessor::Element->new( { name => 'country', } );
-- ok( ref($country) eq 'Database::Accessor::Element', "County is an Element" );
-- my $city = Database::Accessor::Element->new( { name => 'city', } );
-- ok( ref($city) eq 'Database::Accessor::Element', "City is an Element" );
-- my @elements = ( $street, $city, $country );
and then changing my $address new call like this

my $address = Database::Accessor->new(
    {
        view     => {name  => 'person',
                     alias => 'me'},
--       elements => \@elements 
++        elements => [{ name => 'street', },
++                   { name => 'city', },
++                    { name => 'country', } ] 
and a quick test for these Element objects

++foreach my $element (@{$address->elements()}){
++   ok( ref($element) eq 'Database::Accessor::Element', "Element ".$element->name()." is a Database::Accessor::Element" );
++}     
and I run my test and I will get this
Attribute (elements) does not pass the type constraint because: Validation failed for 'ArrayRefofElements' with value [ { name: "street" }, { name: "city" }, { name: "country" } ] at C:\Dwimperl\perl\site\lib\Moose\Object.pm line 24 Moose::Object::new('Database::Accessor', 'HASH(0x534cfdc)') called at 04_coerce.t line 53

Which is what I expect as I have yet to un-comment that coerce para on the 'elements' attribute, once I fix that it I get all test passed, and for kicks I reran the o2_base tests again and they all pass. So there you have it the building blocks of coercion.

IMG_20171225_192139772.jpg

Leave a comment

About byterock

user-pic Long time Perl guy, a few CPAN mods allot of work on DBD::Oracle and a few YAPC presentations