File Coverage

blib/lib/Parse/CPAN/Distributions.pm
Criterion Covered Total %
statement 130 133 97.7
branch 53 56 94.6
condition 12 18 66.6
subroutine 19 19 100.0
pod 7 7 100.0
total 221 233 94.8


line stmt bran cond sub pod time code
1             package Parse::CPAN::Distributions;
2              
3 8     8   153923 use strict;
  8         15  
  8         261  
4 8     8   31 use warnings;
  8         8  
  8         212  
5 8     8   29 use vars qw($VERSION $ERROR);
  8         12  
  8         597  
6              
7             $VERSION = '0.14';
8              
9             #----------------------------------------------------------------------------
10              
11             =head1 NAME
12              
13             Parse::CPAN::Distributions - Provides an index for current CPAN distributions
14              
15             =head1 SYNOPSIS
16              
17             my $oncpan = Parse::CPAN::Distributions->new(database => $db);
18             my $found = $oncpan->listed($distribution,$version);
19             my $any = $oncpan->listed($distribution);
20             my @dists = $oncpan->distributions_by($author);
21             my $author = $oncpan->author_of($distribution,$version);
22             my $version = $oncpan->latest_version($distribution);
23             my @versions = $oncpan->versions($distribution);
24              
25             =head1 DESCRIPTION
26              
27             This distribution provides the ability to index the distributions that are
28             currently listed on CPAN. This is done by parsing the index file find-ls.
29              
30             =cut
31              
32             #----------------------------------------------------------------------------
33             # Library Modules
34              
35 8     8   3603 use CPAN::DistnameInfo;
  8         6101  
  8         195  
36 8     8   41 use File::Basename;
  8         8  
  8         623  
37 8     8   34 use File::Path;
  8         8  
  8         354  
38 8     8   4154 use File::Slurp;
  8         88420  
  8         515  
39 8     8   5691 use File::Temp qw(tempfile);
  8         112167  
  8         478  
40 8     8   2552 use IO::File;
  8         4413  
  8         888  
41 8     8   4313 use IO::Zlib;
  8         405664  
  8         44  
42 8     8   4584 use LWP::UserAgent;
  8         220385  
  8         216  
43 8     8   3327 use version;
  8         11210  
  8         40  
