File Coverage

lib/App/cpanoutdated/fresh.pm
Criterion Covered Total %
statement 102 163 62.5
branch 25 70 35.7
condition 8 39 20.5
subroutine 31 35 88.5
pod 3 3 100.0
total 169 310 54.5


line stmt bran cond sub pod time code
1 4     4   125514 use 5.008; # utf8
  4         15  
  4         182  
2 4     4   24 use strict;
  4         16  
  4         154  
3 4     4   35 use warnings;
  4         8  
  4         262  
4 4     4   5020 use utf8;
  4         50  
  4         209  
5              
6             package App::cpanoutdated::fresh;
7              
8             our $VERSION = '0.001005';
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   446 use Carp qw( croak );
  4         12  
  4         283  
15 4     4   10030 use Moo qw( has );
  4         89817  
  4         34  
16 4     4   12974 use MooX::Lsub qw( lsub );
  4         37729  
  4         31  
17 4     4   7707 use Getopt::Long;
  4         56845  
  4         33  
18 4     4   5948 use Search::Elasticsearch;
  4         203143  
  4         169  
19 4     4   5333 use Search::Elasticsearch::Scroll;
  4         231944  
  4         167  
20 4     4   4989 use Module::Metadata;
  4         104971  
  4         223  
21 4     4   3884 use Path::ScanINC;
  4         107660  
  4         196  
22 4     4   4718 use Pod::Usage qw( pod2usage );
  4         277244  
  4         461  
23 4     4   52 use version;
  4         10  
  4         38  
24              
25              
26              
27              
28              
29             has ua => ( is => 'ro', predicate => 'has_ua' );
30 2     2   4341 lsub trace => sub { undef };
31             lsub es => sub {
32 3     3   4269 my ($self) = @_;
33 3         20 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       23 if ( $self->has_ua ) {
41 2         8 $args{handle} = $self->ua;
42             }
43 3 100       67 if ( $self->trace ) {
44 1         13 $args{trace_to} = 'Stderr';
45             }
46 3         42 return Search::Elasticsearch->new(%args);
47             };
48 1     1   171959 lsub _sort => sub { 'desc' };
49 1     1   886 lsub scroll_size => sub { 1000 };
50 1     1   878 lsub age => sub { '7d' };
51             lsub age_seconds => sub {
52 6     6   7782 my ($self) = @_;
53 6         40 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       149 return $self->age + 0 if $self->age =~ /\A\d+([.]\d+)?\z/msx;
63 5 100       169 if ( my ( $time, $multiplier ) = $self->age =~ /\A(\d+)([[:alpha:]]+)\z/msx ) {
64 4 100       61 if ( not exists $table->{$multiplier} ) {
65 2         392 croak("Unknown time multiplier <$multiplier>");
66             }
67 2         18 return $time * $table->{$multiplier};
68             }
69 1         24 croak( 'Cant parse age <' . $self->age . '>' );
70             };
71             lsub min_timestamp => sub {
72 1     1   878 my ($self) = @_;
73 1         38 return time() - $self->age_seconds;
74             };
75 1     1   975 lsub developer => sub { undef };
76 1     1   747 lsub all_versions => sub { undef };
77 1     1   891 lsub authorized => sub { 1 };
78 2     2   1449 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   9318 my ( $self, $data_hash, $module ) = @_;
133 2 100 33     36 return unless $module->{indexed} and $module->{authorized} and $module->{version};
      66        
