File Coverage

blib/lib/App/cpanoutdated/fresh.pm
Criterion Covered Total %
statement 95 162 58.6
branch 25 74 33.7
condition 7 39 17.9
subroutine 29 34 85.2
pod 3 3 100.0
total 159 312 50.9


line stmt bran cond sub pod time code
1 4     4   60740 use 5.006; # our
  4         11  
2 4     4   15 use strict;
  4         4  
  4         71  
3 4     4   11 use warnings;
  4         8  
  4         203  
4              
5             package App::cpanoutdated::fresh;
6              
7             our $VERSION = '0.001006';
8              
9             # ABSTRACT: Indicate out-of-date modules by walking the metacpan releases backwards
10              
11             our $AUTHORITY = 'cpan:KENTNL'; # AUTHORITY
12              
13 4     4   12 use Carp qw( croak );
  4         4  
  4         159  
14 4     4   1683 use Moo qw( has );
  4         36654  
  4         13  
15 4     4   5388 use MooX::Lsub qw( lsub );
  4         13562  
  4         14  
16 4     4   3544 use Getopt::Long;
  4         29239  
  4         12  
17 4     4   2430 use Search::Elasticsearch;
  4         92493  
  4         109  
18 4     4   1848 use Module::Metadata;
  4         22717  
  4         125  
19 4     4   1535 use Path::ScanINC;
  4         47490  
  4         117  
20 4     4   1822 use Pod::Usage qw( pod2usage );
  4         135050  
  4         263  
21 4     4   22 use version;
  4         7  
  4         16  
