File Coverage

blib/lib/App/RetroPAN.pm
Criterion Covered Total %
statement 30 98 30.6
branch 0 20 0.0
condition 0 17 0.0
subroutine 10 15 66.6
pod 0 5 0.0
total 40 155 25.8


line stmt bran cond sub pod time code
1             package App::RetroPAN;
2             # vim:ts=4:shiftwidth=4:expandtab
3              
4 1     1   103179 use strict;
  1         3  
  1         31  
5 1     1   5 use warnings;
  1         3  
  1         25  
6 1     1   672 use utf8;
  1         16  
  1         5  
7              
8             =encoding utf8
9              
10             =head1 NAME
11              
12             App::RetroPAN - Makes a historic minicpan ⏳
13              
14             =head1 SYNOPSIS
15              
16             use App::RetroCPAN;
17              
18             my ($author, $dist_name, $url) = find_module_on_date("2011-01-01T00:00:00", "Moose");
19              
20             my @dependencies = find_deps_on_date_with_perl("2011-01-01T00:00:00", $], "Moose", "Mouse", "Moo", ...);
21              
22             my @dependencies = find_modules_dependencies($author, $dist_name, $]);
23              
24             =head1 DESCRIPTION
25              
26             Uses the MetaCPAN API to find releases made prior to a given date to
27             satisfy your modules' dependencies.
28              
29             =head2 Core modules
30              
31             Newer versions of modules existing in core are currently ignored. This may be
32             desirable as users may be reluctant to upgrade core modules from CPAN.
33              
34             =head1 SEE ALSO
35              
36             =over
37              
38             =item *
39              
40             L<retropan>
41              
42             =item *
43              
44             L<OrePAN2>
45              
46             =back
47              
48             =head1 LICENSE
49              
50             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
51              
52             =head1 AUTHOR
53              
54             Dave Lambley <dlambley@cpan.org>
55              
56             =cut
57              
58 1     1   586 use HTTP::Request;
  1         23775  
  1         36  
59 1     1   744 use LWP::UserAgent;
  1         26566  
  1         45  
60 1     1   649 use List::MoreUtils qw/ uniq /;
  1         12926  
  1         10  
61 1     1   4526 use Module::CoreList;
  1         133031  
  1         14  
62 1     1   1210 use OrePAN2::Injector;
  1         664024  
  1         45  
63 1     1   543 use OrePAN2::Indexer;
  1         95500  
  1         48  
64              
65 1     1   20 use Cpanel::JSON::XS qw/ encode_json decode_json /;
  1         2  
  1         1101  
66              
67             our $VERSION = '0.03_01';
68              
69             my $ua = LWP::UserAgent->new( keep_alive => 2, agent => "retropan/$VERSION" );
70              
71             sub find_module_dependencies {
72 0     0 0   my ($au, $dist, $perl_version) = @_;
73              
74 0   0       $perl_version ||= $];
75              
76 0           my $q = {
77             "size" => 1,
78             "query" => {
79             "bool" => {
80             "filter" => [
81             {
82             "match" => {
83             "name" => $dist,
84             }
85             },
86             {
87             "match" => {
88             "author" => $au,
89             }
90             },
91              
92             ],
93             }
94             }
95             };
96              
97 0           my $req = HTTP::Request->new( POST => 'https://fastapi.metacpan.org/v1/release/_search', [
98             "Content-Type" => "text/json",
99             "Accept" => "text/json"
100             ], encode_json($q) );
101              
102 0           my $res = $ua->request($req);
103 0 0         die $res->status_line if !$res->is_success;
104 0           my $data = decode_json($res->decoded_content);
105 0           my $hit = $data->{hits}->{hits}->[0];
106 0 0         if (!defined $hit) {
107 0           warn "could not find $au/$dist";
108 0           return;
109             }
110              
111             my @deps =
112 0           grep { !Module::CoreList::is_core($_, undef, $perl_version) }
113 0           grep { $_ ne "perl" }
114 0           map { $_->{module} } @{ $hit->{_source}->{dependency} };
  0            
  0            
