File Coverage

blib/lib/MetaCPAN/Client.pm
Criterion Covered Total %
statement 185 247 74.9
branch 31 58 53.4
condition 20 38 52.6
subroutine 42 50 84.0
pod 20 20 100.0
total 298 413 72.1


line stmt bran cond sub pod time code
1 20     20   2432681 use strict;
  20         44  
  20         620  
2 20     20   85 use warnings;
  20         39  
  20         1509  
3             package MetaCPAN::Client;
4             # ABSTRACT: A comprehensive, DWIM-featured client to the MetaCPAN API
5             $MetaCPAN::Client::VERSION = '2.044000';
6 20     20   9931 use Moo;
  20         135910  
  20         110  
7 20     20   30623 use Carp;
  20         56  
  20         1365  
8 20     20   9204 use JSON::MaybeXS qw< JSON >;
  20         299093  
  20         1581  
9 20     20   6249 use Ref::Util qw< is_arrayref is_hashref is_ref >;
  20         32089  
  20         1649  
10 20     20   8380 use URI::Escape qw< uri_escape_utf8 >;
  20         32709  
  20         1452  
11              
12 20     20   11146 use MetaCPAN::Client::Request;
  20         83  
  20         806  
13 20     20   10855 use MetaCPAN::Client::Author;
  20         105  
  20         835  
14 20     20   11111 use MetaCPAN::Client::Distribution;
  20         80  
  20         730  
15 20     20   9339 use MetaCPAN::Client::DownloadURL;
  20         98  
  20         850  
16 20     20   9941 use MetaCPAN::Client::Module;
  20         97  
  20         814  
17 20     20   187 use MetaCPAN::Client::File;
  20         41  
  20         412  
18 20     20   11409 use MetaCPAN::Client::Favorite;
  20         74  
  20         777  
19 20     20   10399 use MetaCPAN::Client::Pod;
  20         87  
  20         872  
20 20     20   11305 use MetaCPAN::Client::Release;
  20         107  
  20         1052  
21 20     20   11047 use MetaCPAN::Client::Mirror;
  20         94  
  20         822  
22 20     20   10485 use MetaCPAN::Client::Package;
  20         84  
  20         695  
23 20     20   9842 use MetaCPAN::Client::Permission;
  20         82  
  20         734  
24 20     20   9335 use MetaCPAN::Client::ResultSet;
  20         73  
  20         697  
25 20     20   8968 use MetaCPAN::Client::Cover;
  20         81  
  20         775  
26 20     20   9011 use MetaCPAN::Client::Cve;
  20         90  
  20         58230  
