File Coverage

blib/lib/Role/Inspector.pm
Criterion Covered Total %
statement 67 73 91.7
branch 24 36 66.6
condition 13 19 68.4
subroutine 13 13 100.0
pod 2 2 100.0
total 119 143 83.2


line stmt bran cond sub pod time code
1 8     8   3528796 use 5.006;
  8         31  
  8         326  
2 8     8   45 use strict;
  8         16  
  8         264  
3 8     8   74 use warnings;
  8         17  
  8         601  
4              
5             package Role::Inspector;
6              
7             our $AUTHORITY = 'cpan:TOBYINK';
8             our $VERSION = '0.004';
9              
10 8     8   13416 use Exporter::Shiny qw( get_role_info learn );
  8         4583  
  8         80  
11 8     8   505 use Module::Runtime qw( use_package_optimistically );
  8         16  
  8         79  
12 8     8   346 use Scalar::Util qw( blessed );
  8         15  
  8         10612  
13              
14             our @SCANNERS;
15              
16             sub learn (&)
17             {
18 40     40 1 72 push @SCANNERS, $_[0];
19             }
20              
21             sub get_role_info
22             {
23 9     9 1 25 my $me = shift;
24 9         264 use_package_optimistically($_[0]);
25 9         114575 my ($info) = grep defined, map $_->(@_), @SCANNERS;
26 9         343 $me->_canonicalize($info, @_);
27 9         24 return $info;
28             }
29              
30             sub _generate_get_role_info
31             {
32 7     7   1301 my $me = shift;
33 7         18 my ($name, $args, $globals) = @_;
34             return sub {
35 9     9   141 my $info = $me->get_role_info(@_);
36 9 100       50 delete($info->{meta}) if $args->{no_meta};
37 9         174 return $info;
38 7         62 };
39             }
40              
41             sub _canonicalize
42             {
43 9     9   61 my $me = shift;
44 9         19 my ($info) = @_;
45            
46 9 100 66     93 if ( $info->{api} and not( $info->{provides} && $info->{requires} ) )
      100        
47             {
48 1         3 my @provides;
49             my @requires;
50 1         1 for my $method (@{ $info->{api} })
  1         3  
51             {
52 2 100       28 push @{
53 2         3 $info->{name}->can($method) ? \@provides : \@requires
54             }, $method;
55             }
56 1   50     8 $info->{provides} ||= \@provides;
57 1   50     34 $info->{requires} ||= \@requires;
58             }
59            
60 9 100       48 if ( not $info->{api} )
61             {
62 4         45 $info->{api} = [sort(
63 4         31 @{ $info->{provides} },
64 4         7 @{ $info->{requires} },
65             )];
66             }
67             }
68              
69             sub _expand_attributes
70             {
71 4     4   3255 my $me = shift;
72 4         34 my ($role, $meta) = @_;
73            
74 4         109 my @attrs = map {
75 4         41 my $data = $meta->get_attribute($_);
76 4 100       71 $data->{name} = $_ unless exists($data->{name});
77 4         15 $data;
78             } $meta->get_attribute_list;
79 4         9 my %methods;
80            
81 4         12 for my $attr (@attrs)
82             {
83 4 100 66     198 my $is = blessed($attr) && $attr->can('is') ? $attr->is : $attr->{is};
84 4 100 66     113 $methods{blessed($attr) && $attr->can('name') ? $attr->name : $attr->{name} }++
    50          
85             if $is =~ /\A(ro|rw|lazy|rwp)\z/i;
86            
87 4         35 for my $method_type (qw(reader writer accessor clearer predicate))
88             {
89 20 100       140 my $method_name = blessed($attr) ? $attr->$method_type : $attr->{$method_type};
90 20 50       40 ($method_name) = %$method_name if ref($method_name); # HASH :-(
91 20 100       56 $methods{$method_name}++ if defined $method_name;
92             }
93            
94 4         37 my $handles;
95 4 50 66     59 if (blessed($attr) and $attr->can('_canonicalize_handles'))
96             {
97 0 0       0 $handles =
    0          
98             $attr->can('_canonicalize_handles') ? +{ $attr->_canonicalize_handles } :
99             $attr->can('handles') ? $attr->handles :
100             $attr->{handles};
101             }
102             else
103             {
104 4         12 $handles = $attr->{handles};
105             }
106            
107 4 50       47 if (!defined $handles)
    50          
    50          
    0          
108             {
109             # no-op
110             }
111             elsif (not ref($handles))
112             {
113 0         0 $methods{$_}++ for @{ $me->get_info($handles)->{api} };
  0         0  
114             }
115             elsif (ref($handles) eq q(ARRAY))
116             {
117 4         25 $methods{$_}++ for @$handles;
118             }
119             elsif (ref($handles) eq q(HASH))
120             {
121 0         0 $methods{$_}++ for keys %$handles;
122             }
123             else
124             {
125 0         0 require Carp;
126 0         0 Carp::carp(
127             sprintf(
128             "%s contains attribute with delegated methods, but %s cannot determine which methods are being delegated",
129             $role,
130             $me,
131             )
132             );
133             }
134             }
135            
136 4         62 return keys(%methods);
137             }
138              
139             # Learn about mop
140             learn {
141             my $role = shift;
142             return unless $INC{'mop.pm'};
143            
144             my $meta = mop::meta($role);
145             return unless $meta && $meta->isa('mop::role');
146            
147             return {
148             name => $role,
149             type => 'mop::role',
150             provides => [ sort(map($_->name, $meta->methods)) ],
151             requires => [ sort($meta->required_methods) ],
152             meta => $meta,
153             };
154             };
155              
156             # Learn about Role::Tiny and Moo::Role
157             learn {
158             my $role = shift;
159             return unless $INC{'Role/Tiny.pm'};
160            
161             # Moo 1.003000 added is_role, but that's too new to rely on.
162             my @methods;
163             return unless eval {
164             @methods = 'Role::Tiny'->methods_provided_by($role);
165             1;
166             };
167            
168 8     8   59 no warnings qw(once);
  8         24  
  8         11320  
169             my $type =
170             ($INC{'Moo/Role.pm'} and $Moo::Role::INFO{$role}{accessor_maker})
171             ? 'Moo::Role'
172             : 'Role::Tiny';
173            
174             @methods = $type->methods_provided_by($role)
175             if $type ne 'Role::Tiny';
176            
177             return {
178             name => $role,
179             type => $type,
180             api => [ sort(@methods) ], # keep: potentially more accurate
181             provides => [ sort keys %{ $type->_concrete_methods_of($role) } ],
182             requires => [ sort @{ $Role::Tiny::INFO{$role}{requires} or [] } ],
183             };
184             };
185              
186             # Learn about Moose
187             learn {
188             my $role = shift;
189             return unless $INC{'Moose.pm'};
190            
191             require Moose::Util;
192             my $meta = Moose::Util::find_meta($role);
193             return unless $meta && $meta->isa('Moose::Meta::Role');
194            
195             return {
196             name => $role,
197             type => 'Moose::Role',
198             meta => $meta,
199             provides => [ sort($meta->get_method_list, __PACKAGE__->_expand_attributes($role, $meta)) ],
200             requires => [ sort(map($_->name, $meta->get_required_method_list)) ],
201             };
202             };
203              
204             # Learn about Mouse
205             learn {
206             my $role = shift;
207             return unless $INC{'Mouse.pm'};
208            
209             require Mouse::Util;
210             my $meta = Mouse::Util::find_meta($role);
211             return unless $meta && $meta->isa('Mouse::Meta::Role');
212            
213             return {
214             name => $role,
215             type => 'Mouse::Role',
216             meta => $meta,
217             provides => [ sort($meta->get_method_list, __PACKAGE__->_expand_attributes($role, $meta)) ],
218             requires => [ sort($meta->get_required_method_list) ],
219             };
220             };
221              
222             # Learn about Role::Basic
223             learn {
224             my $role = shift;
225             return unless $INC{'Role/Basic.pm'};
226            
227             return unless eval { 'Role::Basic'->_load_role($role) };
228            
229             return {
230             name => $role,
231             type => 'Role::Basic',
232             provides => [ sort(keys(%{ 'Role::Basic'->_get_methods($role) })) ],
233             requires => [ sort('Role::Basic'->get_required_by($role)) ],
234             };
235             };
236              
237             1;
238              
239             __END__
240              
241             =pod
242              
243             =encoding utf-8
244              
245             =for stopwords metaobject
246              
247             =head1 NAME
248              
249             Role::Inspector - introspection for roles
250              
251             =head1 SYNOPSIS
252              
253             use strict;
254             use warnings;
255             use feature qw(say);
256            
257             {
258             package Local::Role;
259             use Role::Tiny; # or Moose::Role, Mouse::Role, etc...
260            
261             requires qw( foo );
262            
263             sub bar { ... }
264             }
265            
266             use Role::Inspector qw( get_role_info );
267            
268             my $info = get_role_info('Local::Role');
269            
270             say $info->{name}; # Local::Role
271             say $info->{type}; # Role::Tiny
272             say for @{$info->{api}}; # bar
273             # foo
274              
275             =head1 DESCRIPTION
276              
277             This module allows you to retrieve a hashref of information about a
278             given role. The following role implementations are supported:
279              
280             =over
281              
282             =item *
283              
284             L<Moose::Role>
285              
286             =item *
287              
288             L<Mouse::Role>
289              
290             =item *
291              
292             L<Moo::Role>
293              
294             =item *
295              
296             L<Role::Tiny>
297              
298             =item *
299              
300             L<Role::Basic>
301              
302             =item *
303              
304             L<p5-mop-redux|https://github.com/stevan/p5-mop-redux>
305              
306             =back
307              
308             =head2 Functions
309              
310             =over
311              
312             =item C<< get_role_info($package_name) >>
313              
314             Returns a hashref of information about a role; returns C<undef> if the
315             package does not appear to be a role. Attempts to load the package
316             using L<Module::Runtime> if it's not already loaded.
317              
318             The hashref may contain the following keys:
319              
320             =over
321              
322             =item *
323              
324             C<name> - the package name of the role
325              
326             =item *
327              
328             C<type> - the role implementation used by the role
329              
330             =item *
331              
332             C<api> - an arrayref of method names required/provided by the role
333              
334             =item *
335              
336             C<provides> and C<requires> - the same as C<api>, but split into lists
337             of methods provided and required by the role
338              
339             =item *
340              
341             C<meta> - a metaobject for the role (e.g. a L<Moose::Meta::Role> object).
342             This key may be absent if the role implementation does not provide a
343             metaobject
344              
345             =back
346              
347             This function may be exported, but is not exported by default. If you do
348             not wish to export it, you may call it as a class method:
349              
350             Role::Inspector->get_role_info($package_name)
351              
352             =item C<< Role::Inspector::learn { BLOCK } >>
353              
354             In the unlikely situation that you have to deal with some other role
355             implementation that Role::Inspector doesn't know about, you can teach
356             it:
357              
358             use Role::Inspector qw( learn );
359            
360             learn {
361             my $r = shift;
362             return unless My::Implementation::is_role($r);
363             return {
364             name => $r,
365             type => 'My::Implementation',
366             provides => [ sort(@{My::Implementation::provides($r)}) ],
367             requires => [ sort(@{My::Implementation::requires($r)}) ],
368             };
369             };
370              
371             An alternative way to do this is:
372              
373             push @Role::Inspector::SCANNERS, sub {
374             my $r = shift;
375             ...;
376             };
377              
378             You can do the C<push> thing without having loaded Role::Inspector.
379             This makes it suitable for doing inside My::Implementation itself,
380             without introducing an additional dependency on Role::Inspector.
381              
382             Note that if you don't provide all of C<provides>, C<requires>, and
383             C<api>, Role::Inspector will attempt to guess the missing parts.
384              
385             =back
386              
387             =head1 CAVEATS
388              
389             =over
390              
391             =item *
392              
393             It is difficult to distinguish between L<Moo::Role> and L<Role::Tiny>
394             roles. (The distinction is not often important anyway.) Thus sometimes
395             the C<type> for a Moo::Role may say C<< "Role::Tiny" >>.
396              
397             =item *
398              
399             The way that Role::Basic roles are detected and introspected is a bit
400             dodgy, relying on undocumented methods.
401              
402             =item *
403              
404             Where Moose or Mouse roles define attributes, those attributes tend to
405             result in accessor methods being generated. However neither of these
406             frameworks provides a decent way of figuring out which accessor methods
407             will result from composing the role with the class.
408              
409             Role::Inspector does its damnedest to figure out the list of likely
410             methods, but (especially in the case of unusual attribute traits) may
411             get things wrong from time to time.
412              
413             =back
414              
415             =head1 BUGS
416              
417             Please report any bugs to
418             L<http://rt.cpan.org/Dist/Display.html?Queue=Role-Inspector>.
419              
420             =head1 SEE ALSO
421              
422             L<Class::Inspector>.
423              
424             =head1 AUTHOR
425              
426             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
427              
428             =head1 COPYRIGHT AND LICENCE
429              
430             This software is copyright (c) 2014 by Toby Inkster.
431              
432             This is free software; you can redistribute it and/or modify it under
433             the same terms as the Perl 5 programming language system itself.
434              
435             =head1 DISCLAIMER OF WARRANTIES
436              
437             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
438             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
439             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
440