Skip to content

Commit ffd8370

Browse files
author
sshaw
committed
I think this is how its done...
0 parents commit ffd8370

File tree

4 files changed

+227
-0
lines changed

4 files changed

+227
-0
lines changed

MANIFEST

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
lib/MooseX/NestedAttributesConstructor.pm
2+
lib/MooseX/NestedAttributesConstructor/Class.pm
3+
Makefile.PL
4+
MANIFEST This list of files
5+
t/basic.t
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package MooseX::NestedAttributesConstructor;
2+
use Moose;
3+
our $VERSION = '0.01';
4+
5+
Moose::Exporter->setup_import_methods(
6+
# class_metaroles => {
7+
# class => ['MooseX::NestedAttributesConstructor::Trait::Class']
8+
# }
9+
base_class_roles => ['MooseX::NestedAttributesConstructor::Class']
10+
);
11+
12+
package MooseX::NestedAttributesConstructor::Meta::Trait::NestedAttribute;
13+
use Moose::Role;
14+
Moose::Util::meta_attribute_alias('NestedAttribute');
15+
16+
1;
17+
18+
=pod
19+
20+
=head1 NAME
21+
22+
MooseX::NestedAttributesConstructor - Create attributes from a nested data structure
23+
24+
=head1 OVERVIEW
25+
26+
package Address
27+
use Moose;
28+
29+
has street => ( is => 'rw' );
30+
has city => ( is => 'rw' );
31+
# ...
32+
33+
package Person;
34+
use Moose;
35+
36+
has name => ( is => 'rw' );
37+
has addresses => ( is => 'rw',
38+
isa => 'ArrayRef[Address]',
39+
traits => ['NestedAttribute'] );
40+
# ...
41+
42+
package main;
43+
use Person;
44+
45+
my $p = Person->new(name => 'sshaw',
46+
addresses => [
47+
{ city => 'LA' },
48+
{ city => 'Da Bay' },
49+
{ city => 'Even São José' }
50+
]);
51+
52+
say $_->city for @{$p->addresses};
53+
54+
=head1 WARNING
55+
56+
This is the first Moose I've cooked.
57+
58+
=head1 DESCRIPTION
59+
60+
This module creates attribute types from a nested data structure passed to an object's constructor.
61+
Just add the C<NestedAttrubute> trait to attributes with a custom or parameterized type.
62+
63+
=head1 AUTHOR
64+
65+
Skye Shaw (sshaw AT lucas.cis.temple.edu)
66+
67+
=head1 SEE ALSO
68+
69+
L<MooseX::StrictConstructor>
70+
71+
=head1 COPYRIGHT
72+
73+
Copyright (c) 2012 Skye Shaw.
74+
75+
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package MooseX::NestedAttributesConstructor::Class;
2+
use Moose::Role;
3+
4+
around BUILDARGS => sub {
5+
my $orig = shift;
6+
my $class = shift;
7+
my $argz = ref $_[0] ? shift : { @_ };
8+
return $class->$orig(_construct($class, $argz));
9+
};
10+
11+
# Called after BUILDARGS
12+
# around new_object => sub {
13+
# my $orig = shift;
14+
# my $self = shift;
15+
# my $argz = @_ == 1 ? $_[0] : {@_};
16+
# return $self->$orig(_construct($self, $argz));
17+
# };
18+
19+
sub _construct
20+
{
21+
my ($class, $options) = @_;
22+
return $options unless $class->can('meta');
23+
24+
while( my ($name, $val) = each %$options ) {
25+
26+
# May or may not be a metaclass
27+
#my $attr = $class->can('get_attribute') ? $class->get_attribute($name) : $class->meta->get_attribute($name);
28+
my $attr = $class->meta->get_attribute($name);
29+
my $vtype = ref($val);
30+
31+
next unless $attr
32+
and ($vtype eq 'ARRAY' or $vtype eq 'HASH')
33+
and !blessed($val)
34+
and $attr->does('NestedAttribute')
35+
and $attr->has_type_constraint
36+
and ($attr->type_constraint->isa('Moose::Meta::TypeConstraint::Class') or
37+
$attr->type_constraint->isa('Moose::Meta::TypeConstraint::Parameterized'));
38+
39+
my $ctype;
40+
my $param = 1;
41+
42+
if($attr->type_constraint->isa('Moose::Meta::TypeConstraint::Parameterized')) {
43+
$ctype = $attr->type_constraint->type_parameter->class;
44+
}
45+
else {
46+
$ctype = $attr->type_constraint->class;
47+
$param = 0;
48+
}
49+
50+
if($vtype eq 'HASH') {
51+
$val = _construct_hash($ctype, $val, $param);
52+
}
53+
elsif($param && ref($val->[0]) eq 'HASH') {
54+
$val = _construct_array($ctype, $val);
55+
}
56+
57+
$options->{$name} = $val;
58+
}
59+
60+
return $options;
61+
}
62+
63+
sub _construct_array
64+
{
65+
my ($class, $val) = @_;
66+
my $collection = [];
67+
for(@$val) {
68+
my $options = _construct($class, $_);
69+
push @$collection, $class->new(%$options);
70+
}
71+
72+
$collection;
73+
}
74+
75+
sub _construct_hash
76+
{
77+
my ($class, $val, $is_param) = @_;
78+
79+
if($is_param) {
80+
my $collection = {};
81+
for(keys %$val) {
82+
my $options = _construct($class, $val->{$_});
83+
$collection->{$_} = $class->new(%$options);
84+
}
85+
86+
return $collection;
87+
}
88+
89+
$class->new(%$val);
90+
}
91+
92+
1;

