File Coverage

blib/lib/Role/Basic.pm
Criterion Covered Total %
statement 259 268 96.6
branch 83 96 86.4
condition 35 44 79.5
subroutine 34 34 100.0
pod 0 4 0.0
total 411 446 92.1


line stmt bran cond sub pod time code
1             package Role::Basic;
2              
3 335     335   509 sub _getglob { \*{ $_[0] } }
  335         15963  
4              
5 37     37   2467809 use strict;
  36         111  
  36         1187  
6 37     37   704 use warnings FATAL => 'all';
  36         74  
  36         1731  
7              
8 34     34   231 use B qw/svref_2object/;
  34         58  
  34         2663  
9 30     30   11769 use Storable ();
  29         73927  
  29         865  
10 29     29   223 use Carp ();
  29         53  
  29         553  
11 28     28   2179 use Data::Dumper ();
  28         30385  
  28         27810  
12              
13             our $VERSION = '0.16';
14              
15             # eventually clean these up
16             my ( %IS_ROLE, %REQUIRED_BY, %HAS_ROLES, %ALLOWED_BY, %PROVIDES );
17              
18             sub import {
19 131     131   18415 my $class = shift;
20 131         341 my $target = caller;
21              
22             # everybody gets 'with' and 'DOES'
23 131         448 *{ _getglob "${target}::with" } = sub {
24 85     85   1762646 $class->apply_roles_to_package( $target, @_ );
25 131         597 };
26             # everybody gets 'with' and 'DOES'
27 131         282 *{ _getglob "${target}::DOES" } = sub {
28 46     46   13203 my ( $proto, $role ) = @_;
29 46   66     219 my $class_or_role = ref $proto || $proto;
30 46 100       160 return 1 if $class_or_role eq $role;
31 39 100       251 return exists $HAS_ROLES{$class_or_role}{$role} ? 1 : 0;
32 131         496 };
33 131 100 100     877 if ( 1 == @_ && 'with' eq $_[0] ) {
    100 100        
    100          
34              
35             # this is a class which is consuming roles
36 56         25264 return;
37             }
38             elsif ( 2 == @_ && 'allow' eq $_[0] ) {
39              
40             # this is a role which allows methods from a foreign class
41 2         6 my $foreign_class = $_[1];
42 2         4 push @{ $ALLOWED_BY{$foreign_class} } => $target;
  2         70  
43 2         10 $class->_declare_role($target);
44             }
45             elsif (@_) {
46 2         7 my $args = join ', ' => @_; # more explicit than $"
47 2         552 Carp::confess(
48             "Multiple or unknown argument(s) in import list: ($args)");
49             }
50             else {
51 71         241 $class->_declare_role($target);
52             }
53             }
54              
55             sub _declare_role {
56 73     73   150 my ($class, $target) = @_;
57 73         162 $IS_ROLE{$target} = 1;
58 73         203 *{ _getglob "${target}::requires" } = sub {
59 12     12   1009567 $class->add_to_requirements( $target, @_ );
60 73         267 };
61             }
62              
63             sub add_to_requirements {
64 72     72 0 187 my ( $class, $role, @methods ) = @_;
65              
66 72   100     350 $REQUIRED_BY{$role} ||= [];
67 72         96 push @{ $REQUIRED_BY{$role} } => @methods;
  72         168  
68 72         95 my %seen;
69 72         219 @{ $REQUIRED_BY{$role} } =
70 72         96 grep { not $seen{$_}++ } @{ $REQUIRED_BY{$role} };
  97         240  
  72         145  
71             }
72              
73             sub get_required_by {
74 283     283 0 1810 my ( $class, $role ) = @_;
75 283 100       723 return unless my $requirements = $REQUIRED_BY{$role};
76 115         310 return @$requirements;
77             }
78              
79             sub requires_method {
80 13     13 0 16910 my ( $class, $role, $method ) = @_;
81 13 50       52 return unless $IS_ROLE{$role};
82 13         41 my %requires = map { $_ => 1 } $class->get_required_by($role);
  9         28  
83 13         66 return $requires{$method};
84             }
85              
86             sub _roles {
87 130     130   226 my ( $class, $target ) = @_;
88 130 100       684 return unless $HAS_ROLES{$target};
89 17         33 my @roles;
90             my %seen;
91 17         31 foreach my $role ( keys %{ $HAS_ROLES{$target} } ) {
  17         49  
92 22         51 my $modifiers = $HAS_ROLES{$target}{$role};
93 22         62 my $role_name = $class->_get_role_name($role,$modifiers);
94 22 50       1407 unless ( $seen{$role_name} ) {
95 22         87 push @roles => $role_name, $class->_roles($role);
96             }
97             }
98 17         57 return @roles;
99             }
100              
101             sub apply_roles_to_package {
102 85     85 0 552 my ( $class, $target, @roles ) = @_;
103              
104 85 100       381 if ( $HAS_ROLES{$target} ) {
105 2         445 Carp::confess("with() may not be called more than once for $target");
106             }
107              
108 83         286 my ( %provided_by, %requires );
109              
110 83         0 my %is_applied;
111              
112             # these are roles which a class does not use directly, but are contained in
113             # the roles the class consumes.
114 83         0 my %contained_roles;
115              
116 83         287 while ( my $role = shift @roles ) {
117              
118             # will need to verify that they're actually a role!
119              
120 117 100       321 my $role_modifiers = shift @roles if ref $roles[0];
121 117   100     357 $role_modifiers ||= {};
122 117         355 my $role_name = $class->_get_role_name( $role, $role_modifiers );
123 117         7954 $is_applied{$role_name} = 1;
124 117         568 $class->_load_role( $role, $role_modifiers->{'-version'} );
125              
126             # XXX this is awful. Don't tell anyone I wrote this
127 114         449 my $role_methods = $class->_add_role_methods_to_target(
128             $role,
129             $target,
130             $role_modifiers
131             );
132              
133             # DOES() in some cases
134 108 100       292 if ( my $roles = $HAS_ROLES{$role} ) {
135 17         50 foreach my $role ( keys %$roles ) {
136 22         57 $HAS_ROLES{$target}{$role} = $roles->{$role};
137             }
138             }
139              
140 108         296 foreach my $method ( $class->get_required_by($role) ) {
141 46         63 push @{ $requires{$method} } => $role;
  46         167  
142             }
143              
144             # roles consuming roles should have the same requirements.
145 108 100       261 if ( $IS_ROLE{$target} ) {
146 33         79 $class->add_to_requirements( $target,
147             $class->get_required_by($role) );
148             }
149              
150 108         372 while ( my ( $method, $data ) = each %$role_methods ) {
151 152   66     773 $PROVIDES{$role_name}{$method} ||= $data;
152             }
153              
154             # any extra roles contained in applied roles must be added
155             # (helps with conflict resolution)
156 108         246 $contained_roles{$role_name} = 1;
157 108         297 foreach my $contained_role ( $class->_roles($role) ) {
158 22 100       70 next if $is_applied{$contained_role};
159 21         49 $contained_roles{$contained_role} = 1;
160 21         101 $is_applied{$contained_role} = 1;
161             }
162             }
163 74         185 foreach my $contained_role (keys %contained_roles) {
164 126         399 my ( $role, $modifiers ) = split /-/ => $contained_role, 2;
165 126         305 foreach my $method ( $class->get_required_by($role) ) {
166 49         63 push @{ $requires{$method} } => $role;
  49         119  
167             }
168             # a role is not a name. A role is a role plus its alias/exclusion. We
169             # now store those in $HAS_ROLE so pull from them
170 126 100       319 if ( my $methods = $PROVIDES{$contained_role} ) {
171 106         238 foreach my $method (keys %$methods) {
172 178         252 push @{ $provided_by{$method} } => $methods->{$method};
  178         491  
173             }
174             }
175             }
176              
177 74         276 $class->_check_conflicts( $target, \%provided_by );
178 64         174 $class->_check_requirements( $target, \%requires );
179             }
180              
181             sub _uniq (@) {
182 20     20   33 my %seen = ();
183 20         34 grep { not $seen{$_}++ } @_;
  42         148  
184             }
185              
186             sub _check_conflicts {
187 74     74   161 my ( $class, $target, $provided_by ) = @_;
188 74         151 my @errors;
189 74         197 foreach my $method (keys %$provided_by) {
190 136         204 my $sources = $provided_by->{$method};
191 136 100       375 next if 1 == @$sources;
192              
193 37         82 my %seen;
194             # what we're doing here is checking to see if code references point to
195             # the same reference. If they do, they can't possibly be in conflict
196             # because they're the same method. This seems strange, but it does
197             # follow the original spec.
198 37         54 my @sources = do {
199 23     23   200 no warnings 'uninitialized';
  23         84  
  23         11990  
200 56         144 map { $_->{source} }
201 37         80 grep { !$seen{ $_->{code} }++ } @$sources;
  79         307  
202             };
203              
204             # more than one role provides the method and it's not overridden by
205             # the consuming class having that method
206 37 100 100     367 if ( @sources > 1 && $target ne _sub_package( $target->can($method) ) )
207             {
208 13         57 my $sources = join "' and '" => sort @sources;
209 13         100 push @errors =>
210             "Due to a method name conflict in roles '$sources', the method '$method' must be implemented or excluded by '$target'";
211             }
212             }
213 74 100       269 if ( my $errors = join "\n" => @errors ) {
214 10         3381 Carp::confess($errors);
215             }
216             }
217              
218             sub _check_requirements {
219 64     64   134 my ( $class, $target, $requires ) = @_;
220              
221             # we return if the target is a role because requirements can be deferred
222             # until final composition
223 64 100       264 return if $IS_ROLE{$target};
224 43         66 my @errors;
225 43         95 foreach my $method ( keys %$requires ) {
226 37 100       452 unless ( $target->can($method) ) {
227 20         49 my $roles = join '|' => _uniq sort @{ $requires->{$method} };
  20         64  
228 20         88 push @errors =>
229             "'$roles' requires the method '$method' to be implemented by '$target'";
230             }
231             }
232 43 100       389 if (@errors) {
233 9         3072 Carp::confess( join "\n" => @errors );
234             }
235             }
236              
237             sub _get_role_name {
238 253     253   567 my ( $class, $role, $modifiers ) = @_;
239 253         458 local $Data::Dumper::Indent = 0;
240 253         383 local $Data::Dumper::Terse = 1;
241 253         337 local $Data::Dumper::Sortkeys = 1;
242 253         1114 return "$role-" . Data::Dumper::Dumper($modifiers);
243             }
244              
245             sub _add_role_methods_to_target {
246 114     114   299 my ( $class, $role, $target, $role_modifiers) = @_;
247              
248 114         6772 my $copied_modifiers = Storable::dclone($role_modifiers);
249 114         483 my $role_name = $class->_get_role_name( $role, $copied_modifiers );
250              
251 114         7353 my $target_methods = $class->_get_methods($target);
252 114         206 my $is_loaded = $PROVIDES{$role_name};
253 114   66     367 my $code_for = $is_loaded || $class->_get_methods($role);
254 114         458 my %original_code_for = %$code_for;
255              
256 114         200 delete $role_modifiers->{'-version'};
257 114         312 my ( $is_excluded, $aliases ) =
258             $class->_get_excludes_and_aliases( $target, $role, $role_modifiers );
259              
260 16     16   164 my $stash = do { no strict 'refs'; \%{"${target}::"} };
  16         90  
  16         6898  
  114         172  
  114         166  
  114         377  
261 114         338 while ( my ( $old_method, $new_method ) = each %$aliases ) {
262 27 100       55 if ( !$is_loaded ) {
263 16 50 66     61 if ( exists $code_for->{$new_method} && !$is_excluded->{$new_method} ) {
264 0         0 Carp::confess(
265             "Cannot alias '$old_method' to existing method '$new_method' in $role"
266             );
267             }
268             else {
269 16         32 $code_for->{$new_method} = $original_code_for{$old_method};
270             }
271             }
272              
273             # We do this because $target->can($new_method) wouldn't be appropriate
274             # since it's OK for a role method to -alias over an inherited one. You
275             # can -alias directly on top of an existing method, though.
276 27 100       92 if ( exists $stash->{$new_method} ) {
277 5         1709 Carp::confess("Cannot alias '$old_method' to '$new_method' as a method of that name already exists in $target");
278             }
279             }
280              
281 109         242 my %was_aliased = reverse %$aliases;
282 109         247 foreach my $method ( keys %$code_for ) {
283 180 100       376 if ( $is_excluded->{$method} ) {
284 30 100       65 unless ($was_aliased{$method}) {
285 27         37 delete $code_for->{$method};
286 27         78 $class->add_to_requirements( $target, $method );
287 27         49 next;
288             }
289             }
290              
291 153 100       337 if ( exists $target_methods->{$method} ) {
292 21 100       89 if ( $ENV{PERL_ROLE_OVERRIDE_DIE} ) {
293 1         303 Carp::confess(
294             "Role '$role' not overriding method '$method' in '$target'"
295             );
296             }
297 20 50       77 if ( $ENV{PERL_ROLE_OVERRIDE_WARN} ) {
298 0         0 Carp::carp(
299             "Role '$role' not overriding method '$method' in '$target'"
300             );
301             }
302 20         41 next;
303             }
304             # XXX we're going to handle this ourselves
305 16     16   120 no strict 'refs';
  16         36  
  16         820  
306 16     16   87 no warnings 'redefine';
  16         29  
  16         8665  
307 132         214 *{"${target}::$method"} = $code_for->{$method}{code};
  132         681  
308             }
309 108         313 $HAS_ROLES{$target}{$role} = $copied_modifiers;
310 108         575 return $code_for;
311             }
312              
313             sub _get_excludes_and_aliases {
314 114     114   249 my ( $class, $target, $role, $role_modifiers ) = @_;
315             # figure out which methods to exclude
316 114   100     394 my $excludes = delete $role_modifiers->{'-excludes'} || [];
317 114   100     329 my $aliases = delete $role_modifiers->{'-alias'} || {};
318 114   100     331 my $renames = delete $role_modifiers->{'-rename'} || {};
319              
320 114 100       267 $excludes = [$excludes] unless ref $excludes;
321 114         284 my %is_excluded = map { $_ => 1 } @$excludes;
  33         94  
322              
323 114         475 while ( my ( $old_method, $new_method ) = each %$renames ) {
324 6         13 $is_excluded{$old_method} = 1;
325 6         22 $aliases->{$old_method} = $new_method;
326             }
327              
328 114 50       318 unless ( 'ARRAY' eq ref $excludes ) {
329 0         0 Carp::confess(
330             "Argument to '-excludes' in package $target must be a scalar or array reference"
331             );
332             }
333              
334             # rename methods to alias
335 114 50       314 unless ( 'HASH' eq ref $aliases ) {
336 0         0 Carp::confess(
337             "Argument to '-alias' in package $target must be a hash reference"
338             );
339             }
340              
341 114 50       358 if ( my $unknown = join ', ' => keys %$role_modifiers ) {
342 0         0 Carp::confess("Unknown arguments in 'with()' statement for $role");
343             }
344 114         460 return ( \%is_excluded, $aliases );
345             }
346              
347             # We can cache this at some point, but for now, the return value is munged
348             sub _get_methods {
349 193     193   400 my ( $class, $target ) = @_;
350              
351 16     16   121 my $stash = do { no strict 'refs'; \%{"${target}::"} };
  16         28  
  16         8581  
  193         258  
  193         244  
  193         691  
352              
353 193         338 my %methods;
354 193         677 foreach my $name ( keys %$stash ) {
355 1172         2916 my $item = $stash->{$name};
356              
357 1172 100       1848 next unless my $code = _get_valid_method( $target, $item );
358              
359             # this prevents a "modification of read-only value" error.
360 234         477 my $source = _sub_package($code);
361 234         1048 $methods{$name} = {
362             code => $code,
363             source => $source,
364             };
365             }
366 193         714 return \%methods;
367             }
368              
369             sub _get_valid_method {
370 1172     1172   2251 my ( $target, $item ) = @_;
371             my $code = ref $item eq 'CODE' ? $item
372             : ref \$item eq 'GLOB' ? *$item{CODE}
373 1172 100       3464 : undef;
    50          
374 1172 100       3103 return if !defined $code;
375              
376 770 50       1262 my $source = _sub_package($code) or return;
377              
378             # XXX There's a potential bug where some idiot could use Role::Basic to
379             # create exportable functions and those get exported into a role. That's
380             # far-fetched enough that I'm not worried about it.
381             my $is_valid =
382             # declared in package, not imported
383             $target eq $source
384             ||
385             # unless we're a role and they're composed from another role
386 770   100     2866 $IS_ROLE{$target} && $IS_ROLE{$source};
387              
388 770 100       1491 unless ($is_valid) {
389 546         687 foreach my $role (@{ $ALLOWED_BY{$source} }) {
  546         1139  
390 10 50       31 return $code if $target->DOES($role);
391             }
392             }
393 760 100       2595 return $is_valid ? $code : ();
394             }
395              
396             sub _sub_package {
397 1022     1022   1525 my ($code) = @_;
398 1022         1283 my $source_package;
399 1022         1447 eval {
400 1022         3219 my $stash = svref_2object($code)->STASH;
401 1022 50 33     4545 if ( $stash && $stash->can('NAME') ) {
402 1022         2771 $source_package = $stash->NAME;
403             }
404             else {
405 0         0 $source_package = '';
406             }
407             };
408 1022 50       2044 if ( my $error = $@ ) {
409 0         0 warn "Could not determine calling source_package: $error";
410             }
411 1022   50     2794 return $source_package || '';
412             }
413              
414             sub _load_role {
415 121     121   216192 my ( $class, $role, $version ) = @_;
416              
417 121   100     695 $version ||= '';
418 16     16   146 my $stash = do { no strict 'refs'; \%{"${role}::"} };
  16         30  
  16         5505  
  121         164  
  121         157  
  121         562  
419 121 100       373 if ( exists $stash->{requires} ) {
420 115         177 my $package = $role;
421 115         489 $package =~ s{::}{/}g;
422 115         208 $package .= ".pm";
423 115 100       365 if ( not exists $INC{$package} ) {
424              
425             # embedded role, not a separate package
426 58         209 $INC{"$package"} = "added to inc by $class";
427             }
428             }
429 121     15   9482 eval "use $role $version";
  15         1709  
  15         200  
  14         342  
430 121 100       1906 Carp::confess($@) if $@;
431              
432 117 100       487 return 1 if $IS_ROLE{$role};
433              
434 1         25 my $requires = $role->can('requires');
435 1 50 33     5 if ( !$requires || $class ne _sub_package($requires) ) {
436 1         342 Carp::confess(
437             "Only roles defined with $class may be loaded with _load_role. '$role' is not allowed.");
438             }
439 0         0 $IS_ROLE{$role} = 1;
440 0         0 return 1;
441             }
442              
443             1;
444              
445             __END__