File Coverage

blib/lib/PAUSE/Packages.pm
Criterion Covered Total %
statement 124 136 91.1
branch 27 42 64.2
condition 8 21 38.1
subroutine 22 22 100.0
pod 2 3 66.6
total 183 224 81.7


line stmt bran cond sub pod time code
1             $PAUSE::Packages::VERSION = '0.18';
2             use 5.8.1;
3 6     6   500919 use autodie 2.29 qw(open);
  6         44  
4 6     6   2680  
  6         89863  
  6         29  
5             use Moo 1.006;
6 6     6   5774  
  6         58772  
  6         32  
7             use CPAN::DistnameInfo;
8 6     6   10402 use Carp;
  6         5094  
  6         169  
9 6     6   39 use File::HomeDir 0.93;
  6         11  
  6         379  
10 6     6   3344 use File::Spec::Functions 'catfile';
  6         29720  
  6         323  
11 6     6   2598 use HTTP::Date qw(time2str);
  6         4541  
  6         387  
12 6     6   1336 use HTTP::Tiny;
  6         10320  
  6         308  
13 6     6   4814 use JSON::MaybeXS;
  6         263972  
  6         238  
14 6     6   3259 use PAUSE::Packages::Module;
  6         36712  
  6         349  
15 6     6   2666 use PAUSE::Packages::Release;
  6         22  
  6         183  
16 6     6   2690 use Safe::Isa;
  6         18  
  6         203  
17 6     6   2678 use Types::URI -all;
  6         2555  
  6         720  
18 6     6   2569 use Types::Standard qw( Bool Object Str );
  6         972146  
  6         198  
19 6     6   13345 use URI;
  6         15  
  6         33  
20 6     6   5117 use URL::Encode qw( url_encode );
  6         12  
  6         134  
21 6     6   2573  
  6         26819  
  6         8368  
