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