File Coverage

blib/lib/Parse/CPAN/Packages.pm
Criterion Covered Total %
statement 132 132 100.0
branch 26 34 76.4
condition 5 12 41.6
subroutine 35 35 100.0
pod 14 16 87.5
total 212 229 92.5


line stmt bran cond sub pod time code
1             package Parse::CPAN::Packages;
2 1     1   9009 use Moo;
  1         13684  
  1         7  
3 1     1   2555 use CPAN::DistnameInfo;
  1         1298  
  1         42  
4 1     1   833 use Compress::Zlib;
  1         68920  
  1         234  
5 1     1   499 use Path::Class ();
  1         27517  
  1         25  
6 1     1   7 use File::Slurp 'read_file';
  1         2  
  1         64  
7 1     1   453 use Parse::CPAN::Packages::Distribution;
  1         3  
  1         41  
8 1     1   490 use Parse::CPAN::Packages::Package;
  1         4  
  1         39  
9 1     1   5 use Types::Standard qw( HashRef Maybe Str );
  1         1  
  1         6  
10 1     1   823 use version;
  1         1392  
  1         10  
11             our $VERSION = '2.39';
12              
13             has 'filename' => ( is => 'rw', isa => Str );
14             has 'mirror_dir' => ( is => 'lazy', isa => Maybe [Str] );
15              
16             has 'details' => ( is => 'rw', isa => HashRef, default => sub { {} } );
17             has 'data' => ( is => 'rw', isa => HashRef, default => sub { {} } );
18             has 'dists' => ( is => 'rw', isa => HashRef, default => sub { {} } );
19             has 'latestdists' => ( is => 'rw', isa => HashRef, default => sub { {} } );
20              
21             sub BUILDARGS {
22 6     6 0 6896 my ( $class, @args ) = @_;
23 6 100       36 return {@args} if @args > 1;
24 5         75 return { filename => $args[0] };
25             }
26              
27             sub BUILD {
28 6     6 0 160 my $self = shift;
29 6         77 my $filename = $self->filename;
30              
31             # read the file then parse it if present
32 6 50       533 $self->parse( $filename ) if $filename;
33              
34 6         82 return $self;
35             }
36              
37             sub _build_mirror_dir {
38 5     5   733 my ( $self ) = @_;
39 5 100       103 return if $self->filename =~ /\n/;
40 3 50       65 return if !-f $self->filename;
41 3         103 my $dir = Path::Class::file( $self->filename )->dir->parent;
42 3         853 return $dir->stringify;
43             }
44              
45             # read the file into memory and return it
46             sub _slurp_details {
47 6     6   8 my ( $self, $filename ) = @_;
48 6   50     13 $filename ||= '02packages.details.txt.gz';
49              
50 6 100       23 return $filename if $filename =~ /Description:/;
51 5 100       18 return Compress::Zlib::memGunzip( $filename ) if $filename =~ /^\037\213/;
52              
53 4         8 my @read_params = ( $filename );
54 4 100       16 push @read_params, ( binmode => ':raw' ) if $filename =~ /\.gz/;
55              
56 4         22 my $data = read_file( @read_params );
57              
58 4 100       458 return Compress::Zlib::memGunzip( $data ) if $filename =~ /\.gz/;
59 2         20 return $data;
60             }
61              
62             for my $subname ( qw(file url description columns intended_for written_by line_count last_updated) ) {
63 1     1   430 no strict 'refs';
  1         20  
  1         621  
64 8     8   690 *{$subname} = sub { return shift->{preamble}{$subname} };
65             }
66              
67             sub parse {
68 6     6 1 9 my ( $self, $filename ) = @_;
69              
70             # read the preamble
71 6         16 my @details = split "\n", $self->_slurp_details( $filename );
72 6         491 while ( @details ) {
73 54         54 local $_ = shift @details;
74 54 100       123 last if /^\s*$/;
75 48 50       149 next unless /^([^:]+):\s*(.*)/;
76 48         144 my ( $key, $value ) = ( lc( $1 ), $2 );
77 48         46 $key =~ tr/-/_/;
78 48         173 $self->{preamble}{$key} = $value;
79             }
80              
81             # run though each line of the file
82 6         12 for my $line ( @details ) {
83              
84             # make a package object from the line
85 54         138 my ( $package_name, $package_version, $prefix ) = split ' ', $line;
86 54         99 $self->add_quick( $package_name, $package_version, $prefix );
87             }
88             }
89              
90             sub add_quick {
91 54     54 1 60 my ( $self, $package_name, $package_version, $prefix ) = @_;
92              
93             # create a distribution object (or get an existing one)
94 54         67 my $dist = $self->distribution_from_prefix( $prefix );
95              
96             # create the package object
97 54         1008 my $m = Parse::CPAN::Packages::Package->new(
98             {
99             package => $package_name,
100             version => $package_version,
101             distribution => $dist
102             }
103             );
104              
105             # make the package have the distribion and the distribution
106             # have the package. Yes, this creates a cirtular reference. eek!
107 54         4684 $dist->add_package( $m );
108              
109             # record this distribution and package
110 54         815 $self->add_distribution( $dist );
111 54         87 $self->add_package( $m );
112             }
113              
114             sub distribution_from_prefix {
115 54     54 1 41 my ( $self, $prefix ) = @_;
116              
117             # see if we have one of these already and return it if we do.
118 54         90 my $d = $self->distribution( $prefix );
119 54 100       775 return $d if $d;
120              
121             # create a new one otherwise
122 42         139 my $i = CPAN::DistnameInfo->new( $prefix );
123 42         2116 $d = Parse::CPAN::Packages::Distribution->new(
124             {
125             prefix => $prefix,
126             dist => $i->dist,
127             version => $i->version,
128             maturity => $i->maturity,
129             filename => $i->filename,
130             cpanid => $i->cpanid,
131             distvname => $i->distvname,
132             mirror_dir => $self->mirror_dir,
133             }
134             );
135 42         1719 return $d;
136             }
137              
138             sub add_package {
139 54     54 1 53 my ( $self, $package ) = @_;
140              
141             # store it
142 54         836 $self->data->{ $package->package } = $package;
143              
144 54         2098 return $self;
145             }
146              
147             sub package {
148 6     6 1 6925 my ( $self, $package_name ) = @_;
149 6         149 return $self->data->{$package_name};
150             }
151              
152             sub packages {
153 7     7 1 3649 my $self = shift;
154 7         12 return values %{ $self->data };
  7         150  
155             }
156              
157             sub add_distribution {
158 54     54 1 56 my ( $self, $dist ) = @_;
159              
160 54         62 $self->_store_distribution( $dist );
161 54         1693 $self->_ensure_latest_distribution( $dist );
162             }
163              
164             sub _store_distribution {
165 54     54   57 my ( $self, $dist ) = @_;
166              
167 54         754 $self->dists->{ $dist->prefix } = $dist;
168             }
169              
170             sub _ensure_latest_distribution {
171 54     54   55 my ( $self, $new ) = @_;
172              
173 54         729 my $latest = $self->latest_distribution( $new->dist );
174 54 100       780 if ( !$latest ) {
175 36         49 $self->_set_latest_distribution( $new );
176 36         682 return;
177             }
178 18         257 my $new_version = $new->version;
179 18         822 my $latest_version = $latest->version;
180 18         66 my ( $newv, $latestv );
181              
182 18         18 eval {
183 1     1   8 no warnings;
  1         2  
  1         106  
184 18   50     175 $newv = version->new( $new_version || 0 );
185 18   50     89 $latestv = version->new( $latest_version || 0 );
186             };
187              
188 18 50       43 $self->_set_latest_distribution( $new ) if $self->_dist_is_latest( $newv, $latestv, $new_version, $latest_version );
189              
190 18         40 return;
191             }
192              
193             sub _dist_is_latest {
194 18     18   28 my ( $self, $newv, $latestv, $new_version, $latest_version ) = @_;
195 18 50 33     526 return 1 if $newv && $latestv && $newv > $latestv;
      33        
196 1     1   4 no warnings;
  1         1  
  1         242  
197 18 50       127 return 1 if $new_version > $latest_version;
198 18         39 return 0;
199             }
200              
201             sub distribution {
202 55     55 1 362 my ( $self, $dist ) = @_;
203 55         803 return $self->dists->{$dist};
204             }
205              
206             sub distributions {
207 1     1 1 3 my $self = shift;
208 1         1 return values %{ $self->dists };
  1         30  
209             }
210              
211             sub _set_latest_distribution {
212 36     36   35 my ( $self, $dist ) = @_;
213 36 50       693 return unless $dist->dist;
214 36         643 $self->latestdists->{ $dist->dist } = $dist;
215             }
216              
217             sub latest_distribution {
218 55     55 1 1235 my ( $self, $dist ) = @_;
219 55 50       110 return unless $dist;
220 55         779 return $self->latestdists->{$dist};
221             }
222              
223             sub latest_distributions {
224 2     2 1 607 my $self = shift;
225 2         3 return values %{ $self->latestdists };
  2         74  
226             }
227              
228             sub package_count {
229 1     1 1 507 my $self = shift;
230 1         3 return scalar scalar $self->packages;
231             }
232              
233             sub distribution_count {
234 1     1 1 382 my $self = shift;
235 1         53 return scalar $self->distributions;
236             }
237              
238             sub latest_distribution_count {
239 1     1 1 356 my $self = shift;
240 1         7 return scalar $self->latest_distributions;
241             }
242              
243             1;
244              
245             __END__
246              
247             =head1 NAME
248              
249             Parse::CPAN::Packages - Parse 02packages.details.txt.gz
250              
251             =head1 SYNOPSIS
252              
253             use Parse::CPAN::Packages;
254              
255             # must have downloaded
256             my $p = Parse::CPAN::Packages->new("02packages.details.txt.gz");
257             # either a filename as above or pass in the contents of the file
258             # (uncompressed)
259             my $p = Parse::CPAN::Packages->new($packages_details_contents);
260              
261             my $m = $p->package("Acme::Colour");
262             # $m is a Parse::CPAN::Packages::Package object
263             print $m->package, "\n"; # Acme::Colour
264             print $m->version, "\n"; # 1.00
265              
266             my $d = $m->distribution();
267             # $d is a Parse::CPAN::Packages::Distribution object
268             print $d->prefix, "\n"; # L/LB/LBROCARD/Acme-Colour-1.00.tar.gz
269             print $d->dist, "\n"; # Acme-Colour
270             print $d->version, "\n"; # 1.00
271             print $d->maturity, "\n"; # released
272             print $d->filename, "\n"; # Acme-Colour-1.00.tar.gz
273             print $d->cpanid, "\n"; # LBROCARD
274             print $d->distvname, "\n"; # Acme-Colour-1.00
275              
276             # all the package objects
277             my @packages = $p->packages;
278              
279             # all the distribution objects
280             my @distributions = $p->distributions;
281              
282             # the latest distribution
283             $d = $p->latest_distribution("Acme-Colour");
284             is($d->prefix, "L/LB/LBROCARD/Acme-Colour-1.00.tar.gz");
285             is($d->version, "1.00");
286              
287             # all the latest distributions
288             my @distributions = $p->latest_distributions;
289              
290             =head1 DESCRIPTION
291              
292             The Comprehensive Perl Archive Network (CPAN) is a very useful
293             collection of Perl code. It has several indices of the files that it
294             hosts, including a file named "02packages.details.txt.gz" in the
295             "modules" directory. This file contains lots of useful information and
296             this module provides a simple interface to the data contained within.
297              
298             In a future release L<Parse::CPAN::Packages::Package> and
299             L<Parse::CPAN::Packages::Distribution> might have more information.
300              
301             =head2 Methods
302              
303             =over
304              
305             =item new
306              
307             Creates a new instance from a details file.
308              
309             The constructor can be passed either the path to the
310             C<02packages.details.txt.gz> file, a path to an ungzipped version of
311             this file, or a scalar containing the entire uncompressed contents of
312             the file.
313              
314             Note that this module does not concern itself with downloading this
315             file. You should do this yourself. For example:
316              
317             use LWP::Simple qw(get);
318             my $data = get("http://www.cpan.org/modules/02packages.details.txt.gz");
319             my $p = Parse::CPAN::Packages->new($data);
320              
321             If you have a configured L<CPAN>, then there's usually already a
322             cached file available:
323              
324             use CPAN;
325             $CPAN::Be_Silent = 1;
326             CPAN::HandleConfig->load;
327             my $file = $CPAN::Config->{keep_source_where} . "/modules/02packages.details.txt.gz";
328             my $p = Parse::CPAN::Packages->new($file);
329              
330             =item package($packagename)
331              
332             Returns a C<Parse::CPAN::Packages::Package> that represents the
333             named package.
334              
335             my $p = Parse::CPAN::Packages->new($gzfilename);
336             my $package = $p->package("Acme::Colour");
337              
338             =item packages()
339              
340             Returns a list of B<Parse::CPAN::Packages::Package> objects
341             representing all the packages that were extracted from the file.
342              
343             =item package_count()
344              
345             Returns the number of packages stored.
346              
347             =item distribution($filename)
348              
349             Returns a B<Parse::CPAN::Packages::Distribution> object that
350             represents the filename passed:
351              
352             my $p = Parse::CPAN::Packages->new($gzfilename);
353             my $dist = $p->distribution('L/LB/LBROCARD/Acme-Colour-1.00.tar.gz');
354              
355             =item distributions()
356              
357             Returns a list of B<Parse::CPAN::Packages::Distribution> objects
358             representing all the known distributions.
359              
360             =item distribution_count()
361              
362             Returns the number of distributions stored.
363              
364             =item latest_distribution($distname)
365              
366             Returns the C<Parse::CPAN::Packages::Distribution> object that
367             represents the latest distribution for the named disribution passed,
368             that is to say it returns the distribution that has the highest
369             version number (as determined by version.pm or number comparison if
370             that fails):
371              
372             my $p = Parse::CPAN::Packages->new($gzfilename);
373             my $dist = $p->distribution('Acme-Color');
374              
375             =item latest_distrbutions()
376              
377             Returns a list of B<Parse::CPAN::Packages::Distribution> objects
378             representing all the latest distributions.
379              
380             =item latest_distribution_count()
381              
382             Returns the number of distributions stored.
383              
384             =back
385              
386             =head2 Preamble Methods
387              
388             These methods return the information from the preamble
389             at the start of the file. They return undef if for any reason
390             no matching preamble line was found.
391              
392             =over
393              
394             =item file()
395              
396             =item url()
397              
398             =item description()
399              
400             =item columns()
401              
402             =item intended_for()
403              
404             =item written_by()
405              
406             =item line_count()
407              
408             =item last_updated()
409              
410             =back
411              
412             =head2 Addtional Methods
413              
414             These are additional methods that you may find useful.
415              
416             =over
417              
418             =item parse($filename)
419              
420             Parses the filename. Works in a similar fashion to the the
421             constructor (i.e. you can pass it a filename for a
422             compressed/1uncompressed file, a uncompressed scalar containing the
423             file. You can also pass nothing to indicate to load the compressed
424             file from the current working directory.)
425              
426             Note that each time this function is run the packages and distribtions
427             found will be C<added> to the current list of packages.
428              
429             =item add_quick($package_name, $package_version, $prefix)
430              
431             Quick way of adding a new package and distribution.
432              
433             =item add_package($package_obj)
434              
435             Adds a package. Note that you'll probably want to add the
436             corrisponding distribution for that package too (it's not done
437             automatically.)
438              
439             =item add_distribution($distribution_obj)
440              
441             Adds a distribution. Note that you'll probably want to add the
442             corresponding packages for that distribution too (it's not done
443             automatically.)
444              
445             =item distribution_from_prefix($prefix)
446              
447             Returns a distribution given a prefix.
448              
449             =item latest_distributions
450              
451             Returns all the latest distributions:
452              
453             my @distributions = $p->latest_distributions;
454              
455             =cut
456              
457             =back
458              
459             =head1 AUTHOR
460              
461             Leon Brocard <acme@astray.com>
462              
463             =head1 COPYRIGHT
464              
465             Copyright (C) 2004-9, Leon Brocard
466              
467             =head1 LICENSE
468              
469             This module is free software; you can redistribute it or modify it under
470             the same terms as Perl itself.
471              
472             =head1 BUGS
473              
474             This module leaks memory as packages hold distributions and
475             distributions hold packages. No attempt has been made to fix this as
476             it's not anticpated that this will be used in long running programs
477             that will dispose of the objects once created.
478              
479             The old interface for C<new> where if you passed no arguments it would
480             look for a C<02packages.details.txt.gz> in your current directory is
481             no longer supported.
482              
483             =head1 TODO
484              
485             delete_* methods. merge_into method. Documentation for other modules.
486              
487             =head1 SEE ALSO
488              
489             L<CPAN::DistInfoname>, L<Parse::CPAN::Packages::Writer>.