22             my $DISTNAME = 'PAUSE-Packages';
23              
24             has from_cache => (
25             is => 'rwp',
26             isa => Bool,
27             init_arg => undef,
28             );
29              
30             has ua => (
31             is => 'ro',
32             isa => Object,
33             default => sub { return HTTP::Tiny->new },
34             );
35              
36             has url => (
37             is => 'ro',
38             isa => Uri,
39             coerce => 1,
40             default =>
41             sub { return 'http://www.cpan.org/modules/02packages.details.txt' },
42             );
43              
44             has path => (
45             is => 'rw',
46             isa => Str,
47             );
48              
49             {
50             my $self = shift;
51              
52 2     2 1 1022 require PAUSE::Packages::ReleaseIterator;
53             return PAUSE::Packages::ReleaseIterator->new( packages => $self, @_ );
54 2         1105 }
55 2         14  
56             {
57             my $self = shift;
58             my $distname = shift;
59             my $fh;
60 3     3 1 689 local $_;
61 3         5  
62 3         4 open($fh, '<', $self->path);
63 3         6 while (<$fh>) {
64             last if /^$/;
65 3         58 }
66 3         2435 while (<$fh>) {
67 10 100       36 chomp;
68             my ($path, $json) = split(/\s+/, $_, 2);
69 3         10 my $di = CPAN::DistnameInfo->new($path);
70 3         7 next RELEASE if !defined($di) || !defined($di->dist);
71 3         18 last if $di->dist gt $distname;
72 3         23 if ($di->dist eq $distname) {
73 3 50 33     239 my $modules = [];
74 3 100       23 foreach my $entry (@{ decode_json($json) }) {
75 2 50       14 my $module = PAUSE::Packages::Module->new(
76 2         11 name => $entry->[0],
77 2         3 version => $entry->[1],
  2         13  
78 2         14 );
79             push(@$modules, $module);
80             }
81             return PAUSE::Packages::Release->new(
82 2         1893 modules => $modules,
83             path => $path,
84 2         13 distinfo => $di,
85             );
86             }
87             }
88             close($fh);
89             return undef;
90             }
91 1         25  
92 1         6 {
93             my $self = shift;
94              
95             # If constructor didn't specify a local file, then mirror the file from CPAN
96             if (not $self->path) {
97 7     7 0 33964 # use a file name which is unique to the URI
98             my $cache_file_name = url_encode( $self->url );
99              
100 7 100       136 $self->path( catfile(File::HomeDir->my_dist_data( $DISTNAME, { create => 1 } ), $cache_file_name) );
101             $self->_cache_file_if_needed();
102 3         55 }
103             }
104 3         118  
105 3         853 {
106             my $self = shift;
107             my $options = $self->ua->$_isa( 'HTTP::Tiny' ) ? {} : [];
108              
109             my $cache_creation_time = (stat($self->path))[9];
110              
111 3     3   7 if (-f $self->path) {
112 3 50       28 if ( $self->ua->$_isa( 'HTTP::Tiny' ) ) {
113             $options->{headers}->{'If-Modified-Since'}
114 3         117 = time2str( $cache_creation_time );
115             }
116 3 100       283 else {
117 1 50       24 $options = [ 'If-Modified-Since' => time2str( $cache_creation_time ) ];
118 0         0 }
119              
120             my $uri = $self->url;
121             $uri->scheme( 'file' ) if !$uri->scheme;
122 1         19  
123             if ( $uri->scheme eq 'file'
124             && -f $uri->path
125 1         26 && ( stat( $uri->path ) )[9] < $cache_creation_time )
126 1 50       4 {
127             $self->_set_from_cache( 1 );
128 1 50 33     21 return;
      33        
129             }
130             }
131              
132 1         93 my $response = $self->ua->get( $self->url,
133 1         35 $self->ua->$_isa( 'HTTP::Tiny' ) ? $options : @{$options} );
134              
135             my $status = $response->$_can('code') ? $response->code : $response->{status};
136             if ( $status == 304) { ; # Not Modified
137             $self->_set_from_cache( 1 );
138 2 50       42 return;
  2         35  
139             }
140 2 50       21664  
141 2 50       53 if ($status == 200) {
142 0         0 $self->_transform_and_cache( $response->$_can('content')
143 0         0 ? $response->content
144             : $response->{content} );
145             $self->_set_from_cache ( 0 );
146 2 100       7 return;
147             }
148              
149 1 50       3 my $reason = $response->$_can('message') ? $response->message : $response->{reason};
150 1         29 croak("request for 02packages failed: $status $reason");
151 1         51 }
152              
153             {
154 1 50       7 my ($self, $content) = @_;
155 1         162 my $inheader = 1;
156             my (%release, %other, $module, $version, $path, $distname);
157              
158             LINE:
159             while ($content =~ m!^(.*)$!gm) {
160 1     1   30 my $line = $1;
161 1         2 if ($line =~ /^$/ && $inheader) {
162 1         2 $inheader = 0;
163             next;
164             }
165 1         6 next LINE if $inheader;
166 3         40 ($module, $version, $path) = split(/\s+/, $line);
167 3 100 66     17  
168 1         3 my $di = CPAN::DistnameInfo->new($path);
169 1         5  
170             if (defined($di) && defined($distname = $di->dist) && defined($di->version)) {
171 2 100       8 if (!exists($release{$distname}) || $release{$distname}->{version} lt $di->version) {
172 1         6 $release{$distname} = {
173             version => $di->version,
174 1         8 modules => [ { name => $module, version => $version } ],
175             path => $path,
176 1 50 33     81 };
      33        
177 1 50 33     19 } elsif ($di->version lt $release{$distname}->{version}) {
    0          
178 1         3 next LINE;
179             } else {
180             push(@{ $release{$distname}->{modules} },
181             { name => $module, version => $version }
182             );
183             }
184 0         0 } else {
185             push(@{ $other{$path} }, { name => $module, version => $version });
186 0         0 }
  0         0  
187             }
188              
189             open(my $fh, '>', $self->path);
190              
191 0         0 print $fh <<"END_HEADER";
  0         0  
192             File: PAUSE Packages data
193             Format: 2
194             Source: CPAN/modules/02packages.details.txt
195 1         36  
196             END_HEADER
197 1         2328  
198             foreach $distname (sort keys %release) {
199             print $fh $release{$distname}->{path}, ' ';
200             print $fh "[", join(",", map { '["'.$_->{name}.'","'.$_->{version}.'"]' } @{ $release{$distname}->{modules} }), "]\n";
201             }
202              
203             foreach my $release (sort keys %other) {
204 1         6 print $fh $release, ' ';
205 1         4 print $fh "[", join(",", map { '["'.$_->{name}.'","'.$_->{version}.'"]' } @{ $other{$release} }), "]\n";
206 1         3 }
  1         8  
  1         4  
207              
208             close($fh);
209 1         4 }
210 0         0  
211 0         0 1;
  0         0  
  0         0  
