File Coverage

blib/lib/App/CPAN/Dependents.pm
Criterion Covered Total %
statement 74 91 81.3
branch 18 42 42.8
condition 2 5 40.0
subroutine 14 14 100.0
pod 1 1 100.0
total 109 153 71.2


line stmt bran cond sub pod time code
1             package App::CPAN::Dependents;
2              
3 2     2   994 use strict;
  2         3  
  2         62  
4 2     2   10 use warnings;
  2         2  
  2         50  
5 2     2   8 use Carp 'croak';
  2         3  
  2         119  
6 2     2   10 use Exporter 'import';
  2         2  
  2         63  
7 2     2   1479 use HTTP::Tiny;
  2         112585  
  2         94  
8 2     2   1182 use JSON::Tiny 'decode_json', 'encode_json';
  2         41149  
  2         203  
9 2     2   1304 use URI::Escape 'uri_escape';
  2         2890  
  2         232  
10              
11             our $VERSION = '0.003';
12              
13             our @EXPORT_OK = ('find_all_dependents');
14              
15 2     2   16 use constant METACPAN_API_ENDPOINT => 'http://api.metacpan.org/v0/';
  2         3  
  2         2364  
16              
17             sub find_all_dependents {
18 4     4 1 1340 my %options = @_;
19 4         15 my $http = delete $options{http};
20 4 50       17 $http = HTTP::Tiny->new unless defined $http;
21 4         12 my $module = delete $options{module};
22 4         10 my $dist = delete $options{dist};
23 4         7 my %dependent_dists;
24 4 100       53 if (defined $dist) {
    50          
25 2         9 my $modules = _dist_modules($http, $dist);
26 1         10158 _find_dependents($http, $modules, \%dependent_dists, \%options);
27             } elsif (defined $module) {
28 2         9 my $dist = _module_dist($http, $module); # check if module is valid
29 1         6417 _find_dependents($http, [$module], \%dependent_dists, \%options);
30             } else {
31 0         0 croak 'No module or distribution defined';
32             }
33 2         17 return [sort keys %dependent_dists];
34             }
35              
36             sub _find_dependents {
37 2     2   10 my ($http, $modules, $dependent_dists, $options) = @_;
38 2 50       21 $dependent_dists = {} unless defined $dependent_dists;
39 2 50       9 $options = {} unless defined $options;
40 2         13 my $dists = _module_dependents($http, $modules, $options);
41 2 50 33     12 if ($options->{debug} and @$dists) {
42 0         0 my @names = map { $_->{name} } @$dists;
  0         0  
43 0         0 warn "Found dependent distributions: @names\n";
44             }
45 2         6 foreach my $dist (@$dists) {
46 0         0 my $name = $dist->{name};
47 0 0       0 next if exists $dependent_dists->{$name};
48 0         0 $dependent_dists->{$name} = 1;
49 0         0 my $modules = $dist->{provides};
50 0 0       0 warn @$modules ? "Modules provided by $name: @$modules\n"
    0          
51             : "No modules provided by $name\n" if $options->{debug};
52 0 0       0 _find_dependents($http, $modules, $dependent_dists, $options) if @$modules;
53             }
54 2         7 return $dependent_dists;
55             }
56              
57             sub _module_dependents {
58 2     2   6 my ($http, $modules, $options) = @_;
59 2         8 my $url = METACPAN_API_ENDPOINT . 'release/_search';
60            
61 2         9 my @relationships = ('requires');
62 2 50       13 push @relationships, 'recommends' if $options->{recommends};
63 2 50       9 push @relationships, 'suggests' if $options->{suggests};
64 2         24 my @dep_filters = (
65             { terms => { 'dependency.module' => $modules } },
66             { terms => { 'dependency.relationship' => \@relationships } },
67             );
68 2 50       21 push @dep_filters, { not => { term => { 'dependency.phase' => 'develop' } } }
69             unless $options->{develop};
70            
71 2         58 my %form = (
72             query => { match_all => {} },
73             size => 5000,
74             fields => [ 'distribution', 'provides' ],
75             filter => {
76             and => [
77             { term => { 'release.maturity' => 'released' } },
78             { term => { 'release.status' => 'latest' } },
79             { nested => {
80             path => 'release.dependency',
81             filter => { and => \@dep_filters },
82             } },
83             ],
84             },
85             );
86            
87 2         19 my $content = encode_json \%form;
88 2         1639 my %headers = ( 'Content-Type' => 'application/json;charset=UTF-8' );
89 2         119 my $response = $http->post($url, { headers => \%headers, content => $content });
90 2 50       168864 _http_err($response) unless $response->{success};
91            
92 2         6 my @results;
93 2 50       27 foreach my $hit (@{decode_json($response->{content})->{hits}{hits} || []}) {
  2         14  
94 0         0 my $name = $hit->{fields}{distribution};
95 0         0 my $provides = $hit->{fields}{provides};
96 0 0       0 $provides = [] unless defined $provides;
97 0 0       0 $provides = [$provides] unless ref $provides;
98 0         0 push @results, { name => $name, provides => $provides };
99             }
100 2         734 return \@results;
101             }
102              
103             sub _dist_modules {
104 2     2   3 my ($http, $dist) = @_;
105 2         14 my $url = METACPAN_API_ENDPOINT . 'release/' . uri_escape $dist;
106 2         143 my $response = $http->get($url);
107 2 100       116752 _http_err($response) unless $response->{success};
108 1   50     7 return decode_json($response->{content})->{provides} || [];
109             }
110              
111             sub _module_dist {
112 2     2   24 my ($http, $module) = @_;
113 2         16 my $url = METACPAN_API_ENDPOINT . 'module/' . uri_escape $module;
114 2         245 my $response = $http->get($url);
115 2 100       110895 _http_err($response) unless $response->{success};
116 1         10 return decode_json($response->{content})->{distribution};
117             }
118              
119             sub _http_err {
120 2     2   7 my $response = shift;
121 2 50       13 return if $response->{success};
122 2 50       10 if ($response->{status} == 599) {
123 0         0 chomp(my $err = $response->{content});
124 0         0 die "HTTP error: $err\n";
125             } else {
126 2         9 chomp(my $reason = $response->{reason});
127 2         40 die "HTTP $response->{status}: $reason\n";
128             }
129             }
130              
131             1;
132              
133             =head1 NAME
134              
135             App::CPAN::Dependents - Recursively find all reverse dependencies for a
136             distribution or module
137              
138             =head1 SYNOPSIS
139              
140             use App::CPAN::Dependents 'find_all_dependents';
141             my $dependents = find_all_dependents(module => 'JSON::Tiny'); # or dist => 'JSON-Tiny'
142             print "Distributions dependent on JSON::Tiny: @$dependents\n";
143            
144             # From the commandline
145             $ cpan-dependents --with-recommends JSON::Tiny
146             $ cpan-dependents -c JSON-Tiny
147              
148             =head1 DESCRIPTION
149              
150             L provides the function L
151             (exportable on demand) for the purpose of determining all distributions which
152             are dependent on a particular CPAN distribution or module.
153              
154             This module uses the MetaCPAN API, and must perform several requests
155             recursively, so it may take a long time (sometimes minutes) to complete. If the
156             function encounters HTTP errors (including when querying a nonexistent module
157             or distribution) or is unable to connect, it will die.
158              
159             This module will only find distributions that explicitly list prerequisites in
160             metadata; C will not be used. Also, it assumes distributions
161             are "well-behaved" and thus declare all provided modules in the C
162             metadata, and only modules which they are authorized to provide. Any
163             distributions that do not follow this behavior may lead to incorrect results.
164              
165             See L for command-line usage.
166              
167             =head1 FUNCTIONS
168              
169             =head2 find_all_dependents
170              
171             my $dependents = find_all_dependents(module => 'JSON::Tiny', recommends => 1);
172              
173             Find all dependent distributions. Returns an array reference of distribution
174             names. The following parameters are accepted:
175              
176             =over
177              
178             =item module
179              
180             The module name to find dependents for. Mutually exclusive with C.
181              
182             =item dist
183              
184             The distribution to find dependents for. Mutually exclusive with C.
185              
186             =item http
187              
188             Optional L object to use for querying MetaCPAN. If not specified, a
189             default L object will be used.
190              
191             =item recommends
192              
193             Boolean value, if true then C prerequisites will be considered in
194             the results. Defaults to false.
195              
196             =item suggests
197              
198             Boolean value, if true then C prerequisites will be considered in the
199             results. Defaults to false.
200              
201             =item develop
202              
203             Boolean value, if true then C phase prerequisites will be considered
204             in the results. Defaults to false.
205              
206             =item debug
207              
208             Boolean value, if true then debugging information will be printed to STDERR as
209             it is retrieved.
210              
211             =back
212              
213             =head1 AUTHOR
214              
215             Dan Book, C
216              
217             =head1 COPYRIGHT AND LICENSE
218              
219             Copyright 2015, Dan Book.
220              
221             This library is free software; you may redistribute it and/or modify it under
222             the terms of the Artistic License version 2.0.
223              
224             =head1 SEE ALSO
225              
226             L, L, L,
227             L