File Coverage

blib/lib/Package/Locator.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             # ABSTRACT: Find a package among CPAN-like repositories
2              
3             package Package::Locator;
4              
5 4     4   499546 use Moose;
  0            
  0            
6             use MooseX::MarkAsMethods (autoclean => 1);
7             use MooseX::Types::Path::Class;
8              
9             use Carp;
10             use File::Temp;
11             use Path::Class;
12             use LWP::UserAgent;
13             use CPAN::DistnameInfo;
14             use URI;
15              
16             use Package::Locator::Index;
17              
18             use version;
19              
20              
21             #------------------------------------------------------------------------------
22              
23             our $VERSION = '0.010'; # VERSION
24              
25             #------------------------------------------------------------------------------
26              
27              
28             has repository_urls => (
29             is => 'ro',
30             isa => 'ArrayRef[URI]',
31             auto_deref => 1,
32             default => sub { [URI->new('http://cpan.perl.org')] },
33             );
34              
35             #------------------------------------------------------------------------------
36              
37              
38             has user_agent => (
39             is => 'ro',
40             isa => 'LWP::UserAgent',
41             builder => '_build_user_agent',
42             );
43              
44             sub _build_user_agent {
45             my ($self) = @_;
46              
47             my $agent = sprintf "%s/%s", ref $self, $self->VERSION || 'UNKNOWN';
48             return LWP::UserAgent->new(agent => $agent, env_proxy => 1, keep_alive => 5);
49             }
50              
51             #------------------------------------------------------------------------------
52              
53              
54             has cache_dir => (
55             is => 'ro',
56             isa => 'Path::Class::Dir',
57             default => sub { Path::Class::Dir->new( File::Temp::tempdir(CLEANUP => 1) ) },
58             coerce => 1,
59             );
60              
61             #------------------------------------------------------------------------------
62              
63              
64             has force => (
65             is => 'ro',
66             isa => 'Bool',
67             default => 0,
68             );
69              
70             #------------------------------------------------------------------------------
71              
72              
73             has indexes => (
74             is => 'ro',
75             isa => 'ArrayRef[Package::Locator::Index]',
76             auto_deref => 1,
77             lazy_build => 1,
78             init_arg => undef,
79             );
80              
81              
82             #------------------------------------------------------------------------------
83              
84             sub _build_indexes {
85             my ($self) = @_;
86              
87             my @indexes = map { Package::Locator::Index->new( force => $self->force(),
88             cache_dir => $self->cache_dir(),
89             user_agent => $self->user_agent(),
90             repository_url => $_ )
91             } $self->repository_urls();
92              
93             return \@indexes;
94             }
95              
96             #------------------------------------------------------------------------------
97              
98              
99             sub locate {
100             my ($self, %args) = @_;
101              
102             $self->_validate_locate_args(%args);
103              
104             if ($args{package}) {
105             my $package = $args{package};
106             my $version = $args{version} || 0;
107             my $latest = $args{latest} || 0;
108             return $self->_locate_package($package, $version, $latest);
109             }
110             else {
111             my $dist = $args{distribution};
112             return $self->_locate_dist($dist);
113             }
114             }
115              
116             #------------------------------------------------------------------------------
117              
118             sub _validate_locate_args {
119             my ($self, %args) = @_;
120              
121             croak 'Cannot specify latest and distribution together'
122             if $args{distribution} and $args{latest};
123              
124             croak 'Cannot specify version and distribution together'
125             if $args{distribution} and $args{version};
126              
127             croak 'Cannot specify package and distribution together'
128             if $args{distribution} and $args{package};
129              
130             croak 'Must specify package or distribution'
131             if not ( $args{distribution} or $args{package} );
132              
133             return 1;
134             }
135              
136             #------------------------------------------------------------------------------
137              
138             sub _locate_package {
139             my ($self, $package, $version, $latest) = @_;
140              
141             my $wanted_version = version->parse($version);
142              
143             my ($latest_found_package, $found_in_index);
144             for my $index ( $self->indexes() ) {
145              
146             my $found_package = $index->packages->{$package};
147             next if not $found_package;
148              
149             my $found_package_version = version->parse( $found_package->{version} );
150             next if $found_package_version < $wanted_version;
151              
152             $found_in_index ||= $index;
153             $latest_found_package ||= $found_package;
154             last unless $latest;
155              
156             ($found_in_index, $latest_found_package) = ($index, $found_package)
157             if $self->__compare_packages($found_package, $latest_found_package) == 1;
158             }
159              
160              
161             if ($latest_found_package) {
162             my $base_url = $found_in_index->repository_url();
163             my $latest_dist_path = $latest_found_package->{distribution};
164             return URI->new( "$base_url/authors/id/" . $latest_dist_path );
165             }
166              
167             return;
168             }
169              
170             #------------------------------------------------------------------------------
171              
172             sub _locate_dist {
173             my ($self, $dist_path) = @_;
174              
175             for my $index ( $self->indexes ) {
176             my $base_url = $index->repository_url();
177             my $dist_url = URI->new("$base_url/authors/id/$dist_path");
178              
179             return $dist_url if $index->distributions->{$dist_path};
180             return $dist_url if $self->user_agent->head($dist_url)->is_success;
181             }
182              
183              
184             return;
185             }
186              
187             #------------------------------------------------------------------------------
188              
189             sub __compare_packages {
190             my ($self, $pkg_a, $pkg_b) = @_;
191              
192             my $pkg_a_version = $self->__versionize( $pkg_a->{version} );
193             my $pkg_b_version = $self->__versionize( $pkg_b->{version} );
194              
195             # TODO: compare dist mtimes (but they are on the server!)
196             return $pkg_a_version <=> $pkg_b_version;
197             }
198              
199             #------------------------------------------------------------------------------
200              
201             sub __versionize {
202             my ($self, $version) = @_;
203              
204             my $v = eval { version->parse($version) };
205              
206             return defined $v ? $v : version->new(0);
207             }
208              
209             #------------------------------------------------------------------------------
210              
211              
212             sub clear_cache {
213             my ($self) = @_;
214              
215             for my $index ( $self->indexes() ) {
216             $index->index_file->remove();
217             }
218              
219             $self->clear_indexes();
220              
221             return $self;
222             }
223              
224             #------------------------------------------------------------------------------
225              
226             __PACKAGE__->meta->make_immutable();
227              
228             #------------------------------------------------------------------------------
229              
230             1;
231              
232             __END__
233              
234             =pod
235              
236             =for :stopwords Jeffrey Ryan Thalhammer Imaginative Software Systems cpan testmatrix url
237             annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata
238             placeholders metacpan
239              
240             =head1 NAME
241              
242             Package::Locator - Find a package among CPAN-like repositories
243              
244             =head1 VERSION
245              
246             version 0.010
247              
248             =head1 SYNOPSIS
249              
250             use Package::Locator;
251              
252             # Basic search...
253             my $locator = Package::Locator->new();
254             my $url = locator->locate( package => 'Test::More' );
255              
256             # Search for first within multiple repositories:
257             my $repos = [ qw(http://cpan.pair.com http://my.company.com/DPAN) ];
258             my $locator = Package::Locator->new( repository_urls => $repos );
259             my $url = locator->locate( package => 'Test::More' );
260              
261             # Search for first where version >= 0.34:
262             my $repos = [ qw(http://cpan.pair.com http://my.company.com/DPAN) ];
263             my $locator = Package::Locator->new( repository_urls => $repos );
264             my $url = locator->locate( package => 'Test::More' version => 0.34);
265              
266             # Search for latest where version >= 0.34:
267             my $repos = [ qw(http://cpan.pair.com http://my.company.com/DPAN) ];
268             my $locator = Package::Locator->new( repository_urls => $repos );
269             my $url = locator->locate( package => 'Test::More' version => 0.34, latest => 1);
270              
271             # Search for specific dist on multiple repositories...:
272             my $repos = [ qw(http://cpan.pair.com http://my.company.com/DPAN) ];
273             my $locator = Package::Locator->new( repository_urls => $repos );
274             my $url = locator->locate( distribution => 'A/AU/AUTHOR/Foo-1.0.tar.gz');
275              
276             =head1 DESCRIPTION
277              
278             L<Package::Locator> attempts to answer the question: "Where can I find
279             a distribution that will provide this package?" The answer is divined
280             by searching the indexes for one or more CPAN-like repositories. If
281             you also provide a specific version number, L<Package::Locator> will
282             attempt to find a distribution with that version of the package, or
283             higher. You can also ask to find the latest version of a package
284             across all the indexes.
285              
286             L<Package::Locator> only looks at the index files for each repository,
287             and those indexes only contain information about the latest versions
288             of the packages within that repository. So L<Package::Locator> is not
289             BackPAN magic -- you cannot use it to find precisely which
290             distribution a particular package (or file) came from. For that
291             stuff, see C<"/See Also">.
292              
293             =head1 CONSTRUCTOR
294              
295             =head2 new( %attributes )
296              
297             All the attributes listed below can be passed to the constructor, and
298             retrieved via accessor methods with the same name. All attributes are
299             read-only, and cannot be changed once the object is constructed.
300              
301             =head1 ATTRIBUTES
302              
303             =head2 repository_urls => [ qw(http://somewhere http://somewhere.else) ]
304              
305             An array reference containing the base URLs of the repositories you
306             want to search. These are usually CPAN mirrors, but can be any
307             website or local directory that is organized in a CPAN-like structure.
308             For each request, repositories are searched in the order you specified
309             them here. This defaults to http://cpan.perl.org.
310              
311             =head2 user_agent => $user_agent_obj
312              
313             The L<LWP::UserAgent> object that will fetch index files. If you do
314             not provide a user agent, then a default one will be constructed for
315             you.
316              
317             =head2 cache_dir => '/some/directory/path'
318              
319             The path (as a string or L<Path::Class::Dir> object) to a directory
320             where the index file will be cached. If the directory does not exist,
321             it will be created for you. If you do not specify a cache directory,
322             then a temporary directory will be used. The temporary directory will
323             be deleted when your application terminates.
324              
325             =head2 force => $boolean
326              
327             Causes any cached index files to be removed, thus forcing a new one to
328             be downloaded when the object is constructed. This only has effect if
329             you specified the C<cache_dir> attribute. The default is false.
330              
331             =head1 METHODS
332              
333             =head2 indexes()
334              
335             Returns a list of L<Package::Locator::Index> objects representing the
336             indexes of each of the repositories. The indexes are only populated
337             on-demand when the C<locate> method is called. The order of the
338             indexes is the same as the order of the repositories defined by the
339             C<repository_urls> attribute.
340              
341             =head2 locate( package => 'Foo::Bar' )
342              
343             =head2 locate( package => 'Foo::Bar', latest => 1 )
344              
345             =head2 locate( package => 'Foo::Bar', version => '1.2')
346              
347             =head2 locate( package => 'Foo::Bar', version => '1.2', latest => 1 )
348              
349             =head2 locate ( distribution => 'A/AU/AUTHOR/Foo-Bar-1.0.tar.gz' )
350              
351             Given the name of a package, searches all the repository indexes and
352             returns the URL to a distribution containing that requested package,
353             or the distribution you requested.
354              
355             If you also specify a C<version>, then you'll always get a
356             distribution that contains that version of the package or higher. If
357             you also specify C<latest> then you'll always get the distribution
358             that contains the latest version of the package that can be found in
359             all the indexes. Otherwise you'll just get the first distribution we
360             can find that satisfies your request.
361              
362             If you give a distribution path instead, then you'll just get back the
363             URL to the first distribution we find at that path in any of the
364             repository indexes.
365              
366             If neither the package nor the distribution path can be found in any
367             of the indexes, returns undef.
368              
369             =head2 clear_cache()
370              
371             Deletes the cached index files. Any subsequent calls to the C<locate>
372             method will cause the index files to be fetched anew.
373              
374             =head1 MOTIVATION
375              
376             The L<CPAN> module also provides a mechanism for locating packages or
377             distributions, much like L<Package::Locator> does. However, L<CPAN>
378             assumes that all repositories are CPAN mirrors, so it only searches
379             the first repository that it can contact.
380              
381             My secret ambition is to fill the world with lots of DarkPAN
382             repositories -- each with its own set of distributions. For that
383             scenario, I need to search multiple repositories at the same time.
384              
385             =head1 SEE ALSO
386              
387             If you need to locate a distribution that contains a precise version
388             of a file rather than just a version that is "new enough", then look
389             at some of these:
390              
391             L<Dist::Surveyor>
392              
393             L<BackPAN::Index>
394              
395             L<BackPAN::Version::Discover>
396              
397             =head1 SUPPORT
398              
399             =head2 Perldoc
400              
401             You can find documentation for this module with the perldoc command.
402              
403             perldoc Package::Locator
404              
405             =head2 Websites
406              
407             The following websites have more information about this module, and may be of help to you. As always,
408             in addition to those websites please use your favorite search engine to discover more resources.
409              
410             =over 4
411              
412             =item *
413              
414             Search CPAN
415              
416             The default CPAN search engine, useful to view POD in HTML format.
417              
418             L<http://search.cpan.org/dist/Package-Locator>
419              
420             =item *
421              
422             CPAN Ratings
423              
424             The CPAN Ratings is a website that allows community ratings and reviews of Perl modules.
425              
426             L<http://cpanratings.perl.org/d/Package-Locator>
427              
428             =item *
429              
430             CPAN Testers
431              
432             The CPAN Testers is a network of smokers who run automated tests on uploaded CPAN distributions.
433              
434             L<http://www.cpantesters.org/distro/P/Package-Locator>
435              
436             =item *
437              
438             CPAN Testers Matrix
439              
440             The CPAN Testers Matrix is a website that provides a visual overview of the test results for a distribution on various Perls/platforms.
441              
442             L<http://matrix.cpantesters.org/?dist=Package-Locator>
443              
444             =item *
445              
446             CPAN Testers Dependencies
447              
448             The CPAN Testers Dependencies is a website that shows a chart of the test results of all dependencies for a distribution.
449              
450             L<http://deps.cpantesters.org/?module=Package::Locator>
451              
452             =back
453              
454             =head2 Bugs / Feature Requests
455              
456             L<https://github.com/thaljef/Package-Locator/issues>
457              
458             =head2 Source Code
459              
460              
461             L<https://github.com/thaljef/Package-Locator>
462              
463             git clone git://github.com/thaljef/Package-Locator.git
464              
465             =head1 AUTHOR
466              
467             Jeffrey Ryan Thalhammer <jeff@imaginative-software.com>
468              
469             =head1 COPYRIGHT AND LICENSE
470              
471             This software is copyright (c) 2011 by Imaginative Software Systems.
472              
473             This is free software; you can redistribute it and/or modify it under
474             the same terms as the Perl 5 programming language system itself.
475              
476             =cut