212              
213             =head1 NAME
214 1         50  
215             PAUSE::Packages - interface to PAUSE's packages file (02packages.details.txt)
216              
217             =head1 SYNOPSIS
218              
219             use PAUSE::Packages 0.12;
220              
221             my $pp = PAUSE::Packages->new;
222             my $iterator = $pp->release_iterator();
223              
224             while (my $release = $iterator->next_release) {
225             print 'path = ', $release->path, "\n";
226             print ' modules = ', join( ', ', map { $_->name } @{ $release->modules } ), "\n";
227             }
228              
229             $release = $pp->release('Module-Path');
230              
231             # to parse a local file
232             my $pp = PAUSE::Packages->new(
233             url => 'file:///path/to/02packages.details.txt',
234             ua => LWP::UserAgent->new,
235             );
236              
237             =head1 DESCRIPTION
238              
239             B<NOTE>: this is very much an alpha release. any and all feedback appreciated.
240              
241             PAUSE::Packages provides an interface to the C<02packages.details.txt>
242             file produced by the Perl Authors Upload Server (PAUSE).
243             The file records what version of what modules are included in each
244             release of a distribution that is on CPAN.
245              
246             PAUSE::Packages processes 02packages.details.txt and caches a transformed
247             version of the data, with the following characteristics:
248              
249             =over 4
250              
251             =item *
252              
253             Only the highest numbered version of a module is included.
254              
255             =item *
256              
257             All modules in a release are written together, to make it efficient to
258             iterate over the file release by release.
259             02packages is sorted by module name, not by release, which means it can't
260             be efficiently processed by an iterator.
261              
262             =back
263              
264             The interface for this distribution is very much still in flux,
265             as is the documentation.
266              
267             =head1 constructor
268              
269             The constructor (C<new()>) can be passed an argument C<path>,
270             along with a path to a local copy of the I<cached format> used by
271             PAUSE::Packages:
272              
273             $pp = PAUSE::Packages->new(path => 'mypackages.txt');
274              
275             Note: this is not the same format used by 02packages.details.txt,
276             as described above.
277              
278             If you don't specify a path, then the local cache path is generated,
279             and you can use the C<path> attribute to find out what it is:
280              
281             $pp = PAUSE::Packages->new();
282             print "cache path = ", $pp->path, "\n";
283              
284             In a future release this will change: there will be separate attributes
285             for the cache path and the path to your own local copy.
286              
287             =head1 METHODS
288              
289             =head2 release_iterator()
290              
291             See the SYNOPSIS.
292              
293             This supports one optional argument, C<well_formed>,
294             which if true says that the iterator should only return releases
295             where the dist name and author's PAUSE id could be found:
296              
297             my $iterator = PAUSE::Packages->new()->release_iterator(
298             well_formed => 1
299             );
300              
301             This saves you from having to write code like the following:
302              
303             while (my $release = $iterator->next_release) {
304             next unless defined($release->distinfo);
305             next unless defined($release->distinfo->dist);
306             next unless defined($release->distinfo->cpanid);
307             ...
308             }
309              
310             =head2 release($DISTNAME)
311              
312             Takes a dist name and returns an instance of L<PAUSE::Packages::Release>,
313             or C<undef> if a release couldn't be found for the specified distname.
314              
315             =head2 ua( MyUserAgent->new )
316              
317             Allows you to provide your own UserAgent. This is useful if you're working off
318             a local copy of 02packages.details.txt The default UserAgent is HTTP::Tiny,
319             which does not support the file:// schema. So, if you want to work with a
320             local file you can either set up local web server using something like
321             L<App::HTTPThis>, or you can provide a your own UserAgent (like
322             L<LWP::UserAgent> which does support this behaviour.
323              
324             =head2 url
325              
326             The URL to the 02packages.details.txt which you would like to parse. Defaults
327             to cpan.org If you want to use the file:// scheme to fetch your local package,
328             be sure to provide your own UserAgent. See the ua method above.
329              
330             =head1 NOTE
331              
332             The behaviour of this module changed between version 0.01 and 0.02,
333             so you should make sure you're using 0.02 or later:
334              
335             use PAUSE::Packages 0.02;
336              
337             =head1 SEE ALSO
338              
339             There are at least three other modules on CPAN
340             for parsing 02packages.details.txt.
341             There are two main differences between these modules and PAUSE::Packages:
342             (1) you have to download 02packages yourself, and
343             (2) if there are multiple releases of a dist on CPAN, containing different modules (eg due to refactoring), then you'll see the union of all modules, instead of just the modules in the most recent release.
344              
345             =over 4
346              
347             =item *
348              
349             L<Parse::CPAN::Packages>
350              
351             =item *
352              
353             L<Parse::CPAN::Packages::Fast> - a 'largely API compatible rewrite' of
354             the above module, which is claimed to be a lot faster.
355              
356             =item *
357              
358             L<Parse::CPAN::Perms>
359              
360             =item *
361              
362             L<CPAN::Common::Index> - aims to be a common interface to all available backends
363              
364             =item *
365              
366             L<CPAN::PackageDetails> - can be used to read an existing copy of
367             02packages.details.txt, or to create your own.
368              
369             =back
370              
371             =head1 REPOSITORY
372              
373             L<https://github.com/neilb/PAUSE-Packages>
374              
375             =head1 AUTHOR
376              
377             Neil Bowers E<lt>neilb@cpan.orgE<gt>
378              
379             =head1 COPYRIGHT AND LICENSE
380              
381             This software is copyright (c) 2013-2014 by Neil Bowers <neilb@cpan.org>.
382              
383             This is free software; you can redistribute it and/or modify it under
384             the same terms as the Perl 5 programming language system itself.
385              
386             =cut
387