Commit | Line | Data |
d24da8ec |
1 | package MooseX::Types::Structured; |
8b810e83 |
2 | # ABSTRACT: Structured Type Constraints for Moose |
d24da8ec |
3 | |
98336987 |
4 | use 5.008; |
c116e19a |
5 | |
8dbdca20 |
6 | use Moose::Util::TypeConstraints 1.06; |
a30fa891 |
7 | use MooseX::Meta::TypeConstraint::Structured; |
abd193e2 |
8 | use MooseX::Meta::TypeConstraint::Structured::Optional; |
2f8e2a40 |
9 | use MooseX::Types::Structured::OverflowHandler; |
9448ea2c |
10 | use MooseX::Types::Structured::MessageStack; |
8dbdca20 |
11 | use MooseX::Types 0.22 -declare => [qw(Dict Map Tuple Optional)]; |
12 | use Sub::Exporter 0.982 -setup => [ qw(Dict Map Tuple Optional slurpy) ]; |
fe5e8860 |
13 | use Devel::PartialDump 0.13; |
2f8e2a40 |
14 | use Scalar::Util qw(blessed); |
011bacc6 |
15 | |
d24da8ec |
16 | =head1 SYNOPSIS |
17 | |
af1d00c9 |
18 | The following is example usage for this module. |
6c2f284c |
19 | |
07a8693b |
20 | package Person; |
46e0d91a |
21 | |
af1d00c9 |
22 | use Moose; |
07a8693b |
23 | use MooseX::Types::Moose qw(Str Int HashRef); |
24 | use MooseX::Types::Structured qw(Dict Tuple Optional); |
190a34eb |
25 | |
26 | ## A name has a first and last part, but middle names are not required |
27 | has name => ( |
28 | isa=>Dict[ |
07a8693b |
29 | first => Str, |
30 | last => Str, |
31 | middle => Optional[Str], |
190a34eb |
32 | ], |
33 | ); |
46e0d91a |
34 | |
07a8693b |
35 | ## description is a string field followed by a HashRef of tagged data. |
36 | has description => ( |
37 | isa=>Tuple[ |
38 | Str, |
39 | Optional[HashRef], |
40 | ], |
41 | ); |
af1d00c9 |
42 | |
8dbdca20 |
43 | ## Remainder of your class attributes and methods |
7caf630f |
44 | |
6c2f284c |
45 | Then you can instantiate this class with something like: |
46 | |
07a8693b |
47 | my $john = Person->new( |
190a34eb |
48 | name => { |
07a8693b |
49 | first => 'John', |
50 | middle => 'James' |
51 | last => 'Napiorkowski', |
190a34eb |
52 | }, |
07a8693b |
53 | description => [ |
54 | 'A cool guy who loves Perl and Moose.', { |
55 | married_to => 'Vanessa Li', |
56 | born_in => 'USA', |
57 | }; |
58 | ] |
190a34eb |
59 | ); |
22727dd5 |
60 | |
61 | Or with: |
62 | |
07a8693b |
63 | my $vanessa = Person->new( |
d87e8b74 |
64 | name => { |
07a8693b |
65 | first => 'Vanessa', |
66 | last => 'Li' |
d87e8b74 |
67 | }, |
07a8693b |
68 | description => ['A great student!'], |
d87e8b74 |
69 | ); |
d24da8ec |
70 | |
8b810e83 |
71 | But all of these would cause a constraint error for the C<name> attribute: |
6c2f284c |
72 | |
07a8693b |
73 | ## Value for 'name' not a HashRef |
74 | Person->new( name => 'John' ); |
46e0d91a |
75 | |
07a8693b |
76 | ## Value for 'name' has incorrect hash key and missing required keys |
77 | Person->new( name => { |
78 | first_name => 'John' |
79 | }); |
46e0d91a |
80 | |
07a8693b |
81 | ## Also incorrect keys |
82 | Person->new( name => { |
83 | first_name => 'John', |
84 | age => 39, |
85 | }); |
46e0d91a |
86 | |
07a8693b |
87 | ## key 'middle' incorrect type, should be a Str not a ArrayRef |
88 | Person->new( name => { |
89 | first => 'Vanessa', |
90 | middle => [1,2], |
91 | last => 'Li', |
46e0d91a |
92 | }); |
07a8693b |
93 | |
8b810e83 |
94 | And these would cause a constraint error for the C<description> attribute: |
07a8693b |
95 | |
96 | ## Should be an ArrayRef |
97 | Person->new( description => 'Hello I am a String' ); |
46e0d91a |
98 | |
07a8693b |
99 | ## First element must be a string not a HashRef. |
100 | Person->new (description => [{ |
101 | tag1 => 'value1', |
102 | tag2 => 'value2' |
103 | }]); |
104 | |
6c2f284c |
105 | Please see the test cases for more examples. |
d24da8ec |
106 | |
107 | =head1 DESCRIPTION |
108 | |
22727dd5 |
109 | A structured type constraint is a standard container L<Moose> type constraint, |
8b810e83 |
110 | such as an C<ArrayRef> or C<HashRef>, which has been enhanced to allow you to |
07a8693b |
111 | explicitly name all the allowed type constraints inside the structure. The |
af1d00c9 |
112 | generalized form is: |
113 | |
07a8693b |
114 | TypeConstraint[@TypeParameters or %TypeParameters] |
af1d00c9 |
115 | |
8b810e83 |
116 | Where C<TypeParameters> is an array reference or hash references of |
c6fece89 |
117 | L<Moose::Meta::TypeConstraint> objects. |
af1d00c9 |
118 | |
22727dd5 |
119 | This type library enables structured type constraints. It is built on top of the |
59deb858 |
120 | L<MooseX::Types> library system, so you should review the documentation for that |
121 | if you are not familiar with it. |
122 | |
5632ada1 |
123 | =head2 Comparing Parameterized types to Structured types |
59deb858 |
124 | |
22727dd5 |
125 | Parameterized constraints are built into core Moose and you are probably already |
8b810e83 |
126 | familiar with the type constraints C<HashRef> and C<ArrayRef>. Structured types |
07a8693b |
127 | have similar functionality, so their syntax is likewise similar. For example, |
22727dd5 |
128 | you could define a parameterized constraint like: |
6c2f284c |
129 | |
d87e8b74 |
130 | subtype ArrayOfInts, |
0e5e997c |
131 | as ArrayRef[Int]; |
6c2f284c |
132 | |
c6fece89 |
133 | which would constrain a value to something like [1,2,3,...] and so on. On the |
22727dd5 |
134 | other hand, a structured type constraint explicitly names all it's allowed |
135 | 'internal' type parameter constraints. For the example: |
6c2f284c |
136 | |
af1d00c9 |
137 | subtype StringFollowedByInt, |
138 | as Tuple[Str,Int]; |
46e0d91a |
139 | |
8b810e83 |
140 | would constrain its value to things like C<< ['hello', 111] >> but C<< ['hello', 'world'] >> |
141 | would fail, as well as C<< ['hello', 111, 'world'] >> and so on. Here's another |
22727dd5 |
142 | example: |
143 | |
7caf630f |
144 | package MyApp::Types; |
145 | |
146 | use MooseX::Types -declare [qw(StringIntOptionalHashRef)]; |
147 | use MooseX::Types::Moose qw(Str Int); |
8dbdca20 |
148 | use MooseX::Types::Structured qw(Tuple Optional); |
7caf630f |
149 | |
22727dd5 |
150 | subtype StringIntOptionalHashRef, |
151 | as Tuple[ |
152 | Str, Int, |
153 | Optional[HashRef] |
154 | ]; |
46e0d91a |
155 | |
22727dd5 |
156 | This defines a type constraint that validates values like: |
157 | |
07a8693b |
158 | ['Hello', 100, {key1 => 'value1', key2 => 'value2'}]; |
22727dd5 |
159 | ['World', 200]; |
46e0d91a |
160 | |
22727dd5 |
161 | Notice that the last type constraint in the structure is optional. This is |
8b810e83 |
162 | enabled via the helper C<Optional> type constraint, which is a variation of the |
163 | core Moose type constraint C<Maybe>. The main difference is that C<Optional> type |
164 | constraints are required to validate if they exist, while C<Maybe> permits |
c6fece89 |
165 | undefined values. So the following example would not validate: |
22727dd5 |
166 | |
167 | StringIntOptionalHashRef->validate(['Hello Undefined', 1000, undef]); |
46e0d91a |
168 | |
22727dd5 |
169 | Please note the subtle difference between undefined and null. If you wish to |
8b810e83 |
170 | allow both null and undefined, you should use the core Moose C<Maybe> type |
07a8693b |
171 | constraint instead: |
22727dd5 |
172 | |
8dbdca20 |
173 | package MyApp::Types; |
7caf630f |
174 | |
c6fece89 |
175 | use MooseX::Types -declare [qw(StringIntMaybeHashRef)]; |
7caf630f |
176 | use MooseX::Types::Moose qw(Str Int Maybe); |
22727dd5 |
177 | use MooseX::Types::Structured qw(Tuple); |
178 | |
c6fece89 |
179 | subtype StringIntMaybeHashRef, |
22727dd5 |
180 | as Tuple[ |
181 | Str, Int, Maybe[HashRef] |
182 | ]; |
183 | |
184 | This would validate the following: |
185 | |
07a8693b |
186 | ['Hello', 100, {key1 => 'value1', key2 => 'value2'}]; |
46e0d91a |
187 | ['World', 200, undef]; |
22727dd5 |
188 | ['World', 200]; |
d87e8b74 |
189 | |
c6fece89 |
190 | Structured constraints are not limited to arrays. You can define a structure |
8b810e83 |
191 | against a C<HashRef> with the C<Dict> type constraint as in this example: |
d87e8b74 |
192 | |
193 | subtype FirstNameLastName, |
07a8693b |
194 | as Dict[ |
195 | firstname => Str, |
196 | lastname => Str, |
197 | ]; |
d87e8b74 |
198 | |
8b810e83 |
199 | This would constrain a C<HashRef> that validates something like: |
d87e8b74 |
200 | |
7caf630f |
201 | {firstname => 'Christopher', lastname => 'Parsons'}; |
46e0d91a |
202 | |
d87e8b74 |
203 | but all the following would fail validation: |
204 | |
07a8693b |
205 | ## Incorrect keys |
206 | {first => 'Christopher', last => 'Parsons'}; |
46e0d91a |
207 | |
07a8693b |
208 | ## Too many keys |
209 | {firstname => 'Christopher', lastname => 'Parsons', middlename => 'Allen'}; |
46e0d91a |
210 | |
07a8693b |
211 | ## Not a HashRef |
46e0d91a |
212 | ['Christopher', 'Parsons']; |
6c2f284c |
213 | |
214 | These structures can be as simple or elaborate as you wish. You can even |
215 | combine various structured, parameterized and simple constraints all together: |
216 | |
c6fece89 |
217 | subtype Crazy, |
af1d00c9 |
218 | as Tuple[ |
219 | Int, |
220 | Dict[name=>Str, age=>Int], |
221 | ArrayRef[Int] |
222 | ]; |
46e0d91a |
223 | |
7caf630f |
224 | Which would match: |
225 | |
8dbdca20 |
226 | [1, {name=>'John', age=>25},[10,11,12]]; |
7caf630f |
227 | |
228 | Please notice how the type parameters can be visually arranged to your liking |
46e0d91a |
229 | and to improve the clarity of your meaning. You don't need to run then |
8b810e83 |
230 | altogether onto a single line. Additionally, since the C<Dict> type constraint |
7caf630f |
231 | defines a hash constraint, the key order is not meaningful. For example: |
232 | |
8dbdca20 |
233 | subtype AnyKeyOrder, |
234 | as Dict[ |
235 | key1=>Int, |
236 | key2=>Str, |
237 | key3=>Int, |
238 | ]; |
7caf630f |
239 | |
240 | Would validate both: |
241 | |
8dbdca20 |
242 | {key1 => 1, key2 => "Hi!", key3 => 2}; |
243 | {key2 => "Hi!", key1 => 100, key3 => 300}; |
7caf630f |
244 | |
8b810e83 |
245 | As you would expect, since underneath it's just a plain old Perl hash at work. |
59deb858 |
246 | |
247 | =head2 Alternatives |
6c2f284c |
248 | |
249 | You should exercise some care as to whether or not your complex structured |
250 | constraints would be better off contained by a real object as in the following |
251 | example: |
252 | |
af1d00c9 |
253 | package MyApp::MyStruct; |
254 | use Moose; |
46e0d91a |
255 | |
07a8693b |
256 | ## lazy way to make a bunch of attributes |
22727dd5 |
257 | has $_ for qw(full_name age_in_years); |
46e0d91a |
258 | |
af1d00c9 |
259 | package MyApp::MyClass; |
260 | use Moose; |
46e0d91a |
261 | |
262 | has person => (isa => 'MyApp::MyStruct'); |
263 | |
af1d00c9 |
264 | my $instance = MyApp::MyClass->new( |
07a8693b |
265 | person=>MyApp::MyStruct->new( |
266 | full_name => 'John', |
c6fece89 |
267 | age_in_years => 39, |
07a8693b |
268 | ), |
af1d00c9 |
269 | ); |
46e0d91a |
270 | |
8b810e83 |
271 | This method may take some additional time to set up but will give you more |
6c2f284c |
272 | flexibility. However, structured constraints are highly compatible with this |
273 | method, granting some interesting possibilities for coercion. Try: |
274 | |
07a8693b |
275 | package MyApp::MyClass; |
46e0d91a |
276 | |
07a8693b |
277 | use Moose; |
22727dd5 |
278 | use MyApp::MyStruct; |
46e0d91a |
279 | |
07a8693b |
280 | ## It's recommended your type declarations live in a separate class in order |
281 | ## to promote reusability and clarity. Inlined here for brevity. |
46e0d91a |
282 | |
22727dd5 |
283 | use MooseX::Types::DateTime qw(DateTime); |
284 | use MooseX::Types -declare [qw(MyStruct)]; |
285 | use MooseX::Types::Moose qw(Str Int); |
286 | use MooseX::Types::Structured qw(Dict); |
287 | |
288 | ## Use class_type to create an ISA type constraint if your object doesn't |
289 | ## inherit from Moose::Object. |
290 | class_type 'MyApp::MyStruct'; |
291 | |
292 | ## Just a shorter version really. |
293 | subtype MyStruct, |
af1d00c9 |
294 | as 'MyApp::MyStruct'; |
46e0d91a |
295 | |
22727dd5 |
296 | ## Add the coercions. |
297 | coerce MyStruct, |
298 | from Dict[ |
299 | full_name=>Str, |
300 | age_in_years=>Int |
301 | ], via { |
302 | MyApp::MyStruct->new(%$_); |
303 | }, |
304 | from Dict[ |
305 | lastname=>Str, |
306 | firstname=>Str, |
307 | dob=>DateTime |
308 | ], via { |
309 | my $name = $_->{firstname} .' '. $_->{lastname}; |
af1d00c9 |
310 | my $age = DateTime->now - $_->{dob}; |
46e0d91a |
311 | |
07a8693b |
312 | MyApp::MyStruct->new( |
313 | full_name=>$name, |
314 | age_in_years=>$age->years, |
315 | ); |
af1d00c9 |
316 | }; |
46e0d91a |
317 | |
318 | has person => (isa=>MyStruct); |
319 | |
07a8693b |
320 | This would allow you to instantiate with something like: |
321 | |
322 | my $obj = MyApp::MyClass->new( person => { |
323 | full_name=>'John Napiorkowski', |
324 | age_in_years=>39, |
325 | }); |
46e0d91a |
326 | |
07a8693b |
327 | Or even: |
328 | |
329 | my $obj = MyApp::MyClass->new( person => { |
330 | lastname=>'John', |
331 | firstname=>'Napiorkowski', |
332 | dob=>DateTime->new(year=>1969), |
333 | }); |
22727dd5 |
334 | |
335 | If you are not familiar with how coercions work, check out the L<Moose> cookbook |
336 | entry L<Moose::Cookbook::Recipe5> for an explanation. The section L</Coercions> |
337 | has additional examples and discussion. |
338 | |
8b810e83 |
339 | =for stopwords Subtyping |
340 | |
22727dd5 |
341 | =head2 Subtyping a Structured type constraint |
16aea7bf |
342 | |
07a8693b |
343 | You need to exercise some care when you try to subtype a structured type as in |
344 | this example: |
d24da8ec |
345 | |
af1d00c9 |
346 | subtype Person, |
07a8693b |
347 | as Dict[name => Str]; |
46e0d91a |
348 | |
af1d00c9 |
349 | subtype FriendlyPerson, |
07a8693b |
350 | as Person[ |
351 | name => Str, |
352 | total_friends => Int, |
353 | ]; |
46e0d91a |
354 | |
16aea7bf |
355 | This will actually work BUT you have to take care that the subtype has a |
a4a88fef |
356 | structure that does not contradict the structure of it's parent. For now the |
59deb858 |
357 | above works, but I will clarify the syntax for this at a future point, so |
22727dd5 |
358 | it's recommended to avoid (should not really be needed so much anyway). For |
59deb858 |
359 | now this is supported in an EXPERIMENTAL way. Your thoughts, test cases and |
07a8693b |
360 | patches are welcomed for discussion. If you find a good use for this, please |
361 | let me know. |
16aea7bf |
362 | |
363 | =head2 Coercions |
364 | |
365 | Coercions currently work for 'one level' deep. That is you can do: |
366 | |
af1d00c9 |
367 | subtype Person, |
07a8693b |
368 | as Dict[ |
369 | name => Str, |
370 | age => Int |
371 | ]; |
46e0d91a |
372 | |
16aea7bf |
373 | subtype Fullname, |
07a8693b |
374 | as Dict[ |
375 | first => Str, |
376 | last => Str |
377 | ]; |
46e0d91a |
378 | |
af1d00c9 |
379 | coerce Person, |
d87e8b74 |
380 | ## Coerce an object of a particular class |
07a8693b |
381 | from BlessedPersonObject, via { |
382 | +{ |
383 | name=>$_->name, |
384 | age=>$_->age, |
385 | }; |
386 | }, |
46e0d91a |
387 | |
d87e8b74 |
388 | ## Coerce from [$name, $age] |
07a8693b |
389 | from ArrayRef, via { |
390 | +{ |
391 | name=>$_->[0], |
392 | age=>$_->[1], |
393 | }, |
394 | }, |
d87e8b74 |
395 | ## Coerce from {fullname=>{first=>...,last=>...}, dob=>$DateTimeObject} |
07a8693b |
396 | from Dict[fullname=>Fullname, dob=>DateTime], via { |
af1d00c9 |
397 | my $age = $_->dob - DateTime->now; |
07a8693b |
398 | my $firstn = $_->{fullname}->{first}; |
399 | my $lastn = $_->{fullname}->{last} |
af1d00c9 |
400 | +{ |
07a8693b |
401 | name => $_->{fullname}->{first} .' '. , |
402 | age =>$age->years |
af1d00c9 |
403 | } |
16aea7bf |
404 | }; |
46e0d91a |
405 | |
16aea7bf |
406 | And that should just work as expected. However, if there are any 'inner' |
8b810e83 |
407 | coercions, such as a coercion on C<Fullname> or on C<DateTime>, that coercion |
16aea7bf |
408 | won't currently get activated. |
409 | |
8b810e83 |
410 | Please see the test F<07-coerce.t> for a more detailed example. Discussion on |
22727dd5 |
411 | extending coercions to support this welcome on the Moose development channel or |
412 | mailing list. |
16aea7bf |
413 | |
c6fece89 |
414 | =head2 Recursion |
415 | |
416 | Newer versions of L<MooseX::Types> support recursive type constraints. That is |
417 | you can include a type constraint as a contained type constraint of itself. For |
418 | example: |
419 | |
8dbdca20 |
420 | subtype Person, |
421 | as Dict[ |
422 | name=>Str, |
423 | friends=>Optional[ |
424 | ArrayRef[Person] |
425 | ], |
426 | ]; |
46e0d91a |
427 | |
8b810e83 |
428 | This would declare a C<Person> subtype that contains a name and an optional |
429 | C<ArrayRef> of C<Person>s who are friends as in: |
c6fece89 |
430 | |
8dbdca20 |
431 | { |
432 | name => 'Mike', |
433 | friends => [ |
434 | { name => 'John' }, |
435 | { name => 'Vincent' }, |
436 | { |
437 | name => 'Tracey', |
438 | friends => [ |
439 | { name => 'Stephenie' }, |
440 | { name => 'Ilya' }, |
441 | ], |
442 | }, |
443 | ], |
444 | }; |
c6fece89 |
445 | |
8b810e83 |
446 | Please take care to make sure the recursion node is either C<Optional>, or declare |
447 | a union with an non-recursive option such as: |
c6fece89 |
448 | |
8dbdca20 |
449 | subtype Value |
450 | as Tuple[ |
451 | Str, |
452 | Str|Tuple, |
453 | ]; |
46e0d91a |
454 | |
c6fece89 |
455 | Which validates: |
456 | |
8dbdca20 |
457 | [ |
458 | 'Hello', [ |
459 | 'World', [ |
460 | 'Is', [ |
461 | 'Getting', |
462 | 'Old', |
463 | ], |
464 | ], |
465 | ], |
466 | ]; |
c6fece89 |
467 | |
8b810e83 |
468 | Otherwise you will define a subtype that is impossible to validate since it is |
c6fece89 |
469 | infinitely recursive. For more information about defining recursive types, |
470 | please see the documentation in L<MooseX::Types> and the test cases. |
471 | |
16aea7bf |
472 | =head1 TYPE CONSTRAINTS |
473 | |
474 | This type library defines the following constraints. |
475 | |
476 | =head2 Tuple[@constraints] |
477 | |
07a8693b |
478 | This defines an ArrayRef based constraint which allows you to validate a specific |
479 | list of contained constraints. For example: |
16aea7bf |
480 | |
af1d00c9 |
481 | Tuple[Int,Str]; ## Validates [1,'hello'] |
c6fece89 |
482 | Tuple[Str|Object, Int]; ## Validates ['hello', 1] or [$object, 2] |
16aea7bf |
483 | |
7caf630f |
484 | The Values of @constraints should ideally be L<MooseX::Types> declared type |
485 | constraints. We do support 'old style' L<Moose> string based constraints to a |
486 | limited degree but these string type constraints are considered deprecated. |
46e0d91a |
487 | There will be limited support for bugs resulting from mixing string and |
7caf630f |
488 | L<MooseX::Types> in your structures. If you encounter such a bug and really |
489 | need it fixed, we will required a detailed test case at the minimum. |
490 | |
22727dd5 |
491 | =head2 Dict[%constraints] |
16aea7bf |
492 | |
07a8693b |
493 | This defines a HashRef based constraint which allowed you to validate a specific |
16aea7bf |
494 | hashref. For example: |
495 | |
af1d00c9 |
496 | Dict[name=>Str, age=>Int]; ## Validates {name=>'John', age=>39} |
d24da8ec |
497 | |
8b810e83 |
498 | The keys in C<%constraints> follow the same rules as C<@constraints> in the above |
7caf630f |
499 | section. |
500 | |
249d5425 |
501 | =head2 Map[ $key_constraint, $value_constraint ] |
502 | |
8b810e83 |
503 | This defines a C<HashRef>-based constraint in which both the keys and values are |
249d5425 |
504 | required to meet certain constraints. For example, to map hostnames to IP |
505 | addresses, you might say: |
506 | |
507 | Map[ HostName, IPAddress ] |
508 | |
8b810e83 |
509 | The type constraint would only be met if every key was a valid C<HostName> and |
510 | every value was a valid C<IPAddress>. |
249d5425 |
511 | |
22727dd5 |
512 | =head2 Optional[$constraint] |
190a34eb |
513 | |
8b810e83 |
514 | This is primarily a helper constraint for C<Dict> and C<Tuple> type constraints. What |
7caf630f |
515 | this allows is for you to assert that a given type constraint is allowed to be |
190a34eb |
516 | null (but NOT undefined). If the value is null, then the type constraint passes |
517 | but if the value is defined it must validate against the type constraint. This |
518 | makes it easy to make a Dict where one or more of the keys doesn't have to exist |
519 | or a tuple where some of the values are not required. For example: |
520 | |
521 | subtype Name() => as Dict[ |
522 | first=>Str, |
523 | last=>Str, |
524 | middle=>Optional[Str], |
525 | ]; |
46e0d91a |
526 | |
8b810e83 |
527 | ...creates a constraint that validates against a hashref with the keys 'first' and |
190a34eb |
528 | 'last' being strings and required while an optional key 'middle' is must be a |
529 | string if it appears but doesn't have to appear. So in this case both the |
530 | following are valid: |
531 | |
532 | {first=>'John', middle=>'James', last=>'Napiorkowski'} |
533 | {first=>'Vanessa', last=>'Li'} |
52ffe972 |
534 | |
8b810e83 |
535 | If you use the C<Maybe> type constraint instead, your values will also validate |
536 | against C<undef>, which may be incorrect for you. |
7caf630f |
537 | |
52ffe972 |
538 | =head1 EXPORTABLE SUBROUTINES |
539 | |
540 | This type library makes available for export the following subroutines |
541 | |
8b810e83 |
542 | =for stopwords slurpy |
543 | |
52ffe972 |
544 | =head2 slurpy |
545 | |
546 | Structured type constraints by their nature are closed; that is validation will |
7559b71f |
547 | depend on an exact match between your structure definition and the arguments to |
52ffe972 |
548 | be checked. Sometimes you might wish for a slightly looser amount of validation. |
549 | For example, you may wish to validate the first 3 elements of an array reference |
550 | and allow for an arbitrary number of additional elements. At first thought you |
551 | might think you could do it this way: |
552 | |
553 | # I want to validate stuff like: [1,"hello", $obj, 2,3,4,5,6,...] |
554 | subtype AllowTailingArgs, |
555 | as Tuple[ |
556 | Int, |
557 | Str, |
558 | Object, |
559 | ArrayRef[Int], |
560 | ]; |
561 | |
562 | However what this will actually validate are structures like this: |
563 | |
564 | [10,"Hello", $obj, [11,12,13,...] ]; # Notice element 4 is an ArrayRef |
565 | |
566 | In order to allow structured validation of, "and then some", arguments, you can |
a59fe2a6 |
567 | use the L</slurpy> method against a type constraint. For example: |
52ffe972 |
568 | |
569 | use MooseX::Types::Structured qw(Tuple slurpy); |
46e0d91a |
570 | |
52ffe972 |
571 | subtype AllowTailingArgs, |
572 | as Tuple[ |
573 | Int, |
574 | Str, |
575 | Object, |
576 | slurpy ArrayRef[Int], |
577 | ]; |
578 | |
579 | This will now work as expected, validating ArrayRef structures such as: |
580 | |
581 | [1,"hello", $obj, 2,3,4,5,6,...] |
46e0d91a |
582 | |
52ffe972 |
583 | A few caveats apply. First, the slurpy type constraint must be the last one in |
584 | the list of type constraint parameters. Second, the parent type of the slurpy |
585 | type constraint must match that of the containing type constraint. That means |
8b810e83 |
586 | that a C<Tuple> can allow a slurpy C<ArrayRef> (or children of C<ArrayRef>s, including |
587 | another C<Tuple>) and a C<Dict> can allow a slurpy C<HashRef> (or children/subtypes of |
588 | HashRef, also including other C<Dict> constraints). |
52ffe972 |
589 | |
8b810e83 |
590 | Please note the technical way this works 'under the hood' is that the |
a59fe2a6 |
591 | slurpy keyword transforms the target type constraint into a coderef. Please do |
52ffe972 |
592 | not try to create your own custom coderefs; always use the slurpy method. The |
593 | underlying technology may change in the future but the slurpy keyword will be |
594 | supported. |
595 | |
7559b71f |
596 | =head1 ERROR MESSAGES |
597 | |
598 | Error reporting has been improved to return more useful debugging messages. Now |
599 | I will stringify the incoming check value with L<Devel::PartialDump> so that you |
600 | can see the actual structure that is tripping up validation. Also, I report the |
601 | 'internal' validation error, so that if a particular element inside the |
602 | Structured Type is failing validation, you will see that. There's a limit to |
603 | how deep this internal reporting goes, but you shouldn't see any of the "failed |
604 | with ARRAY(XXXXXX)" that we got with earlier versions of this module. |
605 | |
606 | This support is continuing to expand, so it's best to use these messages for |
607 | debugging purposes and not for creating messages that 'escape into the wild' |
608 | such as error messages sent to the user. |
609 | |
610 | Please see the test '12-error.t' for a more lengthy example. Your thoughts and |
611 | preferable tests or code patches very welcome! |
612 | |
59deb858 |
613 | =head1 EXAMPLES |
614 | |
615 | Here are some additional example usage for structured types. All examples can |
616 | be found also in the 't/examples.t' test. Your contributions are also welcomed. |
617 | |
618 | =head2 Normalize a HashRef |
619 | |
620 | You need a hashref to conform to a canonical structure but are required accept a |
8b810e83 |
621 | bunch of different incoming structures. You can normalize using the C<Dict> type |
59deb858 |
622 | constraint and coercions. This example also shows structured types mixed which |
8b810e83 |
623 | other L<MooseX::Types> libraries. |
59deb858 |
624 | |
625 | package Test::MooseX::Meta::TypeConstraint::Structured::Examples::Normalize; |
46e0d91a |
626 | |
59deb858 |
627 | use Moose; |
628 | use DateTime; |
46e0d91a |
629 | |
59deb858 |
630 | use MooseX::Types::Structured qw(Dict Tuple); |
631 | use MooseX::Types::DateTime qw(DateTime); |
632 | use MooseX::Types::Moose qw(Int Str Object); |
633 | use MooseX::Types -declare => [qw(Name Age Person)]; |
46e0d91a |
634 | |
59deb858 |
635 | subtype Person, |
c6fece89 |
636 | as Dict[ |
8dbdca20 |
637 | name=>Str, |
638 | age=>Int, |
c6fece89 |
639 | ]; |
46e0d91a |
640 | |
59deb858 |
641 | coerce Person, |
c6fece89 |
642 | from Dict[ |
8dbdca20 |
643 | first=>Str, |
644 | last=>Str, |
645 | years=>Int, |
c6fece89 |
646 | ], via { +{ |
59deb858 |
647 | name => "$_->{first} $_->{last}", |
c6fece89 |
648 | age => $_->{years}, |
59deb858 |
649 | }}, |
c6fece89 |
650 | from Dict[ |
8dbdca20 |
651 | fullname=>Dict[ |
652 | last=>Str, |
653 | first=>Str, |
654 | ], |
655 | dob=>DateTime, |
c6fece89 |
656 | ], |
07a8693b |
657 | ## DateTime needs to be inside of single quotes here to disambiguate the |
658 | ## class package from the DataTime type constraint imported via the |
659 | ## line "use MooseX::Types::DateTime qw(DateTime);" |
59deb858 |
660 | via { +{ |
661 | name => "$_->{fullname}{first} $_->{fullname}{last}", |
662 | age => ($_->{dob} - 'DateTime'->now)->years, |
663 | }}; |
46e0d91a |
664 | |
59deb858 |
665 | has person => (is=>'rw', isa=>Person, coerce=>1); |
46e0d91a |
666 | |
07a8693b |
667 | And now you can instantiate with all the following: |
668 | |
669 | __PACKAGE__->new( |
7559b71f |
670 | person=>{ |
671 | name=>'John Napiorkowski', |
46e0d91a |
672 | age=>39, |
7559b71f |
673 | }, |
07a8693b |
674 | ); |
46e0d91a |
675 | |
07a8693b |
676 | __PACKAGE__->new( |
7559b71f |
677 | person=>{ |
678 | first=>'John', |
679 | last=>'Napiorkowski', |
680 | years=>39, |
681 | }, |
07a8693b |
682 | ); |
46e0d91a |
683 | |
07a8693b |
684 | __PACKAGE__->new( |
7559b71f |
685 | person=>{ |
686 | fullname => { |
687 | first=>'John', |
688 | last=>'Napiorkowski' |
689 | }, |
690 | dob => 'DateTime'->new( |
691 | year=>1969, |
692 | month=>2, |
693 | day=>13 |
46e0d91a |
694 | ), |
07a8693b |
695 | }, |
07a8693b |
696 | ); |
46e0d91a |
697 | |
07a8693b |
698 | This technique is a way to support various ways to instantiate your class in a |
699 | clean and declarative way. |
59deb858 |
700 | |
a30fa891 |
701 | =cut |
702 | |
abd193e2 |
703 | my $Optional = MooseX::Meta::TypeConstraint::Structured::Optional->new( |
b86402a0 |
704 | name => 'MooseX::Types::Structured::Optional', |
705 | package_defined_in => __PACKAGE__, |
706 | parent => find_type_constraint('Item'), |
707 | constraint => sub { 1 }, |
708 | constraint_generator => sub { |
709 | my ($type_parameter, @args) = @_; |
710 | my $check = $type_parameter->_compiled_type_constraint(); |
711 | return sub { |
712 | my (@args) = @_; |
713 | ## Does the arg exist? Something exists if it's a 'real' value |
714 | ## or if it is set to undef. |
715 | if(exists($args[0])) { |
716 | ## If it exists, we need to validate it |
717 | $check->($args[0]); |
718 | } else { |
719 | ## But it's is okay if the value doesn't exists |
720 | return 1; |
721 | } |
722 | } |
723 | } |
724 | ); |
725 | |
a4ae4800 |
726 | my $IsType = sub { |
727 | my ($obj, $type) = @_; |
728 | |
729 | return $obj->can('equals') |
730 | ? $obj->equals($type) |
731 | : undef; |
732 | }; |
733 | |
734 | my $CompiledTC = sub { |
735 | my ($obj) = @_; |
736 | |
737 | my $method = '_compiled_type_constraint'; |
738 | return( |
739 | $obj->$IsType('Any') ? undef |
740 | : $obj->can($method) ? $obj->$method |
741 | : sub { $obj->check(shift) }, |
742 | ); |
743 | }; |
744 | |
b86402a0 |
745 | Moose::Util::TypeConstraints::register_type_constraint($Optional); |
746 | Moose::Util::TypeConstraints::add_parameterizable_type($Optional); |
747 | |
67a8bc04 |
748 | Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint( |
8dbdca20 |
749 | MooseX::Meta::TypeConstraint::Structured->new( |
750 | name => "MooseX::Types::Structured::Tuple" , |
751 | parent => find_type_constraint('ArrayRef'), |
752 | constraint_generator=> sub { |
753 | ## Get the constraints and values to check |
a4ae4800 |
754 | my ($self, $type_constraints) = @_; |
755 | $type_constraints ||= $self->type_constraints; |
8dbdca20 |
756 | my @type_constraints = defined $type_constraints ? |
ff801143 |
757 | @$type_constraints : (); |
46e0d91a |
758 | |
ff801143 |
759 | my $overflow_handler; |
aa4718fe |
760 | if($type_constraints[-1] && blessed $type_constraints[-1] |
2f8e2a40 |
761 | && $type_constraints[-1]->isa('MooseX::Types::Structured::OverflowHandler')) { |
ff801143 |
762 | $overflow_handler = pop @type_constraints; |
763 | } |
46e0d91a |
764 | |
220f2fbb |
765 | my $length = $#type_constraints; |
766 | foreach my $idx (0..$length) { |
767 | unless(blessed $type_constraints[$idx]) { |
768 | ($type_constraints[$idx] = find_type_constraint($type_constraints[$idx])) |
769 | || die "$type_constraints[$idx] is not a registered type"; |
770 | } |
771 | } |
772 | |
a4ae4800 |
773 | my (@checks, @optional, $o_check, $is_compiled); |
774 | return sub { |
775 | my ($values, $err) = @_; |
776 | my @values = defined $values ? @$values : (); |
777 | |
778 | ## initialise on first time run |
779 | unless ($is_compiled) { |
780 | @checks = map { $_->$CompiledTC } @type_constraints; |
781 | @optional = map { $_->is_subtype_of($Optional) } @type_constraints; |
782 | $o_check = $overflow_handler->$CompiledTC |
783 | if $overflow_handler; |
784 | $is_compiled++; |
785 | } |
786 | |
787 | ## Perform the checking |
788 | VALUE: |
789 | for my $type_index (0 .. $#checks) { |
790 | |
791 | my $type_constraint = $checks[ $type_index ]; |
792 | |
793 | if(@values) { |
794 | my $value = shift @values; |
795 | |
796 | next VALUE |
797 | unless $type_constraint; |
798 | |
799 | unless($type_constraint->($value)) { |
800 | if($err) { |
801 | my $message = $type_constraints[ $type_index ]->validate($value,$err); |
802 | $err->add_message({message=>$message,level=>$err->level}); |
803 | } |
804 | return; |
805 | } |
806 | } else { |
807 | ## Test if the TC supports null values |
808 | unless ($optional[ $type_index ]) { |
809 | if($err) { |
810 | my $message = $type_constraints[ $type_index ]->get_message('NULL',$err); |
811 | $err->add_message({message=>$message,level=>$err->level}); |
812 | } |
813 | return; |
21d0e759 |
814 | } |
8dbdca20 |
815 | } |
a4ae4800 |
816 | } |
817 | |
818 | ## Make sure there are no leftovers. |
819 | if(@values) { |
820 | if($overflow_handler) { |
821 | return $o_check->([@values], $err); |
822 | } else { |
823 | if($err) { |
824 | my $message = "More values than Type Constraints!"; |
825 | $err->add_message({message=>$message,level=>$err->level}); |
9448ea2c |
826 | } |
8dbdca20 |
827 | return; |
828 | } |
ff801143 |
829 | } else { |
a4ae4800 |
830 | return 1; |
9448ea2c |
831 | } |
a4ae4800 |
832 | }; |
8dbdca20 |
833 | } |
834 | ) |
67a8bc04 |
835 | ); |
46e0d91a |
836 | |
67a8bc04 |
837 | Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint( |
8dbdca20 |
838 | MooseX::Meta::TypeConstraint::Structured->new( |
839 | name => "MooseX::Types::Structured::Dict", |
840 | parent => find_type_constraint('HashRef'), |
21d0e759 |
841 | constraint_generator => sub { |
8dbdca20 |
842 | ## Get the constraints and values to check |
a4ae4800 |
843 | my ($self, $type_constraints) = @_; |
844 | $type_constraints = $self->type_constraints; |
8dbdca20 |
845 | my @type_constraints = defined $type_constraints ? |
ff801143 |
846 | @$type_constraints : (); |
46e0d91a |
847 | |
ff801143 |
848 | my $overflow_handler; |
aa4718fe |
849 | if($type_constraints[-1] && blessed $type_constraints[-1] |
2f8e2a40 |
850 | && $type_constraints[-1]->isa('MooseX::Types::Structured::OverflowHandler')) { |
ff801143 |
851 | $overflow_handler = pop @type_constraints; |
46e0d91a |
852 | } |
220f2fbb |
853 | my %type_constraints = @type_constraints; |
854 | foreach my $key (keys %type_constraints) { |
855 | unless(blessed $type_constraints{$key}) { |
856 | ($type_constraints{$key} = find_type_constraint($type_constraints{$key})) |
857 | || die "$type_constraints{$key} is not a registered type"; |
858 | } |
859 | } |
a4ae4800 |
860 | |
861 | my (%check, %optional, $o_check, $is_compiled); |
862 | return sub { |
863 | my ($values, $err) = @_; |
864 | my %values = defined $values ? %$values: (); |
865 | |
866 | unless ($is_compiled) { |
867 | %check = map { ($_ => $type_constraints{ $_ }->$CompiledTC) } keys %type_constraints; |
868 | %optional = map { ($_ => $type_constraints{ $_ }->is_subtype_of($Optional)) } keys %type_constraints; |
869 | $o_check = $overflow_handler->$CompiledTC |
870 | if $overflow_handler; |
871 | $is_compiled++; |
872 | } |
873 | |
874 | ## Perform the checking |
875 | KEY: |
876 | for my $key (keys %check) { |
877 | my $type_constraint = $check{ $key }; |
878 | |
879 | if(exists $values{$key}) { |
880 | my $value = $values{$key}; |
881 | delete $values{$key}; |
882 | |
883 | next KEY |
884 | unless $type_constraint; |
885 | |
886 | unless($type_constraint->($value)) { |
887 | if($err) { |
888 | my $message = $type_constraints{ $key }->validate($value,$err); |
889 | $err->add_message({message=>$message,level=>$err->level}); |
890 | } |
891 | return; |
892 | } |
893 | } else { |
894 | ## Test to see if the TC supports null values |
895 | unless ($optional{ $key }) { |
896 | if($err) { |
897 | my $message = $type_constraints{ $key }->get_message('NULL',$err); |
898 | $err->add_message({message=>$message,level=>$err->level}); |
899 | } |
900 | return; |
21d0e759 |
901 | } |
8dbdca20 |
902 | } |
a4ae4800 |
903 | } |
904 | |
905 | ## Make sure there are no leftovers. |
906 | if(%values) { |
907 | if($overflow_handler) { |
908 | return $o_check->(+{%values}); |
909 | } else { |
910 | if($err) { |
911 | my $message = "More values than Type Constraints!"; |
912 | $err->add_message({message=>$message,level=>$err->level}); |
9448ea2c |
913 | } |
8dbdca20 |
914 | return; |
915 | } |
ff801143 |
916 | } else { |
a4ae4800 |
917 | return 1; |
9448ea2c |
918 | } |
8dbdca20 |
919 | } |
920 | }, |
921 | ) |
67a8bc04 |
922 | ); |
d24da8ec |
923 | |
678b4064 |
924 | Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint( |
925 | MooseX::Meta::TypeConstraint::Structured->new( |
926 | name => "MooseX::Types::Structured::Map", |
927 | parent => find_type_constraint('HashRef'), |
46e0d91a |
928 | constraint_generator=> sub { |
678b4064 |
929 | ## Get the constraints and values to check |
a4ae4800 |
930 | my ($self, $type_constraints) = @_; |
931 | $type_constraints = $self->type_constraints; |
678b4064 |
932 | my @constraints = defined $type_constraints ? @$type_constraints : (); |
46e0d91a |
933 | |
678b4064 |
934 | Carp::confess( "too many args for Map type" ) if @constraints > 2; |
935 | |
936 | my ($key_type, $value_type) = @constraints == 2 ? @constraints |
937 | : @constraints == 1 ? (undef, @constraints) |
938 | : (); |
939 | |
a4ae4800 |
940 | my ($key_check, $value_check, $is_compiled); |
941 | return sub { |
942 | my ($values, $err) = @_; |
943 | my %values = defined $values ? %$values: (); |
944 | |
945 | unless ($is_compiled) { |
946 | ($key_check, $value_check) |
947 | = map { $_ ? $_->$CompiledTC : undef } |
948 | $key_type, $value_type; |
949 | $is_compiled++; |
950 | } |
951 | |
952 | ## Perform the checking |
953 | if ($value_check) { |
954 | for my $value (values %$values) { |
955 | unless ($value_check->($value)) { |
956 | if($err) { |
957 | my $message = $value_type->validate($value,$err); |
958 | $err->add_message({message=>$message,level=>$err->level}); |
959 | } |
960 | return; |
961 | } |
9448ea2c |
962 | } |
678b4064 |
963 | } |
a4ae4800 |
964 | if ($key_check) { |
965 | for my $key (keys %$values) { |
966 | unless ($key_check->($key)) { |
967 | if($err) { |
968 | my $message = $key_type->validate($key,$err); |
969 | $err->add_message({message=>$message,level=>$err->level}); |
970 | } |
971 | return; |
972 | } |
9448ea2c |
973 | } |
678b4064 |
974 | } |
678b4064 |
975 | |
a4ae4800 |
976 | return 1; |
977 | }; |
678b4064 |
978 | }, |
979 | ) |
980 | ); |
981 | |
2f8e2a40 |
982 | sub slurpy ($) { |
8dbdca20 |
983 | my ($tc) = @_; |
984 | return MooseX::Types::Structured::OverflowHandler->new( |
2f8e2a40 |
985 | type_constraint => $tc, |
986 | ); |
c116e19a |
987 | } |
e327145a |
988 | |
d24da8ec |
989 | =head1 SEE ALSO |
990 | |
991 | The following modules or resources may be of interest. |
992 | |
22727dd5 |
993 | L<Moose>, L<MooseX::Types>, L<Moose::Meta::TypeConstraint>, |
a30fa891 |
994 | L<MooseX::Meta::TypeConstraint::Structured> |
d24da8ec |
995 | |
d24da8ec |
996 | =cut |
46e0d91a |
997 | |
67a8bc04 |
998 | 1; |