File Coverage

blib/lib/MetaCPAN/Client.pm
Criterion Covered Total %
statement 189 246 76.8
branch 31 58 53.4
condition 20 38 52.6
subroutine 44 51 86.2
pod 20 20 100.0
total 304 413 73.6


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