File Coverage

blib/lib/Perl6/Roles.pm
Criterion Covered Total %
statement 76 76 100.0
branch 18 18 100.0
condition 10 11 90.9
subroutine 13 13 100.0
pod 1 1 100.0
total 118 119 99.1


line stmt bran cond sub pod time code
1             package Perl6::Roles;
2              
3 5     5   107944 use 5.6.0;
  5         20  
  5         220  
4              
5 5     5   25 use strict;
  5         11  
  5         175  
6 5     5   39 use warnings;
  5         11  
  5         246  
7              
8             our $VERSION = '0.01';
9              
10 5     5   29 use Scalar::Util qw( blessed refaddr );
  5         8  
  5         613  
11 5     5   4764 use List::MoreUtils qw( uniq );
  5         8233  
  5         1072  
12              
13             my %does; # {class}{role} = 1
14             my %from; # {class}{method} = role
15              
16             sub _get_all_roles {
17 46     46   56 my $r = shift;
18              
19 46         64 my @roles = ( $r );
20 46   100     53 push @roles, map { _get_all_roles($_) } keys %{$does{$r} ||= {}};
  21         38  
  46         213  
21 46         248 return uniq( @roles );
22             }
23              
24             sub apply {
25 25     25 1 13230 my ($role, $class) = @_;
26              
27 25 100       98 if ( my $old_class = blessed($class) ) {
28 5     5   38 no strict 'refs';
  5         8  
  5         465  
29              
30 2         9 $class = "${old_class}::" . refaddr($class);
31 2         3 push @{ "${class}::ISA" }, $old_class;
  2         38  
32              
33             # This requires direct access into @_ in order to affect the parameter,
34             # not just the copy of the parameter.
35 2         6 bless $_[1], $class;
36             }
37              
38 25         35 my @methods;
39 25         52 foreach my $r ( _get_all_roles( $role ) ) {
40 5     5   25 no strict 'refs';
  5         11  
  5         982  
41              
42             # A role is valid if-and-only-if the following conditions hold:
43             # 1) It is a direct descendent of __PACKAGE__
44             # 2) Its only ancestor is __PACKAGE__
45 42         45 my @isa = @{ "${r}::ISA" };
  42         132  
46 42 100 100     206 if ( @isa > 1 || $isa[0] ne __PACKAGE__ ) {
47 2         14 die "$r is an invalid role because it has inheritance other than "
48             . __PACKAGE__ . "\n";
49             }
50              
51             # No matter what, mark $class as "does $role"
52 40         86 $does{ $class }{ $r } = 1;
53              
54 54         689 push @methods, map { [ $r, $_ ] } grep {
  242         445  
55 242         197 *{"${r}::${_}"}{CODE}
  40         113  
56 40         41 } keys %{"${r}::"};
57             }
58              
59             # Only compose methods in if the $class isn't, itself, a role.
60             # Roles don't flatten until they are composed into a class.
61 23 100       207 if ( !$class->isa( __PACKAGE__ ) ) {
62             METHOD:
63 14         29 foreach my $item (@methods) {
64 34         46 my ($r, $method) = @$item;
65 5     5   25 no strict 'refs';
  5         9  
  5         1096  
66              
67             # Don't override a method that already exists, but we need to
68             # check for conflicts.
69 34 100       565 if (*{"${class}::${method}"}{CODE}) {
  34         136  
70              
71             # If the method was installed by another role, die in order to
72             # force the class owner to resolve the conflict.
73 11         16 my $conflict = $from{ $class }{ $method };
74 11 100 100     39 if ( $conflict && $conflict ne $r ) {
75 3         28 die "Attempt to re-compose '$method' into '$class'\n"
76             . "\tConflicting roles: '$conflict' <-> '$r'\n";
77             }
78              
79             # Otherwise, we skip this method because the class owner has
80             # already (apparently) resolved the conflict.
81 8         19 next METHOD;
82             }
83              
84             # Install the method ...
85 23         41 *{"${class}::${method}"} = \&{"${r}::${method}"};
  23         63  
  23         44  
86              
87             # ... and record which role provides this method.
88 23         62 $from{ $class }{ $method } = $r;
89             }
90             }
91              
92 20         92 return 1;
93             }
94              
95             sub _check_isa {
96 24     24   280 my ($class, $role) = @_;
97              
98 5     5   35 no strict 'refs';
  5         14  
  5         901  
99 24         28 for my $parent ( @{ "${class}::ISA" } ) {
  24         98  
100 21 100 66     64 return 1 if $parent->does( $role ) or _check_isa( $parent, $role );
101             }
102              
103 4         20 return;
104             }
105              
106             *UNIVERSAL::does = sub {
107 66     66   12644 my ($proto, $role) = @_;
108              
109 66         155 my $class = blessed $proto;
110 66 100       152 $class = $proto unless $class;
111              
112 66 100       241 return 1 if $class eq $role;
113 50 100       312 return 1 if $does{ $class }{ $role };
114              
115 23         853 return _check_isa( $class, $role );
116             };
117              
118             1;
119             __END__