c4231afe1e78724ee3664c9c20defffd2b39b671
[gitmo/MooseX-Types-Structured.git] / lib / MooseX / Types / Structured.pm
1 package MooseX::Types::Structured;
2
3 use 5.008;
4 use Moose::Util::TypeConstraints;
5 use MooseX::Meta::TypeConstraint::Structured;
6 use MooseX::Types -declare => [qw(Dict Tuple Optional)];
7
8 our $VERSION = '0.06';
9 our $AUTHORITY = 'cpan:JJNAPIORK';
10
11 =head1 NAME
12
13 MooseX::Types::Structured - Structured Type Constraints for Moose
14
15 =head1 SYNOPSIS
16
17 The following is example usage for this module.
18
19     package MyApp::MyClass;
20         
21     use Moose;
22     use MooseX::Types::Moose qw(Str Int);
23     use MooseX::Types::Structured qw(Dict Optional);
24
25     ## A name has a first and last part, but middle names are not required
26     has name => (
27         isa=>Dict[
28             first=>Str,
29             last=>Str,
30             middle=>Optional[Str],
31         ],
32     );
33
34 Then you can instantiate this class with something like:
35
36     my $john = MyApp::MyClass->new(
37         name => {
38             first=>'John',
39             middle=>'James'
40             last=>'Napiorkowski',
41         },
42     );
43     
44     my $vanessa = MyApp::MyClass->new(
45         name => {
46             first=>'Vanessa',
47             last=>'Li'
48         },
49     );
50
51 But all of these would cause a constraint error for the 'name' attribute:
52
53     MyApp::MyClass->new( name=>'John' );
54     MyApp::MyClass->new( name=>{first_name=>'John'} );
55     MyApp::MyClass->new( name=>{first_name=>'John', age=>39} );
56     MyApp::MyClass->new( name=>{first=>'Vanessa', middle=>[1,2], last=>'Li'} );
57     
58 Please see the test cases for more examples.
59
60 =head1 DESCRIPTION
61
62 A structured type constraint is a standard container L</Moose> type constraint,
63 such as an arrayref or hashref, which has been enhanced to allow you to
64 explicitly name all the allow type constraints inside the structure.  The
65 generalized form is:
66
67     TypeConstraint[TypeParameters]
68
69 Where 'TypeParameters' is an array or hash of L</Moose::Meta::TypeConstraint> 
70 type constraints.
71
72 This type library enables structured type constraints. It is build on top of the
73 L<MooseX::Types> library system, so you should review the documentation for that
74 if you are not familiar with it.
75
76 =head2 Comparing Parameterized types to Structured types
77
78 Parameterized constraints are built into the core Moose types 'HashRef' and
79 'ArrayRef'.  Structured types have similar functionality, so their syntax is
80 likewise similar. For example, you could define a parameterized constraint like:
81
82     subtype ArrayOfInts,
83      as Arrayref[Int];
84
85 which would constraint a value to something like [1,2,3,...] and so on.  On the
86 other hand, a structured type constraint explicitly names all it's allowed type
87 parameter constraints.  For the example:
88
89     subtype StringFollowedByInt,
90      as Tuple[Str,Int];
91         
92 would constrain it's value to something like ['hello', 111] but ['hello', 'world']
93 would fail, as well as ['hello', 111, 'world'] and so on.
94
95 Structured Constraints are not limited to arrays.  You can define a structure
96 against a hashref with 'Dict' as in this example:
97
98     subtype FirstNameLastName,
99      as Dict[firste=>Str, lastname=>Str];
100
101 This would constrain a hashref to something like:
102
103     {firstname=>'Vanessa', lastname=>'Li'};
104     
105 but all the following would fail validation:
106
107      {first=>'Vanessa', last=>'Li'};
108      {firstname=>'Vanessa', lastname=>'Li', middlename=>'NA'};   
109      ['Vanessa', 'Li']; 
110
111 These structures can be as simple or elaborate as you wish.  You can even
112 combine various structured, parameterized and simple constraints all together:
113
114     subtype crazy,
115      as Tuple[
116         Int,
117         Dict[name=>Str, age=>Int],
118         ArrayRef[Int]
119      ];
120         
121 Which would match "[1, {name=>'John', age=>25},[10,11,12]]".  Please notice how
122 the type parameters can be visually arranged to your liking and to improve the
123 clarity of your meaning.  You don't need to run then altogether onto a single
124 line.
125
126 =head2 Alternatives
127
128 You should exercise some care as to whether or not your complex structured
129 constraints would be better off contained by a real object as in the following
130 example:
131
132     package MyApp::MyStruct;
133     use Moose;
134     
135     has $_ for qw(name age);
136     
137     package MyApp::MyClass;
138     use Moose;
139     
140     has person => (isa=>'MyApp::MyStruct');             
141     
142     my $instance = MyApp::MyClass->new(
143         person=>MyApp::MyStruct->new(name=>'John', age=>39),
144     );
145         
146 This method may take some additional time to setup but will give you more
147 flexibility.  However, structured constraints are highly compatible with this
148 method, granting some interesting possibilities for coercion.  Try:
149
150     subtype 'MyStruct',
151      as 'MyApp::MyStruct';
152     
153     coerce 'MyStruct',
154      from (Dict[name=>Str, age=>Int]),
155      via { MyApp::MyStruct->new(%$_) },
156      from (Dict[last_name=>Str, first_name=>Str, dob=>DateTime]),
157      via {
158         my $name = $_->{first_name} .' '. $_->{last_name};
159         my $age = DateTime->now - $_->{dob};
160         MyApp::MyStruct->new( name=>$name, age=>$age->years );
161      };
162          
163 =head2 Subtyping a structured subtype
164
165 You need to exercise some care when you try to subtype a structured type
166 as in this example:
167
168     subtype Person,
169      as Dict[name=>Str, age=>Int];
170          
171     subtype FriendlyPerson,
172      as Person[name=>Str, age=>Int, totalFriends=>Int];
173          
174 This will actually work BUT you have to take care that the subtype has a
175 structure that does not contradict the structure of it's parent.  For now the
176 above works, but I will clarify the syntax for this at a future point, so
177 it's recommended to avoid (should not realy be needed so much anyway).  For
178 now this is supported in an EXPERIMENTAL way.  Your thoughts, test cases and
179 patches are welcomed for discussion.
180
181 =head2 Coercions
182
183 Coercions currently work for 'one level' deep.  That is you can do:
184
185     subtype Person,
186      as Dict[name=>Str, age=>Int];
187     
188     subtype Fullname,
189      as Dict[first=>Str, last=>Str];
190     
191     coerce Person,
192      ## Coerce an object of a particular class
193      from BlessedPersonObject,
194      via { +{name=>$_->name, age=>$_->age} },
195      ## Coerce from [$name, $age]
196      from ArrayRef,
197      via { +{name=>$_->[0], age=>$_->[1] },
198      ## Coerce from {fullname=>{first=>...,last=>...}, dob=>$DateTimeObject}
199      from Dict[fullname=>Fullname, dob=>DateTime],
200      via {
201         my $age = $_->dob - DateTime->now;
202         +{
203             name=> $_->{fullname}->{first} .' '. $_->{fullname}->{last},
204             age=>$age->years
205         }
206      };
207          
208 And that should just work as expected.  However, if there are any 'inner'
209 coercions, such as a coercion on 'Fullname' or on 'DateTime', that coercion
210 won't currently get activated.
211
212 Please see the test '07-coerce.t' for a more detailed example.
213
214 =head1 TYPE CONSTRAINTS
215
216 This type library defines the following constraints.
217
218 =head2 Tuple[@constraints]
219
220 This defines an arrayref based constraint which allows you to validate a specific
221 list of constraints.  For example:
222
223     Tuple[Int,Str]; ## Validates [1,'hello']
224     Tuple[Str|Object, Int]; ##Validates ['hello', 1] or [$object, 2]
225
226 =head2 Dict [%constraints]
227
228 This defines a hashref based constraint which allowed you to validate a specific
229 hashref.  For example:
230
231     Dict[name=>Str, age=>Int]; ## Validates {name=>'John', age=>39}
232
233 =head2 Optional [$constraint]
234
235 This is primarily a helper constraint for Dict and Tuple type constraints.  What
236 this allows if for you to assert that a given type constraint is allowed to be
237 null (but NOT undefined).  If the value is null, then the type constraint passes
238 but if the value is defined it must validate against the type constraint.  This
239 makes it easy to make a Dict where one or more of the keys doesn't have to exist
240 or a tuple where some of the values are not required.  For example:
241
242     subtype Name() => as Dict[
243         first=>Str,
244         last=>Str,
245         middle=>Optional[Str],
246     ];
247         
248 Creates a constraint that validates against a hashref with the keys 'first' and
249 'last' being strings and required while an optional key 'middle' is must be a
250 string if it appears but doesn't have to appear.  So in this case both the
251 following are valid:
252
253     {first=>'John', middle=>'James', last=>'Napiorkowski'}
254     {first=>'Vanessa', last=>'Li'}
255     
256 =head1 EXAMPLES
257
258 Here are some additional example usage for structured types.  All examples can
259 be found also in the 't/examples.t' test.  Your contributions are also welcomed.
260
261 =head2 Normalize a HashRef
262
263 You need a hashref to conform to a canonical structure but are required accept a
264 bunch of different incoming structures.  You can normalize using the Dict type
265 constraint and coercions.  This example also shows structured types mixed which
266 other MooseX::Types libraries.
267
268     package Test::MooseX::Meta::TypeConstraint::Structured::Examples::Normalize;
269     
270     use Moose;
271     use DateTime;
272     
273     use MooseX::Types::Structured qw(Dict Tuple);
274     use MooseX::Types::DateTime qw(DateTime);
275     use MooseX::Types::Moose qw(Int Str Object);
276     use MooseX::Types -declare => [qw(Name Age Person)];
277      
278     subtype Person,
279      as Dict[name=>Str, age=>Int];
280     
281     coerce Person,
282      from Dict[first=>Str, last=>Str, years=>Int],
283      via { +{
284         name => "$_->{first} $_->{last}",
285         age=>$_->{years},
286      }},
287      from Dict[fullname=>Dict[last=>Str, first=>Str], dob=>DateTime],
288      via { +{
289         name => "$_->{fullname}{first} $_->{fullname}{last}",
290         age => ($_->{dob} - 'DateTime'->now)->years,
291      }};
292      
293     has person => (is=>'rw', isa=>Person, coerce=>1);
294
295 =cut
296
297 Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint(
298         MooseX::Meta::TypeConstraint::Structured->new(
299                 name => "MooseX::Types::Structured::Tuple" ,
300                 parent => find_type_constraint('ArrayRef'),
301                 constraint_generator=> sub { 
302                         ## Get the constraints and values to check
303             my ($type_constraints, $values) = @_;
304                         my @type_constraints = defined $type_constraints ? @$type_constraints: ();            
305                         my @values = defined $values ? @$values: ();
306                         ## Perform the checking
307                         while(@type_constraints) {
308                                 my $type_constraint = shift @type_constraints;
309                                 if(@values) {
310                                         my $value = shift @values;
311                                         unless($type_constraint->check($value)) {
312                                                 return;
313                                         }                               
314                                 } else {
315                                         unless($type_constraint->check()) {
316                                                 return;
317                                         }
318                                 }
319                         }
320                         ## Make sure there are no leftovers.
321                         if(@values) {
322                                 return;
323                         } elsif(@type_constraints) {
324                                 return;
325                         }else {
326                                 return 1;
327                         }
328                 }
329         )
330 );
331         
332 Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint(
333         MooseX::Meta::TypeConstraint::Structured->new(
334                 name => "MooseX::Types::Structured::Dict",
335                 parent => find_type_constraint('HashRef'),
336                 constraint_generator=> sub { 
337                         ## Get the constraints and values to check
338             my ($type_constraints, $values) = @_;
339                         my %type_constraints = defined $type_constraints ? @$type_constraints: ();            
340                         my %values = defined $values ? %$values: ();
341                         ## Perform the checking
342                         while(%type_constraints) {
343                                 my($key, $type_constraint) = each %type_constraints;
344                                 delete $type_constraints{$key};
345                                 if(exists $values{$key}) {
346                                         my $value = $values{$key};
347                                         delete $values{$key};
348                                         unless($type_constraint->check($value)) {
349                                                 return;
350                                         }
351                                 } else { 
352                                         unless($type_constraint->check()) {
353                                                 return;
354                                         }
355                                 }
356                         }
357                         ## Make sure there are no leftovers.
358                         if(%values) { 
359                                 return;
360                         } elsif(%type_constraints) {
361                                 return;
362                         }else {
363                                 return 1;
364                         }
365                 },
366         )
367 );
368
369 OPTIONAL: {
370     my $Optional = Moose::Meta::TypeConstraint::Parameterizable->new(
371         name => 'MooseX::Types::Structured::Optional',
372         package_defined_in => __PACKAGE__,
373         parent => find_type_constraint('Item'),
374         constraint => sub { 1 },
375         constraint_generator => sub {
376             my ($type_parameter, @args) = @_;
377             my $check = $type_parameter->_compiled_type_constraint();
378             return sub {
379                 my (@args) = @_;                        
380                 if(exists($args[0])) {
381                     ## If it exists, we need to validate it
382                     $check->($args[0]);
383                 } else {
384                     ## But it's is okay if the value doesn't exists
385                     return 1;
386                 }
387             }
388         }
389     );
390
391     Moose::Util::TypeConstraints::register_type_constraint($Optional);
392     Moose::Util::TypeConstraints::add_parameterizable_type($Optional);
393 }
394
395
396 =head1 SEE ALSO
397
398 The following modules or resources may be of interest.
399
400 L<Moose>, L<MooseX::TypeLibrary>, L<Moose::Meta::TypeConstraint>,
401 L<MooseX::Meta::TypeConstraint::Structured>
402
403 =head1 TODO
404
405 Need to clarify deep coercions, need to clarify subtypes of subtypes.
406
407 =head1 AUTHOR
408
409 John Napiorkowski, C<< <jjnapiork@cpan.org> >>
410
411 =head1 COPYRIGHT & LICENSE
412
413 This program is free software; you can redistribute it and/or modify
414 it under the same terms as Perl itself.
415
416 =cut
417         
418 1;