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.
Leave a comment