File Coverage

blib/lib/Role/Inspector.pm
Criterion Covered Total %
statement 110 116 94.8
branch 57 76 75.0
condition 36 55 65.4
subroutine 18 18 100.0
pod 3 3 100.0
total 224 268 83.5


line stmt bran cond sub pod time code
1 8     8   1680668 use 5.006;
  8         67  
2 8     8   38 use strict;
  8         14  
  8         172  
3 8     8   32 use warnings;
  8         14  
  8         444  
4              
5             package Role::Inspector;
6              
7             our $AUTHORITY = 'cpan:TOBYINK';
8             our $VERSION = '0.006';
9              
10 8     8   3182 use Exporter::Shiny qw( get_role_info learn does_role );
  8         3184  
  8         43  
11 8     8   798 use Module::Runtime qw( use_package_optimistically );
  8         15  
  8         49  
12 8     8   344 use Scalar::Util qw( blessed );
  8         13  
  8         830  
13              
14             BEGIN {
15 8         9751 *uniq = eval { require List::MoreUtils }
16             ? \&List::MoreUtils::uniq
17 105     105   135 : sub { my %already; grep !$already{$_}++, @_ }
  105         539  
18 8 50   8   26 }
19              
20             our @SCANNERS;
21              
22             sub learn (&)
23             {
24 40     40 1 68 push @SCANNERS, $_[0];
25             }
26              
27             sub get_role_info
28             {
29 35     35 1 77 my $me = shift;
30 35         123 use_package_optimistically($_[0]);
31 35         95066 my ($info) = grep defined, map $_->(@_), @SCANNERS;
32 35         525 $me->_canonicalize($info, @_);
33 35         109 return $info;
34             }
35              
36             sub _generate_get_role_info
37             {
38 7     7   806 my $me = shift;
39 7         17 my ($name, $args, $globals) = @_;
40             return sub {
41 19     19   3578 my $info = $me->get_role_info(@_);
42 19 100       56 delete($info->{meta}) if $args->{no_meta};
43 19         232 return $info;
44 7         48 };
45             }
46              
47             sub _canonicalize
48             {
49 35     35   55 my $me = shift;
50 35         69 my ($info) = @_;
51            
52 35 100 66     164 if ( $info->{api} and not( $info->{provides} && $info->{requires} ) )
      100        
53             {
54 1         2 my @provides;
55             my @requires;
56 1         1 for my $method (@{ $info->{api} })
  1         3  
57             {
58             push @{
59 2 100       3 $info->{name}->can($method) ? \@provides : \@requires
  2         15  
60             }, $method;
61             }
62 1   50     6 $info->{provides} ||= \@provides;
63 1   50     4 $info->{requires} ||= \@requires;
64             }
65            
66 35 100       89 if ( not $info->{api} )
67             {
68             $info->{api} = [
69 18   50     53 @{ $info->{provides} ||= [] },
70 18   50     27 @{ $info->{requires} ||= [] },
  18         98  
71             ];
72             }
73            
74             # if a method is in both `provides` and `requires`, remove from `requires`
75 35         48 my %lookup;
76 35         47 undef $lookup{$_} for @{$info->{provides}};
  35         173  
77 35         59 @{$info->{requires}} = grep !exists($lookup{$_}), @{$info->{requires}};
  35         212  
  35         97  
78            
79 35         70 for my $k (qw/ api provides requires /) {
80 105         332 @{ $info->{$k} } = sort(
81             uniq(
82             map ref($_) ? $_->{name} : $_,
83 105 100       130 @{ $info->{$k} }
  105         437  
84             )
85             );
86             }
87             }
88              
89             sub _expand_attributes
90             {
91 23     23   38 my $me = shift;
92 23         51 my ($role, $meta) = @_;
93            
94             my @attrs = map {
95 23         82 my $data = $meta->get_attribute($_);
  21         227  
96 21 100       189 $data->{name} = $_ unless exists($data->{name});
97 21         163 $data;
98             } $meta->get_attribute_list;
99 23         58 my %methods;
100            
101 23         48 for my $attr (@attrs)
102             {
103 21 100 66     663 my $is = blessed($attr) && $attr->can('is') ? $attr->is : $attr->{is};
104 21 100 66     490 $methods{blessed($attr) && $attr->can('name') ? $attr->name : $attr->{name} }++
    50          
105             if $is =~ /\A(ro|rw|lazy|rwp)\z/i;
106            
107 21         51 for my $method_type (qw(reader writer accessor clearer predicate))
108             {
109 105 100       307 my $method_name = blessed($attr) ? $attr->$method_type : $attr->{$method_type};
110 105 50       174 ($method_name) = %$method_name if ref($method_name); # HASH :-(
111 105 100       208 $methods{$method_name}++ if defined $method_name;
112             }
113            
114 21         26 my $handles;
115 21 50 66     113 if (blessed($attr) and $attr->can('_canonicalize_handles'))
116             {
117             $handles =
118             $attr->can('_canonicalize_handles') ? +{ $attr->_canonicalize_handles } :
119             $attr->can('handles') ? $attr->handles :
120 0 0       0 $attr->{handles};
    0          
121             }
122             else
123             {
124 21         38 $handles = $attr->{handles};
125             }
126            
127 21 50       87 if (!defined $handles)
    50          
    50          
    0          
128             {
129             # no-op
130             }
131             elsif (not ref($handles))
132             {
133 0         0 $methods{$_}++ for @{ $me->get_info($handles)->{api} };
  0         0  
134             }
135             elsif (ref($handles) eq q(ARRAY))
136             {
137 21         68 $methods{$_}++ for @$handles;
138             }
139             elsif (ref($handles) eq q(HASH))
140             {
141 0         0 $methods{$_}++ for keys %$handles;
142             }
143             else
144             {
145 0         0 require Carp;
146 0         0 Carp::carp(
147             sprintf(
148             "%s contains attribute with delegated methods, but %s cannot determine which methods are being delegated",
149             $role,
150             $me,
151             )
152             );
153             }
154             }
155            
156 23         105 return keys(%methods);
157             }
158              
159             # Learn about mop
160             learn {
161             my $role = shift;
162             return unless $INC{'mop.pm'};
163            
164             my $meta = mop::meta($role);
165             return unless $meta && $meta->isa('mop::role');
166            
167             return {
168             name => $role,
169             type => 'mop::role',
170             provides => [ sort(map($_->name, $meta->methods)) ],
171             requires => [ sort($meta->required_methods) ],
172             meta => $meta,
173             };
174             };
175              
176             # Learn about Role::Tiny and Moo::Role
177             learn {
178             my $role = shift;
179             return unless $INC{'Role/Tiny.pm'};
180            
181             # Moo 1.003000 added is_role, but that's too new to rely on.
182             my @methods;
183             return unless eval {
184             @methods = 'Role::Tiny'->methods_provided_by($role);
185             1;
186             };
187            
188 8     8   60 no warnings qw(once);
  8         22  
  8         7891  
189             my $type =
190             ($INC{'Moo/Role.pm'} and $Moo::Role::INFO{$role}{accessor_maker})
191             ? 'Moo::Role'
192             : 'Role::Tiny';
193            
194             @methods = $type->methods_provided_by($role)
195             if $type ne 'Role::Tiny';
196            
197             my @requires = @{ $Role::Tiny::INFO{$role}{requires} or [] };
198            
199             my $modifiers = $Role::Tiny::INFO{$role}{modifiers} || [];
200             foreach my $modifier (@$modifiers) {
201             my @modified = @$modifier[ 1 .. $#$modifier - 1 ];
202             # handle: before ['foo', 'bar'] => sub { ... }
203             @modified = @{ $modified[0] } if ref $modified[0] eq 'ARRAY';
204             push @requires, @modified;
205             }
206            
207             return {
208             name => $role,
209             type => $type,
210             api => [ @methods, @requires ],
211             provides => [ keys %{ $type->_concrete_methods_of($role) } ],
212             requires => \@requires,
213             };
214             };
215              
216             # Learn about Moose
217             learn {
218             my $role = shift;
219             return unless $INC{'Moose.pm'};
220            
221             require Moose::Util;
222             my $meta = Moose::Util::find_meta($role);
223             return unless $meta && $meta->isa('Moose::Meta::Role');
224            
225             my (@provides, @requires);
226             push @provides, $meta->get_method_list;
227             push @provides, __PACKAGE__->_expand_attributes($role, $meta);
228             push @requires, map($_->name, $meta->get_required_method_list);
229             for my $kind (qw/before after around/) {
230             my $accessor = "get_${kind}_method_modifiers_map";
231             push @requires, keys %{ $meta->$accessor };
232             }
233            
234             return {
235             name => $role,
236             type => 'Moose::Role',
237             meta => $meta,
238             provides => \@provides,
239             requires => \@requires,
240             };
241             };
242              
243             # Learn about Mouse
244             learn {
245             my $role = shift;
246             return unless $INC{'Mouse.pm'};
247            
248             require Mouse::Util;
249             my $meta = Mouse::Util::find_meta($role);
250             return unless $meta && $meta->isa('Mouse::Meta::Role');
251            
252             my (@provides, @requires);
253             push @provides, $meta->get_method_list;
254             push @provides, __PACKAGE__->_expand_attributes($role, $meta);
255             push @requires, $meta->get_required_method_list;
256             for my $kind (qw/before after around/) {
257             push @requires, keys %{ $meta->{"${kind}_method_modifiers"} };
258             }
259            
260             return {
261             name => $role,
262             type => 'Mouse::Role',
263             meta => $meta,
264             provides => \@provides,
265             requires => \@requires,
266             };
267             };
268              
269             # Learn about Role::Basic
270             learn {
271             my $role = shift;
272             return unless $INC{'Role/Basic.pm'};
273            
274             return unless eval { 'Role::Basic'->_load_role($role) };
275            
276             return {
277             name => $role,
278             type => 'Role::Basic',
279             provides => [ keys %{ 'Role::Basic'->_get_methods($role) } ],
280             requires => [ 'Role::Basic'->get_required_by($role) ],
281             };
282             };
283              
284             sub does_role
285             {
286 25     25 1 60 my $me = shift;
287 25         55 my ($thing, $role) = @_;
288            
289 25 50       75 return !!0 if !defined($thing);
290 25 50 33     78 return !!0 if ref($thing) && !blessed($thing);
291            
292 25   33     362 ref($_) or use_package_optimistically($_) for @_;
293            
294 25 100 66     61390 return !!1 if $thing->can('does') && $thing->does($role);
295 15 100 66     210 return !!1 if $thing->can('DOES') && $thing->DOES($role);
296            
297 10 50       40 my $info = $me->get_role_info($role)
298             or return !!0;
299            
300 10 100 100     56 if ($info->{type} eq 'Role::Tiny' or $info->{type} eq 'Moo::Role')
301             {
302 5 100       16 return !!1 if Role::Tiny::does_role($thing, $role);
303             }
304            
305 8 100       72 if ($info->{type} eq 'Moose::Role')
306             {
307 3         20 require Moose::Util;
308 3 100       17 return !!1 if Moose::Util::does_role($thing, $role);
309             }
310            
311 7 100       291 if ($info->{type} eq 'Mouse::Role')
312             {
313 2         7 require Mouse::Util;
314 2 100       12 return !!1 if Mouse::Util::does_role($thing, $role);
315             }
316            
317 6 50       52 if (not ref $thing)
318             {
319 6   50     20 my $info2 = $me->get_role_info($thing) || { type => '' };
320            
321 6 100 100     35 if ($info2->{type} eq 'Role::Tiny' or $info2->{type} eq 'Moo::Role')
322             {
323 3 50       12 return !!1 if Role::Tiny::does_role($thing, $role);
324             }
325            
326 6 100 66     70 if ($info2->{type} eq 'Moose::Role'
      100        
327             or $INC{'Moose.pm'} && Moose::Util::find_meta($thing))
328             {
329 4         35 require Moose::Util;
330 4 100       13 return !!1 if Moose::Util::does_role($thing, $role);
331             }
332            
333 5 100 33     377 if ($info2->{type} eq 'Mouse::Role'
      66        
334             or $INC{'Mouse.pm'} && Mouse::Util::find_meta($thing))
335             {
336 1         4 require Mouse::Util;
337 1 50       2 return !!1 if Mouse::Util::does_role($thing, $role);
338             }
339             }
340            
341             # No special handling for Role::Basic, but hopefully checking
342             # `DOES` worked!
343            
344 5         64 !!0;
345             }
346              
347             # very simple class method curry
348             sub _generate_does_role
349             {
350 4     4   661 my $me = shift;
351 4     25   28 sub { $me->does_role(@_) };
  25         1060  
352             }
353              
354              
355             1;
356              
357             __END__
358              
359             =pod
360              
361             =encoding utf-8
362              
363             =for stopwords metaobject
364              
365             =head1 NAME
366              
367             Role::Inspector - introspection for roles
368              
369             =head1 SYNOPSIS
370              
371             use strict;
372             use warnings;
373             use feature qw(say);
374            
375             {
376             package Local::Role;
377             use Role::Tiny; # or Moose::Role, Mouse::Role, etc...
378            
379             requires qw( foo );
380            
381             sub bar { ... }
382             }
383            
384             use Role::Inspector qw( get_role_info );
385            
386             my $info = get_role_info('Local::Role');
387            
388             say $info->{name}; # Local::Role
389             say $info->{type}; # Role::Tiny
390             say for @{$info->{api}}; # bar
391             # foo
392              
393             =head1 DESCRIPTION
394              
395             This module allows you to retrieve a hashref of information about a
396             given role. The following role implementations are supported:
397              
398             =over
399              
400             =item *
401              
402             L<Moose::Role>
403              
404             =item *
405              
406             L<Mouse::Role>
407              
408             =item *
409              
410             L<Moo::Role>
411              
412             =item *
413              
414             L<Role::Tiny>
415              
416             =item *
417              
418             L<Role::Basic>
419              
420             =item *
421              
422             L<p5-mop-redux|https://github.com/stevan/p5-mop-redux>
423              
424             =back
425              
426             =head2 Functions
427              
428             =over
429              
430             =item C<< get_role_info($package_name) >>
431              
432             Returns a hashref of information about a role; returns C<undef> if the
433             package does not appear to be a role. Attempts to load the package
434             using L<Module::Runtime> if it's not already loaded.
435              
436             The hashref may contain the following keys:
437              
438             =over
439              
440             =item *
441              
442             C<name> - the package name of the role
443              
444             =item *
445              
446             C<type> - the role implementation used by the role
447              
448             =item *
449              
450             C<api> - an arrayref of method names required/provided by the role
451              
452             =item *
453              
454             C<provides> and C<requires> - the same as C<api>, but split into lists
455             of methods provided and required by the role
456              
457             =item *
458              
459             C<meta> - a metaobject for the role (e.g. a L<Moose::Meta::Role> object).
460             This key may be absent if the role implementation does not provide a
461             metaobject
462              
463             =back
464              
465             This function may be exported, but is not exported by default.
466              
467             =item C<< does_role($thing, $role) >>
468              
469             Returns a boolean indicating if C<< $thing >> does role C<< $role >>.
470             C<< $thing >> can be an object, a class name, or a role name.
471              
472             This should mostly give the same answers as C<< $thing->DOES($role) >>,
473             but may be slightly more reliable in some cross-implementation (i.e.
474             Moose roles consuming Moo roles) cases.
475              
476             This function may be exported, but is not exported by default.
477              
478             =back
479              
480             =head2 Methods
481              
482             If you do not wish to export the functions provided by Role::Inspector,
483             you may call them as a class methods:
484              
485             my $info = Role::Inspector->get_role_info($package_name);
486              
487             $thing->blah() if Role::Inspector->does_role($thing, $role);
488              
489             =head2 Extending Role::Inspector
490              
491             =over
492              
493             =item C<< Role::Inspector::learn { BLOCK } >>
494              
495             In the unlikely situation that you have to deal with some other role
496             implementation that Role::Inspector doesn't know about, you can teach
497             it:
498              
499             use Role::Inspector qw( learn );
500            
501             learn {
502             my $r = shift;
503             return unless My::Implementation::is_role($r);
504             return {
505             name => $r,
506             type => 'My::Implementation',
507             provides => [ sort(@{My::Implementation::provides($r)}) ],
508             requires => [ sort(@{My::Implementation::requires($r)}) ],
509             };
510             };
511              
512             An alternative way to do this is:
513              
514             push @Role::Inspector::SCANNERS, sub {
515             my $r = shift;
516             ...;
517             };
518              
519             You can do the C<push> thing without having loaded Role::Inspector.
520             This makes it suitable for doing inside My::Implementation itself,
521             without introducing an additional dependency on Role::Inspector.
522              
523             Note that if you don't provide all of C<provides>, C<requires>, and
524             C<api>, Role::Inspector will attempt to guess the missing parts.
525              
526             =back
527              
528             =head1 CAVEATS
529              
530             =over
531              
532             =item *
533              
534             It is difficult to distinguish between L<Moo::Role> and L<Role::Tiny>
535             roles. (The distinction is not often important anyway.) Thus sometimes
536             the C<type> for a Moo::Role may say C<< "Role::Tiny" >>.
537              
538             =item *
539              
540             The way that Role::Basic roles are detected and introspected is a bit
541             dodgy, relying on undocumented methods.
542              
543             =item *
544              
545             Where Moose or Mouse roles define attributes, those attributes tend to
546             result in accessor methods being generated. However neither of these
547             frameworks provides a decent way of figuring out which accessor methods
548             will result from composing the role with the class.
549              
550             Role::Inspector does its damnedest to figure out the list of likely
551             methods, but (especially in the case of unusual attribute traits) may
552             get things wrong from time to time.
553              
554             =back
555              
556             =head1 BUGS
557              
558             Please report any bugs to
559             L<http://rt.cpan.org/Dist/Display.html?Queue=Role-Inspector>.
560              
561             =head1 SEE ALSO
562              
563             L<Class::Inspector>.
564              
565             =head1 AUTHOR
566              
567             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
568              
569             =head1 COPYRIGHT AND LICENCE
570              
571             This software is copyright (c) 2014 by Toby Inkster.
572              
573             This is free software; you can redistribute it and/or modify it under
574             the same terms as the Perl 5 programming language system itself.
575              
576             =head1 DISCLAIMER OF WARRANTIES
577              
578             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
579             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
580             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
581