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.07';
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'}
487 Here are some additional example usage for structured types. All examples can
488 be found also in the 't/examples.t' test. Your contributions are also welcomed.
490 =head2 Normalize a HashRef
492 You need a hashref to conform to a canonical structure but are required accept a
493 bunch of different incoming structures. You can normalize using the Dict type
494 constraint and coercions. This example also shows structured types mixed which
495 other MooseX::Types libraries.
497 package Test::MooseX::Meta::TypeConstraint::Structured::Examples::Normalize;
502 use MooseX::Types::Structured qw(Dict Tuple);
503 use MooseX::Types::DateTime qw(DateTime);
504 use MooseX::Types::Moose qw(Int Str Object);
505 use MooseX::Types -declare => [qw(Name Age Person)];
519 name => "$_->{first} $_->{last}",
529 ## DateTime needs to be inside of single quotes here to disambiguate the
530 ## class package from the DataTime type constraint imported via the
531 ## line "use MooseX::Types::DateTime qw(DateTime);"
533 name => "$_->{fullname}{first} $_->{fullname}{last}",
534 age => ($_->{dob} - 'DateTime'->now)->years,
537 has person => (is=>'rw', isa=>Person, coerce=>1);
539 And now you can instantiate with all the following:
542 name=>'John Napiorkowski',
548 last=>'Napiorkowski',
557 dob => 'DateTime'->new(
564 This technique is a way to support various ways to instantiate your class in a
565 clean and declarative way.
569 Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint(
570 MooseX::Meta::TypeConstraint::Structured->new(
571 name => "MooseX::Types::Structured::Tuple" ,
572 parent => find_type_constraint('ArrayRef'),
573 constraint_generator=> sub {
574 ## Get the constraints and values to check
575 my ($type_constraints, $values) = @_;
576 my @type_constraints = defined $type_constraints ?
577 @$type_constraints : ();
579 my $overflow_handler;
580 if(ref $type_constraints[-1] eq 'CODE') {
581 $overflow_handler = pop @type_constraints;
584 my @values = defined $values ? @$values: ();
585 ## Perform the checking
586 while(@type_constraints) {
587 my $type_constraint = shift @type_constraints;
589 my $value = shift @values;
590 unless($type_constraint->check($value)) {
594 ## Test if the TC supports null values
595 unless($type_constraint->check()) {
600 ## Make sure there are no leftovers.
602 if($overflow_handler) {
603 return $overflow_handler->(@values);
607 } elsif(@type_constraints) {
608 warn "I failed due to left over TC";
617 Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint(
618 MooseX::Meta::TypeConstraint::Structured->new(
619 name => "MooseX::Types::Structured::Dict",
620 parent => find_type_constraint('HashRef'),
621 constraint_generator=> sub {
622 ## Get the constraints and values to check
623 my ($type_constraints, $values) = @_;
624 my @type_constraints = defined $type_constraints ?
625 @$type_constraints : ();
627 my $overflow_handler;
628 if(ref $type_constraints[-1] eq 'CODE') {
629 $overflow_handler = pop @type_constraints;
631 my (%type_constraints) = @type_constraints;
632 my %values = defined $values ? %$values: ();
633 ## Perform the checking
634 while(%type_constraints) {
635 my($key, $type_constraint) = each %type_constraints;
636 delete $type_constraints{$key};
637 if(exists $values{$key}) {
638 my $value = $values{$key};
639 delete $values{$key};
640 unless($type_constraint->check($value)) {
644 ## Test to see if the TC supports null values
645 unless($type_constraint->check()) {
650 ## Make sure there are no leftovers.
652 if($overflow_handler) {
653 return $overflow_handler->(%values);
657 } elsif(%type_constraints) {
667 my $Optional = Moose::Meta::TypeConstraint::Parameterizable->new(
668 name => 'MooseX::Types::Structured::Optional',
669 package_defined_in => __PACKAGE__,
670 parent => find_type_constraint('Item'),
671 constraint => sub { 1 },
672 constraint_generator => sub {
673 my ($type_parameter, @args) = @_;
674 my $check = $type_parameter->_compiled_type_constraint();
677 ## Does the arg exist? Something exists if it's a 'real' value
678 ## or if it is set to undef.
679 if(exists($args[0])) {
680 ## If it exists, we need to validate it
683 ## But it's is okay if the value doesn't exists
690 Moose::Util::TypeConstraints::register_type_constraint($Optional);
691 Moose::Util::TypeConstraints::add_parameterizable_type($Optional);
696 ## we don't want to force the TC to be a Moose::Meta::TypeConstraint, we
697 ## just want to make sure it provides the minimum needed bits to function.
698 if($tc and ref $tc and $tc->can('check') and $tc->can('is_subtype_of') ) {
700 if($tc->is_subtype_of('HashRef')) {
701 return $tc->check(+{@_});
702 } elsif($tc->is_subtype_of('ArrayRef')) {
703 return $tc->check([@_]);
709 ## For now just pass it all to check and cross our fingers
711 return $tc->check(@_);
718 The following modules or resources may be of interest.
720 L<Moose>, L<MooseX::Types>, L<Moose::Meta::TypeConstraint>,
721 L<MooseX::Meta::TypeConstraint::Structured>
725 Here's a list of stuff I would be happy to get volunteers helping with:
727 All POD examples need test cases in t/documentation/*.t
728 Want to break out the examples section to a separate cookbook style POD.
729 Want more examples and best practice / usage guidance for authors
730 Need to clarify deep coercions,
731 Need to clarify subtypes of subtypes.
735 John Napiorkowski, C<< <jjnapiork@cpan.org> >>
737 =head1 COPYRIGHT & LICENSE
739 This program is free software; you can redistribute it and/or modify
740 it under the same terms as Perl itself.