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   370879 use strict;
  8         22  
  8         322  
4 8     8   44 use warnings;
  8         18  
  8         286  
5 8     8   44 use vars qw($VERSION $ERROR);
  8         20  
  8         669  
6              
7             $VERSION = '0.13';
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   7310 use CPAN::DistnameInfo;
  8         9600  
  8         263  
36 8     8   52 use File::Basename;
  8         14  
  8         1103  
37 8     8   51 use File::Path;
  8         17  
  8         492  
38 8     8   9876 use File::Slurp;
  8         197103  
  8         735  
39 8     8   20002 use File::Temp qw(tempfile);
  8         230504  
  8         647  
40 8     8   6298 use IO::File;
  8         7310  
  8         1359  
41 8     8   8675 use IO::Zlib;
  8         767850  
  8         67  
42 8     8   8942 use LWP::UserAgent;
  8         482997  
  8         304  
43 8     8   8061 use version;
  8         22559  
  8         62  
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 241425 my ($class,%hash) = @_;
78 7         35 my $self = { file => $hash{file} };
79 7         20 bless $self, $class;
80              
81 7 50       32 if(my $error = $self->parse) {
82 0         0 $ERROR = $error;
83 0         0 return;
84             }
85              
86 7         462990 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 2997 my ($self,$distribution,$version) = @_;
102              
103 16 100       50 return 0 unless(defined $distribution);
104 14 100       55 return 0 unless(defined $distros{$distribution});
105 12 100       41 return 1 unless(defined $version);
106 8 100       38 return 1 if($distros{$distribution}->{$version});
107 6         27 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 2764 my ($self,$author) = @_;
119              
120 8 100       31 return () unless(defined $author);
121 6 100       33 return () unless(defined $authors{$author});
122 3         6 my @dists = sort keys %{$authors{$author}};
  3         66  
123 3         27 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 5107 my ($self,$distribution,$version) = @_;
135              
136 19 100       65 return unless(defined $distribution);
137 17 100       70 return unless(defined $distros{$distribution});
138 15 100       53 return unless(defined $version);
139 12 100       90 return $distros{$distribution}->{$version}
140             if($distros{$distribution}->{$version});
141 3         14 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 1459 my ($self,$distribution,$author) = @_;
155              
156 12 100       37 return 0 unless(defined $distribution);
157 10 100       35 return 0 unless(defined $distros{$distribution});
158              
159 38         74 my @versions =
160 60         116 map {$_->{external}}
161 38         40 sort {$b->{internal} <=> $a->{internal}}
162 8 100       13 map {my $v; eval {$v = version->new($_)}; {internal => $@ ? $_ : $v->numify, external => $_}} keys %{$distros{$distribution}};
  38         46  
  38         223  
  38         318  
  8         33  
163              
164 8 100       52 if($author) {
165 4         8 for my $version (@versions) {
166 14 100       57 return $version if($distros{$distribution}{$version} eq $author);
167             }
168 0         0 return 0;
169             }
170              
171 4         18 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 6357 my ($self,$distribution,$author) = @_;
184 16         21 my (%versions,@versions);
185              
186 16 100       49 return () unless(defined $distribution);
187 14 100 100     105 return () if(defined $author && !defined $authors{$author});
188              
189 12 100       31 if($author) {
190 6         10 %versions = map {$_ => 1} @{$authors{$author}{$distribution}};
  20         59  
  6         19  
191 14         33 @versions =
192 16         37 map {$_->{external}}
193 14         17 sort {$a->{internal} <=> $b->{internal}}
194 6 100       23 map {my $v; eval {$v = version->new($_)}; {internal => $@ ? $_ : $v->numify, external => $_}} keys %versions;
  14         22  
  14         95  
  14         125  
195 6         41 return @versions;
196             }
197              
198 6 100       23 return () unless(defined $distros{$distribution});
199              
200 4         9 %versions = map {$_ => 1} keys %{$distros{$distribution}};
  14         38  
  4         15  
201 14         31 @versions =
202 20         40 map {$_->{external}}
203 14         18 sort {$a->{internal} <=> $b->{internal}}
204 4 100       15 map {my $v; eval {$v = version->new($_)}; {internal => $@ ? $_ : $v->numify, external => $_}} keys %versions;
  14         17  
  14         83  
  14         117  
205 4         27 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 16 my $self = shift;
216 7         13 my $temp = 0;
217              
218             #print STDERR "#file=$self->{file}\n";
219              
220 7 100 100     329 unless($self->{file} && -f $self->{file}) {
221 4         11 my $url = 'http://www.cpan.org/indices/find-ls.gz';
222 4         84 my $ua = LWP::UserAgent->new;
223 4         1584 $ua->timeout(180);
224              
225 4         58 my $filename='find-ls-temp.gz';
226 4         9 my $response;
227 4         9 eval { $response = $ua->mirror($url,$filename) };
  4         23  
228             #use Data::Dumper;
229             #print STDERR "#url=[$url], filename=[$filename], response=[".Dumper($response)."] [$@]\n";
230 4 50 33     14700802 return "Error fetching $url [$@]" if($@ || ! -f $filename);
231 4         23 $self->{file} = $filename;
232 4         187 $temp = 1;
233             }
234              
235 7         15 my $fh;
236 7 100       83 if ( $self->{file} =~ /\.gz/ ) {
237 5   50     69 $fh = IO::Zlib->new( $self->{file}, "rb" )
238             || return "Failed to read archive [$self->{file}]: $!";
239             } else {
240 2   50     22 $fh = IO::File->new( $self->{file}, 'r' )
241             || return "Failed to read file [$self->{file}]: $!";
242             }
243              
244 7         31618 while(<$fh>) {
245 2030880         295213110 s/\s+$//;
246             #print STDERR "# line =[$_]\n";
247              
248 2030880 100       20207791 next unless(m!\s(authors/id/[A-Z]/../[^/]+/.*$archive)!);
249              
250             #print STDERR "# file =[$1]\n";
251              
252 410116         1654470 my $dist = CPAN::DistnameInfo->new($1);
253              
254             #print STDERR "# dist =[".($dist ? 'OBJECT' : 'undef')."]\n";
255              
256 410116 50 33     35298139 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 410116   100     3412341 my $version = $dist->version || '';
263              
264 410116         3021105 $distros{ $dist->dist }->{ $version } = $dist->cpanid;
265 410116         3464039 push @{$authors{ $dist->cpanid }{ $dist->dist }}, $version;
  410116         1078206  
266             }
267              
268 7 100       1025 unlink($self->{file}) if($temp);
269 7         173 return;
270             }
271              
272             q("Everybody loves QA Automation!");
273              
274             __END__