more documentation and edits to the changelog in preparation for new release
[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
190a34eb 8our $VERSION = '0.06';
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
af1d00c9 19 package MyApp::MyClass;
6c2f284c 20
af1d00c9 21 use Moose;
22 use MooseX::Types::Moose qw(Str Int);
190a34eb 23 use MooseX::Types::Structured qw(Dict Optional);
24
25 ## A name has a first and last part, but middle names are not required
26 has name => (
27 isa=>Dict[
28 first=>Str,
29 last=>Str,
30 middle=>Optional[Str],
31 ],
32 );
af1d00c9 33
6c2f284c 34Then you can instantiate this class with something like:
35
190a34eb 36 my $john = MyApp::MyClass->new(
37 name => {
38 first=>'John',
39 middle=>'James'
40 last=>'Napiorkowski',
41 },
42 );
22727dd5 43
44Or with:
45
190a34eb 46 my $vanessa = MyApp::MyClass->new(
d87e8b74 47 name => {
190a34eb 48 first=>'Vanessa',
49 last=>'Li'
d87e8b74 50 },
51 );
d24da8ec 52
d87e8b74 53But all of these would cause a constraint error for the 'name' attribute:
6c2f284c 54
d87e8b74 55 MyApp::MyClass->new( name=>'John' );
56 MyApp::MyClass->new( name=>{first_name=>'John'} );
57 MyApp::MyClass->new( name=>{first_name=>'John', age=>39} );
190a34eb 58 MyApp::MyClass->new( name=>{first=>'Vanessa', middle=>[1,2], last=>'Li'} );
59
6c2f284c 60Please see the test cases for more examples.
d24da8ec 61
62=head1 DESCRIPTION
63
22727dd5 64A structured type constraint is a standard container L<Moose> type constraint,
af1d00c9 65such as an arrayref or hashref, which has been enhanced to allow you to
59deb858 66explicitly name all the allow type constraints inside the structure. The
af1d00c9 67generalized form is:
68
22727dd5 69 TypeConstraint[@TypeParameters|%TypeParameters]
af1d00c9 70
22727dd5 71Where 'TypeParameters' is an array or hash of L<Moose::Meta::TypeConstraint>.
af1d00c9 72
22727dd5 73This type library enables structured type constraints. It is built on top of the
59deb858 74L<MooseX::Types> library system, so you should review the documentation for that
75if you are not familiar with it.
76
5632ada1 77=head2 Comparing Parameterized types to Structured types
59deb858 78
22727dd5 79Parameterized constraints are built into core Moose and you are probably already
80familuar with the type constraints 'HashRef' and 'ArrayRef'. Structured types
81have similar functionality, so their syntax is likewise similar. For example,
82you could define a parameterized constraint like:
6c2f284c 83
d87e8b74 84 subtype ArrayOfInts,
85 as Arrayref[Int];
6c2f284c 86
af1d00c9 87which would constraint a value to something like [1,2,3,...] and so on. On the
22727dd5 88other hand, a structured type constraint explicitly names all it's allowed
89'internal' type parameter constraints. For the example:
6c2f284c 90
af1d00c9 91 subtype StringFollowedByInt,
92 as Tuple[Str,Int];
6c2f284c 93
59deb858 94would constrain it's value to something like ['hello', 111] but ['hello', 'world']
22727dd5 95would fail, as well as ['hello', 111, 'world'] and so on. Here's another
96example:
97
98 subtype StringIntOptionalHashRef,
99 as Tuple[
100 Str, Int,
101 Optional[HashRef]
102 ];
103
104This defines a type constraint that validates values like:
105
106 ['Hello', 100, {key1=>'value1', key2=>'value2'}];
107 ['World', 200];
108
109Notice that the last type constraint in the structure is optional. This is
110enabled via the helper Optional type constraint, which is a variation of the
111core Moose type constraint Maybe. The main difference is that Optional type
112constraints are required to validate if they exist, while Maybe permits undefined
113values. So the following example would not validate:
114
115 StringIntOptionalHashRef->validate(['Hello Undefined', 1000, undef]);
116
117Please note the subtle difference between undefined and null. If you wish to
118allow both null and undefined, you should use the core Moose Maybe type constraint
119instead:
120
121 use MooseX::Types -declare [qw(StringIntOptionalHashRef)];
122 use MooseX::Types::Moose qw(Maybe);
123 use MooseX::Types::Structured qw(Tuple);
124
125 subtype StringIntOptionalHashRef,
126 as Tuple[
127 Str, Int, Maybe[HashRef]
128 ];
129
130This would validate the following:
131
132 ['Hello', 100, {key1=>'value1', key2=>'value2'}];
133 ['World', 200, undef];
134 ['World', 200];
d87e8b74 135
136Structured Constraints are not limited to arrays. You can define a structure
137against a hashref with 'Dict' as in this example:
138
139 subtype FirstNameLastName,
22727dd5 140 as Dict[firstname=>Str, lastname=>Str];
d87e8b74 141
142This would constrain a hashref to something like:
143
144 {firstname=>'Vanessa', lastname=>'Li'};
145
146but all the following would fail validation:
147
148 {first=>'Vanessa', last=>'Li'};
149 {firstname=>'Vanessa', lastname=>'Li', middlename=>'NA'};
150 ['Vanessa', 'Li'];
6c2f284c 151
152These structures can be as simple or elaborate as you wish. You can even
153combine various structured, parameterized and simple constraints all together:
154
af1d00c9 155 subtype crazy,
156 as Tuple[
157 Int,
158 Dict[name=>Str, age=>Int],
159 ArrayRef[Int]
160 ];
6c2f284c 161
af1d00c9 162Which would match "[1, {name=>'John', age=>25},[10,11,12]]". Please notice how
59deb858 163the type parameters can be visually arranged to your liking and to improve the
164clarity of your meaning. You don't need to run then altogether onto a single
165line.
166
167=head2 Alternatives
6c2f284c 168
169You should exercise some care as to whether or not your complex structured
170constraints would be better off contained by a real object as in the following
171example:
172
af1d00c9 173 package MyApp::MyStruct;
174 use Moose;
175
22727dd5 176 has $_ for qw(full_name age_in_years);
af1d00c9 177
178 package MyApp::MyClass;
179 use Moose;
180
181 has person => (isa=>'MyApp::MyStruct');
182
183 my $instance = MyApp::MyClass->new(
22727dd5 184 person=>MyApp::MyStruct->new(full_name=>'John', age_in_years=>39),
af1d00c9 185 );
6c2f284c 186
187This method may take some additional time to setup but will give you more
188flexibility. However, structured constraints are highly compatible with this
189method, granting some interesting possibilities for coercion. Try:
190
22727dd5 191 use MyApp::MyStruct;
192 use MooseX::Types::DateTime qw(DateTime);
193 use MooseX::Types -declare [qw(MyStruct)];
194 use MooseX::Types::Moose qw(Str Int);
195 use MooseX::Types::Structured qw(Dict);
196
197 ## Use class_type to create an ISA type constraint if your object doesn't
198 ## inherit from Moose::Object.
199 class_type 'MyApp::MyStruct';
200
201 ## Just a shorter version really.
202 subtype MyStruct,
af1d00c9 203 as 'MyApp::MyStruct';
204
22727dd5 205 ## Add the coercions.
206 coerce MyStruct,
207 from Dict[
208 full_name=>Str,
209 age_in_years=>Int
210 ], via {
211 MyApp::MyStruct->new(%$_);
212 },
213 from Dict[
214 lastname=>Str,
215 firstname=>Str,
216 dob=>DateTime
217 ], via {
218 my $name = $_->{firstname} .' '. $_->{lastname};
af1d00c9 219 my $age = DateTime->now - $_->{dob};
22727dd5 220 MyApp::MyStruct->new( full_name=>$name, age_in_years=>$age->years );
af1d00c9 221 };
22727dd5 222
223If you are not familiar with how coercions work, check out the L<Moose> cookbook
224entry L<Moose::Cookbook::Recipe5> for an explanation. The section L</Coercions>
225has additional examples and discussion.
226
227=head2 Subtyping a Structured type constraint
16aea7bf 228
229You need to exercise some care when you try to subtype a structured type
a4a88fef 230as in this example:
d24da8ec 231
af1d00c9 232 subtype Person,
d87e8b74 233 as Dict[name=>Str, age=>Int];
a4a88fef 234
af1d00c9 235 subtype FriendlyPerson,
236 as Person[name=>Str, age=>Int, totalFriends=>Int];
a4a88fef 237
16aea7bf 238This will actually work BUT you have to take care that the subtype has a
a4a88fef 239structure that does not contradict the structure of it's parent. For now the
59deb858 240above works, but I will clarify the syntax for this at a future point, so
22727dd5 241it's recommended to avoid (should not really be needed so much anyway). For
59deb858 242now this is supported in an EXPERIMENTAL way. Your thoughts, test cases and
243patches are welcomed for discussion.
16aea7bf 244
245=head2 Coercions
246
247Coercions currently work for 'one level' deep. That is you can do:
248
af1d00c9 249 subtype Person,
16aea7bf 250 as Dict[name=>Str, age=>Int];
af1d00c9 251
16aea7bf 252 subtype Fullname,
253 as Dict[first=>Str, last=>Str];
af1d00c9 254
255 coerce Person,
d87e8b74 256 ## Coerce an object of a particular class
af1d00c9 257 from BlessedPersonObject,
258 via { +{name=>$_->name, age=>$_->age} },
d87e8b74 259 ## Coerce from [$name, $age]
af1d00c9 260 from ArrayRef,
261 via { +{name=>$_->[0], age=>$_->[1] },
d87e8b74 262 ## Coerce from {fullname=>{first=>...,last=>...}, dob=>$DateTimeObject}
16aea7bf 263 from Dict[fullname=>Fullname, dob=>DateTime],
264 via {
af1d00c9 265 my $age = $_->dob - DateTime->now;
266 +{
267 name=> $_->{fullname}->{first} .' '. $_->{fullname}->{last},
268 age=>$age->years
269 }
16aea7bf 270 };
271
272And that should just work as expected. However, if there are any 'inner'
273coercions, such as a coercion on 'Fullname' or on 'DateTime', that coercion
274won't currently get activated.
275
22727dd5 276Please see the test '07-coerce.t' for a more detailed example. Discussion on
277extending coercions to support this welcome on the Moose development channel or
278mailing list.
16aea7bf 279
280=head1 TYPE CONSTRAINTS
281
282This type library defines the following constraints.
283
284=head2 Tuple[@constraints]
285
286This defines an arrayref based constraint which allows you to validate a specific
287list of constraints. For example:
288
af1d00c9 289 Tuple[Int,Str]; ## Validates [1,'hello']
290 Tuple[Str|Object, Int]; ##Validates ['hello', 1] or [$object, 2]
16aea7bf 291
22727dd5 292=head2 Dict[%constraints]
16aea7bf 293
294This defines a hashref based constraint which allowed you to validate a specific
295hashref. For example:
296
af1d00c9 297 Dict[name=>Str, age=>Int]; ## Validates {name=>'John', age=>39}
d24da8ec 298
22727dd5 299=head2 Optional[$constraint]
190a34eb 300
301This is primarily a helper constraint for Dict and Tuple type constraints. What
302this allows if for you to assert that a given type constraint is allowed to be
303null (but NOT undefined). If the value is null, then the type constraint passes
304but if the value is defined it must validate against the type constraint. This
305makes it easy to make a Dict where one or more of the keys doesn't have to exist
306or a tuple where some of the values are not required. For example:
307
308 subtype Name() => as Dict[
309 first=>Str,
310 last=>Str,
311 middle=>Optional[Str],
312 ];
313
314Creates a constraint that validates against a hashref with the keys 'first' and
315'last' being strings and required while an optional key 'middle' is must be a
316string if it appears but doesn't have to appear. So in this case both the
317following are valid:
318
319 {first=>'John', middle=>'James', last=>'Napiorkowski'}
320 {first=>'Vanessa', last=>'Li'}
321
59deb858 322=head1 EXAMPLES
323
324Here are some additional example usage for structured types. All examples can
325be found also in the 't/examples.t' test. Your contributions are also welcomed.
326
327=head2 Normalize a HashRef
328
329You need a hashref to conform to a canonical structure but are required accept a
330bunch of different incoming structures. You can normalize using the Dict type
331constraint and coercions. This example also shows structured types mixed which
332other MooseX::Types libraries.
333
334 package Test::MooseX::Meta::TypeConstraint::Structured::Examples::Normalize;
335
336 use Moose;
337 use DateTime;
338
339 use MooseX::Types::Structured qw(Dict Tuple);
340 use MooseX::Types::DateTime qw(DateTime);
341 use MooseX::Types::Moose qw(Int Str Object);
342 use MooseX::Types -declare => [qw(Name Age Person)];
343
344 subtype Person,
345 as Dict[name=>Str, age=>Int];
346
347 coerce Person,
348 from Dict[first=>Str, last=>Str, years=>Int],
349 via { +{
350 name => "$_->{first} $_->{last}",
351 age=>$_->{years},
352 }},
353 from Dict[fullname=>Dict[last=>Str, first=>Str], dob=>DateTime],
354 via { +{
355 name => "$_->{fullname}{first} $_->{fullname}{last}",
356 age => ($_->{dob} - 'DateTime'->now)->years,
357 }};
358
359 has person => (is=>'rw', isa=>Person, coerce=>1);
360
a30fa891 361=cut
362
67a8bc04 363Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint(
364 MooseX::Meta::TypeConstraint::Structured->new(
365 name => "MooseX::Types::Structured::Tuple" ,
366 parent => find_type_constraint('ArrayRef'),
e327145a 367 constraint_generator=> sub {
67a8bc04 368 ## Get the constraints and values to check
e327145a 369 my ($type_constraints, $values) = @_;
370 my @type_constraints = defined $type_constraints ? @$type_constraints: ();
371 my @values = defined $values ? @$values: ();
67a8bc04 372 ## Perform the checking
373 while(@type_constraints) {
374 my $type_constraint = shift @type_constraints;
a30fa891 375 if(@values) {
67a8bc04 376 my $value = shift @values;
377 unless($type_constraint->check($value)) {
378 return;
379 }
380 } else {
190a34eb 381 unless($type_constraint->check()) {
382 return;
383 }
a30fa891 384 }
385 }
67a8bc04 386 ## Make sure there are no leftovers.
387 if(@values) {
388 return;
389 } elsif(@type_constraints) {
390 return;
391 }else {
392 return 1;
393 }
394 }
395 )
396);
397
398Moose::Util::TypeConstraints::get_type_constraint_registry->add_type_constraint(
399 MooseX::Meta::TypeConstraint::Structured->new(
400 name => "MooseX::Types::Structured::Dict",
401 parent => find_type_constraint('HashRef'),
e327145a 402 constraint_generator=> sub {
67a8bc04 403 ## Get the constraints and values to check
e327145a 404 my ($type_constraints, $values) = @_;
405 my %type_constraints = defined $type_constraints ? @$type_constraints: ();
406 my %values = defined $values ? %$values: ();
67a8bc04 407 ## Perform the checking
408 while(%type_constraints) {
409 my($key, $type_constraint) = each %type_constraints;
410 delete $type_constraints{$key};
411 if(exists $values{$key}) {
412 my $value = $values{$key};
413 delete $values{$key};
414 unless($type_constraint->check($value)) {
a30fa891 415 return;
416 }
e327145a 417 } else {
190a34eb 418 unless($type_constraint->check()) {
419 return;
420 }
a30fa891 421 }
67a8bc04 422 }
423 ## Make sure there are no leftovers.
e327145a 424 if(%values) {
67a8bc04 425 return;
426 } elsif(%type_constraints) {
427 return;
428 }else {
429 return 1;
430 }
431 },
432 )
433);
d24da8ec 434
e327145a 435OPTIONAL: {
436 my $Optional = Moose::Meta::TypeConstraint::Parameterizable->new(
437 name => 'MooseX::Types::Structured::Optional',
438 package_defined_in => __PACKAGE__,
439 parent => find_type_constraint('Item'),
440 constraint => sub { 1 },
441 constraint_generator => sub {
442 my ($type_parameter, @args) = @_;
443 my $check = $type_parameter->_compiled_type_constraint();
444 return sub {
445 my (@args) = @_;
446 if(exists($args[0])) {
447 ## If it exists, we need to validate it
448 $check->($args[0]);
449 } else {
450 ## But it's is okay if the value doesn't exists
451 return 1;
452 }
453 }
454 }
455 );
456
457 Moose::Util::TypeConstraints::register_type_constraint($Optional);
458 Moose::Util::TypeConstraints::add_parameterizable_type($Optional);
459}
460
461
d24da8ec 462=head1 SEE ALSO
463
464The following modules or resources may be of interest.
465
22727dd5 466L<Moose>, L<MooseX::Types>, L<Moose::Meta::TypeConstraint>,
a30fa891 467L<MooseX::Meta::TypeConstraint::Structured>
d24da8ec 468
16aea7bf 469=head1 TODO
470
471Need to clarify deep coercions, need to clarify subtypes of subtypes.
472
d24da8ec 473=head1 AUTHOR
474
475John Napiorkowski, C<< <jjnapiork@cpan.org> >>
476
477=head1 COPYRIGHT & LICENSE
478
479This program is free software; you can redistribute it and/or modify
480it under the same terms as Perl itself.
481
482=cut
67a8bc04 483
4841;