File Coverage

blib/lib/Parse/CPAN/Packages.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


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