Add "optimize_as" sugar to TC
[gitmo/Mouse.git] / lib / Mouse / Util / TypeConstraints.pm
CommitLineData
3b46bd49 1package Mouse::Util::TypeConstraints;
bc69ee88 2use Mouse::Util qw(does_role not_supported); # enables strict and warnings
9baf5d6b 3
f5ee065f 4use Carp qw(confess);
6c169c50 5use Scalar::Util qw/blessed looks_like_number openhandle/;
6d28c5cf 6
684db121 7use Mouse::Meta::TypeConstraint;
bc69ee88 8use Mouse::Exporter;
9
10Mouse::Exporter->setup_import_methods(
11 as_is => [qw(
5d4810c1 12 as where message optimize_as
13 from via
bc69ee88 14 type subtype coerce class_type role_type enum
15 find_type_constraint
16 )],
d60c78b9 17
bc69ee88 18 _export_to_main => 1,
139d92d2 19);
20
cceb0e25 21my %TYPE;
4188b837 22
5d4810c1 23sub as ($) { (as => $_[0]) }
24sub where (&) { (where => $_[0]) }
25sub message (&) { (message => $_[0]) }
26sub optimize_as (&) { (optimize_as => $_[0]) }
61a02a3a 27
73766a27 28sub from { @_ }
cd2b9201 29sub via (&) { $_[0] }
61a02a3a 30
321e5271 31BEGIN {
993e62a7 32 my %builtins = (
f5ee065f 33 Any => undef, # null check
34 Item => undef, # null check
35 Maybe => undef, # null check
73766a27 36
37 Bool => sub { $_[0] ? $_[0] eq '1' : 1 },
c91d12e0 38 Undef => sub { !defined($_[0]) },
39 Defined => sub { defined($_[0]) },
40 Value => sub { defined($_[0]) && !ref($_[0]) },
41 Num => sub { !ref($_[0]) && looks_like_number($_[0]) },
42 Int => sub { defined($_[0]) && !ref($_[0]) && $_[0] =~ /^-?[0-9]+$/ },
43 Str => sub { defined($_[0]) && !ref($_[0]) },
c91d12e0 44 Ref => sub { ref($_[0]) },
45
46 ScalarRef => sub { ref($_[0]) eq 'SCALAR' },
47 ArrayRef => sub { ref($_[0]) eq 'ARRAY' },
48 HashRef => sub { ref($_[0]) eq 'HASH' },
49 CodeRef => sub { ref($_[0]) eq 'CODE' },
50 RegexpRef => sub { ref($_[0]) eq 'Regexp' },
51 GlobRef => sub { ref($_[0]) eq 'GLOB' },
381f326a 52
53 FileHandle => sub {
c91d12e0 54 ref($_[0]) eq 'GLOB' && openhandle($_[0])
381f326a 55 or
c91d12e0 56 blessed($_[0]) && $_[0]->isa("IO::Handle")
abe4e887 57 },
381f326a 58
c91d12e0 59 Object => sub { blessed($_[0]) && blessed($_[0]) ne 'Regexp' },
73766a27 60
61 ClassName => sub { Mouse::Util::is_class_loaded($_[0]) },
d4571def 62 RoleName => sub { (Mouse::Util::find_meta($_[0]) || return 0)->isa('Mouse::Meta::Role') },
8a7f2a8a 63 );
993e62a7 64
65 while (my ($name, $code) = each %builtins) {
73766a27 66 $TYPE{$name} = Mouse::Meta::TypeConstraint->new(
3b89ea91 67 name => $name,
68 optimized => $code,
73766a27 69 );
78b13827 70 }
d3982c7e 71
deb9a0f3 72 sub optimized_constraints { # DEPRECATED
f5ee065f 73 Carp::cluck('optimized_constraints() has been deprecated');
74 return \%TYPE;
75 }
d4571def 76
993e62a7 77 my @builtins = keys %TYPE;
78 sub list_all_builtin_type_constraints { @builtins }
79
80 sub list_all_type_constraints { keys %TYPE }
381f326a 81}
d3982c7e 82
f5ee065f 83sub _create_type{
84 my $mode = shift;
85
73766a27 86 my $name;
f5ee065f 87 my %args;
73766a27 88
f5ee065f 89 if(@_ == 1 && ref $_[0]){ # @_ : { name => $name, where => ... }
90 %args = %{$_[0]};
73766a27 91 }
f5ee065f 92 elsif(@_ == 2 && ref $_[1]){ # @_ : $name => { where => ... }
73766a27 93 $name = $_[0];
f5ee065f 94 %args = %{$_[1]};
73766a27 95 }
f5ee065f 96 elsif(@_ % 2){ # @_ : $name => ( where => ... )
97 ($name, %args) = @_;
73766a27 98 }
f5ee065f 99 else{ # @_ : (name => $name, where => ...)
100 %args = @_;
73766a27 101 }
102
f5ee065f 103 if(!defined $name){
104 if(!defined($name = $args{name})){
105 $name = '__ANON__';
106 }
cd2b9201 107 }
d4571def 108
f5ee065f 109 $args{name} = $name;
b8434acc 110 my $parent;
111 if($mode eq 'subtype'){
112 $parent = delete $args{as};
113 if(!$parent){
114 $parent = delete $args{name};
115 $name = '__ANON__';
116 }
117 }
7dbebb1b 118
f5ee065f 119 my $package_defined_in = $args{package_defined_in} ||= caller(1);
d4571def 120
f5ee065f 121 my $existing = $TYPE{$name};
122 if($existing && $existing->{package_defined_in} ne $package_defined_in){
123 confess("The type constraint '$name' has already been created in "
124 . "$existing->{package_defined_in} and cannot be created again in $package_defined_in");
125 }
d4571def 126
b8434acc 127 $args{constraint} = delete $args{where} if exists $args{where};
24410e3a 128 $args{optimized} = delete $args{optimized_as} if exists $args{optimized_as};
0d9fea22 129
f5ee065f 130 my $constraint;
131 if($mode eq 'subtype'){
24410e3a 132 $constraint = find_or_create_isa_type_constraint($parent)->create_child_type(%args);
7a50b450 133 }
134 else{
f5ee065f 135 $constraint = Mouse::Meta::TypeConstraint->new(%args);
73766a27 136 }
7dbebb1b 137
f5ee065f 138 return $TYPE{$name} = $constraint;
139}
7dbebb1b 140
f5ee065f 141sub type {
142 return _create_type('type', @_);
143}
d4571def 144
f5ee065f 145sub subtype {
146 return _create_type('subtype', @_);
4188b837 147}
148
139d92d2 149sub coerce {
ffbbf459 150 my $type_name = shift;
61a02a3a 151
ffbbf459 152 my $type = find_type_constraint($type_name)
153 or confess("Cannot find type '$type_name', perhaps you forgot to load it.");
61a02a3a 154
ffbbf459 155 $type->_add_type_coercions(@_);
cd2b9201 156 return;
4188b837 157}
158
139d92d2 159sub class_type {
ecc6e3b1 160 my($name, $conf) = @_;
d9f8c878 161 if ($conf && $conf->{class}) {
162 # No, you're using this wrong
163 warn "class_type() should be class_type(ClassName). Perhaps you're looking for subtype $name => as '$conf->{class}'?";
f5ee065f 164 _create_type 'type', $name => (
165 as => $conf->{class},
166
167 type => 'Class',
168 );
a497c7d3 169 }
170 else {
f5ee065f 171 _create_type 'type', $name => (
172 optimized_as => sub { blessed($_[0]) && $_[0]->isa($name) },
173
174 type => 'Class',
d9f8c878 175 );
176 }
ecc6e3b1 177}
178
139d92d2 179sub role_type {
47f36c05 180 my($name, $conf) = @_;
f5ee065f 181 my $role = ($conf && $conf->{role}) ? $conf->{role} : $name;
182 _create_type 'type', $name => (
183 optimized_as => sub { blessed($_[0]) && does_role($_[0], $role) },
184
185 type => 'Role',
61a02a3a 186 );
47f36c05 187}
188
deb9a0f3 189sub typecast_constraints { # DEPRECATED
ffbbf459 190 my($class, $pkg, $type, $value) = @_;
2efc0af1 191 Carp::croak("wrong arguments count") unless @_ == 4;
eec1bb49 192
e763d56e 193 Carp::cluck("typecast_constraints() has been deprecated, which was an internal utility anyway");
194
ffbbf459 195 return $type->coerce($value);
4188b837 196}
197
d44f0d03 198sub enum {
f5ee065f 199 my($name, %valid);
200
01904723 201 # enum ['small', 'medium', 'large']
202 if (ref($_[0]) eq 'ARRAY') {
f5ee065f 203 %valid = map{ $_ => undef } @{ $_[0] };
204 $name = sprintf '(%s)', join '|', sort @{$_[0]};
205 }
206 # enum size => 'small', 'medium', 'large'
207 else{
208 $name = shift;
209 %valid = map{ $_ => undef } @_;
210 }
211 return _create_type 'type', $name => (
212 optimized_as => sub{ defined($_[0]) && !ref($_[0]) && exists $valid{$_[0]} },
213
214 type => 'Enum',
215 );
216}
217
218sub _find_or_create_regular_type{
219 my($spec) = @_;
220
221 return $TYPE{$spec} if exists $TYPE{$spec};
01904723 222
7727a2f0 223 my $meta = Mouse::Util::get_metaclass_by_name($spec);
f5ee065f 224
225 if(!$meta){
226 return;
01904723 227 }
228
f5ee065f 229 my $check;
230 my $type;
57f0e313 231 if($meta->isa('Mouse::Meta::Role')){
f5ee065f 232 $check = sub{
233 return blessed($_[0]) && $_[0]->does($spec);
234 };
235 $type = 'Role';
236 }
237 else{
238 $check = sub{
239 return blessed($_[0]) && $_[0]->isa($spec);
240 };
241 $type = 'Class';
242 }
243
f5ee065f 244 return $TYPE{$spec} = Mouse::Meta::TypeConstraint->new(
3b89ea91 245 name => $spec,
246 optimized => $check,
f5ee065f 247
3b89ea91 248 type => $type,
d44f0d03 249 );
250}
251
f5ee065f 252$TYPE{ArrayRef}{constraint_generator} = sub {
253 my($type_parameter) = @_;
3b89ea91 254 my $check = $type_parameter->_compiled_type_constraint;
321e5271 255
f5ee065f 256 return sub{
257 foreach my $value (@{$_}) {
258 return undef unless $check->($value);
259 }
260 return 1;
261 }
262};
263$TYPE{HashRef}{constraint_generator} = sub {
264 my($type_parameter) = @_;
3b89ea91 265 my $check = $type_parameter->_compiled_type_constraint;
f5ee065f 266
267 return sub{
268 foreach my $value(values %{$_}){
269 return undef unless $check->($value);
270 }
271 return 1;
272 };
273};
2efc0af1 274
f5ee065f 275# 'Maybe' type accepts 'Any', so it requires parameters
276$TYPE{Maybe}{constraint_generator} = sub {
277 my($type_parameter) = @_;
3b89ea91 278 my $check = $type_parameter->_compiled_type_constraint;
2efc0af1 279
f5ee065f 280 return sub{
281 return !defined($_) || $check->($_);
282 };
283};
284
285sub _find_or_create_parameterized_type{
286 my($base, $param) = @_;
287
288 my $name = sprintf '%s[%s]', $base->name, $param->name;
289
290 $TYPE{$name} ||= do{
f5ee065f 291 my $generator = $base->{constraint_generator};
292
293 if(!$generator){
294 confess("The $name constraint cannot be used, because $param doesn't subtype from a parameterizable type");
2efc0af1 295 }
f5ee065f 296
297 Mouse::Meta::TypeConstraint->new(
298 name => $name,
299 parent => $base,
300 constraint => $generator->($param),
301
302 type => 'Parameterized',
303 );
304 }
305}
306sub _find_or_create_union_type{
30b28db3 307 my @types = sort{ $a cmp $b } map{ $_->{type_constraints} ? @{$_->{type_constraints}} : $_ } @_;
f5ee065f 308
30b28db3 309 my $name = join '|', @types;
f5ee065f 310
311 $TYPE{$name} ||= do{
f5ee065f 312 return Mouse::Meta::TypeConstraint->new(
3b89ea91 313 name => $name,
314 type_constraints => \@types,
f5ee065f 315
3b89ea91 316 type => 'Union',
f5ee065f 317 );
318 };
319}
320
321# The type parser
322sub _parse_type{
323 my($spec, $start) = @_;
324
325 my @list;
326 my $subtype;
327
328 my $len = length $spec;
329 my $i;
330
331 for($i = $start; $i < $len; $i++){
332 my $char = substr($spec, $i, 1);
333
334 if($char eq '['){
57f0e313 335 my $base = _find_or_create_regular_type( substr($spec, $start, $i - $start) )
f5ee065f 336 or return;
337
338 ($i, $subtype) = _parse_type($spec, $i+1)
339 or return;
340 $start = $i+1; # reset
341
342 push @list, _find_or_create_parameterized_type($base => $subtype);
321e5271 343 }
f5ee065f 344 elsif($char eq ']'){
345 $len = $i+1;
346 last;
321e5271 347 }
f5ee065f 348 elsif($char eq '|'){
57f0e313 349 my $type = _find_or_create_regular_type( substr($spec, $start, $i - $start) );
350
57f0e313 351 if(!defined $type){
3b89ea91 352 # XXX: Mouse creates a new class type, but Moose does not.
353 $type = class_type( substr($spec, $start, $i - $start) );
57f0e313 354 }
f5ee065f 355
356 push @list, $type;
357
358 ($i, $subtype) = _parse_type($spec, $i+1)
359 or return;
360
361 $start = $i+1; # reset
362
363 push @list, $subtype;
321e5271 364 }
365 }
f5ee065f 366 if($i - $start){
367 push @list, _find_or_create_regular_type(substr $spec, $start, $i - $start);
368 }
321e5271 369
f5ee065f 370 if(@list == 0){
371 return;
372 }
373 elsif(@list == 1){
374 return ($len, $list[0]);
993e62a7 375 }
376 else{
f5ee065f 377 return ($len, _find_or_create_union_type(@list));
993e62a7 378 }
321e5271 379}
380
f5ee065f 381
382sub find_type_constraint {
383 my($spec) = @_;
e98220ab 384 return $spec if blessed($spec) && $spec->isa('Mouse::Meta::TypeConstraint');
f5ee065f 385
386 $spec =~ s/\s+//g;
387 return $TYPE{$spec};
2efc0af1 388}
389
f5ee065f 390sub find_or_parse_type_constraint {
391 my($spec) = @_;
e98220ab 392 return $spec if blessed($spec) && $spec->isa('Mouse::Meta::TypeConstraint');
9c85e9dc 393
f5ee065f 394 $spec =~ s/\s+//g;
395 return $TYPE{$spec} || do{
396 my($pos, $type) = _parse_type($spec, 0);
397 $type;
398 };
399}
321e5271 400
f5ee065f 401sub find_or_create_does_type_constraint{
9f74c401 402 my $type = find_or_parse_type_constraint(@_) || role_type(@_);
94593ae8 403
f5ee065f 404 if($type->{type} && $type->{type} ne 'Role'){
405 Carp::cluck("$type is not a role type");
321e5271 406 }
f5ee065f 407 return $type;
408}
409
410sub find_or_create_isa_type_constraint {
411 return find_or_parse_type_constraint(@_) || class_type(@_);
321e5271 412}
413
d60c78b9 4141;
415
6feb83f1 416__END__
417
418=head1 NAME
419
5893ee36 420Mouse::Util::TypeConstraints - Type constraint system for Mouse
421
422=head2 SYNOPSIS
423
424 use Mouse::Util::TypeConstraints;
425
426 subtype 'Natural'
427 => as 'Int'
428 => where { $_ > 0 };
429
430 subtype 'NaturalLessThanTen'
431 => as 'Natural'
432 => where { $_ < 10 }
433 => message { "This number ($_) is not less than ten!" };
434
435 coerce 'Num'
436 => from 'Str'
437 => via { 0+$_ };
438
439 enum 'RGBColors' => qw(red green blue);
440
441 no Mouse::Util::TypeConstraints;
442
443=head1 DESCRIPTION
444
445This module provides Mouse with the ability to create custom type
446constraints to be used in attribute definition.
447
448=head2 Important Caveat
449
450This is B<NOT> a type system for Perl 5. These are type constraints,
451and they are not used by Mouse unless you tell it to. No type
452inference is performed, expressions are not typed, etc. etc. etc.
453
454A type constraint is at heart a small "check if a value is valid"
455function. A constraint can be associated with an attribute. This
456simplifies parameter validation, and makes your code clearer to read,
457because you can refer to constraints by name.
458
459=head2 Slightly Less Important Caveat
460
461It is B<always> a good idea to quote your type names.
462
463This prevents Perl from trying to execute the call as an indirect
464object call. This can be an issue when you have a subtype with the
465same name as a valid class.
466
467For instance:
468
469 subtype DateTime => as Object => where { $_->isa('DateTime') };
470
471will I<just work>, while this:
472
473 use DateTime;
474 subtype DateTime => as Object => where { $_->isa('DateTime') };
475
476will fail silently and cause many headaches. The simple way to solve
477this, as well as future proof your subtypes from classes which have
478yet to have been created, is to quote the type name:
479
480 use DateTime;
481 subtype 'DateTime' => as 'Object' => where { $_->isa('DateTime') };
482
483=head2 Default Type Constraints
484
485This module also provides a simple hierarchy for Perl 5 types, here is
486that hierarchy represented visually.
487
488 Any
489 Item
490 Bool
491 Maybe[`a]
492 Undef
493 Defined
494 Value
495 Num
496 Int
497 Str
498 ClassName
499 RoleName
500 Ref
501 ScalarRef
502 ArrayRef[`a]
503 HashRef[`a]
504 CodeRef
505 RegexpRef
506 GlobRef
507 FileHandle
508 Object
5893ee36 509
510B<NOTE:> Any type followed by a type parameter C<[`a]> can be
511parameterized, this means you can say:
512
513 ArrayRef[Int] # an array of integers
514 HashRef[CodeRef] # a hash of str to CODE ref mappings
515 Maybe[Str] # value may be a string, may be undefined
516
517If Mouse finds a name in brackets that it does not recognize as an
518existing type, it assumes that this is a class name, for example
519C<ArrayRef[DateTime]>.
520
521B<NOTE:> Unless you parameterize a type, then it is invalid to include
522the square brackets. I.e. C<ArrayRef[]> will be treated as a new type
523name, I<not> as a parameterization of C<ArrayRef>.
524
525B<NOTE:> The C<Undef> type constraint for the most part works
526correctly now, but edge cases may still exist, please use it
527sparingly.
528
529B<NOTE:> The C<ClassName> type constraint does a complex package
530existence check. This means that your class B<must> be loaded for this
531type constraint to pass.
532
533B<NOTE:> The C<RoleName> constraint checks a string is a I<package
534name> which is a role, like C<'MyApp::Role::Comparable'>. The C<Role>
535constraint checks that an I<object does> the named role.
536
537=head2 Type Constraint Naming
538
539Type name declared via this module can only contain alphanumeric
540characters, colons (:), and periods (.).
541
542Since the types created by this module are global, it is suggested
543that you namespace your types just as you would namespace your
544modules. So instead of creating a I<Color> type for your
545B<My::Graphics> module, you would call the type
546I<My::Graphics::Types::Color> instead.
547
548=head2 Use with Other Constraint Modules
549
550This module can play nicely with other constraint modules with some
551slight tweaking. The C<where> clause in types is expected to be a
552C<CODE> reference which checks it's first argument and returns a
553boolean. Since most constraint modules work in a similar way, it
554should be simple to adapt them to work with Mouse.
555
556For instance, this is how you could use it with
557L<Declare::Constraints::Simple> to declare a completely new type.
558
559 type 'HashOfArrayOfObjects',
560 {
561 where => IsHashRef(
562 -keys => HasLength,
563 -values => IsArrayRef(IsObject)
564 )
565 };
566
567Here is an example of using L<Test::Deep> and it's non-test
568related C<eq_deeply> function.
569
570 type 'ArrayOfHashOfBarsAndRandomNumbers'
571 => where {
572 eq_deeply($_,
573 array_each(subhashof({
574 bar => isa('Bar'),
575 random_number => ignore()
576 })))
577 };
6feb83f1 578
579=head1 METHODS
580
24410e3a 581=head2 C<< list_all_builtin_type_constraints -> (Names) >>
6feb83f1 582
24410e3a 583Returns the names of builtin type constraints.
584
585=head2 C<< list_all_type_constraints -> (Names) >>
586
587Returns the names of all the type constraints.
6feb83f1 588
c91d12e0 589=head1 FUNCTIONS
590
591=over 4
592
1820fffe 593=item C<< subtype 'Name' => as 'Parent' => where { } ... -> Mouse::Meta::TypeConstraint >>
c91d12e0 594
1820fffe 595=item C<< subtype as 'Parent' => where { } ... -> Mouse::Meta::TypeConstraint >>
c91d12e0 596
1820fffe 597=item C<< class_type ($class, ?$options) -> Mouse::Meta::TypeConstraint >>
c91d12e0 598
1820fffe 599=item C<< role_type ($role, ?$options) -> Mouse::Meta::TypeConstraint >>
c91d12e0 600
1820fffe 601=item C<< enum (\@values) -> Mouse::Meta::TypeConstraint >>
602
603=back
604
605=over 4
606
607=item C<< find_type_constraint(Type) -> Mouse::Meta::TypeConstraint >>
c91d12e0 608
609=back
610
5893ee36 611=head1 THANKS
612
1820fffe 613Much of this documentation was taken from C<Moose::Util::TypeConstraints>
614
615=head1 SEE ALSO
616
617L<Moose::Util::TypeConstraints>
5893ee36 618
6feb83f1 619=cut
620
621