File Coverage

lib/App/cpanoutdated/fresh.pm
Criterion Covered Total %
statement 100 153 65.3
branch 21 56 37.5
condition 5 24 20.8
subroutine 31 35 88.5
pod 3 3 100.0
total 160 271 59.0


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