t/basic.t

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package A;
2+
use Moose;
3+
use MooseX::NestedAttributesConstructor;
4+
5+
has 'a', traits => ['NestedAttribute'], is => 'rw', isa => 'A';
6+
has 'arrayref', traits => ['NestedAttribute'], is => 'rw', isa => 'ArrayRef[A]';
7+
has 'hashref', traits => ['NestedAttribute'], is => 'rw', isa => 'HashRef[A]';
8+
has 'str', is => 'rw', isa => 'Str';
9+
10+
package main;
11+
use strict;
12+
use warnings;
13+
14+
use Test::More;
15+
16+
my $a;
17+
subtest 'without nested attributes' => sub {
18+
$a = A->new(str => '123');
19+
is($a->str, 123, '$a->str');
20+
21+
$a = A->new(arrayref => [ A->new(str => '123') ]);
22+
isa_ok($a->arrayref, 'ARRAY', '$a->arrayref');
23+
isa_ok($a->arrayref->[0], 'A', '$a->arrayref->[0]');
24+
is($a->arrayref->[0]->str, '123', '$a->arrayref->[0]->str');
25+
};
26+
27+
subtest 'with nested attributes' => sub {
28+
eval { A->new(str => []) };
29+
like($@, qr/validation failed/i, '$a->str not nested');
30+
31+
eval { A->new(a => { a => 'A' }) };
32+
like($@, qr/validation failed/i, '$a->a->a nested but wrong type');
33+
34+
$a = A->new(str => '123', a => { str => '456' });
35+
is($a->str, '123', '$a->str');
36+
isa_ok($a->a, 'A', '$a->a');
37+
is($a->a->str, '456', '$a->a->str');
38+
39+
$a = A->new(str => '123', arrayref => [
40+
{ a => { str => '123' } },
41+
{ a => { str => 'XYZ' } }
42+
]);
43+
is($a->str, '123', '$a->str');
44+
isa_ok($a->arrayref, 'ARRAY', '$a->arrayref');
45+
is(@{$a->arrayref}, 2, '$a->arrayref size');
46+
isa_ok($a->arrayref->[0], 'A', '$a->arrayref->[0]');
47+
isa_ok($a->arrayref->[0]->a, 'A', '$a->arrayref->[0]->a');
48+
is($a->arrayref->[0]->a->str, '123', '$a->arrayref->[0]->a->str');
49+
isa_ok($a->arrayref->[1], 'A', '$a->arrayref->[1]');
50+
isa_ok($a->arrayref->[1]->a, 'A', '$a->arrayref->[1]->a');
51+
is($a->arrayref->[1]->a->str, 'XYZ', '$a->arrayref->[1]->a->str');
52+
};
53+
54+
done_testing();
55+

0 commit comments

Comments
 (0)