File Coverage

blib/lib/Net/GitHub/V3/Query.pm
Criterion Covered Total %
statement 50 187 26.7
branch 0 74 0.0
condition 4 38 10.5
subroutine 14 24 58.3
pod 2 4 50.0
total 70 327 21.4


line stmt bran cond sub pod time code
1             package Net::GitHub::V3::Query;
2              
3             our $VERSION = '0.76';
4             our $AUTHORITY = 'cpan:FAYLAND';
5              
6 1     1   6685 use URI;
  1         1753  
  1         22  
7 1     1   431 use JSON::MaybeXS;
  1         4213  
  1         47  
8 1     1   425 use MIME::Base64;
  1         494  
  1         47  
9 1     1   551 use LWP::UserAgent;
  1         25835  
  1         28  
10 1     1   7 use HTTP::Request;
  1         0  
  1         22  
11 1     1   3 use Carp qw/croak/;
  1         1  
  1         42  
12 1     1   3 use URI::Escape;
  1         8  
  1         45  
13 1     1   3 use Types::Standard qw(Int Str Bool InstanceOf Object);
  1         2  
  1         13  
14 1     1   1042 use Cache::LRU;
  1         427  
  1         22  
15              
16 1     1   4 use Moo::Role;
  1         2  
  1         7  