27              
28             has request => (
29             is => 'ro',
30             handles => [qw],
31             );
32              
33             my @supported_searches = qw<
34             author distribution favorite module release mirror file permission package cover cve
35             >;
36              
37             sub BUILDARGS {
38 20     20 1 6184342 my ( $class, %args ) = @_;
39              
40             $args{'request'} ||= MetaCPAN::Client::Request->new(
41             ( ua => $args{ua} )x!! $args{ua},
42             ( ua_args => $args{ua_args} )x!! $args{ua_args},
43             ( domain => $args{domain} )x!! $args{domain},
44             ( debug => $args{debug} )x!! $args{debug},
45 20   33     495 );
46              
47 20         1326 return \%args;
48             }
49              
50             sub author {
51 7     7 1 232671 my $self = shift;
52 7         20 my $arg = shift;
53 7         63 my $params = shift;
54              
55 7         119 return $self->_get_or_search( 'author', $arg, $params );
56             }
57              
58             sub module {
59 2     2 1 1016 my $self = shift;
60 2         7 my $arg = shift;
61 2         4 my $params = shift;
62              
63 2         20 return $self->_get_or_search( 'module', $arg, $params );
64             }
65              
66             sub distribution {
67 31     31 1 49285 my $self = shift;
68 31         75 my $arg = shift;
69 31         69 my $params = shift;
70              
71 31         125 return $self->_get_or_search( 'distribution', $arg, $params );
72             }
73              
74             sub file {
75 1     1 1 887 my $self = shift;
76 1         2 my $arg = shift;
77 1         2 my $params = shift;
78              
79 1         3 return $self->_get_or_search( 'file', $arg, $params );
80             }
81              
82             sub package {
83 1     1 1 1128 my $self = shift;
84 1         4 my $arg = shift;
85 1         2 my $params = shift;
86              
87 1         5 return $self->_get_or_search( 'package', $arg, $params );
88             }
89              
90             sub permission {
91 1     1 1 1288 my $self = shift;
92 1         2 my $arg = shift;
93 1         3 my $params = shift;
94              
95 1         5 return $self->_get_or_search( 'permission', $arg, $params );
96             }
97              
98             sub cover {
99 1     1 1 766 my $self = shift;
100 1         3 my $arg = shift;
101 1         1 my $params = shift;
102              
103 1         5 return $self->_get_or_search( 'cover', $arg, $params );
104             }
105              
106             sub cve {
107 1     1 1 1168 my $self = shift;
108 1         3 my $arg = shift;
109 1         2 my $params = shift;
110              
111 1         5 return $self->_get_or_search( 'cve', $arg, $params );
112             }
113              
114             sub pod {
115 1     1 1 1317 my $self = shift;
116 1         3 my $name = shift;
117 1   50     9 my $params = shift || {};
118              
119 1         41 return MetaCPAN::Client::Pod->new({
120             request => $self->request,
121             name => $name,
122             %$params
123             });
124             }
125              
126             sub favorite {
127 2     2 1 95251 my $self = shift;
128 2         6 my $args = shift;
129 2         5 my $params = shift;
130              
131 2 50       11 is_hashref($args)
132             or croak 'favorite takes a hash ref as parameter';
133              
134 2         12 return $self->_search( 'favorite', $args, $params );
135             }
136              
137             sub release {
138 2     2 1 1293 my $self = shift;
139 2         5 my $arg = shift;
140 2         4 my $params = shift;
141              
142 2         8 return $self->_get_or_search( 'release', $arg, $params );
143             }
144              
145             sub mirror {
146 0     0 1 0 my $self = shift;
147 0         0 my $arg = shift;
148 0         0 my $params = shift;
149              
150 0         0 return $self->_get_or_search( 'mirror', $arg, $params );
151             }
152              
153             sub reverse_dependencies {
154 1     1 1 1240 my $self = shift;
155 1         2 my $dist = shift;
156              
157 1         5 $dist =~ s/::/-/g;
158              
159 1         4 return $self->_reverse_deps($dist);
160             }
161              
162             *rev_deps = *reverse_dependencies;
163              
164             sub recent {
165 0     0 1 0 my $self = shift;
166 0   0     0 my $size = shift || 100;
167              
168 0 0       0 $size eq 'today'
169             and return $self->_recent(
170             size => 1000,
171             query => {
172             bool => {
173             filter => _filter_today()
174             }
175             }
176             );
177              
178 0 0       0 $size =~ /^[0-9]+$/
179             and return $self->_recent( size => $size );
180              
181 0         0 croak "recent: invalid size value";
182             }
183              
184             sub count {
185 2     2 1 1738 my $self = shift;
186 2         6 my $index = shift;
187              
188 2         4 my $res;
189              
190             eval {
191 2         70 $res = $self->fetch( sprintf '%s/_count', $index );
192 2         10 1;
193              
194 2 50       4 } or do {
195 0         0 warn $@;
196 0         0 return [];
197             };
198              
199 2         35 return $res->{count};
200             }
201              
202             sub all {
203 0     0 1 0 my $self = shift;
204 0         0 my $index = shift;
205 0         0 my $params = shift;
206              
207             # This endpoint used to support only pluralized indices (mostly) and convert
208             # to singular indices before redispatching. Now it accepts both plural and
209             # unplural forms directly and relies on the underlying methods it
210             # dispatches to to check indices (using the global supported indices array).
211 0         0 $index =~ s/s$//;
212              
213 0 0 0     0 if ( $params and !is_hashref($params) ) {
214 0         0 croak "all: params must be a hashref";
215             }
216              
217 0 0 0     0 if ( exists $params->{fields} and !is_arrayref($params->{fields}) ) {
218 0         0 $params->{fields} = [ split /,/ => $params->{fields} ];
219             }
220              
221 0         0 return $self->$index( { __MATCH_ALL__ => 1 }, $params );
222             }
223              
224             sub download_url {
225 4     4 1 7602 my $self = shift;
226 4         10 my $module = shift;
227 4         9 my $version_or_range = shift;
228 4         8 my $dev = shift;
229              
230 4         8 my $uri = $module;
231 4         6 my @extra;
232 4 100       20 if ( defined $version_or_range ) {
233              
234 3         15 my @valid_ranges = qw{ == != <= >= < > ! };
235 3         6 my $is_using_range;
236 3         8 foreach my $range ( @valid_ranges ) {
237 15 100       45 if ( index( $version_or_range, $range ) >= 0 ) {
238 2         3 $is_using_range = 1;
239 2         6 last;
240             }
241             }
242             # by default use the '==' operator when no range set
243 3 100       13 $version_or_range = '==' . $version_or_range unless $is_using_range;
244              
245             # version=>0.21,<0.27,!=0.26&dev=1
246 3         23 push @extra, 'version=' .uri_escape_utf8($version_or_range);
247             }
248 4 100       969 if ( defined $dev ) {
249 1         4 push @extra, 'dev=' . uri_escape_utf8($dev);
250             }
251              
252 4 100       32 $uri .= '?'.join('&', @extra) if scalar @extra;
253              
254 4         16 return $self->_get( 'download_url', $uri );
255             }
256              
257             sub autocomplete {
258 0     0 1 0 my $self = shift;
259 0         0 my $q = shift;
260              
261 0         0 my $res;
262              
263             eval {
264 0         0 $res = $self->fetch( '/search/autocomplete?q=' . uri_escape_utf8($q) );
265 0         0 1;
266              
267 0 0       0 } or do {
268 0         0 warn $@;
269 0         0 return [];
270             };
271              
272             return [
273 0         0 map { $_->{fields} } @{ $res->{hits}{hits} }
  0         0  
  0         0  
274             ];
275             }
276              
277             sub autocomplete_suggest {
278 0     0 1 0 my $self = shift;
279 0         0 my $q = shift;
280              
281 0         0 my $res;
282              
283             eval {
284 0         0 $res = $self->fetch( '/search/autocomplete/suggest?q=' . uri_escape_utf8($q) );
285 0         0 1;
286              
287 0 0       0 } or do {
288 0         0 warn $@;
289 0         0 return [];
290             };
291              
292 0         0 return $res->{suggestions};
293             }
294              
295             ###
296              
297             sub _get {
298 49     49   3731 my $self = shift;
299              
300 49 100 66     927 ( scalar(@_) == 2
      66        
      66        
301             or ( scalar(@_) == 3 and ( !defined $_[2] or is_hashref($_[2]) ) ) )
302             or croak '_get takes index and search string as parameters (and an optional params hash)';
303              
304 46         113 my $index = shift;
305 46         97 my $arg = shift;
306 46         92 my $params = shift;
307              
308 46         284 my $fields_filter = $self->_read_fields( $params );
309              
310 46   50     1879 my $response = $self->fetch(
311             sprintf("%s/%s%s", $index ,$arg, $fields_filter||'')
312             );
313 46 100       1948 is_hashref($response)
314             or croak sprintf( 'Failed to fetch %s (%s)', ucfirst($index), $arg );
315              
316 45 100       180 $index = 'DownloadURL' if $index eq 'download_url';
317              
318             # deal with API response inconsistency
319 45 50 66     204 if ( $index eq 'cve' and is_hashref($response) and is_arrayref($response->{cve} ) ) {
      66        
320 1         5 $response = $response->{cve}[0];
321             }
322              
323 45         179 my $class = 'MetaCPAN::Client::' . ucfirst($index);
324 45         517 return $class->new_from_request($response, $self);
325             }
326              
327             sub _read_fields {
328 46     46   85 my $self = shift;
329 46         93 my $params = shift;
330 46 50       186 $params or return;
331              
332 0         0 my $fields = delete $params->{fields};
333 0 0       0 $fields or return;
334              
335 0 0       0 if ( is_arrayref($fields) ) {
    0          
336 0 0       0 grep { ref $_ } @$fields
  0         0  
337             and croak "fields array should not contain any refs.";
338              
339 0         0 return sprintf( "?fields=%s", join q{,} => @$fields );
340              
341             } elsif ( !ref $fields ) {
342              
343 0         0 return "?fields=$fields";
344             }
345              
346 0         0 croak "invalid param: fields";
347             }
348              
349             sub _search {
350 14     14   4820 my $self = shift;
351 14         34 my $index = shift;
352 14         24 my $args = shift;
353 14         27 my $params = shift;
354              
355 14 100       195 is_hashref($args)
356             or croak '_search takes a hash ref as query';
357              
358 13 100 100     261 ! defined $params or is_hashref($params)
359             or croak '_search takes a hash ref as query parameters';
360              
361 12   100     73 $params ||= {};
362              
363 12 100       39 grep { $_ eq $index } @supported_searches
  132         403  
364             or croak 'search index is not supported';
365              
366 11         341 my $scroller = $self->ssearch($index, $args, $params);
367              
368 11         4673 return MetaCPAN::Client::ResultSet->new(
369             scroller => $scroller,
370             index => $index,
371             );
372             }
373              
374             sub _get_or_search {
375 50     50   6380 my $self = shift;
376 50         104 my $index = shift;
377 50         105 my $arg = shift;
378 50         139 my $params = shift;
379              
380 50 100       307 is_hashref($arg) and
381             return $self->_search( $index, $arg, $params );
382              
383 42 100 66     571 defined $arg and !is_ref($arg)
384             and return $self->_get($index, $arg, $params);
385              
386 1         221 croak "$index: invalid args (takes scalar value or search parameters hashref)";
387             }
388              
389             sub _reverse_deps {
390 1     1   1 my $self = shift;
391 1         3 my $dist = shift;
392              
393 1         1 my $res;
394              
395             eval {
396 1         10 $res = $self->fetch(
397             "/reverse_dependencies/dist/$dist",
398             {
399             size => 5000,
400             query => {
401             bool => {
402             must => [
403             { term => { 'status' => 'latest' } },
404             { term => { 'authorized' => JSON->true } },
405             ]
406             }
407             },
408             }
409             );
410 1         16 1;
411              
412 1 50       2 } or do {
413 0         0 warn $@;
414 0         0 return _empty_result_set('release'),
415             };
416              
417             return MetaCPAN::Client::ResultSet->new(
418 1         20 items => $res->{'data'},
419             index => 'release',
420             );
421             }
422              
423             sub _recent {
424 0     0     my $self = shift;
425 0           my @args = @_;
426              
427 0           my $res;
428              
429             eval {
430 0           $res = $self->fetch(
431             '/release/_search',
432             {
433             from => 0,
434             query => { match_all => {} },
435             @args,
436             sort => [ { 'date' => { order => "desc" } } ],
437             }
438             );
439 0           1;
440              
441 0 0         } or do {
442 0           warn $@;
443 0           return _empty_result_set('release');
444             };
445              
446             return MetaCPAN::Client::ResultSet->new(
447 0           items => $res->{'hits'}{'hits'},
448             index => 'release',
449             );
450             }
451              
452             sub _filter_today {
453 0     0     return { range => { date => { from => "now/1d+0h" } } };
454             }
455              
456             sub _empty_result_set {
457 0     0     my $index = shift;
458              
459 0           return MetaCPAN::Client::ResultSet->new(
460             items => [],
461             index => $index,
462             );
463             }
464              
465             1;
466              
467             __END__