File Coverage

blib/lib/MooseX/DataModel.pm
Criterion Covered Total %
statement 95 96 98.9
branch 34 40 85.0
condition 11 12 91.6
subroutine 13 13 100.0
pod 3 4 75.0
total 156 165 94.5


line stmt bran cond sub pod time code
1             package MooseX::DataModel {
2 15     15   2573201 use Moose;
  15         5264995  
  15         106  
3 15     15   108921 use Moose::Exporter;
  15         37  
  15         78  
4 15     15   774 use Moose::Util::TypeConstraints qw/find_type_constraint register_type_constraint coerce subtype from via/;
  15         30  
  15         141  
5             our $VERSION = "1.01";
6              
7             Moose::Exporter->setup_import_methods(
8             with_meta => [ qw/ key array object / ],
9             also => [ 'Moose', 'Moose::Util::TypeConstraints' ],
10             );
11              
12             sub key {
13 40     40 1 104556 my ($meta, $key_name, %properties) = @_;
14              
15 40 50       167 die "Must specify isa in an object declaration" if (not defined $properties{isa});
16              
17 40         96 $properties{ is } = 'ro';
18              
19 40         83 my $location = delete $properties{ location };
20 40 100       105 $properties{ init_arg } = $location if ($location);
21              
22 40         74 my $type = $properties{isa};
23              
24 40 50       136 if (my $constraint = find_type_constraint($type)) {
25 40 100 66     4850 if ($constraint->isa('Moose::Meta::TypeConstraint::Class') and
      100        
26             (not $constraint->has_coercion or
27             not $constraint->coercion->has_coercion_for_type('HashRef'))
28             ){
29             coerce $type, from 'HashRef', via {
30 1     1   452 $type->new(%$_)
31             }
32 4         64 }
33              
34 40 100       6576 if ($constraint->has_coercion){
35 10         67 $properties{ coerce } = 1
36             }
37             } else {
38 0         0 die "FATAL: Didn't find a type constraint for $key_name";
39             }
40              
41 40         400 $meta->add_attribute($key_name, \%properties);
42             }
43              
44             sub _alias_for_paramtype {
45 26     26   105 my $name = shift;
46 26         264 $name =~ s/\[(.*)]$/Of$1/;
47 26         89 return $name;
48             }
49              
50             sub object {
51 14     14 1 42583 my ($meta, $key_name, %properties) = @_;
52              
53 14 50       63 die "Must specify isa in an object declaration" if (not defined $properties{isa});
54              
55 14         37 my $location = delete $properties{ location };
56 14 100       44 $properties{ init_arg } = $location if ($location);
57              
58 14         32 my ($inner_type, $type, $type_alias);
59              
60 14 100       43 if (ref($properties{isa})) {
61 2         9 $type = find_type_constraint($properties{isa});
62 2 50       38 die "FATAL: Didn't find a type constraint for $key_name" if (not defined $type);
63              
64 2         35 $type_alias = _alias_for_paramtype('HashRef[' . $properties{isa}->name . ']');
65             $type = Moose::Meta::TypeConstraint::Parameterized->new(
66             name => $type_alias,
67             parent => find_type_constraint('HashRef'),
68             type_parameter => $properties{isa}
69 2         8 );
70 2         5617 register_type_constraint($type);
71              
72 2         256 $inner_type = $properties{isa}->name;
73             } else {
74 12         27 $inner_type = $properties{isa};
75 12         58 $type_alias = _alias_for_paramtype("HashRef[$inner_type]");
76              
77 12         60 $type = find_type_constraint("HashRef[$inner_type]");
78              
79 12 100       751 if (not defined $type) {
80 11         62 subtype $type_alias, { as => "HashRef[$inner_type]" };
81             }
82             }
83              
84 14         79400 my $key_isa = delete $properties{key_isa};
85              
86 14         58 my $type_constraint = find_type_constraint($inner_type);
87 14 100 100     1576 if (defined $type_constraint and not $type_constraint->has_coercion) {
88             coerce $inner_type, from 'HashRef', via {
89 2     2   356 return $inner_type->new(%$_);
90             }
91 5         64 }
92              
93 14 100       4709 if (not find_type_constraint($type_alias)->has_coercion) {
94             coerce $type_alias, from 'HashRef', via {
95 6     6   27759 my $uncoerced = $_;
96 6         14 my $coerce_routine = $type_constraint;
97 6         22 return { map { ($_ => $coerce_routine->coerce($uncoerced->{$_}, $_[1])) } keys %$uncoerced }
  8         1465  
98 13         1664 };
99             }
100              
101 14         9344 $properties{ coerce } = 1;
102 14         39 $properties{ isa } = $type_alias;
103 14         38 $properties{ is } = 'ro';
104              
105 14         85 $meta->add_attribute($key_name, \%properties);
106             }
107              
108             sub array {
109 12     12 1 40869 my ($meta, $key_name, %properties) = @_;
110              
111 12 50       61 die "Must specify isa in an array declaration" if (not defined $properties{isa});
112              
113 12         29 my $location = delete $properties{ location };
114 12 100       52 $properties{ init_arg } = $location if ($location);
115              
116 12         25 my ($inner_type, $type, $type_alias);
117              
118 12 100       41 if (ref($properties{isa})) {
119 2         8 $type = find_type_constraint($properties{isa});
120 2 50       38 die "FATAL: Didn't find a type constraint for $key_name" if (not defined $type);
121              
122 2         44 $type_alias = _alias_for_paramtype('ArrayRef[' . $properties{isa}->name . ']');
123             $type = Moose::Meta::TypeConstraint::Parameterized->new(
124             name => $type_alias,
125             parent => find_type_constraint('ArrayRef'),
126             type_parameter => $properties{isa}
127 2         9 );
128 2         4784 register_type_constraint($type);
129              
130 2         306 $inner_type = $properties{isa}->name;
131 2         19 $properties{ isa } = $type;
132             } else {
133 10         22 $inner_type = $properties{isa};
134 10         59 $type_alias = _alias_for_paramtype("ArrayRef[$inner_type]");
135              
136 10         39 $type = find_type_constraint($type_alias);
137              
138 10 100       648 if (not defined $type) {
139 9         67 subtype $type_alias, { as => "ArrayRef[$inner_type]" };
140             }
141 10         62701 $properties{ isa } = $type_alias;
142             }
143              
144 12         47 my $type_constraint = find_type_constraint($inner_type);
145 12 100 100     1444 if (defined $type_constraint and not $type_constraint->has_coercion) {
146             coerce $inner_type, from 'HashRef', via {
147 6     6   901 return $inner_type->new(%$_);
148             }
149 8         112 }
150              
151 12 100       6745 if (not find_type_constraint($type_alias)->has_coercion) {
152             coerce $type_alias, from 'ArrayRef', via {
153 6     6   328567 my $type_c = find_type_constraint($inner_type);
154 6         632 my $parent = $_[1];
155 6 100       221 if ($type_c->has_coercion) {
156 5         41 return [ map { $type_c->coerce($_) } @$_ ]
  7         1357  
157             } else {
158 1         9 return [ map { $_ } @$_ ]
  1         7  
159             }
160 11         1331 };
161             }
162              
163 12         8101 $properties{ coerce } = 1;
164 12         36 $properties{ is } = 'ro';
165 12         62 $meta->add_attribute($key_name, \%properties);
166             }
167              
168             sub new_from_json {
169 1     1 0 2138 my ($class, $json) = @_;
170 1         8 require JSON::MaybeXS;
171 1         22 return $class->new(JSON::MaybeXS::decode_json($json));
172             }
173              
174             }
175              
176             1;
177             ### main pod documentation begin ###
178              
179             =encoding UTF-8
180              
181             =head1 NAME
182              
183             MooseX::DataModel - Create object models from datastructures
184              
185             =head1 SYNOPSIS
186              
187             package MyModel {
188             use MooseX::DataModel;
189              
190             version => (isa => 'Int');
191             description => (isa => 'Str', required => 1);
192              
193             sub do_something {
194             my $self = shift;
195             if(shift->version == 3) ...
196             }
197             # Moose is imported for your convenience
198             has foo => (...);
199             }
200              
201             my $obj = MyModel->MooseX::DataModel::new_from_json('{"version":3,"description":"a json document"}');
202             # $obj is just a plain old Moose object
203             print $obj->version;
204              
205             my $obj = MyModel->new({ version => 6, description => 'A description' });
206             $obj->do_something;
207              
208             =head1 DESCRIPTION
209              
210             Working with "plain datastructures" (nested hashrefs, arrayrefs and scalars) that come from other
211             systems can be a pain.
212              
213             Normally those datastructures are not arbitrary: they have some structure to them: most of them
214             come to express "object like" things. MooseX::DataModel tries to make converting these datastructures
215             into objects in an easy, declarative fashion.
216              
217             Lots of times
218              
219             MooseX::DataModel also helps you validate the datastructures. If you get an object back, it conforms
220             to your object model. So if you declare a required key, and the passed datastructure doesn't contain
221             it: you will get an exception. If the type of the key passed is different from the one declared: you
222             get an exception. The advantage over using a JSON validator, is that after validation you still have
223             your original datastructure. With MooseX::DataModel you get full-blown objects, to which you can
224             attach logic.
225              
226             =head1 USAGE
227              
228             Just use MooseX::DataModel in a class. It will import three keywords C<key>, C<array>, C<object>.
229             With these keywords we can specify attributes in our class
230              
231             =head2 key attribute => (isa => $type, [required => 1, location => $location])
232              
233             Declares an attribute named "attribute" that is of type $type. $type can be a string with a
234             Moose type constraint (Str, Int), or any user defined subtype (MyPositiveInt). Also it can
235             be the name of a class. If it's a class, MooseX::DataModel will coerce a HashRef to the
236             specified class (using the HashRef as the objects' constructor parameters).
237              
238             package VersionObject {
239             use MooseX::DataModel;
240             key major => (isa => 'Int');
241             key minor => (isa => 'Int');
242             }
243             package MyObject {
244             use MooseX::DataModel;
245             key version => (isa => 'VersionObject');
246             }
247              
248             my $o = MyObject->MooseX::DataModel::new_from_json('{"version":{"major":3,"minor":5}}');
249             # $o->version->isa('VersionObject') == true
250             print $o->version->major;
251             # prints 3
252             print $o->version->minor;
253             # prints 5
254              
255             required => 1: declare that this attribute is obliged to be set in the passed datastructure
256              
257             package MyObject {
258             use MooseX::DataModel;
259             key version => (isa => 'Int', required => 1);
260             }
261             my $o = MyObject->MooseX::DataModel::new_from_json('{"document_version":3}');
262             # exception, since "version" doesn't exist
263            
264             my $o = MyObject->MooseX::DataModel::new_from_json('{"version":3}');
265             print $o->version;
266             # prints 3
267              
268             location => $location: $location is a string that specifies in what key of the datastructure to
269             find the attributes' value:
270              
271             package MyObject {
272             use MooseX::DataModel;
273             key Version => (isa => 'Int', location => 'document_version');
274             }
275             my $o = MyObject->MooseX::DataModel::new_from_json('{"document_version":3}');
276             print $o->Version;
277             # prints 3
278              
279             =head2 array attribute => (isa => $type, [required => 1, location => $location])
280              
281             Declares an attribute that holds an array whose elements are of a certain type.
282              
283             $type, required and location work as in "key"
284              
285             package MyObject {
286             use MooseX::DataModel;
287             key name => (isa => 'Str', required => 1);
288             array likes => (isa => 'Str', required => 1, location => 'his_tastes');
289             }
290             my $o = MyObject->MooseX::DataModel::new_from_json('{"name":"pplu":"his_tastes":["cars","ice cream"]}");
291             print $o->likes->[0];
292             # prints 'cars'
293              
294             =head2 object attribute => (isa => $type, [required => 1, location => $location])
295              
296             Declares an attribute that holds an hash ("JS object") whose elements are of a certain type. This
297             is useful when in the datastructure you have a hash with arbitrary keys (for known keys you would
298             describe an object with the "key" keyword.
299              
300             $type, required and location work as in "key"
301              
302             package MyObject {
303             use MooseX::DataModel;
304             key name => (isa => 'Str', required => 1);
305             object likes => (isa => 'Int', required => 1, location => 'his_tastes');
306             }
307             my $o = MyObject->MooseX::DataModel::new_from_json('{"name":"pplu":"his_tastes":{"cars":9,"ice cream":6}}");
308             print $o->likes->{ cars };
309             # prints 9
310              
311             =head1 METHODS
312              
313             =head2 new
314              
315             Your class gets the default Moose constructor. You can pass it a hashref with the datastructure
316              
317             my $o = MyObject->new({ name => 'pplu', his_tastes => { cars => 9, 'ice cream' => 6 }});
318              
319             =head2 MooseX::DataModel::from_json
320              
321             There is a convenience constructor for parsing a JSON (so you don't have to do it from the outside)
322              
323             my $o = MyObject->MooseX::DataModel::from_json("JSON STRING");
324              
325             =head1 INNER WORKINGS
326              
327             All this can be done with plain Moose, using subtypes, coercions and declaring the
328             appropiate attributes (that's what really happens on the inside, although it's not guaranteed
329             to stay that way forever). MooseX::DataModel just wants to help you write less code :)
330              
331             =head1 BUGS and SOURCE
332              
333             The source code is located here: https://github.com/pplu/moosex-datamodel
334              
335             Please report bugs to:
336              
337             =head1 COPYRIGHT and LICENSE
338              
339             Copyright (c) 2015 by CAPSiDE
340              
341             This code is distributed under the Apache 2 License. The full text of the license can be found in the LICENSE file included with this module.
342              
343             =cut