File Coverage

blib/lib/MooseX/DataModel.pm
Criterion Covered Total %
statement 93 96 96.8
branch 34 40 85.0
condition 11 12 91.6
subroutine 12 13 92.3
pod 3 4 75.0
total 153 165 92.7


line stmt bran cond sub pod time code
1             package MooseX::DataModel {
2 14     14   1918133 use Moose;
  14         4696187  
  14         116  
3 14     14   100090 use Moose::Exporter;
  14         40  
  14         77  
4 14     14   683 use Moose::Util::TypeConstraints qw/find_type_constraint register_type_constraint coerce subtype from via/;
  14         33  
  14         147  
5             our $VERSION = "1.00";
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 39     39 1 105668 my ($meta, $key_name, %properties) = @_;
14              
15 39 50       166 die "Must specify isa in an object declaration" if (not defined $properties{isa});
16              
17 39         110 $properties{ is } = 'ro';
18              
19 39         90 my $location = delete $properties{ location };
20 39 100       123 $properties{ init_arg } = $location if ($location);
21              
22 39         87 my $type = $properties{isa};
23              
24 39 50       163 if (my $constraint = find_type_constraint($type)) {
25 39 100 66     5106 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 0     0   0 $type->new(%$_)
31             }
32 4         88 }
33              
34 39 100       6272 if ($constraint->has_coercion){
35 10         75 $properties{ coerce } = 1
36             }
37             } else {
38 0         0 die "FATAL: Didn't find a type constraint for $key_name";
39             }
40              
41 39         400 $meta->add_attribute($key_name, \%properties);
42             }
43              
44             sub _alias_for_paramtype {
45 24     24   95 my $name = shift;
46 24         239 $name =~ s/\[(.*)]$/Of$1/;
47 24         88 return $name;
48             }
49              
50             sub object {
51 13     13 1 38990 my ($meta, $key_name, %properties) = @_;
52              
53 13 50       67 die "Must specify isa in an object declaration" if (not defined $properties{isa});
54              
55 13         39 my $location = delete $properties{ location };
56 13 100       50 $properties{ init_arg } = $location if ($location);
57              
58 13         34 my ($inner_type, $type, $type_alias);
59              
60 13 100       55 if (ref($properties{isa})) {
61 2         11 $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         32 $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         10 );
70 2         4737 register_type_constraint($type);
71              
72 2         229 $inner_type = $properties{isa}->name;
73             } else {
74 11         31 $inner_type = $properties{isa};
75 11         65 $type_alias = _alias_for_paramtype("HashRef[$inner_type]");
76              
77 11         82 $type = find_type_constraint("HashRef[$inner_type]");
78              
79 11 100       739 if (not defined $type) {
80 10         80 subtype $type_alias, { as => "HashRef[$inner_type]" };
81             }
82             }
83              
84 13         68419 my $key_isa = delete $properties{key_isa};
85              
86 13         112 my $type_constraint = find_type_constraint($inner_type);
87 13 100 100     1365 if (defined $type_constraint and not $type_constraint->has_coercion) {
88             coerce $inner_type, from 'HashRef', via {
89 2     2   314 return $inner_type->new(%$_);
90             }
91 5         78 }
92              
93 13 100       4609 if (not find_type_constraint($type_alias)->has_coercion) {
94             coerce $type_alias, from 'HashRef', via {
95 5     5   38425 my $uncoerced = $_;
96 5         16 my $coerce_routine = $type_constraint;
97 5         19 return { map { ($_ => $coerce_routine->coerce($uncoerced->{$_}, $_[1])) } keys %$uncoerced }
  6         786  
98 12         1305 };
99             }
100              
101 13         8743 $properties{ coerce } = 1;
102 13         44 $properties{ isa } = $type_alias;
103 13         39 $properties{ is } = 'ro';
104              
105 13         91 $meta->add_attribute($key_name, \%properties);
106             }
107              
108             sub array {
109 11     11 1 37323 my ($meta, $key_name, %properties) = @_;
110              
111 11 50       58 die "Must specify isa in an array declaration" if (not defined $properties{isa});
112              
113 11         73 my $location = delete $properties{ location };
114 11 100       44 $properties{ init_arg } = $location if ($location);
115              
116 11         30 my ($inner_type, $type, $type_alias);
117              
118 11 100       43 if (ref($properties{isa})) {
119 2         8 $type = find_type_constraint($properties{isa});
120 2 50       36 die "FATAL: Didn't find a type constraint for $key_name" if (not defined $type);
121              
122 2         31 $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         10 );
128 2         4079 register_type_constraint($type);
129              
130 2         240 $inner_type = $properties{isa}->name;
131 2         14 $properties{ isa } = $type;
132             } else {
133 9         24 $inner_type = $properties{isa};
134 9         55 $type_alias = _alias_for_paramtype("ArrayRef[$inner_type]");
135              
136 9         47 $type = find_type_constraint($type_alias);
137              
138 9 100       557 if (not defined $type) {
139 8         58 subtype $type_alias, { as => "ArrayRef[$inner_type]" };
140             }
141 9         52713 $properties{ isa } = $type_alias;
142             }
143              
144 11         54 my $type_constraint = find_type_constraint($inner_type);
145 11 100 100     1189 if (defined $type_constraint and not $type_constraint->has_coercion) {
146             coerce $inner_type, from 'HashRef', via {
147 1     1   161 return $inner_type->new(%$_);
148             }
149 7         124 }
150              
151 11 100       5919 if (not find_type_constraint($type_alias)->has_coercion) {
152             coerce $type_alias, from 'ArrayRef', via {
153 5     5   232143 my $type_c = find_type_constraint($inner_type);
154 5         606 my $parent = $_[1];
155 5 100       192 if ($type_c->has_coercion) {
156 4         36 return [ map { $type_c->coerce($_) } @$_ ]
  4         25  
157             } else {
158 1         9 return [ map { $_ } @$_ ]
  1         6  
159             }
160 10         1133 };
161             }
162              
163 11         6831 $properties{ coerce } = 1;
164 11         34 $properties{ is } = 'ro';
165 11         78 $meta->add_attribute($key_name, \%properties);
166             }
167              
168             sub new_from_json {
169 1     1 0 2256 my ($class, $json) = @_;
170 1         239 require JSON;
171 0           return $class->new(JSON::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