package MooseX::Types::Structured;
use 5.008;
+
use Moose::Util::TypeConstraints;
use MooseX::Meta::TypeConstraint::Structured;
+use MooseX::Types::Structured::OverflowHandler;
use MooseX::Types -declare => [qw(Dict Tuple Optional)];
+use Sub::Exporter -setup => { exports => [ qw(Dict Tuple Optional slurpy) ] };
+use Devel::PartialDump;
+use Scalar::Util qw(blessed);
-our $VERSION = '0.06';
+our $VERSION = '0.14';
our $AUTHORITY = 'cpan:JJNAPIORK';
=head1 NAME
TypeConstraint[@TypeParameters or %TypeParameters]
-Where 'TypeParameters' is an array or hash of L<Moose::Meta::TypeConstraint>.
+Where 'TypeParameters' is an array reference or hash references of
+L<Moose::Meta::TypeConstraint> objects.
This type library enables structured type constraints. It is built on top of the
L<MooseX::Types> library system, so you should review the documentation for that
subtype ArrayOfInts,
as Arrayref[Int];
-which would constraint a value to something like [1,2,3,...] and so on. On the
+which would constrain a value to something like [1,2,3,...] and so on. On the
other hand, a structured type constraint explicitly names all it's allowed
'internal' type parameter constraints. For the example:
subtype StringFollowedByInt,
as Tuple[Str,Int];
-would constrain it's value to something like ['hello', 111] but ['hello', 'world']
+would constrain it's value to things like ['hello', 111] but ['hello', 'world']
would fail, as well as ['hello', 111, 'world'] and so on. Here's another
example:
Notice that the last type constraint in the structure is optional. This is
enabled via the helper Optional type constraint, which is a variation of the
core Moose type constraint 'Maybe'. The main difference is that Optional type
-constraints are required to validate if they exist, while Maybe permits undefined
-values. So the following example would not validate:
+constraints are required to validate if they exist, while 'Maybe' permits
+undefined values. So the following example would not validate:
StringIntOptionalHashRef->validate(['Hello Undefined', 1000, undef]);
allow both null and undefined, you should use the core Moose 'Maybe' type
constraint instead:
- use MooseX::Types -declare [qw(StringIntOptionalHashRef)];
+ use MooseX::Types -declare [qw(StringIntMaybeHashRef)];
use MooseX::Types::Moose qw(Maybe);
use MooseX::Types::Structured qw(Tuple);
- subtype StringIntOptionalHashRef,
+ subtype StringIntMaybeHashRef,
as Tuple[
Str, Int, Maybe[HashRef]
];
['World', 200, undef];
['World', 200];
-Structured Constraints are not limited to arrays. You can define a structure
-against a hashref with 'Dict' as in this example:
+Structured constraints are not limited to arrays. You can define a structure
+against a HashRef with 'Dict' as in this example:
subtype FirstNameLastName,
as Dict[
These structures can be as simple or elaborate as you wish. You can even
combine various structured, parameterized and simple constraints all together:
- subtype crazy,
+ subtype Crazy,
as Tuple[
Int,
Dict[name=>Str, age=>Int],
my $instance = MyApp::MyClass->new(
person=>MyApp::MyStruct->new(
full_name => 'John',
- age_in_years => 39
+ age_in_years => 39,
),
);
extending coercions to support this welcome on the Moose development channel or
mailing list.
+=head2 Recursion
+
+Newer versions of L<MooseX::Types> support recursive type constraints. That is
+you can include a type constraint as a contained type constraint of itself. For
+example:
+
+ subtype Person,
+ as Dict[
+ name=>Str,
+ friends=>Optional[
+ ArrayRef[Person]
+ ],
+ ];
+
+This would declare a Person subtype that contains a name and an optional
+ArrayRef of Persons who are friends as in:
+
+ {
+ name => 'Mike',
+ friends => [
+ { name => 'John' },
+ { name => 'Vincent' },
+ {
+ name => 'Tracey',
+ friends => [
+ { name => 'Stephenie' },
+ { name => 'Ilya' },
+ ],
+ },
+ ],
+ };
+
+Please take care to make sure the recursion node is either Optional, or declare
+a Union with an non recursive option such as:
+
+ subtype Value
+ as Tuple[
+ Str,
+ Str|Tuple,
+ ];
+
+Which validates:
+
+ [
+ 'Hello', [
+ 'World', [
+ 'Is', [
+ 'Getting',
+ 'Old',
+ ],
+ ],
+ ],
+ ];
+
+Otherwise you will define a subtype thatis impossible to validate since it is
+infinitely recursive. For more information about defining recursive types,
+please see the documentation in L<MooseX::Types> and the test cases.
+
=head1 TYPE CONSTRAINTS
This type library defines the following constraints.
list of contained constraints. For example:
Tuple[Int,Str]; ## Validates [1,'hello']
- Tuple[Str|Object, Int]; ##Validates ['hello', 1] or [$object, 2]
+ Tuple[Str|Object, Int]; ## Validates ['hello', 1] or [$object, 2]
=head2 Dict[%constraints]
{first=>'John', middle=>'James', last=>'Napiorkowski'}
{first=>'Vanessa', last=>'Li'}
+
+=head1 EXPORTABLE SUBROUTINES
+
+This type library makes available for export the following subroutines
+
+=head2 slurpy
+
+Structured type constraints by their nature are closed; that is validation will
+depend on an exact match between your structure definition and the arguments to
+be checked. Sometimes you might wish for a slightly looser amount of validation.
+For example, you may wish to validate the first 3 elements of an array reference
+and allow for an arbitrary number of additional elements. At first thought you
+might think you could do it this way:
+
+ # I want to validate stuff like: [1,"hello", $obj, 2,3,4,5,6,...]
+ subtype AllowTailingArgs,
+ as Tuple[
+ Int,
+ Str,
+ Object,
+ ArrayRef[Int],
+ ];
+
+However what this will actually validate are structures like this:
+
+ [10,"Hello", $obj, [11,12,13,...] ]; # Notice element 4 is an ArrayRef
+
+In order to allow structured validation of, "and then some", arguments, you can
+use the </slurpy> method against a type constraint. For example:
+
+ use MooseX::Types::Structured qw(Tuple slurpy);
+
+ subtype AllowTailingArgs,
+ as Tuple[
+ Int,
+ Str,
+ Object,
+ slurpy ArrayRef[Int],
+ ];
+
+This will now work as expected, validating ArrayRef structures such as:
+
+ [1,"hello", $obj, 2,3,4,5,6,...]
+A few caveats apply. First, the slurpy type constraint must be the last one in
+the list of type constraint parameters. Second, the parent type of the slurpy
+type constraint must match that of the containing type constraint. That means
+that a Tuple can allow a slurpy ArrayRef (or children of ArrayRefs, including
+another Tuple) and a Dict can allow a slurpy HashRef (or children/subtypes of
+HashRef, also including other Dict constraints).
+
+Please note the the technical way this works 'under the hood' is that the
+slurpy keywork transforms the target type constraint into a coderef. Please do
+not try to create your own custom coderefs; always use the slurpy method. The
+underlying technology may change in the future but the slurpy keyword will be
+supported.
+
+=head1 ERROR MESSAGES
+
+Error reporting has been improved to return more useful debugging messages. Now
+I will stringify the incoming check value with L<Devel::PartialDump> so that you
+can see the actual structure that is tripping up validation. Also, I report the
+'internal' validation error, so that if a particular element inside the
+Structured Type is failing validation, you will see that. There's a limit to
+how deep this internal reporting goes, but you shouldn't see any of the "failed
+with ARRAY(XXXXXX)" that we got with earlier versions of this module.
+
+This support is continuing to expand, so it's best to use these messages for
+debugging purposes and not for creating messages that 'escape into the wild'
+such as error messages sent to the user.
+
+Please see the test '12-error.t' for a more lengthy example. Your thoughts and
+preferable tests or code patches very welcome!
+
=head1 EXAMPLES
Here are some additional example usage for structured types. All examples can
use MooseX::Types -declare => [qw(Name Age Person)];
subtype Person,
- as Dict[name=>Str, age=>Int];
+ as Dict[
+ name=>Str,
+ age=>Int,
+ ];
coerce Person,
- from Dict[first=>Str, last=>Str, years=>Int],
- via { +{
+ from Dict[
+ first=>Str,
+ last=>Str,
+ years=>Int,
+ ], via { +{
name => "$_->{first} $_->{last}",
- age=>$_->{years},
+ age => $_->{years},
}},
- from Dict[fullname=>Dict[last=>Str, first=>Str], dob=>DateTime],
+ from Dict[
+ fullname=>Dict[
+ last=>Str,
+ first=>Str,
+ ],
+ dob=>DateTime,
+ ],
## DateTime needs to be inside of single quotes here to disambiguate the
## class package from the DataTime type constraint imported via the
## line "use MooseX::Types::DateTime qw(DateTime);"
And now you can instantiate with all the following:
__PACKAGE__->new(
- name=>'John Napiorkowski',
- age=>39,
+ person=>{
+ name=>'John Napiorkowski',
+ age=>39,
+ },
);
__PACKAGE__->new(
- first=>'John',
- last=>'Napiorkowski',
- years=>39,
+ person=>{
+ first=>'John',
+ last=>'Napiorkowski',
+ years=>39,
+ },
);
__PACKAGE__->new(
- fullname => {
- first=>'John',
- last=>'Napiorkowski'
+ person=>{
+ fullname => {
+ first=>'John',
+ last=>'Napiorkowski'
+ },
+ dob => 'DateTime'->new(
+ year=>1969,
+ month=>2,
+ day=>13
+ ),
},
- dob => 'DateTime'->new(
- year=>1969,
- month=>2,
- day=>13
- ),
);
This technique is a way to support various ways to instantiate your class in a
## Get the constraints and values to check
my ($type_constraints, $values) = @_;
my @type_constraints = defined $type_constraints ?
- @$type_constraints : ();
+ @$type_constraints : ();
+
+ my $overflow_handler;
+ if($type_constraints[-1] && blessed $type_constraints[-1]
+ && $type_constraints[-1]->isa('MooseX::Types::Structured::OverflowHandler')) {
+ $overflow_handler = pop @type_constraints;
+ }
+
my @values = defined $values ? @$values: ();
## Perform the checking
while(@type_constraints) {
if(@values) {
my $value = shift @values;
unless($type_constraint->check($value)) {
+ $_[2]->{message} = $type_constraint->get_message($value)
+ if ref $_[2];
return;
}
} else {
## Test if the TC supports null values
unless($type_constraint->check()) {
+ $_[2]->{message} = $type_constraint->get_message('NULL')
+ if ref $_[2];
return;
}
}
}
## Make sure there are no leftovers.
if(@values) {
- return;
+ if($overflow_handler) {
+ return $overflow_handler->check([@values], $_[2]);
+ } else {
+ $_[2]->{message} = "More values than Type Constraints!"
+ if ref $_[2];
+ return;
+ }
} elsif(@type_constraints) {
+ $_[2]->{message} =
+ "Not enough values for all defined type constraints. Remaining: ". join(', ',@type_constraints)
+ if ref $_[2];
return;
} else {
return 1;
constraint_generator=> sub {
## Get the constraints and values to check
my ($type_constraints, $values) = @_;
- my %type_constraints = defined $type_constraints ?
- @$type_constraints : ();
+ my @type_constraints = defined $type_constraints ?
+ @$type_constraints : ();
+
+ my $overflow_handler;
+ if($type_constraints[-1] && blessed $type_constraints[-1]
+ && $type_constraints[-1]->isa('MooseX::Types::Structured::OverflowHandler')) {
+ $overflow_handler = pop @type_constraints;
+ }
+ my (%type_constraints) = @type_constraints;
my %values = defined $values ? %$values: ();
## Perform the checking
while(%type_constraints) {
my $value = $values{$key};
delete $values{$key};
unless($type_constraint->check($value)) {
+ $_[2]->{message} = $type_constraint->get_message($value)
+ if ref $_[2];
return;
}
} else {
## Test to see if the TC supports null values
unless($type_constraint->check()) {
+ $_[2]->{message} = $type_constraint->get_message('NULL')
+ if ref $_[2];
return;
}
}
}
## Make sure there are no leftovers.
if(%values) {
- return;
+ if($overflow_handler) {
+ return $overflow_handler->check(+{%values});
+ } else {
+ $_[2]->{message} = "More values than Type Constraints!"
+ if ref $_[2];
+ return;
+ }
} elsif(%type_constraints) {
+ $_[2]->{message} =
+ "Not enough values for all defined type constraints. Remaining: ". join(', ',values %values)
+ if ref $_[2];
return;
} else {
return 1;
Moose::Util::TypeConstraints::add_parameterizable_type($Optional);
}
+sub slurpy ($) {
+ my ($tc) = @_;
+ return MooseX::Types::Structured::OverflowHandler->new(
+ type_constraint => $tc,
+ );
+}
=head1 SEE ALSO
=head1 TODO
-Need to clarify deep coercions, need to clarify subtypes of subtypes. Would
-like more and better examples and probably some best practices guidence.
+Here's a list of stuff I would be happy to get volunteers helping with:
+
+All POD examples need test cases in t/documentation/*.t
+Want to break out the examples section to a separate cookbook style POD.
+Want more examples and best practice / usage guidance for authors
+Need to clarify deep coercions,
+Need to clarify subtypes of subtypes.
=head1 AUTHOR