App-CPAN-Dependents/lib/App/CPAN/Dependents.pm
package App::CPAN::Dependents;
use strict;
use warnings;
use Carp 'croak';
use Exporter 'import';
use MetaCPAN::Client;
our $VERSION = '1.000';
our @EXPORT_OK = ('find_all_dependents');
sub find_all_dependents {
my %options = @_;
my $mcpan = delete $options{mcpan};
unless (defined $mcpan) {
my $http = delete $options{http};
my %mcpan_params;
$mcpan_params{ua} = $http if defined $http;
$mcpan = MetaCPAN::Client->new(%mcpan_params);
}
my $module = delete $options{module};
my $dist = delete $options{dist};
my %dependent_dists;
if (defined $dist) {
my $modules = _dist_modules($mcpan, $dist);
_find_dependents($mcpan, $modules, \%dependent_dists, \%options);
} elsif (defined $module) {
my $dist = _module_dist($mcpan, $module); # check if module is valid
_find_dependents($mcpan, [$module], \%dependent_dists, \%options);
} else {
croak 'No module or distribution defined';
}
return [sort keys %dependent_dists];
}
sub _find_dependents {
my ($mcpan, $modules, $dependent_dists, $options) = @_;
$dependent_dists //= {};
$options //= {};
my $dists = _module_dependents($mcpan, $modules, $options);
if ($options->{debug} and @$dists) {
my @names = map { $_->{name} } @$dists;
warn "Found dependent distributions: @names\n";
}
foreach my $dist (@$dists) {
my $name = $dist->{name};
next if exists $dependent_dists->{$name};
$dependent_dists->{$name} = 1;
my $modules = $dist->{provides};
warn @$modules ? "Modules provided by $name: @$modules\n"
: "No modules provided by $name\n" if $options->{debug};
_find_dependents($mcpan, $modules, $dependent_dists, $options) if @$modules;
}
return $dependent_dists;
}
sub _module_dependents {
my ($mcpan, $modules, $options) = @_;
my @relationships = ('requires');
push @relationships, 'recommends' if $options->{recommends};
push @relationships, 'suggests' if $options->{suggests};
my @dep_filters = (
{ terms => { 'dependency.module' => $modules } },
{ terms => { 'dependency.relationship' => \@relationships } },
);
push @dep_filters, { not => { term => { 'dependency.phase' => 'develop' } } }
unless $options->{develop};
my %filter = (
and => [
{ term => { maturity => 'released' } },
{ term => { status => 'latest' } },
{ nested => {
path => 'dependency',
filter => { and => \@dep_filters },
} },
],
);
my $response = $mcpan->all('releases', {
fields => [ 'distribution', 'provides' ],
es_filter => \%filter,
});
my @results;
while (my $hit = $response->next) {
my $name = $hit->distribution;
my $provides = $hit->provides // [];
$provides = [$provides] unless ref $provides;
push @results, { name => $name, provides => $provides };
}
return \@results;
}
sub _dist_modules {
my ($mcpan, $dist) = @_;
my $response = $mcpan->release($dist) // return [];
return $response->provides // [];
}
sub _module_dist {
my ($mcpan, $module) = @_;
my $response = $mcpan->module($module) // return undef;
return $response->distribution;
}
1;
=head1 NAME
App::CPAN::Dependents - Recursively find all reverse dependencies for a
distribution or module
=head1 SYNOPSIS
use App::CPAN::Dependents 'find_all_dependents';
my $dependents = find_all_dependents(module => 'JSON::Tiny'); # or dist => 'JSON-Tiny'
print "Distributions dependent on JSON::Tiny: @$dependents\n";
# From the commandline
$ cpan-dependents --with-recommends JSON::Tiny
$ cpan-dependents -c JSON-Tiny
=head1 DESCRIPTION
L<App::CPAN::Dependents> provides the function L</"find_all_dependents">
(exportable on demand) for the purpose of determining all distributions which
are dependent on a particular CPAN distribution or module.
This module uses the MetaCPAN API, and must perform several requests
recursively, so it may take a long time (sometimes minutes) to complete. If the
function encounters HTTP errors (including when querying a nonexistent module
or distribution) or is unable to connect, it will die.
This module will only find distributions that explicitly list prerequisites in
metadata; C<dynamic_config> will not be used. Also, it assumes the MetaCPAN API
will correctly extract the provided modules for distributions, so any unindexed
or unauthorized modules will be ignored.
See L<cpan-dependents> for command-line usage.
=head1 FUNCTIONS
=head2 find_all_dependents
my $dependents = find_all_dependents(module => 'JSON::Tiny', recommends => 1);
Find all dependent distributions. Returns an array reference of distribution
names. The following parameters are accepted:
=over
=item module
The module name to find dependents for. Mutually exclusive with C<dist>.
=item dist
The distribution to find dependents for. Mutually exclusive with C<module>.
=item http
Optional L<HTTP::Tiny> object to use for building the default
L<MetaCPAN::Client> object.
=item mcpan
Optional L<MetaCPAN::Client> object to use for querying MetaCPAN. If not
specified, a default L<MetaCPAN::Client> object will be created using
L</"http"> if specified.
=item recommends
Boolean value, if true then C<recommends> prerequisites will be considered in
the results. Defaults to false.
=item suggests
Boolean value, if true then C<suggests> prerequisites will be considered in the
results. Defaults to false.
=item develop
Boolean value, if true then C<develop> phase prerequisites will be considered
in the results. Defaults to false.
=item debug
Boolean value, if true then debugging information will be printed to STDERR as
it is retrieved.
=back
=head1 AUTHOR
Dan Book, C<dbook@cpan.org>
=head1 COPYRIGHT AND LICENSE
Copyright 2015, Dan Book.
This library is free software; you may redistribute it and/or modify it under
the terms of the Artistic License version 2.0.
=head1 SEE ALSO
L<cpan-dependents>, L<Test::DependentModules>, L<MetaCPAN::Client>,
L<CPAN::Meta::Spec>