115              
116 0           return @deps;
117             }
118              
119             sub find_module_on_date {
120 0     0 0   my ($module, $before, $perl_version) = @_;
121              
122 0   0       $perl_version ||= $];
123 0 0         return if Module::CoreList::is_core($module, undef, $perl_version);
124              
125             # We prefer authorized modules, but can fall back to unauthorized if none
126             # available.
127 0           my $q = {
128             "size" => 30, # TODO, keep search open.
129             "sort" => [
130             { "module.authorized" => "desc" },
131             { "version_numified" => "desc" },
132             "_score",
133             ],
134             "query" => {
135             "bool" => {
136             "filter" => [
137             {
138             "match" => {
139             "module.name" => $module,
140             }
141             },
142             {
143             "match" => {
144             "maturity" => "released",
145             }
146             },
147             {
148             "range" => { "date" => {"lt" => $before }}
149             },
150             ],
151             }
152             }
153             };
154              
155 0           my $req = HTTP::Request->new( POST => 'https://fastapi.metacpan.org/v1/module/_search', [
156             "Content-Type" => "text/json",
157             "Accept" => "text/json"
158             ], encode_json($q) );
159              
160 0           my $res = $ua->request($req);
161 0 0         die $res->status_line if !$res->is_success;
162 0           my $data = decode_json($res->decoded_content);
163              
164              
165 0           my $author;
166 0           my $version = -1;
167 0           my $release;
168             my $url;
169 0           my $authorized;
170              
171             # Some distributions re-release existing modules outside their own
172             # distribution, eg., perl-5.005-minimal-bin-0-arm-linux
173             # We therefore iterate through all modules returned to find the newest
174             # version.
175 0           foreach my $hit (@{ $data->{hits}->{hits} }) {
  0            
176 0 0         next if $hit->{_source}->{distribution} eq 'perl';
177 0           foreach my $mod (@{ $hit->{_source}->{module} }) {
  0            
178 0 0 0       if (($authorized ? $mod->{authorized} : 1) && $mod->{name} eq $module && $mod->{version_numified} > $version) {
    0 0        
179 0           $author = $hit->{_source}->{author};
180 0           $release = $hit->{_source}->{release};
181 0           $url = $hit->{_source}->{download_url};
182 0           $version = $mod->{version_numified};
183 0           $authorized = $mod->{authorized};
184             }
185             }
186             }
187              
188              
189 0 0         if (!defined $release) {
190 0           warn "could not find $module before $before";
191 0           return;
192             }
193              
194 0           return ($author, $release, $url);
195             }
196              
197             sub find_deps_on_date {
198 0     0 0   my ($before, @modules) = @_;
199 0           return find_deps_on_date_with_perl($before, $], @modules);
200             }
201              
202             sub find_deps_on_date_with_perl {
203 0     0 0   my ($before, $perl_version, @modules) = @_;
204              
205 0           my %done_modules;
206             my @dists_required;
207 0           my %dist_to_url;
208              
209 0           while (@modules) {
210 0           my $mod = pop @modules;
211 0 0         next if $done_modules{$mod};
212              
213 0           my ($au, $dist, $url) = find_module_on_date($mod, $before, $perl_version);
214 0           $done_modules{$mod} = 1;
215 0 0 0       next if !defined($au) || !defined($dist);
216 0           $dist_to_url{"$au/$dist"} = $url;
217              
218 0           push @modules, find_module_dependencies($au, $dist, $perl_version);
219 0           unshift @dists_required, "$au/$dist";
220             }
221              
222             return (
223 0           [uniq @dists_required],
224             \%dist_to_url,
225             );
226             }
227              
228             sub make_minicpan {
229 0     0 0   my ($localdir, $dists_required, $dist_to_url) = @_;
230              
231 0           my $injector = OrePAN2::Injector->new(
232             directory => $localdir,
233             author_subdir => 1
234             );
235              
236 0           foreach my $d (@{ $dists_required }) {
  0            
237 0           my ($author, $dist) = split(/\//, $d, 2);
238             $injector->inject(
239 0   0       $dist_to_url->{$d} // die,
240             {
241             author => $author,
242             }
243             );
244             }
245              
246             # XXX undocumented.
247 0           my $orepan = OrePAN2::Indexer->new(
248             directory => $localdir,
249             metacpan => 0,
250             simple => 1,
251             );
252 0           $orepan->make_index(
253             no_compress => 1,
254             );
255 0           return;
256             }