How to instantiate Moose classes from a big hash - perl

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.

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',
);

Coercing ArrayRef[MyClass] from ArrayRef[HashRef]

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.

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 );
`

How to find whether there is a 'child' doc. in MongoDBx::Class

I am learning the Perl MongoDBX::Class and writing a blog app.
Bellow are Post and Comment model. If call this method:
my $comments = $a_post->comments;
it halt the application if there is no comment for this post. The question is how to check whether this post has comment?
Thanks.
package Model::Schema::Post;
use MongoDBx::Class::Moose;
use namespace::autoclean;
with 'MongoDBx::Class::Document';
has 'title' => (is => 'rw', isa => 'Str', required => 1,);
belongs_to 'author' => (is => 'ro', isa => 'Author', required => 1);
has 'post_date' => (is => 'ro', isa => 'DateTime', traits => ['Parsed'], required => 1);
has 'text' => (is => 'rw', isa => 'Str', required => 1);
joins_many 'comments' => (is => 'ro', isa => 'Comment', coll => 'comments', ref => 'post');
holds_many 'tags' => (is => 'rw', isa => 'Tag', predicate => 'has_tag');
__PACKAGE__->meta->make_immutable;
package Model::Schema::Comment;
use MongoDBx::Class::Moose;
use namespace::autoclean;
with 'MongoDBx::Class::Document';
belongs_to 'post' => (is => 'ro', isa => 'Post', required => 1);
has 'author' => (is => 'ro', isa => 'Author', required => 1);
has 'comment_date' => (is => 'ro', isa => 'DateTime', traits => ['Parsed'], required => 1);
has 'text' => (is => 'rw', isa => 'Str', required => 1);
has 'rateing' => (is => 'rw', isa => 'Int');
__PACKAGE__->meta->make_immutable;
I'm not sure why your application halts. Looking at your schema, running
my $comments = $a_post->comments;
Should return a MongoDBx::Class::Cursor object (which is really just a MongoDB::Cursor object). On this cursor, you can run:
my $num_comments = $comments->count;
if ($num_comments > 0) {
my #comments = $comments->all;
...
}
If this doesn't help you, and your application still hangs, It'll help me if you send me a sample of your code so I can try to find out what's going on.
By the way I'm the author of MongoDBx::Class.
P.S. If you feel you've stumbled upon some bug in MongoDBx::Class, feel free to open a bug report as described on the MongoDBx::Class page on CPAN.