File Coverage

blib/lib/MetaCPAN/API/Tiny.pm
Criterion Covered Total %
statement 113 127 88.9
branch 57 72 79.1
condition 20 37 54.0
subroutine 15 16 93.7
pod 9 9 100.0
total 214 261 81.9


line stmt bran cond sub pod time code
1             package MetaCPAN::API::Tiny;
2             {
3             $MetaCPAN::API::Tiny::VERSION = '1.131730';
4             }
5 10     10   286624 use strict;
  10         29  
  10         321  
6 10     10   191 use warnings;
  10         21  
  10         254  
7             # ABSTRACT: A Tiny API client for MetaCPAN
8              
9 10     10   54 use Carp;
  10         21  
  10         1141  
10 10     10   13798 use JSON::PP 'encode_json', 'decode_json';
  10         289926  
  10         1183  
11 10     10   14549 use HTTP::Tiny;
  10         686139  
  10         19045  
12              
13              
14             sub new {
15 11     11 1 1024 my ($class, @args) = @_;
16              
17 11 50       61 $#_ % 2 == 0
18             or croak 'Arguments must be provided as name/value pairs';
19            
20 11         49 my %params = @args;
21              
22 11 50 66     133 die 'ua_args must be an array reference'
23             if $params{ua_args} && ref($params{ua_args}) ne 'ARRAY';
24              
25             my $self = +{
26             base_url => $params{base_url} || 'http://api.metacpan.org/v0',
27             ua => $params{ua} || HTTP::Tiny->new(
28             $params{ua_args}
29 11   50     154 ? @{$params{ua_args}}
      33        
30             : (agent => 'MetaCPAN::API::Tiny/'
31             . ($MetaCPAN::API::VERSION || 'xx'))),
32             };
33            
34 11         962 return bless($self, $class);
35             }
36              
37             sub _build_extra_params {
38 10     10   1868 my $self = shift;
39              
40 10 100       234 @_ % 2 == 0
41             or croak 'Incorrect number of params, must be key/value';
42              
43 9         28 my %extra = @_;
44 9         39 my $ua = $self->{ua};
45              
46 9         33 foreach my $key (keys %extra)
47             {
48             # The implementation in HTTP::Tiny uses + instead of %20, fix that
49 5         25 $extra{$key} = $ua->_uri_escape($extra{$key});
50 5         141 $extra{$key} =~ s/\+/%20/g;
51             }
52              
53 9         45 my $params = join '&', map { "$_=" . $extra{$_} } sort keys %extra;
  5         22  
54              
55 9         38 return $params;
56             }
57              
58              
59             # /source/{author}/{release}/{path}
60             sub source {
61 3     3 1 2586 my $self = shift;
62 3 100       15 my %opts = @_ ? @_ : ();
63 3         7 my $url = '';
64 3         5 my $error = "Provide 'author' and 'release' and 'path'";
65              
66 3 100       177 %opts or croak $error;
67              
68 2 100 66     18 if (
      66        
69             defined ( my $author = $opts{'author'} ) &&
70             defined ( my $release = $opts{'release'} ) &&
71             defined ( my $path = $opts{'path'} )
72             ) {
73 1         4 $url = "source/$author/$release/$path";
74             } else {
75 1         98 croak $error;
76             }
77              
78 1         8 $url = $self->{base_url} . "/$url";
79            
80 1         37 my $result = $self->{ua}->get($url);
81 1 50       29131 $result->{'success'}
82             or croak "Failed to fetch '$url': " . $result->{'reason'};
83              
84 1         414 return $result->{'content'};
85             }
86              
87              
88             # /release/{distribution}
89             # /release/{author}/{release}
90             sub release {
91 4     4 1 2796 my $self = shift;
92 4 100       21 my %opts = @_ ? @_ : ();
93 4         8 my $url = '';
94 4         7 my $error = "Either provide 'distribution', or 'author' and 'release', " .
95             "or 'search'";
96              
97 4 100       166 %opts or croak $error;
98              
99 3         7 my %extra_opts = ();
100              
101 3 100 66     25 if ( defined ( my $dist = $opts{'distribution'} ) ) {
    100          
    50          
102 1         3 $url = "release/$dist";
103             } elsif (
104             defined ( my $author = $opts{'author'} ) &&
105             defined ( my $release = $opts{'release'} )
106             ) {
107 1         5 $url = "release/$author/$release";
108             } elsif ( defined ( my $search_opts = $opts{'search'} ) ) {
109 0 0 0     0 ref $search_opts && ref $search_opts eq 'HASH'
110             or croak $error;
111              
112 0         0 %extra_opts = %{$search_opts};
  0         0  
113 0         0 $url = 'release/_search';
114             } else {
115 1         145 croak $error;
116             }
117              
118 2         11 return $self->fetch( $url, %extra_opts );
119             }
120              
121              
122             # /pod/{module}
123             # /pod/{author}/{release}/{path}
124             sub pod {
125 9     9 1 10390 my $self = shift;
126 9 100       67 my %opts = @_ ? @_ : ();
127 9         22 my $url = '';
128 9         17 my $error = "Either provide 'module' or 'author and 'release' and 'path'";
129              
130 9 100       199 %opts or croak $error;
131              
132 8 100 66     55 if ( defined ( my $module = $opts{'module'} ) ) {
    100 66        
133 6         18 $url = "pod/$module";
134             } elsif (
135             defined ( my $author = $opts{'author'} ) &&
136             defined ( my $release = $opts{'release'} ) &&
137             defined ( my $path = $opts{'path'} )
138             ) {
139 1         7 $url = "pod/$author/$release/$path";
140             } else {
141 1         102 croak $error;
142             }
143              
144             # check content-type
145 7         17 my %extra = ();
146 7 100       31 if ( defined ( my $type = $opts{'content-type'} ) ) {
147 5 100       231 $type =~ m{^ text/ (?: html|plain|x-pod|x-markdown ) $}x
148             or croak 'Incorrect content-type provided';
149              
150 4         18 $extra{headers}{'content-type'} = $type;
151             }
152              
153 6         34 $url = $self->{base_url}. "/$url";
154            
155 6         505 my $result = $self->{ua}->get( $url, \%extra );
156 6 50       926194 $result->{'success'}
157             or croak "Failed to fetch '$url': " . $result->{'reason'};
158              
159 6         187 return $result->{'content'};
160             }
161              
162              
163             # /module/{module}
164             sub module {
165 2     2 1 1866 my $self = shift;
166 2         3 my $name = shift;
167              
168 2 100       167 $name or croak 'Please provide a module name';
169              
170 1         7 return $self->fetch("module/$name");
171             }
172              
173              
174             # file() is a synonym of module
175 0     0 1 0 sub file { goto &module }
176              
177              
178             # /author/{author}
179             sub author {
180 2     2 1 1369 my $self = shift;
181 2         5 my ( $pause_id, $url, %extra_opts );
182              
183 2 100       8 if ( @_ == 1 ) {
    50          
184 1         4 $url = 'author/' . shift;
185             } elsif ( @_ == 2 ) {
186 0         0 my %opts = @_;
187              
188 0 0       0 if ( defined $opts{'pauseid'} ) {
    0          
189 0         0 $url = "author/" . $opts{'pauseid'};
190             } elsif ( defined $opts{'search'} ) {
191 0         0 my $search_opts = $opts{'search'};
192              
193 0 0 0     0 ref $search_opts && ref $search_opts eq 'HASH'
194             or croak "'search' key must be hashref";
195              
196 0         0 %extra_opts = %{$search_opts};
  0         0  
197 0         0 $url = 'author/_search';
198             } else {
199 0         0 croak 'Unknown option given';
200             }
201             } else {
202 1         164 croak 'Please provide an author PAUSEID or a "search"';
203             }
204              
205 1         5 return $self->fetch( $url, %extra_opts );
206             }
207              
208              
209              
210             sub fetch {
211 6     6 1 1206 my $self = shift;
212 6         13 my $url = shift;
213 6         27 my $extra = $self->_build_extra_params(@_);
214 6         15 my $base = $self->{base_url};
215 6 100       33 my $req_url = $extra ? "$base/$url?$extra" : "$base/$url";
216            
217 6         167 my $result = $self->{ua}->get($req_url);
218 6         504700 return $self->_decode_result( $result, $req_url );
219             }
220              
221              
222             sub post {
223 5     5 1 6953 my $self = shift;
224 5         10 my $url = shift;
225 5         7 my $query = shift;
226 5         15 my $base = $self->{base_url};
227              
228 5 100       240 defined $url
229             or croak 'First argument of URL must be provided';
230              
231 4 100 66     250 ref $query and ref $query eq 'HASH'
232             or croak 'Second argument of query hashref must be provided';
233              
234 2         14 my $query_json = encode_json( $query );
235 2         611 my $result = $self->{ua}->request(
236             'POST',
237             "$base/$url",
238             {
239             headers => { 'Content-Type' => 'application/json' },
240             content => $query_json,
241             }
242             );
243              
244 2         15911 return $self->_decode_result( $result, $url, $query_json );
245             }
246              
247             sub _decode_result {
248 15     15   2344 my $self = shift;
249 15         34 my ( $result, $url, $original ) = @_;
250 15         27 my $decoded_result;
251              
252 15 100 66     291 ref $result and ref $result eq 'HASH'
253             or croak 'First argument must be hashref';
254              
255 14 100       121 defined $url
256             or croak 'Second argument of a URL must be provided';
257              
258 13 100       59 if ( defined ( my $success = $result->{'success'} ) ) {
259 12   100     65 my $reason = $result->{'reason'} || '';
260 12 100       43 $reason .= ( defined $original ? " (request: $original)" : '' );
261              
262 12 100       208 $success or croak "Failed to fetch '$url': $reason";
263             } else {
264 1         97 croak 'Missing success in return value';
265             }
266              
267 10 50       419 defined ( my $content = $result->{'content'} )
268             or croak 'Missing content in return value';
269              
270 10         57 eval { $decoded_result = decode_json $content; 1 }
  9         1139173  
271 10 100       23 or do { croak "Couldn't decode '$content': $@" };
  1         333  
272              
273 9         152 return $decoded_result;
274             }
275              
276             1;
277              
278             __END__