Add a dummy TypeConstraint namespace so you can treat them as objects
[gitmo/Mouse.git] / lib / Mouse / Util / TypeConstraints.pm
1 package Mouse::Util::TypeConstraints;
2 use strict;
3 use warnings;
4 use base 'Exporter';
5
6 use Carp ();
7 use Scalar::Util qw/blessed looks_like_number openhandle/;
8
9 our @EXPORT = qw(
10     as where message from via type subtype coerce class_type role_type enum
11 );
12
13 my %TYPE;
14 my %TYPE_SOURCE;
15 my %COERCE;
16 my %COERCE_KEYS;
17
18 sub as ($) {
19     as => $_[0]
20 }
21 sub where (&) {
22     where => $_[0]
23 }
24 sub message (&) {
25     message => $_[0]
26 }
27
28 sub from { @_ }
29 sub via (&) {
30     $_[0]
31 }
32
33 BEGIN {
34     no warnings 'uninitialized';
35     %TYPE = (
36         Any        => sub { 1 },
37         Item       => sub { 1 },
38         Bool       => sub {
39             !defined($_[0]) || $_[0] eq "" || "$_[0]" eq '1' || "$_[0]" eq '0'
40         },
41         Undef      => sub { !defined($_[0]) },
42         Defined    => sub { defined($_[0]) },
43         Value      => sub { defined($_[0]) && !ref($_[0]) },
44         Num        => sub { !ref($_[0]) && looks_like_number($_[0]) },
45         Int        => sub { defined($_[0]) && !ref($_[0]) && $_[0] =~ /^-?[0-9]+$/ },
46         Str        => sub { defined($_[0]) && !ref($_[0]) },
47         ClassName  => sub { Mouse::is_class_loaded($_[0]) },
48         Ref        => sub { ref($_[0]) },
49
50         ScalarRef  => sub { ref($_[0]) eq 'SCALAR' },
51         ArrayRef   => sub { ref($_[0]) eq 'ARRAY'  },
52         HashRef    => sub { ref($_[0]) eq 'HASH'   },
53         CodeRef    => sub { ref($_[0]) eq 'CODE'   },
54         RegexpRef  => sub { ref($_[0]) eq 'Regexp' },
55         GlobRef    => sub { ref($_[0]) eq 'GLOB'   },
56
57         FileHandle => sub {
58             ref($_[0]) eq 'GLOB' && openhandle($_[0])
59             or
60             blessed($_[0]) && $_[0]->isa("IO::Handle")
61         },
62
63         Object     => sub { blessed($_[0]) && blessed($_[0]) ne 'Regexp' },
64     );
65     foreach my $code (values %TYPE) {
66         bless $code, 'Mouse::Meta::TypeConstraint';
67     }
68
69     sub optimized_constraints { \%TYPE }
70     my @TYPE_KEYS = keys %TYPE;
71     sub list_all_builtin_type_constraints { @TYPE_KEYS }
72
73     @TYPE_SOURCE{@TYPE_KEYS} = (__PACKAGE__) x @TYPE_KEYS;
74 }
75
76 sub type {
77     my $pkg = caller(0);
78     my($name, %conf) = @_;
79     if ($TYPE{$name} && $TYPE_SOURCE{$name} ne $pkg) {
80         Carp::croak "The type constraint '$name' has already been created in $TYPE_SOURCE{$name} and cannot be created again in $pkg";
81     };
82     my $constraint = $conf{where} || do {
83         my $as = delete $conf{as} || 'Any';
84         if (! exists $TYPE{$as}) {
85             $TYPE{$as} = _build_type_constraint($as);
86         }
87         $TYPE{$as};
88     };
89
90     $TYPE_SOURCE{$name} = $pkg;
91     $TYPE{$name} = sub { local $_ = $_[0]; $constraint->($_[0]) };
92 }
93
94 sub subtype {
95     my $pkg = caller(0);
96     my($name, %conf) = @_;
97     if ($TYPE{$name} && $TYPE_SOURCE{$name} ne $pkg) {
98         Carp::croak "The type constraint '$name' has already been created in $TYPE_SOURCE{$name} and cannot be created again in $pkg";
99     };
100     my $constraint = $conf{where};
101     my $as_constraint = find_or_create_isa_type_constraint($conf{as} || 'Any');
102
103     $TYPE_SOURCE{$name} = $pkg;
104     $TYPE{$name} = $constraint ? 
105         sub {
106             local $_ = $_[0];
107             $as_constraint->($_[0]) && $constraint->($_[0])
108         } :
109         sub {
110             local $_ = $_[0];
111             $as_constraint->($_[0]);
112         }
113     ;
114
115     return $name;
116 }
117
118 sub coerce {
119     my($name, %conf) = @_;
120
121     Carp::croak "Cannot find type '$name', perhaps you forgot to load it."
122         unless $TYPE{$name};
123
124     unless ($COERCE{$name}) {
125         $COERCE{$name}      = {};
126         $COERCE_KEYS{$name} = [];
127     }
128     while (my($type, $code) = each %conf) {
129         Carp::croak "A coercion action already exists for '$type'"
130             if $COERCE{$name}->{$type};
131
132         if (! $TYPE{$type}) {
133             # looks parameterized
134             if ($type =~ /^[^\[]+\[.+\]$/) {
135                 _build_type_constraint($type);
136             } else {
137                 Carp::croak "Could not find the type constraint ($type) to coerce from"
138             }
139         }
140
141         push @{ $COERCE_KEYS{$name} }, $type;
142         $COERCE{$name}->{$type} = $code;
143     }
144 }
145
146 sub class_type {
147     my($name, $conf) = @_;
148     if ($conf && $conf->{class}) {
149         # No, you're using this wrong
150         warn "class_type() should be class_type(ClassName). Perhaps you're looking for subtype $name => as '$conf->{class}'?";
151         subtype($name, as => $conf->{class});
152     } else {
153         subtype(
154             $name => where => sub { $_->isa($name) }
155         );
156     }
157 }
158
159 sub role_type {
160     my($name, $conf) = @_;
161     my $role = $conf->{role};
162     subtype(
163         $name => where => sub {
164             return unless defined $_ && ref($_) && $_->isa('Mouse::Object');
165             $_->meta->does_role($role);
166         }
167     );
168 }
169
170 sub typecast_constraints {
171     my($class, $pkg, $type_constraint, $types, $value) = @_;
172
173     local $_;
174     for my $type (ref($types) eq 'ARRAY' ? @{ $types } : ( $types )) {
175         next unless $COERCE{$type};
176         for my $coerce_type (@{ $COERCE_KEYS{$type}}) {
177             $_ = $value;
178             next unless $TYPE{$coerce_type}->($value);
179             $_ = $value;
180             $_ = $COERCE{$type}->{$coerce_type}->($value);
181             return $_ if $type_constraint->($_);
182         }
183     }
184     return $value;
185 }
186
187 my $serial_enum = 0;
188 sub enum {
189     # enum ['small', 'medium', 'large']
190     if (ref($_[0]) eq 'ARRAY') {
191         my @elements = @{ shift @_ };
192
193         my $name = 'Mouse::Util::TypeConstaints::Enum::Serial::'
194                  . ++$serial_enum;
195         enum($name, @elements);
196         return $name;
197     }
198
199     # enum size => 'small', 'medium', 'large'
200     my $name = shift;
201     my %is_valid = map { $_ => 1 } @_;
202
203     subtype(
204         $name => where => sub { $is_valid{$_} }
205     );
206 }
207
208 sub _build_type_constraint {
209
210     my $spec = shift;
211     my $code;
212     if ($spec =~ /^([^\[]+)\[(.+)\]$/) {
213         # parameterized
214         my $constraint = $1;
215         my $param      = $2;
216         my $parent;
217         if ($constraint eq 'Maybe') {
218             $parent = _build_type_constraint('Undef');
219         } else {
220             $parent = _build_type_constraint($constraint);
221         }
222         my $child = _build_type_constraint($param);
223         if ($constraint eq 'ArrayRef') {
224             my $code_str = 
225                 "#line " . __LINE__ . ' "' . __FILE__ . "\"\n" .
226                 "sub {\n" .
227                 "    if (\$parent->(\$_[0])) {\n" .
228                 "        foreach my \$e (\@{\$_[0]}) {\n" .
229                 "            return () unless \$child->(\$e);\n" .
230                 "        }\n" .
231                 "        return 1;\n" .
232                 "    }\n" .
233                 "    return ();\n" .
234                 "};\n"
235             ;
236             $code = eval $code_str or Carp::confess("Failed to generate inline type constraint: $@");
237         } elsif ($constraint eq 'HashRef') {
238             my $code_str = 
239                 "#line " . __LINE__ . ' "' . __FILE__ . "\"\n" .
240                 "sub {\n" .
241                 "    if (\$parent->(\$_[0])) {\n" .
242                 "        foreach my \$e (values \%{\$_[0]}) {\n" .
243                 "            return () unless \$child->(\$e);\n" .
244                 "        }\n" .
245                 "        return 1;\n" .
246                 "    }\n" .
247                 "    return ();\n" .
248                 "};\n"
249             ;
250             $code = eval $code_str or Carp::confess($@);
251         } elsif ($constraint eq 'Maybe') {
252             my $code_str =
253                 "#line " . __LINE__ . ' "' . __FILE__ . "\"\n" .
254                 "sub {\n" .
255                 "    return \$child->(\$_[0]) || \$parent->(\$_[0]);\n" .
256                 "};\n"
257             ;
258             $code = eval $code_str or Carp::confess($@);
259         } else {
260             Carp::confess("Support for parameterized types other than ArrayRef or HashRef is not implemented yet");
261         }
262         $TYPE{$spec} = $code;
263     } else {
264         $code = $TYPE{ $spec };
265         if (! $code) {
266             my $code_str = 
267                 "#line " . __LINE__ . ' "' . __FILE__ . "\"\n" .
268                 "sub {\n" .
269                 "    Scalar::Util::blessed(\$_[0]) && \$_[0]->isa('$spec');\n" .
270                 "}"
271             ;
272             $code = eval $code_str  or Carp::confess($@);
273             $TYPE{$spec} = bless $code, 'Mouse::Meta::TypeConstraint';
274         }
275     }
276     return bless $code, 'Mouse::Meta::TypeConstraint';
277 }
278
279 sub find_type_constraint {
280     my $type_constraint = shift;
281     return $TYPE{$type_constraint};
282 }
283
284 sub find_or_create_isa_type_constraint {
285     my $type_constraint = shift;
286
287     my $code;
288
289     $type_constraint =~ s/\s+//g;
290     my @type_constraints = split /\|/, $type_constraint;
291     if (@type_constraints == 1) {
292         $code = $TYPE{$type_constraints[0]} ||
293             _build_type_constraint($type_constraints[0]);
294     } else {
295         my @code_list = map {
296             $TYPE{$_} || _build_type_constraint($_)
297         } @type_constraints;
298         $code = bless sub {
299             my $i = 0;
300             for my $code (@code_list) {
301                 return 1 if $code->($_[0]);
302             }
303             return 0;
304         }, 'Mouse::Meta::TypeConstraint';
305     }
306     return $code;
307 }
308
309 package # Hide from pause
310     Mouse::Meta::TypeConstraint;
311
312 sub check { 
313     $_[0]->($_[1])
314 }
315
316
317 1;
318
319 __END__
320
321 =head1 NAME
322
323 Mouse::Util::TypeConstraints - simple type constraints
324
325 =head1 METHODS
326
327 =head2 optimized_constraints -> HashRef[CODE]
328
329 Returns the simple type constraints that Mouse understands.
330
331 =head1 FUNCTIONS
332
333 =over 4
334
335 =item B<subtype 'Name' => as 'Parent' => where { } ...>
336
337 =item B<subtype as 'Parent' => where { } ...>
338
339 =item B<class_type ($class, ?$options)>
340
341 =item B<role_type ($role, ?$options)>
342
343 =item B<enum (\@values)>
344
345 =back
346
347 =cut
348
349