17              
18             # configurable args
19              
20             # Authentication
21             has 'login' => ( is => 'rw', isa => Str, predicate => 'has_login' );
22             has 'pass' => ( is => 'rw', isa => Str, predicate => 'has_pass' );
23             has 'access_token' => ( is => 'rw', isa => Str, predicate => 'has_access_token' );
24              
25             # return raw unparsed JSON
26             has 'raw_string' => (is => 'rw', isa => Bool, default => 0);
27             has 'raw_response' => (is => 'rw', isa => Bool, default => 0);
28              
29             has 'api_url' => (is => 'ro', default => 'https://api.github.com');
30             has 'api_throttle' => ( is => 'rw', isa => Bool, default => 1 );
31              
32             has 'upload_url' => (is => 'ro', default => 'https://uploads.github.com');
33              
34             # pagination
35             has 'next_url' => ( is => 'rw', isa => Str, predicate => 'has_next_page', clearer => 'clear_next_url' );
36             has 'last_url' => ( is => 'rw', isa => Str, predicate => 'has_last_page', clearer => 'clear_last_url' );
37             has 'first_url' => ( is => 'rw', isa => Str, predicate => 'has_first_page', clearer => 'clear_first_url' );
38             has 'prev_url' => ( is => 'rw', isa => Str, predicate => 'has_prev_page', clearer => 'clear_prev_url' );
39             has 'per_page' => ( is => 'rw', isa => Str, default => 100 );
40              
41             # Error handle
42             has 'RaiseError' => ( is => 'rw', isa => Bool, default => 1 );
43              
44             # Rate limits
45             has 'rate_limit' => ( is => 'rw', isa => Int, default => 0 );
46             has 'rate_limit_remaining' => ( is => 'rw', isa => Int, default => 0 );
47             has 'rate_limit_reset' => ( is => 'rw', isa => Str, default => 0 );
48              
49             # optional
50             has 'u' => (is => 'rw', isa => Str);
51             has 'repo' => (is => 'rw', isa => Str);
52              
53             has 'is_main_module' => (is => 'ro', isa => Bool, default => 0);
54             sub set_default_user_repo {
55 0     0 0 0 my ($self, $user, $repo) = @_;
56              
57 0         0 $self->u($user);
58 0         0 $self->repo($repo);
59              
60             # need apply to all sub modules
61 0 0       0 if ($self->is_main_module) {
62 0 0       0 if ($self->is_repos_init) {
63 0         0 $self->repos->u($user); $self->repos->repo($repo);
  0         0  
64             }
65 0 0       0 if ($self->is_issue_init) {
66 0         0 $self->issue->u($user); $self->issue->repo($repo);
  0         0  
67             }
68 0 0       0 if ($self->is_pull_request_init) {
69 0         0 $self->pull_request->u($user); $self->pull_request->repo($repo);
  0         0  
70             }
71 0 0       0 if ($self->is_git_data_init) {
72 0         0 $self->git_data->u($user); $self->git_data->repo($repo);
  0         0  
73             }
74             }
75              
76 0         0 return $self;
77             }
78              
79             sub args_to_pass {
80 0     0 0 0 my $self = shift;
81 0         0 my $ret;
82 0         0 foreach my $col ('login', 'pass', 'access_token', 'raw_string', 'raw_response', 'api_url', 'api_throttle', 'u', 'repo', 'next_url', 'last_url', 'first_url', 'prev_url', 'per_page', 'ua') {
83 0         0 my $v = $self->$col;
84 0 0       0 $ret->{$col} = $v if defined $v;
85             }
86 0         0 return $ret;
87             }
88              
89             has 'ua' => (
90             isa => InstanceOf['LWP::UserAgent'],
91             is => 'ro',
92             lazy => 1,
93             default => sub {
94             LWP::UserAgent->new(
95             agent => "perl-net-github/$VERSION",
96             cookie_jar => {},
97             keep_alive => 4,
98             timeout => 60,
99             );
100             },
101             );
102              
103             has 'json' => (
104             is => 'ro',
105             isa => Object, # InstanceOf['JSON::MaybeXS'],
106             lazy => 1,
107             default => sub {
108             return JSON::MaybeXS->new( utf8 => 1 );
109             }
110             );
111              
112             has 'cache' => (
113             isa => InstanceOf['Cache::LRU'],
114             is => 'rw',
115             lazy => 1,
116             default => sub {
117             Cache::LRU->new(
118             size => 200
119             );
120             }
121             );
122              
123             sub query {
124 0     0 1 0 my $self = shift;
125              
126             # fix ARGV, not sure if it's the good idea
127 0         0 my @args = @_;
128 0 0 0     0 if (@args == 1) {
    0          
129 0         0 unshift @args, 'GET'; # method by default
130 0         0 } elsif (@args > 1 and not (grep { $args[0] eq $_ } ('GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'DELETE')) ) {
131 0         0 unshift @args, 'POST'; # if POST content
132             }
133 0         0 my $request_method = shift @args;
134 0         0 my $url = shift @args;
135 0         0 my $data = shift @args;
136              
137 0         0 my $ua = $self->ua;
138              
139             ## always go with login:pass or access_token (for private repos)
140 0 0 0     0 if ($self->has_access_token) {
    0          
141 0         0 $ua->default_header('Authorization', "token " . $self->access_token);
142             } elsif ($self->has_login and $self->has_pass) {
143 0         0 my $auth_basic = $self->login . ':' . $self->pass;
144 0         0 $ua->default_header('Authorization', 'Basic ' . encode_base64($auth_basic));
145             }
146              
147 0 0       0 $url = $self->api_url . $url unless $url =~ /^https\:/;
148 0 0       0 if ($request_method eq 'GET') {
149 0 0       0 if ($url !~ /per_page=\d/) {
150             ## auto add per_page in url for GET no matter it supports or not
151 0         0 my $uri = URI->new($url);
152 0         0 my %query_form = $uri->query_form;
153 0   0     0 $query_form{per_page} ||= $self->per_page;
154 0         0 $uri->query_form(%query_form);
155 0         0 $url = $uri->as_string;
156             }
157             }
158              
159 0 0       0 print STDERR ">>> $request_method $url\n" if $ENV{NG_DEBUG};
160 0         0 my $req = HTTP::Request->new( $request_method, $url );
161 0         0 $req->accept_decodable;
162 0 0       0 if ($data) {
163 0         0 my $json = $self->json->encode($data);
164 0 0 0     0 print STDERR ">>> $data\n" if $ENV{NG_DEBUG} and $ENV{NG_DEBUG} > 1;
165 0         0 $req->content($json);
166             }
167 0         0 $req->header( 'Content-Length' => length $req->content );
168              
169 0         0 my $res = $self->_make_request($req);
170              
171             # get the rate limit information from the http response headers
172 0         0 $self->rate_limit( $res->header('x-ratelimit-limit') );
173 0         0 $self->rate_limit_remaining( $res->header('x-ratelimit-remaining') );
174 0         0 $self->rate_limit_reset( $res->header('x-ratelimit-reset') );
175              
176             # Slow down if we're approaching the rate limit
177             # By the way GitHub mistakes days for minutes in their documentation --
178             # the rate limit is per minute, not per day.
179 0 0       0 if ( $self->api_throttle ) {
180 0 0 0     0 sleep 2 if (($self->rate_limit_remaining || 0)
      0        
181             < ($self->rate_limit || 60) / 2);
182             }
183              
184 0 0 0     0 print STDERR "<<< " . $res->decoded_content . "\n" if $ENV{NG_DEBUG} and $ENV{NG_DEBUG} > 1;
185 0 0       0 return $res if $self->raw_response;
186 0 0       0 return $res->decoded_content if $self->raw_string;
187              
188 0 0 0     0 if ($res->header('Content-Type') and $res->header('Content-Type') =~ 'application/json') {
189 0         0 my $json = $res->decoded_content;
190 0         0 $data = eval { $self->json->decode($json) };
  0         0  
191 0 0       0 unless ($data) {
192             # We tolerate bad JSON for errors,
193             # otherwise we just rethrow the JSON parsing problem.
194 0 0       0 die unless $res->is_error;
195 0         0 $data = { message => $res->message };
196             }
197             } else {
198 0         0 $data = { message => $res->message };
199             }
200              
201 0 0       0 if ( $self->RaiseError ) {
202             # check for 'Client Errors'
203 0 0 0     0 if (not $res->is_success and ref $data eq 'HASH' and exists $data->{message}) {
      0        
204 0         0 my $message = $data->{message};
205              
206             # Include any additional error information that was returned by the API
207 0 0       0 if (exists $data->{errors}) {
208             $message .= ': '.join(' - ',
209 0         0 map { $_->{message} }
210 0         0 grep { exists $_->{message} }
211 0         0 @{ $data->{errors} });
  0         0  
212             }
213 0         0 croak $message;
214             }
215             }
216              
217 0         0 $self->_clear_pagination;
218 0 0       0 if ($res->header('link')) {
219 0         0 my @rel_strs = split ',', $res->header('link');
220 0         0 $self->_extract_link_url(\@rel_strs);
221             }
222              
223             ## be smarter
224 0 0       0 if (wantarray) {
225 0 0       0 return @$data if ref $data eq 'ARRAY';
226 0 0       0 return %$data if ref $data eq 'HASH';
227             }
228              
229 0         0 return $data;
230             }
231              
232             sub next_page {
233 0     0 1 0 my $self = shift;
234 0         0 return $self->query($self->next_url);
235             }
236              
237             sub _clear_pagination {
238 0     0   0 my $self = shift;
239 0         0 foreach my $page (qw/first last prev next/) {
240 0         0 my $clearer = 'clear_' . $page . '_url';
241 0         0 $self->$clearer;
242             }
243 0         0 return 1;
244             }
245              
246             sub _extract_link_url {
247 0     0   0 my ($self, $raw_strs) = @_;
248 0         0 foreach my $str (@$raw_strs) {
249 0         0 my ($link_url, $rel) = split ';', $str;
250              
251 0         0 $link_url =~ s/^\s*//;
252 0         0 $link_url =~ s/^
253 0         0 $link_url =~ s/>$//;
254              
255 0         0 $rel =~ m/rel="(next|last|first|prev)"/;
256 0         0 $rel = $1;
257              
258 0         0 my $url_attr = $rel . "_url";
259 0         0 $self->$url_attr($link_url);
260             }
261              
262 0         0 return 1;
263             }
264              
265             sub _make_request {
266 0     0   0 my($self, $req) = @_;
267              
268 0         0 my $cached_res = $self->_get_shared_cache($req->uri);
269              
270 0 0       0 if ($cached_res) {
271 0         0 $req->header("If-None-Match" => $cached_res->header("ETag"));
272 0         0 my $res = $self->ua->request($req);
273              
274 0 0       0 if ($res->code == 304) {
275 0         0 return $cached_res;
276             }
277              
278 0         0 $self->_set_shared_cache($req->uri, $res);
279              
280 0         0 return $res;
281             } else {
282 0         0 my $res = $self->ua->request($req);
283 0         0 $self->_set_shared_cache( $req->uri, $res);
284 0         0 return $res;
285             }
286             }
287              
288             sub _get_shared_cache {
289 0     0   0 my ($self, $uri) = @_;
290 0         0 return $self->cache->get($uri);
291             }
292              
293             sub _set_shared_cache {
294 0     0   0 my($self, $uri, $response) = @_;
295 0         0 $self->cache->set($uri, $response);
296             }
297              
298             ## build methods on fly
299             sub __build_methods {
300 9     9   10 my $package = shift;
301 9         37 my %methods = @_;
302              
303 9         29 foreach my $m (keys %methods) {
304 180         141 my $v = $methods{$m};
305 180         162 my $url = $v->{url};
306 180   100     317 my $method = $v->{method} || 'GET';
307 180   100     307 my $args = $v->{args} || 0; # args for ->query
308 180         115 my $check_status = $v->{check_status};
309 180         116 my $is_u_repo = $v->{is_u_repo}; # need auto shift u/repo
310              
311 1     1   1495 no strict 'refs';
  1         1  
  1         46  
312 1     1   4 no warnings 'once';
  1         1  
  1         218  
313 180         615 *{"${package}::${m}"} = sub {
314 0     0     my $self = shift;
315              
316             # count how much %s inside u
317 0           my $n = 0; while ($url =~ /\%s/g) { $n++ }
  0            
  0            
318              
319             ## if is_u_repo, both ($user, $repo, @args) or (@args) should be supported
320 0 0 0       if ( ($is_u_repo or index($url, '/repos/%s/%s') > -1) and @_ < $n + $args) {
      0        
321 0           unshift @_, ($self->u, $self->repo);
322             }
323              
324             # make url, replace %s with real args
325 0           my @uargs = splice(@_, 0, $n);
326 0           my $u = sprintf($url, @uargs);
327              
328             # args for json data POST
329 0 0         my @qargs = $args ? splice(@_, 0, $args) : ();
330 0 0         if ($check_status) { # need check Response Status
331 0           my $old_raw_response = $self->raw_response;
332 0           $self->raw_response(1); # need check header
333 0           my $res = $self->query($method, $u, @qargs);
334 0           $self->raw_response($old_raw_response);
335 0 0         return index($res->header('Status'), $check_status) > -1 ? 1 : 0;
336             } else {
337 0           return $self->query($method, $u, @qargs);
338             }
339 180         601 };
340             }
341             }
342              
343 1     1   4 no Moo::Role;
  1         1  
  1         4  
344              
345             1;
346             __END__