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