134              
135 1         9 my (@parts) = split /::/msx, $module->{name};
136 1         5 $parts[-1] .= '.pm';
137              
138 1         6 my $file = $self->_inc_scanner->first_file(@parts);
139 1 50       1142 return unless $file;
140              
141 1         15 my $mm = Module::Metadata->new_from_file($file);
142 1 50       13061 return if not $mm;
143              
144 1         8 my $mm_version = $mm->version;
145              
146 1         28 my $v = version->parse( $module->{version} );
147              
148 1 0 33     6 if ( not defined $v and not defined $mm_version ) {
149 0         0 return;
150             }
151 1 50 33     25 if ( defined $v and not defined $mm_version ) {
    50 33        
    50          
152              
153             # noop, upstream got defined vs local, == upgrade
154             }
155             elsif ( not defined $v and defined $mm_version ) {
156              
157             # uhh, have version locally but not upstream, DONT upgrade
158 0         0 return;
159             }
160             elsif ( $mm_version >= $v ) {
161 0         0 return;
162             }
163              
164             return {
165 1 50       77 name => $module->{name},
    50          
166             cpan => ( $v ? $v->stringify : q[0] ),
167             release => $data_hash->{release},
168             installed => ( $mm_version ? $mm_version->stringify : q[0] ),
169             meta => $data_hash,
170             };
171              
172             }
173              
174             sub _get_next {
175 0     0   0 my ( $self, $scroll ) = @_;
176 0 0       0 if ( not exists $self->{stash_cache} ) {
177 0         0 $self->{stash_cache} = {};
178             }
179 0 0       0 if ( not exists $self->{upgrade_cache} ) {
180 0         0 $self->{upgrade_cache} = {};
181             }
182              
183 0         0 my $stash_cache = $self->{stash_cache};
184 0         0 my $upgrade_cache = $self->{upgrade_cache};
185              
186 0         0 while ( my $scroll_result = $scroll->next ) {
187 0 0       0 return unless $scroll_result;
188 0   0     0 my $data_hash = $scroll_result->{'_source'} || $scroll_result->{'fields'};
189              
190 0         0 my $cache_key = $data_hash->{path};
191 0   0     0 my $upgrade_key =
      0        
      0        
192             ( $data_hash->{author} || 'NOAUTHOR' ) . q[/]
193             . ( $data_hash->{distribution} || 'NODISTRIBUTION' ) . q[/]
194             . ( $data_hash->{version} || 'NOVERSION' );
195 0 0       0 if ( $self->all_versions ) {
196 0         0 $cache_key = $data_hash->{release};
197             }
198              
199             # pp($data_hash);
200 0 0       0 next if exists $stash_cache->{$cache_key};
201 0 0 0     0 next if not $self->developer and 'developer' eq $data_hash->{maturity};
202              
203 0 0       0 next if $data_hash->{path} =~ /\Ax?t\//msx;
204 0 0       0 next unless $data_hash->{path} =~ /[.]pm\z/msx;
205 0 0       0 next unless $data_hash->{module};
206 0 0       0 next unless @{ $data_hash->{module} };
  0         0  
207 0         0 for my $module ( @{ $data_hash->{module} } ) {
  0         0  
208 0         0 my $fresh_data = $self->_check_fresh( $data_hash, $module );
209 0 0       0 next unless $fresh_data;
210 0 0       0 next if $upgrade_cache->{$upgrade_key};
211 0         0 $upgrade_cache->{$upgrade_key} = 1;
212 0         0 $stash_cache->{$cache_key} = 1;
213 0         0 return $fresh_data;
214             }
215 0         0 $stash_cache->{$cache_key} = 1;
216             }
217 0         0 return;
218             }
219              
220              
221              
222              
223              
224              
225              
226              
227              
228             sub new_from_command {
229 6     6 1 5210 my ( $class, $defaults ) = @_;
230 6         24 Getopt::Long::Configure('bundling');
231 6   50     182 $defaults ||= {};
232 6         9 my ( $help, $man );
233             Getopt::Long::GetOptions(
234             'age|a=s' => sub {
235 1     1   531 my ( undef, $value ) = @_;
236 1         4 $defaults->{age} = $value;
237             },
238             'develop|devel|dev!' => sub {
239 2     2   842 my ( undef, $value ) = @_;
240 2 100       6 if ($value) {
241 1         3 $defaults->{developer} = 1;
242 1         4 return;
243             }
244 1         3 $defaults->{developer} = undef;
245             },
246             'authorized|authed!' => sub {
247 2     2   6551 my ( undef, $value ) = @_;
248 2 100       7 if ($value) {
249 1         4 $defaults->{authorized} = 1;
250             }
251             else {
252 1         4 $defaults->{authorized} = undef;
253             }
254             },
255             'help|h|?' => \$help,
256             'man' => \$man,
257 6 50       74 ) or do { $help = 1 };
  0         0  
258 6 50 33     616 if ( $help or $man ) {
259 0 0       0 if ($help) {
260 0         0 return pod2usage( { -exitval => 1, }, );
261             }
262 0         0 return pod2usage( { -exitval => 1, -verbose => 2, }, );
263             }
264 6         8 return $class->new( %{$defaults} );
  6         148  
265             }
266              
267              
268              
269              
270              
271              
272              
273              
274              
275             sub run {
276 0     0 1   my ($self) = @_;
277 0           my $iterator = $self->_mk_scroll;
278 0           while ( my $result = $self->_get_next($iterator) ) {
279 0           printf "%s\@%s\n", $result->{name}, $result->{cpan};
280             }
281 0           return 0;
282             }
283              
284              
285              
286              
287              
288              
289              
290              
291              
292             sub run_command {
293 0     0 1   my ($class) = @_;
294 0           return $class->new_from_command->run();
295             }
296              
297 4     4   8742 no Moo;
  4         9  
  4         37  
298              
299             1;
300              
301             __END__
302              
303             =pod
304              
305             =encoding UTF-8
306              
307             =head1 NAME
308              
309             App::cpanoutdated::fresh - Indicate out-of-date modules by walking the metacpan releases backwards
310              
311             =head1 VERSION
312              
313             version 0.001005
314              
315             =head1 METHODS
316              
317             =head2 new_from_command
318              
319             Create an instance of this class parsing options from C<@ARGV>
320              
321             my $instance = App::cpanoutdated::fresh->new_from_command;
322              
323             =head2 run
324              
325             Execute the main logic and printing found modules to C<STDOUT>
326              
327             $object->run;
328              
329             =head2 run_command
330              
331             Shorthand for
332              
333             $class->new_from_command->run();
334              
335             =for Pod::Coverage ua has_ua
336              
337             =head1 AUTHOR
338              
339             Kent Fredric <kentnl@cpan.org>
340              
341             =head1 COPYRIGHT AND LICENSE
342              
343             This software is copyright (c) 2014 by Kent Fredric <kentfredric@gmail.com>.
344              
345             This is free software; you can redistribute it and/or modify it under
346             the same terms as the Perl 5 programming language system itself.
347              
348             =cut