1 package MooseX::Types::Structured;
5 use Moose::Util::TypeConstraints;
6 use MooseX::Meta::TypeConstraint::Structured;
7 use MooseX::Types -declare => [qw(Dict Tuple Optional)];
8 use Sub::Exporter -setup => { exports => [ qw(Dict Tuple Optional slurpy) ] };
10 our $VERSION = '0.09';
11 our $AUTHORITY = 'cpan:JJNAPIORK';
15 MooseX::Types::Structured - Structured Type Constraints for Moose
19 The following is example usage for this module.
24 use MooseX::Types::Moose qw(Str Int HashRef);
25 use MooseX::Types::Structured qw(Dict Tuple Optional);
27 ## A name has a first and last part, but middle names are not required
32 middle => Optional[Str],
36 ## description is a string field followed by a HashRef of tagged data.
44 Then you can instantiate this class with something like:
46 my $john = Person->new(
50 last => 'Napiorkowski',
53 'A cool guy who loves Perl and Moose.', {
54 married_to => 'Vanessa Li',
62 my $vanessa = Person->new(
67 description => ['A great student!'],
70 But all of these would cause a constraint error for the 'name' attribute:
72 ## Value for 'name' not a HashRef
73 Person->new( name => 'John' );
75 ## Value for 'name' has incorrect hash key and missing required keys
76 Person->new( name => {
80 ## Also incorrect keys
81 Person->new( name => {
86 ## key 'middle' incorrect type, should be a Str not a ArrayRef
87 Person->new( name => {
93 And these would cause a constraint error for the 'description' attribute:
95 ## Should be an ArrayRef
96 Person->new( description => 'Hello I am a String' );
98 ## First element must be a string not a HashRef.
99 Person->new (description => [{
104 Please see the test cases for more examples.
108 A structured type constraint is a standard container L<Moose> type constraint,
109 such as an ArrayRef or HashRef, which has been enhanced to allow you to
110 explicitly name all the allowed type constraints inside the structure. The
113 TypeConstraint[@TypeParameters or %TypeParameters]
115 Where 'TypeParameters' is an array reference or hash references of
116 L<Moose::Meta::TypeConstraint> objects.
118 This type library enables structured type constraints. It is built on top of the
119 L<MooseX::Types> library system, so you should review the documentation for that
120 if you are not familiar with it.
122 =head2 Comparing Parameterized types to Structured types
124 Parameterized constraints are built into core Moose and you are probably already
125 familar with the type constraints 'HashRef' and 'ArrayRef'. Structured types
126 have similar functionality, so their syntax is likewise similar. For example,
127 you could define a parameterized constraint like:
132 which would constrain a value to something like [1,2,3,...] and so on. On the
133 other hand, a structured type constraint explicitly names all it's allowed
134 'internal' type parameter constraints. For the example:
136 subtype StringFollowedByInt,
139 would constrain it's value to things like ['hello', 111] but ['hello', 'world']
140 would fail, as well as ['hello', 111, 'world'] and so on. Here's another
143 subtype StringIntOptionalHashRef,
149 This defines a type constraint that validates values like:
151 ['Hello', 100, {key1 => 'value1', key2 => 'value2'}];
154 Notice that the last type constraint in the structure is optional. This is
155 enabled via the helper Optional type constraint, which is a variation of the
156 core Moose type constraint 'Maybe'. The main difference is that Optional type
157 constraints are required to validate if they exist, while 'Maybe' permits
158 undefined values. So the following example would not validate:
160 StringIntOptionalHashRef->validate(['Hello Undefined', 1000, undef]);
162 Please note the subtle difference between undefined and null. If you wish to
163 allow both null and undefined, you should use the core Moose 'Maybe' type
166 use MooseX::Types -declare [qw(StringIntMaybeHashRef)];
167 use MooseX::Types::Moose qw(Maybe);
168 use MooseX::Types::Structured qw(Tuple);
170 subtype StringIntMaybeHashRef,
172 Str, Int, Maybe[HashRef]
175 This would validate the following:
177 ['Hello', 100, {key1 => 'value1', key2 => 'value2'}];
178 ['World', 200, undef];
181 Structured constraints are not limited to arrays. You can define a structure
182 against a HashRef with 'Dict' as in this example:
184 subtype FirstNameLastName,
190 This would constrain a HashRef to something like:
192 {firstname => 'Christopher', lastname= > 'Parsons'};
194 but all the following would fail validation:
197 {first => 'Christopher', last => 'Parsons'};
200 {firstname => 'Christopher', lastname => 'Parsons', middlename => 'Allen'};
203 ['Christopher', 'Christopher'];
205 These structures can be as simple or elaborate as you wish. You can even
206 combine various structured, parameterized and simple constraints all together:
211 Dict[name=>Str, age=>Int],
215 Which would match "[1, {name=>'John', age=>25},[10,11,12]]". Please notice how
216 the type parameters can be visually arranged to your liking and to improve the
217 clarity of your meaning. You don't need to run then altogether onto a single
222 You should exercise some care as to whether or not your complex structured
223 constraints would be better off contained by a real object as in the following
226 package MyApp::MyStruct;
229 ## lazy way to make a bunch of attributes
230 has $_ for qw(full_name age_in_years);
232 package MyApp::MyClass;
235 has person => (isa => 'MyApp::MyStruct');
237 my $instance = MyApp::MyClass->new(
238 person=>MyApp::MyStruct->new(
244 This method may take some additional time to setup but will give you more
245 flexibility. However, structured constraints are highly compatible with this
246 method, granting some interesting possibilities for coercion. Try:
248 package MyApp::MyClass;
253 ## It's recommended your type declarations live in a separate class in order
254 ## to promote reusability and clarity. Inlined here for brevity.
256 use MooseX::Types::DateTime qw(DateTime);
257 use MooseX::Types -declare [qw(MyStruct)];
258 use MooseX::Types::Moose qw(Str Int);
259 use MooseX::Types::Structured qw(Dict);
261 ## Use class_type to create an ISA type constraint if your object doesn't
262 ## inherit from Moose::Object.
263 class_type 'MyApp::MyStruct';
265 ## Just a shorter version really.
267 as 'MyApp::MyStruct';
269 ## Add the coercions.
275 MyApp::MyStruct->new(%$_);
282 my $name = $_->{firstname} .' '. $_->{lastname};
283 my $age = DateTime->now - $_->{dob};
285 MyApp::MyStruct->new(
287 age_in_years=>$age->years,
291 has person => (isa=>MyStruct);
293 This would allow you to instantiate with something like:
295 my $obj = MyApp::MyClass->new( person => {
296 full_name=>'John Napiorkowski',
302 my $obj = MyApp::MyClass->new( person => {
304 firstname=>'Napiorkowski',
305 dob=>DateTime->new(year=>1969),
308 If you are not familiar with how coercions work, check out the L<Moose> cookbook
309 entry L<Moose::Cookbook::Recipe5> for an explanation. The section L</Coercions>
310 has additional examples and discussion.
312 =head2 Subtyping a Structured type constraint
314 You need to exercise some care when you try to subtype a structured type as in
318 as Dict[name => Str];
320 subtype FriendlyPerson,
323 total_friends => Int,
326 This will actually work BUT you have to take care that the subtype has a
327 structure that does not contradict the structure of it's parent. For now the
328 above works, but I will clarify the syntax for this at a future point, so
329 it's recommended to avoid (should not really be needed so much anyway). For
330 now this is supported in an EXPERIMENTAL way. Your thoughts, test cases and
331 patches are welcomed for discussion. If you find a good use for this, please
336 Coercions currently work for 'one level' deep. That is you can do:
351 ## Coerce an object of a particular class
352 from BlessedPersonObject, via {
359 ## Coerce from [$name, $age]
366 ## Coerce from {fullname=>{first=>...,last=>...}, dob=>$DateTimeObject}
367 from Dict[fullname=>Fullname, dob=>DateTime], via {
368 my $age = $_->dob - DateTime->now;
369 my $firstn = $_->{fullname}->{first};
370 my $lastn = $_->{fullname}->{last}
372 name => $_->{fullname}->{first} .' '. ,
377 And that should just work as expected. However, if there are any 'inner'
378 coercions, such as a coercion on 'Fullname' or on 'DateTime', that coercion
379 won't currently get activated.
381 Please see the test '07-coerce.t' for a more detailed example. Discussion on
382 extending coercions to support this welcome on the Moose development channel or
387 Newer versions of L<MooseX::Types> support recursive type constraints. That is
388 you can include a type constraint as a contained type constraint of itself. For
399 This would declare a Person subtype that contains a name and an optional
400 ArrayRef of Persons who are friends as in:
406 { name => 'Vincent' },
410 { name => 'Stephenie' },
417 Please take care to make sure the recursion node is either Optional, or declare
418 a Union with an non recursive option such as:
439 Otherwise you will define a subtype thatis impossible to validate since it is
440 infinitely recursive. For more information about defining recursive types,
441 please see the documentation in L<MooseX::Types> and the test cases.
443 =head1 TYPE CONSTRAINTS
445 This type library defines the following constraints.
447 =head2 Tuple[@constraints]
449 This defines an ArrayRef based constraint which allows you to validate a specific
450 list of contained constraints. For example:
452 Tuple[Int,Str]; ## Validates [1,'hello']
453 Tuple[Str|Object, Int]; ## Validates ['hello', 1] or [$object, 2]
455 =head2 Dict[%constraints]
457 This defines a HashRef based constraint which allowed you to validate a specific
458 hashref. For example:
460 Dict[name=>Str, age=>Int]; ## Validates {name=>'John', age=>39}
462 =head2 Optional[$constraint]
464 This is primarily a helper constraint for Dict and Tuple type constraints. What
465 this allows if for you to assert that a given type constraint is allowed to be
466 null (but NOT undefined). If the value is null, then the type constraint passes
467 but if the value is defined it must validate against the type constraint. This
468 makes it easy to make a Dict where one or more of the keys doesn't have to exist
469 or a tuple where some of the values are not required. For example:
471 subtype Name() => as Dict[
474 middle=>Optional[Str],
477 Creates a constraint that validates against a hashref with the keys 'first' and
478 'last' being strings and required while an optional key 'middle' is must be a
479 string if it appears but doesn't have to appear. So in this case both the
482 {first=>'John', middle=>'James', last=>'Napiorkowski'}
483 {first=>'Vanessa', last=>'Li'}
485 =head1 EXPORTABLE SUBROUTINES
487 This type library makes available for export the following subroutines
491 Structured type constraints by their nature are closed; that is validation will
492 depend and an exact match between your structure definition and the arguments to
493 be checked. Sometimes you might wish for a slightly looser amount of validation.
494 For example, you may wish to validate the first 3 elements of an array reference
495 and allow for an arbitrary number of additional elements. At first thought you
496 might think you could do it this way:
498 # I want to validate stuff like: [1,"hello", $obj, 2,3,4,5,6,...]
499 subtype AllowTailingArgs,
507 However what this will actually validate are structures like this:
509 [10,"Hello", $obj, [11,12,13,...] ]; # Notice element 4 is an ArrayRef
511 In order to allow structured validation of, "and then some", arguments, you can
512 use the </slurpy> method against a type constraint. For example:
514 use MooseX::Types::Structured qw(Tuple slurpy);
516 subtype AllowTailingArgs,
521 slurpy ArrayRef[Int],
524 This will now work as expected, validating ArrayRef structures such as:
526 [1,"hello", $obj, 2,3,4,5,6,...]
528 A few caveats apply. First, the slurpy type constraint must be the last one in
529 the list of type constraint parameters. Second, the parent type of the slurpy
530 type constraint must match that of the containing type constraint. That means
531 that a Tuple can allow a slurpy ArrayRef (or children of ArrayRefs, including
532 another Tuple) and a Dict can allow a slurpy HashRef (or children/subtypes of
533 HashRef, also including other Dict constraints).
535 Please note the the technical way this works 'under the hood' is that the
536 slurpy keywork transforms the target type constraint into a coderef. Please do
537 not try to create your own custom coderefs; always use the slurpy method. The
538 underlying technology may change in the future but the slurpy keyword will be
543 Here are some additional example usage for structured types. All examples can
544 be found also in the 't/examples.t' test. Your contributions are also welcomed.
546 =head2 Normalize a HashRef
548 You need a hashref to conform to a canonical structure but are required accept a
549 bunch of different incoming structures. You can normalize using the Dict type
550 constraint and coercions. This example also shows structured types mixed which
551 other MooseX::Types libraries.
553 package Test::MooseX::Meta::TypeConstraint::Structured::Examples::Normalize;
558 use MooseX::Types::Structured qw(Dict Tuple);
559 use MooseX::Types::DateTime qw(DateTime);
560 use MooseX::Types::Moose qw(Int Str Object);
561 use MooseX::Types -declare => [qw(Name Age Person)];
575 name => "$_->{first} $_->{last}",
585 ## DateTime needs to be inside of single quotes here to disambiguate the
586 ## class package from the DataTime type constraint imported via the
587 ## line "use MooseX::Types::DateTime qw(DateTime);"
589 name => "$_->{fullname}{first} $_->{fullname}{last}",
590 age => ($_->{dob} - 'DateTime'->now)->years,
593 has person => (is=>'rw', isa=>Person, coerce=>1);
595 And now you can instantiate with all the following:
598 name=>'John Napiorkowski',
604 last=>'Napiorkowski',
613 dob => 'DateTime'->new(
620 This technique is a way to support various ways to instantiate your class in a
621 clean and declarative way.
625 Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint(
626 MooseX::Meta::TypeConstraint::Structured->new(
627 name => "MooseX::Types::Structured::Tuple" ,
628 parent => find_type_constraint('ArrayRef'),
629 constraint_generator=> sub {
630 ## Get the constraints and values to check
631 my ($type_constraints, $values) = @_;
632 my @type_constraints = defined $type_constraints ?
633 @$type_constraints : ();
635 my $overflow_handler;
636 if(ref $type_constraints[-1] eq 'CODE') {
637 $overflow_handler = pop @type_constraints;
640 my @values = defined $values ? @$values: ();
641 ## Perform the checking
642 while(@type_constraints) {
643 my $type_constraint = shift @type_constraints;
645 my $value = shift @values;
646 unless($type_constraint->check($value)) {
650 ## Test if the TC supports null values
651 unless($type_constraint->check()) {
656 ## Make sure there are no leftovers.
658 if($overflow_handler) {
659 return $overflow_handler->([@values]);
663 } elsif(@type_constraints) {
664 warn "I failed due to left over TC";
673 Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint(
674 MooseX::Meta::TypeConstraint::Structured->new(
675 name => "MooseX::Types::Structured::Dict",
676 parent => find_type_constraint('HashRef'),
677 constraint_generator=> sub {
678 ## Get the constraints and values to check
679 my ($type_constraints, $values) = @_;
680 my @type_constraints = defined $type_constraints ?
681 @$type_constraints : ();
683 my $overflow_handler;
684 if(ref $type_constraints[-1] eq 'CODE') {
685 $overflow_handler = pop @type_constraints;
687 my (%type_constraints) = @type_constraints;
688 my %values = defined $values ? %$values: ();
689 ## Perform the checking
690 while(%type_constraints) {
691 my($key, $type_constraint) = each %type_constraints;
692 delete $type_constraints{$key};
693 if(exists $values{$key}) {
694 my $value = $values{$key};
695 delete $values{$key};
696 unless($type_constraint->check($value)) {
700 ## Test to see if the TC supports null values
701 unless($type_constraint->check()) {
706 ## Make sure there are no leftovers.
708 if($overflow_handler) {
709 return $overflow_handler->(+{%values});
713 } elsif(%type_constraints) {
723 my $Optional = Moose::Meta::TypeConstraint::Parameterizable->new(
724 name => 'MooseX::Types::Structured::Optional',
725 package_defined_in => __PACKAGE__,
726 parent => find_type_constraint('Item'),
727 constraint => sub { 1 },
728 constraint_generator => sub {
729 my ($type_parameter, @args) = @_;
730 my $check = $type_parameter->_compiled_type_constraint();
733 ## Does the arg exist? Something exists if it's a 'real' value
734 ## or if it is set to undef.
735 if(exists($args[0])) {
736 ## If it exists, we need to validate it
739 ## But it's is okay if the value doesn't exists
746 Moose::Util::TypeConstraints::register_type_constraint($Optional);
747 Moose::Util::TypeConstraints::add_parameterizable_type($Optional);
759 The following modules or resources may be of interest.
761 L<Moose>, L<MooseX::Types>, L<Moose::Meta::TypeConstraint>,
762 L<MooseX::Meta::TypeConstraint::Structured>
766 Here's a list of stuff I would be happy to get volunteers helping with:
768 All POD examples need test cases in t/documentation/*.t
769 Want to break out the examples section to a separate cookbook style POD.
770 Want more examples and best practice / usage guidance for authors
771 Need to clarify deep coercions,
772 Need to clarify subtypes of subtypes.
776 John Napiorkowski, C<< <jjnapiork@cpan.org> >>
778 =head1 COPYRIGHT & LICENSE
780 This program is free software; you can redistribute it and/or modify
781 it under the same terms as Perl itself.