more pod updates and clarification
[gitmo/MooseX-Types-Structured.git] / lib / MooseX / Types / Structured.pm
CommitLineData
d24da8ec 1package MooseX::Types::Structured;
2
98336987 3use 5.008;
6c2f284c 4use Moose::Util::TypeConstraints;
a30fa891 5use MooseX::Meta::TypeConstraint::Structured;
e327145a 6use MooseX::Types -declare => [qw(Dict Tuple Optional)];
011bacc6 7
8885cba0 8our $VERSION = '0.07';
d24da8ec 9our $AUTHORITY = 'cpan:JJNAPIORK';
10
11=head1 NAME
12
af1d00c9 13MooseX::Types::Structured - Structured Type Constraints for Moose
d24da8ec 14
15=head1 SYNOPSIS
16
af1d00c9 17The following is example usage for this module.
6c2f284c 18
07a8693b 19 package Person;
6c2f284c 20
af1d00c9 21 use Moose;
07a8693b 22 use MooseX::Types::Moose qw(Str Int HashRef);
23 use MooseX::Types::Structured qw(Dict Tuple Optional);
190a34eb 24
25 ## A name has a first and last part, but middle names are not required
26 has name => (
27 isa=>Dict[
07a8693b 28 first => Str,
29 last => Str,
30 middle => Optional[Str],
190a34eb 31 ],
32 );
07a8693b 33
34 ## description is a string field followed by a HashRef of tagged data.
35 has description => (
36 isa=>Tuple[
37 Str,
38 Optional[HashRef],
39 ],
40 );
af1d00c9 41
6c2f284c 42Then you can instantiate this class with something like:
43
07a8693b 44 my $john = Person->new(
190a34eb 45 name => {
07a8693b 46 first => 'John',
47 middle => 'James'
48 last => 'Napiorkowski',
190a34eb 49 },
07a8693b 50 description => [
51 'A cool guy who loves Perl and Moose.', {
52 married_to => 'Vanessa Li',
53 born_in => 'USA',
54 };
55 ]
190a34eb 56 );
22727dd5 57
58Or with:
59
07a8693b 60 my $vanessa = Person->new(
d87e8b74 61 name => {
07a8693b 62 first => 'Vanessa',
63 last => 'Li'
d87e8b74 64 },
07a8693b 65 description => ['A great student!'],
d87e8b74 66 );
d24da8ec 67
d87e8b74 68But all of these would cause a constraint error for the 'name' attribute:
6c2f284c 69
07a8693b 70 ## Value for 'name' not a HashRef
71 Person->new( name => 'John' );
72
73 ## Value for 'name' has incorrect hash key and missing required keys
74 Person->new( name => {
75 first_name => 'John'
76 });
77
78 ## Also incorrect keys
79 Person->new( name => {
80 first_name => 'John',
81 age => 39,
82 });
83
84 ## key 'middle' incorrect type, should be a Str not a ArrayRef
85 Person->new( name => {
86 first => 'Vanessa',
87 middle => [1,2],
88 last => 'Li',
89 });
90
91And these would cause a constraint error for the 'description' attribute:
92
93 ## Should be an ArrayRef
94 Person->new( description => 'Hello I am a String' );
190a34eb 95
07a8693b 96 ## First element must be a string not a HashRef.
97 Person->new (description => [{
98 tag1 => 'value1',
99 tag2 => 'value2'
100 }]);
101
6c2f284c 102Please see the test cases for more examples.
d24da8ec 103
104=head1 DESCRIPTION
105
22727dd5 106A structured type constraint is a standard container L<Moose> type constraint,
07a8693b 107such as an ArrayRef or HashRef, which has been enhanced to allow you to
108explicitly name all the allowed type constraints inside the structure. The
af1d00c9 109generalized form is:
110
07a8693b 111 TypeConstraint[@TypeParameters or %TypeParameters]
af1d00c9 112
c6fece89 113Where 'TypeParameters' is an array reference or hash references of
114L<Moose::Meta::TypeConstraint> objects.
af1d00c9 115
22727dd5 116This type library enables structured type constraints. It is built on top of the
59deb858 117L<MooseX::Types> library system, so you should review the documentation for that
118if you are not familiar with it.
119
5632ada1 120=head2 Comparing Parameterized types to Structured types
59deb858 121
22727dd5 122Parameterized constraints are built into core Moose and you are probably already
07a8693b 123familar with the type constraints 'HashRef' and 'ArrayRef'. Structured types
124have similar functionality, so their syntax is likewise similar. For example,
22727dd5 125you could define a parameterized constraint like:
6c2f284c 126
d87e8b74 127 subtype ArrayOfInts,
128 as Arrayref[Int];
6c2f284c 129
c6fece89 130which would constrain a value to something like [1,2,3,...] and so on. On the
22727dd5 131other hand, a structured type constraint explicitly names all it's allowed
132'internal' type parameter constraints. For the example:
6c2f284c 133
af1d00c9 134 subtype StringFollowedByInt,
135 as Tuple[Str,Int];
6c2f284c 136
c6fece89 137would constrain it's value to things like ['hello', 111] but ['hello', 'world']
22727dd5 138would fail, as well as ['hello', 111, 'world'] and so on. Here's another
139example:
140
141 subtype StringIntOptionalHashRef,
142 as Tuple[
143 Str, Int,
144 Optional[HashRef]
145 ];
146
147This defines a type constraint that validates values like:
148
07a8693b 149 ['Hello', 100, {key1 => 'value1', key2 => 'value2'}];
22727dd5 150 ['World', 200];
151
152Notice that the last type constraint in the structure is optional. This is
153enabled via the helper Optional type constraint, which is a variation of the
07a8693b 154core Moose type constraint 'Maybe'. The main difference is that Optional type
c6fece89 155constraints are required to validate if they exist, while 'Maybe' permits
156undefined values. So the following example would not validate:
22727dd5 157
158 StringIntOptionalHashRef->validate(['Hello Undefined', 1000, undef]);
159
160Please note the subtle difference between undefined and null. If you wish to
07a8693b 161allow both null and undefined, you should use the core Moose 'Maybe' type
162constraint instead:
22727dd5 163
c6fece89 164 use MooseX::Types -declare [qw(StringIntMaybeHashRef)];
22727dd5 165 use MooseX::Types::Moose qw(Maybe);
166 use MooseX::Types::Structured qw(Tuple);
167
c6fece89 168 subtype StringIntMaybeHashRef,
22727dd5 169 as Tuple[
170 Str, Int, Maybe[HashRef]
171 ];
172
173This would validate the following:
174
07a8693b 175 ['Hello', 100, {key1 => 'value1', key2 => 'value2'}];
22727dd5 176 ['World', 200, undef];
177 ['World', 200];
d87e8b74 178
c6fece89 179Structured constraints are not limited to arrays. You can define a structure
180against a HashRef with 'Dict' as in this example:
d87e8b74 181
182 subtype FirstNameLastName,
07a8693b 183 as Dict[
184 firstname => Str,
185 lastname => Str,
186 ];
d87e8b74 187
07a8693b 188This would constrain a HashRef to something like:
d87e8b74 189
07a8693b 190 {firstname => 'Christopher', lastname= > 'Parsons'};
d87e8b74 191
192but all the following would fail validation:
193
07a8693b 194 ## Incorrect keys
195 {first => 'Christopher', last => 'Parsons'};
196
197 ## Too many keys
198 {firstname => 'Christopher', lastname => 'Parsons', middlename => 'Allen'};
199
200 ## Not a HashRef
201 ['Christopher', 'Christopher'];
6c2f284c 202
203These structures can be as simple or elaborate as you wish. You can even
204combine various structured, parameterized and simple constraints all together:
205
c6fece89 206 subtype Crazy,
af1d00c9 207 as Tuple[
208 Int,
209 Dict[name=>Str, age=>Int],
210 ArrayRef[Int]
211 ];
6c2f284c 212
af1d00c9 213Which would match "[1, {name=>'John', age=>25},[10,11,12]]". Please notice how
59deb858 214the type parameters can be visually arranged to your liking and to improve the
215clarity of your meaning. You don't need to run then altogether onto a single
216line.
217
218=head2 Alternatives
6c2f284c 219
220You should exercise some care as to whether or not your complex structured
221constraints would be better off contained by a real object as in the following
222example:
223
af1d00c9 224 package MyApp::MyStruct;
225 use Moose;
226
07a8693b 227 ## lazy way to make a bunch of attributes
22727dd5 228 has $_ for qw(full_name age_in_years);
af1d00c9 229
230 package MyApp::MyClass;
231 use Moose;
232
07a8693b 233 has person => (isa => 'MyApp::MyStruct');
af1d00c9 234
235 my $instance = MyApp::MyClass->new(
07a8693b 236 person=>MyApp::MyStruct->new(
237 full_name => 'John',
c6fece89 238 age_in_years => 39,
07a8693b 239 ),
af1d00c9 240 );
6c2f284c 241
242This method may take some additional time to setup but will give you more
243flexibility. However, structured constraints are highly compatible with this
244method, granting some interesting possibilities for coercion. Try:
245
07a8693b 246 package MyApp::MyClass;
247
248 use Moose;
22727dd5 249 use MyApp::MyStruct;
07a8693b 250
251 ## It's recommended your type declarations live in a separate class in order
252 ## to promote reusability and clarity. Inlined here for brevity.
253
22727dd5 254 use MooseX::Types::DateTime qw(DateTime);
255 use MooseX::Types -declare [qw(MyStruct)];
256 use MooseX::Types::Moose qw(Str Int);
257 use MooseX::Types::Structured qw(Dict);
258
259 ## Use class_type to create an ISA type constraint if your object doesn't
260 ## inherit from Moose::Object.
261 class_type 'MyApp::MyStruct';
262
263 ## Just a shorter version really.
264 subtype MyStruct,
af1d00c9 265 as 'MyApp::MyStruct';
266
22727dd5 267 ## Add the coercions.
268 coerce MyStruct,
269 from Dict[
270 full_name=>Str,
271 age_in_years=>Int
272 ], via {
273 MyApp::MyStruct->new(%$_);
274 },
275 from Dict[
276 lastname=>Str,
277 firstname=>Str,
278 dob=>DateTime
279 ], via {
280 my $name = $_->{firstname} .' '. $_->{lastname};
af1d00c9 281 my $age = DateTime->now - $_->{dob};
07a8693b 282
283 MyApp::MyStruct->new(
284 full_name=>$name,
285 age_in_years=>$age->years,
286 );
af1d00c9 287 };
07a8693b 288
289 has person => (isa=>MyStruct);
290
291This would allow you to instantiate with something like:
292
293 my $obj = MyApp::MyClass->new( person => {
294 full_name=>'John Napiorkowski',
295 age_in_years=>39,
296 });
297
298Or even:
299
300 my $obj = MyApp::MyClass->new( person => {
301 lastname=>'John',
302 firstname=>'Napiorkowski',
303 dob=>DateTime->new(year=>1969),
304 });
22727dd5 305
306If you are not familiar with how coercions work, check out the L<Moose> cookbook
307entry L<Moose::Cookbook::Recipe5> for an explanation. The section L</Coercions>
308has additional examples and discussion.
309
310=head2 Subtyping a Structured type constraint
16aea7bf 311
07a8693b 312You need to exercise some care when you try to subtype a structured type as in
313this example:
d24da8ec 314
af1d00c9 315 subtype Person,
07a8693b 316 as Dict[name => Str];
a4a88fef 317
af1d00c9 318 subtype FriendlyPerson,
07a8693b 319 as Person[
320 name => Str,
321 total_friends => Int,
322 ];
a4a88fef 323
16aea7bf 324This will actually work BUT you have to take care that the subtype has a
a4a88fef 325structure that does not contradict the structure of it's parent. For now the
59deb858 326above works, but I will clarify the syntax for this at a future point, so
22727dd5 327it's recommended to avoid (should not really be needed so much anyway). For
59deb858 328now this is supported in an EXPERIMENTAL way. Your thoughts, test cases and
07a8693b 329patches are welcomed for discussion. If you find a good use for this, please
330let me know.
16aea7bf 331
332=head2 Coercions
333
334Coercions currently work for 'one level' deep. That is you can do:
335
af1d00c9 336 subtype Person,
07a8693b 337 as Dict[
338 name => Str,
339 age => Int
340 ];
af1d00c9 341
16aea7bf 342 subtype Fullname,
07a8693b 343 as Dict[
344 first => Str,
345 last => Str
346 ];
af1d00c9 347
348 coerce Person,
d87e8b74 349 ## Coerce an object of a particular class
07a8693b 350 from BlessedPersonObject, via {
351 +{
352 name=>$_->name,
353 age=>$_->age,
354 };
355 },
356
d87e8b74 357 ## Coerce from [$name, $age]
07a8693b 358 from ArrayRef, via {
359 +{
360 name=>$_->[0],
361 age=>$_->[1],
362 },
363 },
d87e8b74 364 ## Coerce from {fullname=>{first=>...,last=>...}, dob=>$DateTimeObject}
07a8693b 365 from Dict[fullname=>Fullname, dob=>DateTime], via {
af1d00c9 366 my $age = $_->dob - DateTime->now;
07a8693b 367 my $firstn = $_->{fullname}->{first};
368 my $lastn = $_->{fullname}->{last}
af1d00c9 369 +{
07a8693b 370 name => $_->{fullname}->{first} .' '. ,
371 age =>$age->years
af1d00c9 372 }
16aea7bf 373 };
374
375And that should just work as expected. However, if there are any 'inner'
376coercions, such as a coercion on 'Fullname' or on 'DateTime', that coercion
377won't currently get activated.
378
22727dd5 379Please see the test '07-coerce.t' for a more detailed example. Discussion on
380extending coercions to support this welcome on the Moose development channel or
381mailing list.
16aea7bf 382
c6fece89 383=head2 Recursion
384
385Newer versions of L<MooseX::Types> support recursive type constraints. That is
386you can include a type constraint as a contained type constraint of itself. For
387example:
388
389 subtype Person,
390 as Dict[
391 name=>Str,
392 friends=>Optional[
393 ArrayRef[Person]
394 ],
395 ];
396
397This would declare a Person subtype that contains a name and an optional
398ArrayRef of Persons who are friends as in:
399
400 {
401 name => 'Mike',
402 friends => [
403 { name => 'John' },
404 { name => 'Vincent' },
405 {
406 name => 'Tracey',
407 friends => [
408 { name => 'Stephenie' },
409 { name => 'Ilya' },
410 ],
411 },
412 ],
413 };
414
415Please take care to make sure the recursion node is either Optional, or declare
416a Union with an non recursive option such as:
417
418 subtype Value
419 as Tuple[
420 Str,
421 Str|Tuple,
422 ];
423
424Which validates:
425
426 [
427 'Hello', [
428 'World', [
429 'Is', [
430 'Getting',
431 'Old',
432 ],
433 ],
434 ],
435 ];
436
437Otherwise you will define a subtype thatis impossible to validate since it is
438infinitely recursive. For more information about defining recursive types,
439please see the documentation in L<MooseX::Types> and the test cases.
440
16aea7bf 441=head1 TYPE CONSTRAINTS
442
443This type library defines the following constraints.
444
445=head2 Tuple[@constraints]
446
07a8693b 447This defines an ArrayRef based constraint which allows you to validate a specific
448list of contained constraints. For example:
16aea7bf 449
af1d00c9 450 Tuple[Int,Str]; ## Validates [1,'hello']
c6fece89 451 Tuple[Str|Object, Int]; ## Validates ['hello', 1] or [$object, 2]
16aea7bf 452
22727dd5 453=head2 Dict[%constraints]
16aea7bf 454
07a8693b 455This defines a HashRef based constraint which allowed you to validate a specific
16aea7bf 456hashref. For example:
457
af1d00c9 458 Dict[name=>Str, age=>Int]; ## Validates {name=>'John', age=>39}
d24da8ec 459
22727dd5 460=head2 Optional[$constraint]
190a34eb 461
462This is primarily a helper constraint for Dict and Tuple type constraints. What
463this allows if for you to assert that a given type constraint is allowed to be
464null (but NOT undefined). If the value is null, then the type constraint passes
465but if the value is defined it must validate against the type constraint. This
466makes it easy to make a Dict where one or more of the keys doesn't have to exist
467or a tuple where some of the values are not required. For example:
468
469 subtype Name() => as Dict[
470 first=>Str,
471 last=>Str,
472 middle=>Optional[Str],
473 ];
474
475Creates a constraint that validates against a hashref with the keys 'first' and
476'last' being strings and required while an optional key 'middle' is must be a
477string if it appears but doesn't have to appear. So in this case both the
478following are valid:
479
480 {first=>'John', middle=>'James', last=>'Napiorkowski'}
481 {first=>'Vanessa', last=>'Li'}
482
59deb858 483=head1 EXAMPLES
484
485Here are some additional example usage for structured types. All examples can
486be found also in the 't/examples.t' test. Your contributions are also welcomed.
487
488=head2 Normalize a HashRef
489
490You need a hashref to conform to a canonical structure but are required accept a
491bunch of different incoming structures. You can normalize using the Dict type
492constraint and coercions. This example also shows structured types mixed which
493other MooseX::Types libraries.
494
495 package Test::MooseX::Meta::TypeConstraint::Structured::Examples::Normalize;
496
497 use Moose;
498 use DateTime;
499
500 use MooseX::Types::Structured qw(Dict Tuple);
501 use MooseX::Types::DateTime qw(DateTime);
502 use MooseX::Types::Moose qw(Int Str Object);
503 use MooseX::Types -declare => [qw(Name Age Person)];
504
505 subtype Person,
c6fece89 506 as Dict[
507 name=>Str,
508 age=>Int,
509 ];
59deb858 510
511 coerce Person,
c6fece89 512 from Dict[
513 first=>Str,
514 last=>Str,
515 years=>Int,
516 ], via { +{
59deb858 517 name => "$_->{first} $_->{last}",
c6fece89 518 age => $_->{years},
59deb858 519 }},
c6fece89 520 from Dict[
521 fullname=>Dict[
522 last=>Str,
523 first=>Str,
524 ],
525 dob=>DateTime,
526 ],
07a8693b 527 ## DateTime needs to be inside of single quotes here to disambiguate the
528 ## class package from the DataTime type constraint imported via the
529 ## line "use MooseX::Types::DateTime qw(DateTime);"
59deb858 530 via { +{
531 name => "$_->{fullname}{first} $_->{fullname}{last}",
532 age => ($_->{dob} - 'DateTime'->now)->years,
533 }};
534
535 has person => (is=>'rw', isa=>Person, coerce=>1);
07a8693b 536
537And now you can instantiate with all the following:
538
539 __PACKAGE__->new(
540 name=>'John Napiorkowski',
541 age=>39,
542 );
543
544 __PACKAGE__->new(
545 first=>'John',
546 last=>'Napiorkowski',
547 years=>39,
548 );
549
550 __PACKAGE__->new(
551 fullname => {
552 first=>'John',
553 last=>'Napiorkowski'
554 },
555 dob => 'DateTime'->new(
556 year=>1969,
557 month=>2,
558 day=>13
559 ),
560 );
561
562This technique is a way to support various ways to instantiate your class in a
563clean and declarative way.
59deb858 564
a30fa891 565=cut
566
67a8bc04 567Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint(
568 MooseX::Meta::TypeConstraint::Structured->new(
569 name => "MooseX::Types::Structured::Tuple" ,
570 parent => find_type_constraint('ArrayRef'),
e327145a 571 constraint_generator=> sub {
67a8bc04 572 ## Get the constraints and values to check
e327145a 573 my ($type_constraints, $values) = @_;
07a8693b 574 my @type_constraints = defined $type_constraints ?
575 @$type_constraints : ();
e327145a 576 my @values = defined $values ? @$values: ();
67a8bc04 577 ## Perform the checking
578 while(@type_constraints) {
579 my $type_constraint = shift @type_constraints;
a30fa891 580 if(@values) {
67a8bc04 581 my $value = shift @values;
582 unless($type_constraint->check($value)) {
583 return;
584 }
585 } else {
07a8693b 586 ## Test if the TC supports null values
190a34eb 587 unless($type_constraint->check()) {
588 return;
589 }
a30fa891 590 }
591 }
67a8bc04 592 ## Make sure there are no leftovers.
593 if(@values) {
c6fece89 594 warn "I failed since there were left over values";
67a8bc04 595 return;
596 } elsif(@type_constraints) {
c6fece89 597 warn "I failed due to left over TC";
67a8bc04 598 return;
07a8693b 599 } else {
67a8bc04 600 return 1;
601 }
602 }
603 )
604);
605
606Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint(
607 MooseX::Meta::TypeConstraint::Structured->new(
608 name => "MooseX::Types::Structured::Dict",
609 parent => find_type_constraint('HashRef'),
e327145a 610 constraint_generator=> sub {
67a8bc04 611 ## Get the constraints and values to check
e327145a 612 my ($type_constraints, $values) = @_;
07a8693b 613 my %type_constraints = defined $type_constraints ?
614 @$type_constraints : ();
e327145a 615 my %values = defined $values ? %$values: ();
67a8bc04 616 ## Perform the checking
617 while(%type_constraints) {
618 my($key, $type_constraint) = each %type_constraints;
619 delete $type_constraints{$key};
620 if(exists $values{$key}) {
621 my $value = $values{$key};
622 delete $values{$key};
623 unless($type_constraint->check($value)) {
a30fa891 624 return;
625 }
07a8693b 626 } else {
627 ## Test to see if the TC supports null values
190a34eb 628 unless($type_constraint->check()) {
629 return;
630 }
a30fa891 631 }
67a8bc04 632 }
633 ## Make sure there are no leftovers.
e327145a 634 if(%values) {
67a8bc04 635 return;
636 } elsif(%type_constraints) {
637 return;
07a8693b 638 } else {
67a8bc04 639 return 1;
640 }
641 },
642 )
643);
d24da8ec 644
e327145a 645OPTIONAL: {
646 my $Optional = Moose::Meta::TypeConstraint::Parameterizable->new(
647 name => 'MooseX::Types::Structured::Optional',
648 package_defined_in => __PACKAGE__,
649 parent => find_type_constraint('Item'),
650 constraint => sub { 1 },
651 constraint_generator => sub {
652 my ($type_parameter, @args) = @_;
653 my $check = $type_parameter->_compiled_type_constraint();
654 return sub {
07a8693b 655 my (@args) = @_;
656 ## Does the arg exist? Something exists if it's a 'real' value
657 ## or if it is set to undef.
e327145a 658 if(exists($args[0])) {
659 ## If it exists, we need to validate it
660 $check->($args[0]);
661 } else {
662 ## But it's is okay if the value doesn't exists
663 return 1;
664 }
665 }
666 }
667 );
668
669 Moose::Util::TypeConstraints::register_type_constraint($Optional);
670 Moose::Util::TypeConstraints::add_parameterizable_type($Optional);
671}
672
673
d24da8ec 674=head1 SEE ALSO
675
676The following modules or resources may be of interest.
677
22727dd5 678L<Moose>, L<MooseX::Types>, L<Moose::Meta::TypeConstraint>,
a30fa891 679L<MooseX::Meta::TypeConstraint::Structured>
d24da8ec 680
16aea7bf 681=head1 TODO
682
c6fece89 683Here's a list of stuff I would be happy to get volunteers helping with:
684
685All POD examples need test cases in t/documentation/*.t
686Want to break out the examples section to a separate cookbook style POD.
687Want more examples and best practice / usage guidance for authors
688Need to clarify deep coercions,
689Need to clarify subtypes of subtypes.
16aea7bf 690
d24da8ec 691=head1 AUTHOR
692
693John Napiorkowski, C<< <jjnapiork@cpan.org> >>
694
695=head1 COPYRIGHT & LICENSE
696
697This program is free software; you can redistribute it and/or modify
698it under the same terms as Perl itself.
699
700=cut
67a8bc04 701
7021;