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