3669b873d469316e1762090df176ec119d7b801f
[gitmo/Moose.git] / lib / Moose / Meta / Role.pm
1
2 package Moose::Meta::Role;
3
4 use strict;
5 use warnings;
6 use metaclass;
7
8 use Carp         'confess';
9 use Scalar::Util 'blessed';
10 use B            'svref_2object';
11
12 use Moose::Meta::Class;
13
14 our $VERSION = '0.04';
15
16 use base 'Class::MOP::Module';
17
18 ## Attributes
19
20 ## roles
21
22 __PACKAGE__->meta->add_attribute('roles' => (
23     reader  => 'get_roles',
24     default => sub { [] }
25 ));
26
27 ## excluded roles
28
29 __PACKAGE__->meta->add_attribute('excluded_roles_map' => (
30     reader  => 'get_excluded_roles_map',
31     default => sub { {} }
32 ));
33
34 ## attributes
35
36 __PACKAGE__->meta->add_attribute('attribute_map' => (
37     reader   => 'get_attribute_map',
38     default  => sub { {} }
39 ));
40
41 ## required methods
42
43 __PACKAGE__->meta->add_attribute('required_methods' => (
44     reader  => 'get_required_methods_map',
45     default => sub { {} }
46 ));
47
48 ## Methods 
49
50 sub method_metaclass { 'Moose::Meta::Role::Method' }
51
52 ## subroles
53
54 sub add_role {
55     my ($self, $role) = @_;
56     (blessed($role) && $role->isa('Moose::Meta::Role'))
57         || confess "Roles must be instances of Moose::Meta::Role";
58     push @{$self->get_roles} => $role;
59 }
60
61 sub calculate_all_roles {
62     my $self = shift;
63     my %seen;
64     grep { !$seen{$_->name}++ } $self, map { $_->calculate_all_roles } @{ $self->get_roles };
65 }
66
67 sub does_role {
68     my ($self, $role_name) = @_;
69     (defined $role_name)
70         || confess "You must supply a role name to look for";
71     # if we are it,.. then return true
72     return 1 if $role_name eq $self->name;
73     # otherwise.. check our children
74     foreach my $role (@{$self->get_roles}) {
75         return 1 if $role->does_role($role_name);
76     }
77     return 0;
78 }
79
80 ## excluded roles
81
82 sub add_excluded_roles {
83     my ($self, @excluded_role_names) = @_;
84     $self->get_excluded_roles_map->{$_} = undef foreach @excluded_role_names;
85 }
86
87 sub get_excluded_roles_list {
88     my ($self) = @_;
89     keys %{$self->get_excluded_roles_map};
90 }
91
92 sub excludes_role {
93     my ($self, $role_name) = @_;
94     exists $self->get_excluded_roles_map->{$role_name} ? 1 : 0;
95 }
96
97 ## required methods
98
99 sub add_required_methods {
100     my ($self, @methods) = @_;
101     $self->get_required_methods_map->{$_} = undef foreach @methods;
102 }
103
104 sub remove_required_methods {
105     my ($self, @methods) = @_;
106     delete $self->get_required_methods_map->{$_} foreach @methods;
107 }
108
109 sub get_required_method_list {
110     my ($self) = @_;
111     keys %{$self->get_required_methods_map};
112 }
113
114 sub requires_method {
115     my ($self, $method_name) = @_;
116     exists $self->get_required_methods_map->{$method_name} ? 1 : 0;
117 }
118
119 sub _clean_up_required_methods {
120     my $self = shift;
121     foreach my $method ($self->get_required_method_list) {
122         $self->remove_required_methods($method)
123             if $self->has_method($method);
124     } 
125 }
126
127 ## methods
128
129 sub get_method          { (shift)->Moose::Meta::Class::get_method(@_)          }
130 sub find_method_by_name { (shift)->Moose::Meta::Class::find_method_by_name(@_) }
131 sub has_method          { (shift)->Moose::Meta::Class::has_method(@_)          }
132 sub alias_method        { (shift)->Moose::Meta::Class::alias_method(@_)        }
133 sub get_method_list { 
134     my ($self) = @_;
135     grep { 
136         # NOTE:
137         # this is a kludge for now,... these functions 
138         # should not be showing up in the list at all, 
139         # but they do, so we need to switch Moose::Role
140         # and Moose to use Sub::Exporter to prevent this
141         !/^(meta|has|extends|blessed|confess|augment|inner|override|super|before|after|around|with|requires)$/ 
142     } $self->Moose::Meta::Class::get_method_list;
143 }
144
145 # ... however the items in statis (attributes & method modifiers)
146 # can be removed and added to through this API
147
148 # attributes
149
150 sub add_attribute {
151     my $self = shift;
152     my $name = shift;
153     my $attr_desc;
154     if (scalar @_ == 1 && ref($_[0]) eq 'HASH') {
155         $attr_desc = $_[0];
156     }
157     else {
158         $attr_desc = { @_ };
159     }
160     $self->get_attribute_map->{$name} = $attr_desc;
161 }
162
163 sub has_attribute {
164     my ($self, $name) = @_;
165     exists $self->get_attribute_map->{$name} ? 1 : 0;
166 }
167
168 sub get_attribute {
169     my ($self, $name) = @_;
170     $self->get_attribute_map->{$name}
171 }
172
173 sub remove_attribute {
174     my ($self, $name) = @_;
175     delete $self->get_attribute_map->{$name}
176 }
177
178 sub get_attribute_list {
179     my ($self) = @_;
180     keys %{$self->get_attribute_map};
181 }
182
183
184 ## applying a role to a class ...
185
186 sub _check_excluded_roles {
187     my ($self, $other) = @_;
188     if ($other->excludes_role($self->name)) {
189         confess "Conflict detected: " . $other->name . " excludes role '" . $self->name . "'";
190     }
191     foreach my $excluded_role_name ($self->get_excluded_roles_list) {
192         if ($other->does_role($excluded_role_name)) { 
193             confess "The class " . $other->name . " does the excluded role '$excluded_role_name'";
194         }
195         else {
196             if ($other->isa('Moose::Meta::Role')) {
197                 $other->add_excluded_roles($excluded_role_name);
198             }
199             # else -> ignore it :) 
200         }
201     }    
202 }
203
204 sub _check_required_methods {
205     my ($self, $other) = @_;
206     # NOTE:
207     # we might need to move this down below the 
208     # the attributes so that we can require any 
209     # attribute accessors. However I am thinking 
210     # that maybe those are somehow exempt from 
211     # the require methods stuff.  
212     foreach my $required_method_name ($self->get_required_method_list) {
213         
214         unless ($other->find_method_by_name($required_method_name)) {
215             if ($other->isa('Moose::Meta::Role')) {
216                 $other->add_required_methods($required_method_name);
217             }
218             else {
219                 confess "'" . $self->name . "' requires the method '$required_method_name' " . 
220                         "to be implemented by '" . $other->name . "'";
221             }
222         }
223     }    
224 }
225
226 sub _apply_attributes {
227     my ($self, $other) = @_;    
228     foreach my $attribute_name ($self->get_attribute_list) {
229         # it if it has one already
230         if ($other->has_attribute($attribute_name) &&
231             # make sure we haven't seen this one already too
232             $other->get_attribute($attribute_name) != $self->get_attribute($attribute_name)) {
233             # see if we are being composed  
234             # into a role or not
235             if ($other->isa('Moose::Meta::Role')) {                
236                 # all attribute conflicts between roles 
237                 # result in an immediate fatal error 
238                 confess "Role '" . $self->name . "' has encountered an attribute conflict " . 
239                         "during composition. This is fatal error and cannot be disambiguated.";
240             }
241             else {
242                 # but if this is a class, we 
243                 # can safely skip adding the 
244                 # attribute to the class
245                 next;
246             }
247         }
248         else {
249             $other->add_attribute(
250                 $attribute_name,
251                 $self->get_attribute($attribute_name)
252             );
253         }
254     }    
255 }
256
257 sub _apply_methods {
258     my ($self, $other) = @_;   
259     foreach my $method_name ($self->get_method_list) {
260         # it if it has one already
261         if ($other->has_method($method_name) &&
262             # and if they are not the same thing ...
263             $other->get_method($method_name) != $self->get_method($method_name)) {
264             # see if we are composing into a role
265             if ($other->isa('Moose::Meta::Role')) { 
266                 # method conflicts between roles result 
267                 # in the method becoming a requirement
268                 $other->add_required_methods($method_name);
269                 # NOTE:
270                 # we have to remove the method from our 
271                 # role, if this is being called from combine()
272                 # which means the meta is an anon class
273                 # this *may* cause problems later, but it 
274                 # is probably fairly safe to assume that 
275                 # anon classes will only be used internally
276                 # or by people who know what they are doing
277                 $other->Moose::Meta::Class::remove_method($method_name)
278                     if $other->name =~ /__ANON__/;
279             }
280             else {
281                 next;
282             }
283         }
284         else {
285             # add it, although it could be overriden 
286             $other->alias_method(
287                 $method_name,
288                 $self->get_method($method_name)
289             );
290         }
291     }     
292 }
293
294 sub apply {
295     my ($self, $other) = @_;
296     
297     ($other->isa('Moose::Meta::Class') || $other->isa('Moose::Meta::Role'))
298         || confess "You must apply a role to a metaclass, not ($other)";
299     
300     $self->_check_excluded_roles($other);
301     $self->_check_required_methods($other);  
302
303     $self->_apply_attributes($other);         
304     $self->_apply_methods($other);         
305
306     $other->add_role($self);
307 }
308
309 my $anon_counter = 0;
310
311 sub combine {
312     my ($class, @roles) = @_;
313     
314     my $pkg_name = __PACKAGE__ . "::__ANON__::" . $anon_counter++;
315     eval "package " . $pkg_name . "; our \$VERSION = '0.00';";
316     die $@ if $@;
317     
318     my $combined = $class->initialize($pkg_name);
319     
320     foreach my $role (@roles) {
321         $role->apply($combined);
322     }
323     
324     $combined->_clean_up_required_methods;   
325     
326     return $combined;
327 }
328
329 package Moose::Meta::Role::Method;
330
331 use strict;
332 use warnings;
333
334 our $VERSION = '0.01';
335
336 use base 'Class::MOP::Method';
337
338 1;
339
340 __END__
341
342 =pod
343
344 =head1 NAME
345
346 Moose::Meta::Role - The Moose Role metaclass
347
348 =head1 DESCRIPTION
349
350 Moose's Roles are being actively developed, please see L<Moose::Role> 
351 for more information. For the most part, this has no user-serviceable 
352 parts inside. It's API is still subject to some change (although 
353 probably not that much really).
354
355 =head1 METHODS
356
357 =over 4
358
359 =item B<meta>
360
361 =item B<new>
362
363 =item B<apply>
364
365 =item B<combine>
366
367 =back
368
369 =over 4
370
371 =item B<name>
372
373 =item B<version>
374
375 =item B<role_meta>
376
377 =back
378
379 =over 4
380
381 =item B<get_roles>
382
383 =item B<add_role>
384
385 =item B<does_role>
386
387 =back
388
389 =over 4
390
391 =item B<add_excluded_roles>
392
393 =item B<excludes_role>
394
395 =item B<get_excluded_roles_list>
396
397 =item B<get_excluded_roles_map>
398
399 =item B<calculate_all_roles>
400
401 =back
402
403 =over 4
404
405 =item B<method_metaclass>
406
407 =item B<find_method_by_name>
408
409 =item B<get_method>
410
411 =item B<has_method>
412
413 =item B<alias_method>
414
415 =item B<get_method_list>
416
417 =back
418
419 =over 4
420
421 =item B<add_attribute>
422
423 =item B<has_attribute>
424
425 =item B<get_attribute>
426
427 =item B<get_attribute_list>
428
429 =item B<get_attribute_map>
430
431 =item B<remove_attribute>
432
433 =back
434
435 =over 4
436
437 =item B<add_required_methods>
438
439 =item B<remove_required_methods>
440
441 =item B<get_required_method_list>
442
443 =item B<get_required_methods_map>
444
445 =item B<requires_method>
446
447 =back
448
449 =head1 BUGS
450
451 All complex software has bugs lurking in it, and this module is no 
452 exception. If you find a bug please either email me, or add the bug
453 to cpan-RT.
454
455 =head1 AUTHOR
456
457 Stevan Little E<lt>stevan@iinteractive.comE<gt>
458
459 =head1 COPYRIGHT AND LICENSE
460
461 Copyright 2006 by Infinity Interactive, Inc.
462
463 L<http://www.iinteractive.com>
464
465 This library is free software; you can redistribute it and/or modify
466 it under the same terms as Perl itself. 
467
468 =cut