44              
45             #----------------------------------------------------------------------------
46             # Variables
47              
48             my (%distros,%authors);
49             my $archive = qr{\.(?:tar\.(?:bz2|gz|Z)|t(?:gz|bz)|zip)$}i;
50             $ERROR = '';
51              
52             # -------------------------------------
53             # Routines
54              
55             =head1 INTERFACE
56              
57             =head2 The Constructor
58              
59             =over
60              
61             =item new
62              
63             Parses find-ls, extracting the list of all the module distributions.
64              
65             Takes one optional hash key/pair, 'file', which can be used to specify the
66             path an existing compressed or uncompressed 'find-ls' file. By default a copy
67             will be downloaded and automatically loaded into memory.
68              
69             If new returns undef, $Parse::CPAN::Distributions::ERROR will contain the
70             error message recorded.
71              
72             =back
73              
74             =cut
75              
76             sub new {
77 7     7 1 106044 my ($class,%hash) = @_;
78 7         28 my $self = { file => $hash{file} };
79 7         19 bless $self, $class;
80              
81 7 50       24 if(my $error = $self->parse) {
82 0         0 $ERROR = $error;
83 0         0 return;
84             }
85              
86 7         9751 return $self;
87             }
88              
89             =head2 Methods
90              
91             =over
92              
93             =item listed
94              
95             Given a distribution and version, returns 1 if on CPAN, otherwise 0. Note that
96             if version is not provided it will assume you are looking for any version.
97              
98             =cut
99              
100             sub listed {
101 16     16 1 2026 my ($self,$distribution,$version) = @_;
102              
103 16 100       39 return 0 unless(defined $distribution);
104 14 100       39 return 0 unless(defined $distros{$distribution});
105 12 100       29 return 1 unless(defined $version);
106 8 100       31 return 1 if($distros{$distribution}->{$version});
107 6         20 return 0;
108             }
109              
110             =item distributions_by
111              
112             Given an author ID, returns a sorted list of the versioned distributions
113             currently available on CPAN.
114              
115             =cut
116              
117             sub distributions_by {
118 8     8 1 2079 my ($self,$author) = @_;
119              
120 8 100       22 return () unless(defined $author);
121 6 100       25 return () unless(defined $authors{$author});
122 3         3 my @dists = sort keys %{$authors{$author}};
  3         48  
123 3         19 return @dists;
124             }
125              
126             =item author_of
127              
128             Given a distribution and version, returns the author ID if available on CPAN,
129             otherwise undef is returned.
130              
131             =cut
132              
133             sub author_of {
134 19     19 1 3783 my ($self,$distribution,$version) = @_;
135              
136 19 100       62 return unless(defined $distribution);
137 17 100       57 return unless(defined $distros{$distribution});
138 15 100       36 return unless(defined $version);
139 12 100       62 return $distros{$distribution}->{$version}
140             if($distros{$distribution}->{$version});
141 3         11 return;
142             }
143              
144             =item latest_version
145              
146             Given a distribution, returns the latest known version on CPAN. If given a
147             distribution and author, will return the latest version for that author.
148              
149             Note that a return value of 0, implies unknown.
150              
151             =cut
152              
153             sub latest_version {
154 12     12 1 1218 my ($self,$distribution,$author) = @_;
155              
156 12 100       31 return 0 unless(defined $distribution);
157 10 100       29 return 0 unless(defined $distros{$distribution});
158              
159 38         44 my @versions =
160 66         87 map {$_->{external}}
161 38         34 sort {$b->{internal} <=> $a->{internal}}
162 8 100       10 map {my $v; eval {$v = version->new($_)}; {internal => $@ ? $_ : $v->numify, external => $_}} keys %{$distros{$distribution}};
  38         28  
  38         143  
  38         198  
  8         28  
163              
164 8 100       37 if($author) {
165 4         6 for my $version (@versions) {
166 14 100       44 return $version if($distros{$distribution}{$version} eq $author);
167             }
168 0         0 return 0;
169             }
170              
171 4         17 return shift @versions;
172             }
173              
174             =item versions
175              
176             Given a distribution will return all the versions available on CPAN. Given a
177             dsitribution and author, will return all the versions attributed to that
178             author.
179              
180             =cut
181              
182             sub versions {
183 16     16 1 5194 my ($self,$distribution,$author) = @_;
184 16         17 my (%versions,@versions);
185              
186 16 100       35 return () unless(defined $distribution);
187 14 100 100     59 return () if(defined $author && !defined $authors{$author});
188              
189 12 100       22 if($author) {
190 6         9 %versions = map {$_ => 1} @{$authors{$author}{$distribution}};
  20         40  
  6         17  
191 14         24 @versions =
192 14         23 map {$_->{external}}
193 14         13 sort {$a->{internal} <=> $b->{internal}}
194 6 100       14 map {my $v; eval {$v = version->new($_)}; {internal => $@ ? $_ : $v->numify, external => $_}} keys %versions;
  14         15  
  14         63  
  14         83  
195 6         27 return @versions;
196             }
197              
198 6 100       17 return () unless(defined $distros{$distribution});
199              
200 4         4 %versions = map {$_ => 1} keys %{$distros{$distribution}};
  14         25  
  4         13  
201 14         21 @versions =
202 22         30 map {$_->{external}}
203 14         10 sort {$a->{internal} <=> $b->{internal}}
204 4 100       12 map {my $v; eval {$v = version->new($_)}; {internal => $@ ? $_ : $v->numify, external => $_}} keys %versions;
  14         10  
  14         56  
  14         75  
205 4         19 return @versions;
206             }
207              
208             =item parse
209              
210             Parse find-ls, extracting the list of all the module distributions.
211              
212             =cut
213              
214             sub parse {
215 7     7 1 12 my $self = shift;
216 7         10 my $temp = 0;
217              
218             #print STDERR "#file=$self->{file}\n";
219              
220 7 100 100     156 unless($self->{file} && -f $self->{file}) {
221 4         7 my $url = 'http://www.cpan.org/indices/find-ls.gz';
222 4         39 my $ua = LWP::UserAgent->new;
223 4         929 $ua->timeout(180);
224              
225 4         41 my $filename='find-ls-temp.gz';
226 4         7 my $response;
227 4         6 eval { $response = $ua->mirror($url,$filename) };
  4         14  
228             #use Data::Dumper;
229             #print STDERR "#url=[$url], filename=[$filename], response=[".Dumper($response)."] [$@]\n";
230 4 50 33     5292496 return "Error fetching $url [$@]" if($@ || ! -f $filename);
231 4         14 $self->{file} = $filename;
232 4         151 $temp = 1;
233             }
234              
235 7         13 my $fh;
236 7 100       64 if ( $self->{file} =~ /\.gz/ ) {
237 5   50     52 $fh = IO::Zlib->new( $self->{file}, "rb" )
238             || return "Failed to read archive [$self->{file}]: $!";
239             } else {
240 2   50     18 $fh = IO::File->new( $self->{file}, 'r' )
241             || return "Failed to read file [$self->{file}]: $!";
242             }
243              
244 7         8524 while(<$fh>) {
245 2066580         146332084 s/\s+$//;
246             #print STDERR "# line =[$_]\n";
247              
248 2066580 100       12317953 next unless(m!\s(authors/id/[A-Z]/../[^/]+/.*$archive)!);
249              
250             #print STDERR "# file =[$1]\n";
251              
252 414924         986424 my $dist = CPAN::DistnameInfo->new($1);
253              
254             #print STDERR "# dist =[".($dist ? 'OBJECT' : 'undef')."]\n";
255              
256 414924 50 33     20103009 next unless($dist && $dist->dist);
257              
258             #print STDERR "# dist =[".($dist->dist)."]\n";
259             #print STDERR "# version=[".($dist->version)."]\n";
260             #print STDERR "# author =[".($dist->cpanid)."]\n";
261              
262 414924   100     1984541 my $version = $dist->version || '';
263              
264 414924         1558426 $distros{ $dist->dist }->{ $version } = $dist->cpanid;
265 414924         1788041 push @{$authors{ $dist->cpanid }{ $dist->dist }}, $version;
  414924         575845  
266             }
267              
268 7 100       779 unlink($self->{file}) if($temp);
269 7         125 return;
270             }
271              
272             q("Everybody loves QA Automation!");
273              
274             __END__