Coercing ArrayRef[MyClass] from ArrayRef[HashRef] - perl

In trying to answer How to instantiate Moose classes from a big hash, I think I have hit another place where I don't fully understand Moose type coercions. For some reason, the below code issues warnings:
You cannot coerce an attribute (departments) unless its type (ArrayRef[Company::Department]) has a coercion at ./test.pl line 12.
You cannot coerce an attribute (employees) unless its type (ArrayRef[Company::Person]) has a coercion at ./test.pl line 23.
but then succeeds.
#!/usr/bin/env perl
use warnings;
use strict;
package Company;
use Moose;
use Moose::Util::TypeConstraints;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRef[Company::Department]', coerce => 1);
coerce 'ArrayRef[Company::Department]',
from 'ArrayRef[HashRef]',
via { [ map { Company::Department->new($_) } #$_ ] };
package Company::Department;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRef[Company::Person]', coerce => 1);
package Company::Person;
use Moose;
use Moose::Util::TypeConstraints;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'age' => (is => 'ro', isa => 'Num');
coerce 'ArrayRef[Company::Person]',
from 'ArrayRef[HashRef]',
via { [ map { Company::Person->new($_) } #$_ ] };
package main;
my %hash = (
company => {
id => 1,
name => 'CorpInc',
departments => [
{
id => 1,
name => 'Sales',
employees => [
{
id => 1,
name => 'John Smith',
age => '30',
},
],
},
{
id => 2,
name => 'IT',
employees => [
{
id => 2,
name => 'Lucy Jones',
age => '28',
},
{
id => 3,
name => 'Miguel Cerveza',
age => '25',
},
],
},
],
}
);
my $company = Company->new($hash{company});
use Data::Dumper;
print Dumper $company;
How should this have been done? P.S. I tried simply doing
coerce 'Company::Department',
from 'HashRef',
via { Company::Department->new($_) };
but it died horribly.

Well, it doesn't succeed completely, and you should feel it when you'll try to update these fields with coerce => 1. That's why:
You cannot pass coerce => 1 unless the attribute's type constraint has
a coercion
Previously, this was accepted, and it sort of worked,
except that if you attempted to set the attribute after the object was
created, you would get a runtime error.
Now you will get an error when you attempt to define the attribute.
Still, I think I find the way to fix it, by introducing subtypes, first, and changing the order of packages, second:
package Company::Person;
use Moose;
use Moose::Util::TypeConstraints;
subtype 'ArrayRefCompanyPersons',
as 'ArrayRef[Company::Person]';
coerce 'ArrayRefCompanyPersons',
from 'ArrayRef[HashRef]',
via { [ map { Company::Person->new($_) } #$_ ] };
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'age' => (is => 'ro', isa => 'Num');
package Company::Department;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRefCompanyPersons', coerce => 1);
package Company;
use Moose;
use Moose::Util::TypeConstraints;
subtype 'ArrayRefCompanyDepartments',
as 'ArrayRef[Company::Department]';
coerce 'ArrayRefCompanyDepartments',
from 'ArrayRef[HashRef]',
via { [ map { Company::Department->new($_) } #$_ ] };
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRefCompanyDepartments', coerce => 1);
The rest of the code is the same as in your version. This works without any warnings, and more-o-less behaves like (again, I think) it should be.

From Moose::Manual::Type docs:
LOAD ORDER ISSUES
Because Moose types are defined at runtime, you may run into load order problems. In particular, you may want to use a class's type constraint before that type has been defined.
In order to ameliorate this problem, we recommend defining all of your custom types in one module, MyApp::Types, and then loading this module in all of your other modules.
So to add to raina77ow subtype & package order answer (+1) I would recommend creating a Company::Types module:
package Company::Types;
use Moose;
use Moose::Util::TypeConstraints;
subtype 'CompanyDepartments'
=> as 'ArrayRef[Company::Department]';
subtype 'CompanyPersons'
=> as 'ArrayRef[Company::Person]';
coerce 'CompanyDepartments'
=> from 'ArrayRef[HashRef]'
=> via {
require Company::Department;
[ map { Company::Department->new($_) } #$_ ];
};
coerce 'CompanyPersons'
=> from 'ArrayRef[HashRef]'
=> via { require Company::Person; [ map { Company::Person->new($_) } #$_ ] };
1;
And then put use Company::Types in all your Company:: classes.

Related

Disallowing non-attribute parameters in a Moose Class

Is there a way to die if there are extra parameters in a constructor call in Moose that are not attributes? For example, this:
package Shoe;
use Moose;
has 'size' => (is => 'ro', isa => 'Num');
has 'color' => (is => 'ro', isa => 'Str', default => 'brown');
1;
would die on
my $sneaker = Shoe->new(size => 11, colour => 'white');
because colour is not an attribute of Shoe.
I could swear I've seen a module or something to do this but I can't find it.
For me works MooseX::StrictConstructor:
package Shoe;
use Moose;
use MooseX::StrictConstructor; # <-- that's all what need
has 'size' => (is => 'ro', isa => 'Num');
has 'color' => (is => 'ro', isa => 'Str');
1;
package main;
my $sneaker = Shoe->new(size => 11, colour => 'white'); #blows up

Moose empty value for typed attributes

Is there some way in Moose to specify that I want an attribute to have a specific type, but also allow there to be a null value (undef?).
For example I am writing a simple implementation of a Linked List and have a Node class where the next and prev pointers are required to be of type Node (this is probably what you would expect)
package Node;
{
use Moose;
has 'value' => (
is => 'rw',
isa => 'Any', # Nodes can be of any type
);
has 'prev' => (
is => 'rw',
isa => 'Node',
predicate => 'has_prev',
);
has 'next' => (
is => 'rw',
isa => 'Node',
predicate => 'has_next',
);
}
But I was hoping to use a sentinel, empty node stored at the head of the list to mark the head, instead of an actual element of the list. So a list of elements [1, 2, 3] would actually look like:
EMPTY -> 1 -> 2 -> 3
I was hoping to be able to specify a empty value (like undef) for the next and prev pointers, but when I create an empty Node in my List class:
package List;
{
use Moose;
has 'head' => (
is => 'rw',
isa => 'Node',
# empty head node
default => sub {
Node->new( value => undef, next => undef, prev => undef );
},
);
Moose complains because undef is not of type Node.
Is there a way around this ?
You can use the Maybe[type] syntax to allow the type or undef. For your example:
has 'head' => (
is => 'rw',
isa => 'Maybe[Node]',
# empty head node
default => sub {
Node->new( value => undef, next => undef, prev => undef );
}
);
The next:
use 5.014;
use warnings;
package Node {
use Moose;
has 'value' => ( is => 'rw');
has 'prev' => ( is => 'rw', isa => 'Undef|Node', predicate => 'has_prev', default=>undef );
has 'next' => ( is => 'rw', isa => 'Undef|Node', predicate => 'has_next', default=>undef );
}
package List {
use Moose;
has 'head' => ( is => 'rw', isa => 'Node', default => sub { Node->new() } );
}
package main;
use Data::Dumper;
my $list = List->new();
say Dumper $list;
prints:
$VAR1 = bless( {
'head' => bless( {
'next' => undef,
'prev' => undef
}, 'Node' )
}, 'List' );
The Moose::Manual::Types says for the basic hier:
Undef <---- undefined
Defined
Value
Str
Num
Int
ClassName <---- Class name
RoleName
and later in the section TYPE UNIONS says:
Moose allows you to say that an attribute can be of two or more
disparate types. For example, we might allow an Object or FileHandle:
has 'output' => (
is => 'rw',
isa => 'Object | FileHandle', );
As others already says, here is a Maybe[Something] too, I haven't idea what is better, but the Something | SomethingOther looks more "perlish" (IMHO). ;)
The authors prefer Undef|Node over Maybe[Node].
has 'prev' => (
is => 'rw',
isa => 'Undef|Node',
predicate => 'has_prev',
);

How to instantiate Moose classes from a big hash

I have a big hash many levels deep, and I'd like to turn this hash into a set of Moose classes.
The hash looks something like this:
my %hash = (
company => {
id => 1,
name => 'CorpInc',
departments => [
{
id => 1,
name => 'Sales',
employees => [
{
id => 1,
name => 'John Smith',
age => '30',
},
],
},
{
id => 2,
name => 'IT',
employees => [
{
id => 2,
name => 'Lucy Jones',
age => '28',
},
{
id => 3,
name => 'Miguel Cerveza',
age => '25',
},
],
},
],
}
);
And the Moose classes:
package Company;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRef[Company::Department]');
1;
package Company::Department;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRef[Company::Person]');
1;
package Company::Person;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'first_name' => (is => 'ro', isa => 'Str');
has 'last_name' => (is => 'ro', isa => 'Str');
has 'age' => (is => 'ro', isa => 'Num');
1;
Whats the best way to turn this hash into a Company object?
The options I've considered so far are:
Manually loop the %hash, find the deepest "classes" (e.g Person), create these first, then manually add these to the newly created higher level classes (Department), and so on.
Add some kind of coercion functionality to each class, which lets me do something like Company->new(%hash), and make each class create its own "subclasses" (via coercion)
Convert the %hash into a structure similar to what MooseX::Storage would serialize to, then use MooseX::Storage to instatiate everything for me...
Any other ideas or suggestions?
You could have a BUILDARGS handler which converts unblessed references in those slots to objects. Coercions is probably the best, but it takes more doing. (Unless this is all coming from a RDBMS, in which case use DBIx::Class).
#!/usr/bin/env perl
use warnings;
use strict;
package Company;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRef[Company::Department]');
sub BUILDARGS {
my $self = shift;
my $args = $self->SUPER::BUILDARGS(#_);
#{ $args->{departments} } =
map { eval{ $_->isa('Company::Department') } ? $_ : Company::Department->new($_) }
#{ $args->{departments} };
return $args;
};
package Company::Department;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRef[Company::Person]');
sub BUILDARGS {
my $self = shift;
my $args = $self->SUPER::BUILDARGS(#_);
#{ $args->{employees} } =
map { eval{ $_->isa('Company::Person') } ? $_ : Company::Person->new($_) }
#{ $args->{employees} };
return $args;
};
package Company::Person;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'age' => (is => 'ro', isa => 'Num');
package main;
my %hash = (
company => {
id => 1,
name => 'CorpInc',
departments => [
{
id => 1,
name => 'Sales',
employees => [
{
id => 1,
name => 'John Smith',
age => '30',
},
],
},
{
id => 2,
name => 'IT',
employees => [
{
id => 2,
name => 'Lucy Jones',
age => '28',
},
{
id => 3,
name => 'Miguel Cerveza',
age => '25',
},
],
},
],
}
);
my $company = Company->new($hash{company});
use Data::Dumper;
print Dumper $company;
I've used your option 2 several times and it worked fine for me. Last instance was inflating JIRA REST API results into real objects. Note that with coercions you can also lookup an existing instance by id and create only if it does not exist.
Edit: Here is some code to demonstrate those coercions:
package Company::Types;
use Moose::Util::TypeConstraints;
subtype 'Company::Departments', as 'ArrayRef[Company::Department]';
coerce 'Company::Departments', from 'ArrayRef', via {
require Company::Department;
[ map { Company::Department->new($_) } #$_ ]
};
subtype 'Company::Persons', as 'ArrayRef[Company::Person]';
coerce 'Company::Persons', from 'ArrayRef', via {
require Company::Person;
[ map { Company::Person->new($_) } #$_ ]
};
no Moose::Util::TypeConstraints;
and in those classes:
use Company::Types;
has 'departments' => (is => 'ro', isa => 'Company::Departments', coerce => 1);
has 'employees' => (is => 'ro', isa => 'Company::Persons', coerce => 1);
then you can pass whole structure into Company constructor and all gets inflated properly.

Moose - coercing from Num to ArrayRef[Num]?

Ok, what am I doing wrong - Moose is ignoring my coercion:
package moo;
use Moose;
use Moose::Util::TypeConstraints;
subtype Bar => as 'ArrayRef[Num]';
coerce 'Bar' =>
from 'Num' => via { [ 10 ] }; # this doesn't seem to be getting called
has x => (
is => 'rw',
isa => 'Bar',
);
package main;
my $m1 = moo->new(x => [ 3 ]); # works
my $m2 = moo->new(x => 5); # doesn't work
Maybe you forgot coerce => 1 while defining x attribute.
has x => ( is => 'rw', isa => 'Bar', coerce => 1 );
`

Apply specific check (beyond Moose types) to Moose attribute

Moose types are great, but sometimes you need to be more specific. You all know these data type rules: that parameter may only be 'A', 'B' or 'C', or only a currency symbol, or must conform to some regular expression.
Take a look at the following example which has two constrained attributes, one must be either 'm' or 'f', the other must be a valid ISO date. What's the best way in Moose to specify these constraints? I'd think of the SQL CHECK clause, but AFAICS there is no check keyword in Moose. So I used trigger, but it sounds wrong. Anyone has a better answer?
package Person;
use Moose;
has gender => is => 'rw', isa => 'Str', trigger =>
sub { confess 'either m or f' if $_[1] !~ m/^m|f$/ };
has name => is => 'rw', isa => 'Str';
has dateOfBirth => is => 'rw', isa => 'Str', trigger =>
sub { confess 'not an ISO date' if $_[1] !~ m/^\d\d\d\d-\d\d-\d\d$/ };
no Moose;
__PACKAGE__->meta->make_immutable;
package main;
use Test::More;
use Test::Exception;
dies_ok { Person->new( gender => 42 ) } 'gender must be m or f';
dies_ok { Person->new( dateOfBirth => 42 ) } 'must be an ISO date';
done_testing;
Here's what I wound up using:
package Blabla::Customer;
use Moose::Util::TypeConstraints;
use Moose;
subtype ISODate => as 'Str' => where { /^\d\d\d\d-\d\d-\d\d$/ };
has id => is => 'rw', isa => 'Str';
has gender => is => 'rw', isa => enum ['m', 'f'];
has firstname => is => 'rw', isa => 'Str';
has dateOfBirth => is => 'rw', isa => 'ISODate';
no Moose;
__PACKAGE__->meta->make_immutable;
This is Moose version 1.19, in case it matters. I got the following warning for the wrong subtype as => 'Str', where => { ... } syntax I erroneously introduced: Calling subtype() with a simple list of parameters is deprecated. So I had to change it a bit according to the fine manual.
Just define your own subtype, and use that.
package Person;
use Moose::Util::TypeConstraints;
use namespace::clean;
use Moose;
has gender => (
is => 'rw',
isa => subtype(
as 'Str',
where { /^[mf]$/ }
),
);
has name => (
is => 'rw',
isa => 'Str'
);
has dateOfBirth => (
is => 'rw',
isa => subtype(
as 'Str',
where { /^\d\d\d\d-\d\d-\d\d$/ }
),
);
no Moose;
__PACKAGE__->meta->make_immutable;
1;
package main;
use Test::More;
use Test::Exception;
dies_ok { Person->new( gender => 42 ) } 'gender must be m or f';
dies_ok { Person->new( dateOfBirth => 42 ) } 'must be an ISO date';
done_testing;
Or you could use the MooseX::Types module.
package Person::TypeConstraints;
use MooseX::Types::Moose qw'Str';
use MooseX::Types -declare => [qw'
Gender ISODate
'];
subtype Gender, (
as Str,
where { /^[mf]$/ },
);
subtype ISODate, (
as Str,
where { /^\d\d\d\d-\d\d-\d\d$/ }
);
1;
package Person:
use MooseX::Types::Moose qw'Str';
use Person::TypeConstraints qw'Gender ISODate';
use namespace::clean;
use Moose;
has gender => (
is => 'rw',
isa => Gender,
);
has name => (
is => 'rw',
isa => Str,
);
has dateOfBirth => (
is => 'rw',
isa => ISODate,
);
no Moose;
__PACKAGE__->meta->make_immutable;
1;
Adding your own type like Brad said:
use Moose::Util::TypeConstraints;
my $gender_constraint = subtype as 'Str', where { $_ =~ /^[FfMm]$/ };
has gender => ( is => 'rw', isa => $gender_constraint );
You could try using MooseX-Types-Parameterizable to implement types that take parameters for the cases you present (untested, just sketched):
package YourTypes;
use MooseX::Types -declare => [qw( OneOfStr MatchingStr )];
use MooseX::Types::Moose qw( Str ArrayRef RegexpRef );
subtype OneOfStr,
as Parameterizable[ Str, ArrayRef[ Str ] ],
where {
my ($str, $allowed) = #_;
return scalar grep { $_ eq $str } #$allowed;
};
subtype MatchingStr,
as Parameterizable[ Str, RegexpRef ],
where {
my ($str, $rx) = #_;
return scalar $str =~ $rx;
};
1;
and you would use it like this:
use YourTypes qw( OneOfStr MatchingStr );
has gender => (is => 'ro', isa => OneOfStr[ [qw( f m )] ]);
has dob => (is => 'ro', isa => MatchingStr[ qr/^$yourregex$/ ]);