22              
23              
24              
25              
26              
27             has ua => ( is => 'ro', predicate => 'has_ua' );
28 2     2   1392 lsub trace => sub { undef };
29             lsub es => sub {
30 3     3   2490 my ($self) = @_;
31 3         12 my %args = (
32             nodes => 'api.metacpan.org',
33             cxn_pool => 'Static::NoPing',
34             send_get_body_as => 'POST',
35              
36             # trace_to => 'Stderr',
37             );
38 3 100       13 if ( $self->has_ua ) {
39 2         5 $args{handle} = $self->ua;
40             }
41 3 100       42 if ( $self->trace ) {
42 1         5 $args{trace_to} = 'Stderr';
43             }
44 3         34 return Search::Elasticsearch->new(%args);
45             };
46 1     1   146238 lsub _sort => sub { 'desc' };
47 1     1   253 lsub scroll_size => sub { 1000 };
48 1     1   240 lsub age => sub { '7d' };
49             lsub age_seconds => sub {
50 6     6   2607 my ($self) = @_;
51 6         21 my $table = {
52             'm' => (60),
53             'h' => ( 60 * 60 ),
54             's' => (1),
55             'd' => ( 24 * 60 * 60 ),
56             'w' => ( 7 * 24 * 60 * 60 ),
57             'M' => ( 31 * 24 * 60 * 60 ),
58             'Y' => ( 365 * 24 * 60 * 60 ),
59             };
60 6 100       73 return $self->age + 0 if $self->age =~ /\A\d+([.]\d+)?\z/msx;
61 5 100       90 if ( my ( $time, $multiplier ) = $self->age =~ /\A(\d+)([[:alpha:]]+)\z/msx ) {
62 4 100       33 if ( not exists $table->{$multiplier} ) {
63 2         172 croak("Unknown time multiplier <$multiplier>");
64             }
65 2         9 return $time * $table->{$multiplier};
66             }
67 1         18 croak( 'Cant parse age <' . $self->age . '>' );
68             };
69             lsub min_timestamp => sub {
70 1     1   234 my ($self) = @_;
71 1         18 return time() - $self->age_seconds;
72             };
73 1     1   237 lsub developer => sub { undef };
74 1     1   256 lsub all_versions => sub { undef };
75 1     1   252 lsub authorized => sub { 1 };
76 2     2   290 lsub _inc_scanner => sub { Path::ScanINC->new() };
77              
78             sub _es_version {
79 0     0   0 my ( $self, $wanted_version ) = @_;
80 0         0 local $@ = undef;
81 0         0 return eval { $self->es->VERSION($wanted_version); 1 }; ## no critic (RequireCheckingReturnValueOfEval)
  0         0  
  0         0  
82             }
83              
84             sub _mk_scroll {
85 0     0   0 my ($self) = @_;
86              
87 0         0 my $body = {
88             query => {
89             range => {
90             'stat.mtime' => {
91             gte => $self->min_timestamp,
92             },
93             },
94             },
95             };
96 0 0 0     0 if ( not $self->developer or $self->authorized ) {
97 0   0     0 $body->{filter} ||= {};
98 0   0     0 $body->{filter}->{term} ||= {};
99             }
100 0 0       0 if ( not $self->developer ) {
101 0         0 $body->{filter}->{term}->{'maturity'} = 'released';
102             }
103 0 0       0 if ( $self->authorized ) {
104 0         0 $body->{filter}->{term}->{'authorized'} = 'true';
105 0         0 $body->{filter}->{term}->{'module.authorized'} = 'true';
106             }
107              
108 0         0 my $fields = [
109             qw(
110             name distribution path
111             stat.mtime module author
112             authorized date indexed
113             directory maturity release
114             status version
115             ),
116             ];
117              
118 0 0       0 my %scrollargs = (
119             scroll => '5m',
120             index => 'v0',
121             type => 'module',
122             size => $self->scroll_size,
123             body => $body,
124             ( $self->_es_version(5) ? 'stored_fields' : 'fields' ) => $fields,
125             );
126 0 0       0 if ( not $self->_sort ) {
127 0         0 $scrollargs{'search_type'} = 'scan';
128             }
129             else {
130 0         0 $body->{sort} = { 'stat.mtime' => $self->_sort };
131             }
132 0 0       0 ( $self->_es_version(5) )
133             ? ( require Search::Elasticsearch::Client::5_0::Scroll )
134             : ( require Search::Elasticsearch::Scroll );
135              
136 0         0 return $self->es->scroll_helper(%scrollargs);
137             }
138              
139             sub _check_fresh {
140 2     2   1738 my ( $self, $data_hash, $module ) = @_;
141 2 100 33     14 return unless $module->{indexed} and $module->{authorized} and $module->{version};
      33        
142              
143 1         5 my (@parts) = split /::/msx, $module->{name};
144 1         2 $parts[-1] .= '.pm';
145              
146 1         21 my $file = $self->_inc_scanner->first_file(@parts);
147 1 50       534 return unless $file;
148              
149 1         5 my $mm = Module::Metadata->new_from_file($file);
150 1 50       7848 return if not $mm;
151              
152 1         3 my $mm_version = $mm->version;
153              
154 1         15 my $v = version->parse( $module->{version} );
155              
156 1 0 33     13 if ( not defined $v and not defined $mm_version ) {
157 0         0 return;
158             }
159 1 50 33     13 if ( defined $v and not defined $mm_version ) {
    50 33        
    50          
160              
161             # noop, upstream got defined vs local, == upgrade
162             }
163             elsif ( not defined $v and defined $mm_version ) {
164              
165             # uhh, have version locally but not upstream, DONT upgrade
166 0         0 return;
167             }
168             elsif ( $mm_version >= $v ) {
169 0         0 return;
170             }
171              
172             return {
173             name => $module->{name},
174             cpan => ( $v ? $v->stringify : q[0] ),
175             release => $data_hash->{release},
176 1 50       30 installed => ( $mm_version ? $mm_version->stringify : q[0] ),
    50          
177             meta => $data_hash,
178             };
179              
180             }
181              
182             sub _get_next {
183 0     0   0 my ( $self, $scroll ) = @_;
184 0 0       0 if ( not exists $self->{stash_cache} ) {
185 0         0 $self->{stash_cache} = {};
186             }
187 0 0       0 if ( not exists $self->{upgrade_cache} ) {
188 0         0 $self->{upgrade_cache} = {};
189             }
190              
191 0         0 my $stash_cache = $self->{stash_cache};
192 0         0 my $upgrade_cache = $self->{upgrade_cache};
193              
194 0         0 while ( my $scroll_result = $scroll->next ) {
195 0 0       0 return unless $scroll_result;
196 0   0     0 my $data_hash = $scroll_result->{'_source'} || $scroll_result->{'fields'};
197              
198 0         0 my $cache_key = $data_hash->{path};
199             my $upgrade_key =
200             ( $data_hash->{author} || 'NOAUTHOR' ) . q[/]
201             . ( $data_hash->{distribution} || 'NODISTRIBUTION' ) . q[/]
202 0   0     0 . ( $data_hash->{version} || 'NOVERSION' );
      0        
      0        
203 0 0       0 if ( $self->all_versions ) {
204 0         0 $cache_key = $data_hash->{release};
205             }
206              
207             # pp($data_hash);
208 0 0       0 next if exists $stash_cache->{$cache_key};
209 0 0 0     0 next if not $self->developer and 'developer' eq $data_hash->{maturity};
210              
211 0 0       0 next if $data_hash->{path} =~ /\Ax?t\//msx;
212 0 0       0 next unless $data_hash->{path} =~ /[.]pm\z/msx;
213 0 0       0 next unless $data_hash->{module};
214 0 0       0 next unless @{ $data_hash->{module} };
  0         0  
215 0         0 for my $module ( @{ $data_hash->{module} } ) {
  0         0  
216 0         0 my $fresh_data = $self->_check_fresh( $data_hash, $module );
217 0 0       0 next unless $fresh_data;
218 0 0       0 next if $upgrade_cache->{$upgrade_key};
219 0         0 $upgrade_cache->{$upgrade_key} = 1;
220 0         0 $stash_cache->{$cache_key} = 1;
221 0         0 return $fresh_data;
222             }
223 0         0 $stash_cache->{$cache_key} = 1;
224             }
225 0         0 return;
226             }
227              
228              
229              
230              
231              
232              
233              
234              
235              
236             sub new_from_command {
237 6     6 1 2727 my ( $class, $defaults ) = @_;
238 6         17 Getopt::Long::Configure('bundling');
239 6   50     131 $defaults ||= {};
240 6         5 my ( $help, $man );
241             Getopt::Long::GetOptions(
242             'age|a=s' => sub {
243 1     1   355 my ( undef, $value ) = @_;
244 1         2 $defaults->{age} = $value;
245             },
246             'develop|devel|dev!' => sub {
247 2     2   558 my ( undef, $value ) = @_;
248 2 100       5 if ($value) {
249 1         1 $defaults->{developer} = 1;
250 1         2 return;
251             }
252 1         3 $defaults->{developer} = undef;
253             },
254             'authorized|authed!' => sub {
255 2     2   548 my ( undef, $value ) = @_;
256 2 100       4 if ($value) {
257 1         2 $defaults->{authorized} = 1;
258             }
259             else {
260 1         3 $defaults->{authorized} = undef;
261             }
262             },
263             'help|h|?' => \$help,
264             'man' => \$man,
265 6 50       30 ) or do { $help = 1 };
  0         0  
266 6 50 33     448 if ( $help or $man ) {
267 0 0       0 if ($help) {
268 0         0 return pod2usage( { -exitval => 1, }, );
269             }
270 0         0 return pod2usage( { -exitval => 1, -verbose => 2, }, );
271             }
272 6         7 return $class->new( %{$defaults} );
  6         103  
273             }
274              
275              
276              
277              
278              
279              
280              
281              
282              
283             sub run {
284 0     0 1   my ($self) = @_;
285 0           my $iterator = $self->_mk_scroll;
286 0           while ( my $result = $self->_get_next($iterator) ) {
287 0           printf "%s\@%s\n", $result->{name}, $result->{cpan};
288             }
289 0           return 0;
290             }
291              
292              
293              
294              
295              
296              
297              
298              
299              
300             sub run_command {
301 0     0 1   my ($class) = @_;
302 0           return $class->new_from_command->run();
303             }
304              
305 4     4   5143 no Moo;
  4         5  
  4         28  
306              
307             1;
308              
309             __END__
310              
311             =pod
312              
313             =encoding UTF-8
314              
315             =head1 NAME
316              
317             App::cpanoutdated::fresh - Indicate out-of-date modules by walking the metacpan releases backwards
318              
319             =head1 VERSION
320              
321             version 0.001006
322              
323             =head1 METHODS
324              
325             =head2 new_from_command
326              
327             Create an instance of this class parsing options from C<@ARGV>
328              
329             my $instance = App::cpanoutdated::fresh->new_from_command;
330              
331             =head2 run
332              
333             Execute the main logic and printing found modules to C<STDOUT>
334              
335             $object->run;
336              
337             =head2 run_command
338              
339             Shorthand for
340              
341             $class->new_from_command->run();
342              
343             =for Pod::Coverage ua has_ua
344              
345             =head1 AUTHOR
346              
347             Kent Fredric <kentnl@cpan.org>
348              
349             =head1 COPYRIGHT AND LICENSE
350              
351             This software is copyright (c) 2017 by Kent Fredric <kentfredric@gmail.com>.
352              
353             This is free software; you can redistribute it and/or modify it under
354             the same terms as the Perl 5 programming language system itself.
355              
356             =cut