Commit | Line | Data |
d24da8ec |
1 | package MooseX::Types::Structured; |
2 | |
98336987 |
3 | use 5.008; |
6c2f284c |
4 | use Moose::Util::TypeConstraints; |
a30fa891 |
5 | use MooseX::Meta::TypeConstraint::Structured; |
e327145a |
6 | use MooseX::Types -declare => [qw(Dict Tuple Optional)]; |
011bacc6 |
7 | |
190a34eb |
8 | our $VERSION = '0.06'; |
d24da8ec |
9 | our $AUTHORITY = 'cpan:JJNAPIORK'; |
10 | |
11 | =head1 NAME |
12 | |
af1d00c9 |
13 | MooseX::Types::Structured - Structured Type Constraints for Moose |
d24da8ec |
14 | |
15 | =head1 SYNOPSIS |
16 | |
af1d00c9 |
17 | The following is example usage for this module. |
6c2f284c |
18 | |
af1d00c9 |
19 | package MyApp::MyClass; |
6c2f284c |
20 | |
af1d00c9 |
21 | use Moose; |
22 | use MooseX::Types::Moose qw(Str Int); |
190a34eb |
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 | ); |
af1d00c9 |
33 | |
6c2f284c |
34 | Then you can instantiate this class with something like: |
35 | |
190a34eb |
36 | my $john = MyApp::MyClass->new( |
37 | name => { |
38 | first=>'John', |
39 | middle=>'James' |
40 | last=>'Napiorkowski', |
41 | }, |
42 | ); |
22727dd5 |
43 | |
44 | Or with: |
45 | |
190a34eb |
46 | my $vanessa = MyApp::MyClass->new( |
d87e8b74 |
47 | name => { |
190a34eb |
48 | first=>'Vanessa', |
49 | last=>'Li' |
d87e8b74 |
50 | }, |
51 | ); |
d24da8ec |
52 | |
d87e8b74 |
53 | But all of these would cause a constraint error for the 'name' attribute: |
6c2f284c |
54 | |
d87e8b74 |
55 | MyApp::MyClass->new( name=>'John' ); |
56 | MyApp::MyClass->new( name=>{first_name=>'John'} ); |
57 | MyApp::MyClass->new( name=>{first_name=>'John', age=>39} ); |
190a34eb |
58 | MyApp::MyClass->new( name=>{first=>'Vanessa', middle=>[1,2], last=>'Li'} ); |
59 | |
6c2f284c |
60 | Please see the test cases for more examples. |
d24da8ec |
61 | |
62 | =head1 DESCRIPTION |
63 | |
22727dd5 |
64 | A structured type constraint is a standard container L<Moose> type constraint, |
af1d00c9 |
65 | such as an arrayref or hashref, which has been enhanced to allow you to |
59deb858 |
66 | explicitly name all the allow type constraints inside the structure. The |
af1d00c9 |
67 | generalized form is: |
68 | |
22727dd5 |
69 | TypeConstraint[@TypeParameters|%TypeParameters] |
af1d00c9 |
70 | |
22727dd5 |
71 | Where 'TypeParameters' is an array or hash of L<Moose::Meta::TypeConstraint>. |
af1d00c9 |
72 | |
22727dd5 |
73 | This type library enables structured type constraints. It is built on top of the |
59deb858 |
74 | L<MooseX::Types> library system, so you should review the documentation for that |
75 | if you are not familiar with it. |
76 | |
5632ada1 |
77 | =head2 Comparing Parameterized types to Structured types |
59deb858 |
78 | |
22727dd5 |
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: |
6c2f284c |
83 | |
d87e8b74 |
84 | subtype ArrayOfInts, |
85 | as Arrayref[Int]; |
6c2f284c |
86 | |
af1d00c9 |
87 | which would constraint a value to something like [1,2,3,...] and so on. On the |
22727dd5 |
88 | other hand, a structured type constraint explicitly names all it's allowed |
89 | 'internal' type parameter constraints. For the example: |
6c2f284c |
90 | |
af1d00c9 |
91 | subtype StringFollowedByInt, |
92 | as Tuple[Str,Int]; |
6c2f284c |
93 | |
59deb858 |
94 | would constrain it's value to something like ['hello', 111] but ['hello', 'world'] |
22727dd5 |
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]; |
d87e8b74 |
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, |
22727dd5 |
140 | as Dict[firstname=>Str, lastname=>Str]; |
d87e8b74 |
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']; |
6c2f284c |
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 | |
af1d00c9 |
155 | subtype crazy, |
156 | as Tuple[ |
157 | Int, |
158 | Dict[name=>Str, age=>Int], |
159 | ArrayRef[Int] |
160 | ]; |
6c2f284c |
161 | |
af1d00c9 |
162 | Which would match "[1, {name=>'John', age=>25},[10,11,12]]". Please notice how |
59deb858 |
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 |
6c2f284c |
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 | |
af1d00c9 |
173 | package MyApp::MyStruct; |
174 | use Moose; |
175 | |
22727dd5 |
176 | has $_ for qw(full_name age_in_years); |
af1d00c9 |
177 | |
178 | package MyApp::MyClass; |
179 | use Moose; |
180 | |
181 | has person => (isa=>'MyApp::MyStruct'); |
182 | |
183 | my $instance = MyApp::MyClass->new( |
22727dd5 |
184 | person=>MyApp::MyStruct->new(full_name=>'John', age_in_years=>39), |
af1d00c9 |
185 | ); |
6c2f284c |
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 | |
22727dd5 |
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, |
af1d00c9 |
203 | as 'MyApp::MyStruct'; |
204 | |
22727dd5 |
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}; |
af1d00c9 |
219 | my $age = DateTime->now - $_->{dob}; |
22727dd5 |
220 | MyApp::MyStruct->new( full_name=>$name, age_in_years=>$age->years ); |
af1d00c9 |
221 | }; |
22727dd5 |
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 |
16aea7bf |
228 | |
229 | You need to exercise some care when you try to subtype a structured type |
a4a88fef |
230 | as in this example: |
d24da8ec |
231 | |
af1d00c9 |
232 | subtype Person, |
d87e8b74 |
233 | as Dict[name=>Str, age=>Int]; |
a4a88fef |
234 | |
af1d00c9 |
235 | subtype FriendlyPerson, |
236 | as Person[name=>Str, age=>Int, totalFriends=>Int]; |
a4a88fef |
237 | |
16aea7bf |
238 | This will actually work BUT you have to take care that the subtype has a |
a4a88fef |
239 | structure that does not contradict the structure of it's parent. For now the |
59deb858 |
240 | above works, but I will clarify the syntax for this at a future point, so |
22727dd5 |
241 | it's recommended to avoid (should not really be needed so much anyway). For |
59deb858 |
242 | now this is supported in an EXPERIMENTAL way. Your thoughts, test cases and |
243 | patches are welcomed for discussion. |
16aea7bf |
244 | |
245 | =head2 Coercions |
246 | |
247 | Coercions currently work for 'one level' deep. That is you can do: |
248 | |
af1d00c9 |
249 | subtype Person, |
16aea7bf |
250 | as Dict[name=>Str, age=>Int]; |
af1d00c9 |
251 | |
16aea7bf |
252 | subtype Fullname, |
253 | as Dict[first=>Str, last=>Str]; |
af1d00c9 |
254 | |
255 | coerce Person, |
d87e8b74 |
256 | ## Coerce an object of a particular class |
af1d00c9 |
257 | from BlessedPersonObject, |
258 | via { +{name=>$_->name, age=>$_->age} }, |
d87e8b74 |
259 | ## Coerce from [$name, $age] |
af1d00c9 |
260 | from ArrayRef, |
261 | via { +{name=>$_->[0], age=>$_->[1] }, |
d87e8b74 |
262 | ## Coerce from {fullname=>{first=>...,last=>...}, dob=>$DateTimeObject} |
16aea7bf |
263 | from Dict[fullname=>Fullname, dob=>DateTime], |
264 | via { |
af1d00c9 |
265 | my $age = $_->dob - DateTime->now; |
266 | +{ |
267 | name=> $_->{fullname}->{first} .' '. $_->{fullname}->{last}, |
268 | age=>$age->years |
269 | } |
16aea7bf |
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 | |
22727dd5 |
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. |
16aea7bf |
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 | |
af1d00c9 |
289 | Tuple[Int,Str]; ## Validates [1,'hello'] |
290 | Tuple[Str|Object, Int]; ##Validates ['hello', 1] or [$object, 2] |
16aea7bf |
291 | |
22727dd5 |
292 | =head2 Dict[%constraints] |
16aea7bf |
293 | |
294 | This defines a hashref based constraint which allowed you to validate a specific |
295 | hashref. For example: |
296 | |
af1d00c9 |
297 | Dict[name=>Str, age=>Int]; ## Validates {name=>'John', age=>39} |
d24da8ec |
298 | |
22727dd5 |
299 | =head2 Optional[$constraint] |
190a34eb |
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 | |
59deb858 |
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 | |
a30fa891 |
361 | =cut |
362 | |
67a8bc04 |
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'), |
e327145a |
367 | constraint_generator=> sub { |
67a8bc04 |
368 | ## Get the constraints and values to check |
e327145a |
369 | my ($type_constraints, $values) = @_; |
370 | my @type_constraints = defined $type_constraints ? @$type_constraints: (); |
371 | my @values = defined $values ? @$values: (); |
67a8bc04 |
372 | ## Perform the checking |
373 | while(@type_constraints) { |
374 | my $type_constraint = shift @type_constraints; |
a30fa891 |
375 | if(@values) { |
67a8bc04 |
376 | my $value = shift @values; |
377 | unless($type_constraint->check($value)) { |
378 | return; |
379 | } |
380 | } else { |
190a34eb |
381 | unless($type_constraint->check()) { |
382 | return; |
383 | } |
a30fa891 |
384 | } |
385 | } |
67a8bc04 |
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'), |
e327145a |
402 | constraint_generator=> sub { |
67a8bc04 |
403 | ## Get the constraints and values to check |
e327145a |
404 | my ($type_constraints, $values) = @_; |
405 | my %type_constraints = defined $type_constraints ? @$type_constraints: (); |
406 | my %values = defined $values ? %$values: (); |
67a8bc04 |
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)) { |
a30fa891 |
415 | return; |
416 | } |
e327145a |
417 | } else { |
190a34eb |
418 | unless($type_constraint->check()) { |
419 | return; |
420 | } |
a30fa891 |
421 | } |
67a8bc04 |
422 | } |
423 | ## Make sure there are no leftovers. |
e327145a |
424 | if(%values) { |
67a8bc04 |
425 | return; |
426 | } elsif(%type_constraints) { |
427 | return; |
428 | }else { |
429 | return 1; |
430 | } |
431 | }, |
432 | ) |
433 | ); |
d24da8ec |
434 | |
e327145a |
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 | |
d24da8ec |
462 | =head1 SEE ALSO |
463 | |
464 | The following modules or resources may be of interest. |
465 | |
22727dd5 |
466 | L<Moose>, L<MooseX::Types>, L<Moose::Meta::TypeConstraint>, |
a30fa891 |
467 | L<MooseX::Meta::TypeConstraint::Structured> |
d24da8ec |
468 | |
16aea7bf |
469 | =head1 TODO |
470 | |
471 | Need to clarify deep coercions, need to clarify subtypes of subtypes. |
472 | |
d24da8ec |
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 |
67a8bc04 |
483 | |
484 | 1; |