File Coverage

blib/lib/Pithub/Base.pm
Criterion Covered Total %
statement 123 124 99.1
branch 58 60 96.6
condition 12 14 85.7
subroutine 20 21 95.2
pod 3 3 100.0
total 216 222 97.3


line stmt bran cond sub pod time code
1             package Pithub::Base;
2             our $AUTHORITY = 'cpan:PLU';
3             # ABSTRACT: Github v3 base class for all Pithub modules
4              
5 23     23   11460 use Moo;
  23         51  
  23         101  
6              
7             our $VERSION = '0.01039';
8              
9 23     23   6089 use Carp qw( croak );
  23         40  
  23         960  
10 23     23   4931 use HTTP::Headers ();
  23         39660  
  23         468  
11 23     23   9146 use HTTP::Request ();
  23         141941  
  23         560  
12 23     23   4922 use JSON::MaybeXS qw( JSON );
  23         59369  
  23         1096  
13 23     23   13811 use LWP::UserAgent ();
  23         480996  
  23         555  
14 23     23   10908 use Pithub::Result ();
  23         76  
  23         739  
15 23     23   177 use URI ();
  23         43  
  23         63477  
16              
17             with 'Pithub::Result::SharedCache';
18              
19              
20             has 'auto_pagination' => (
21             default => sub { 0 },
22             is => 'rw',
23             );
24              
25              
26             has 'api_uri' => (
27             default => sub { URI->new('https://api.github.com') },
28             is => 'rw',
29             trigger => sub {
30             my ( $self, $uri ) = @_;
31             $self->{api_uri} = URI->new("$uri");
32             },
33             );
34              
35              
36             has 'jsonp_callback' => (
37             clearer => 'clear_jsonp_callback',
38             is => 'rw',
39             predicate => 'has_jsonp_callback',
40             required => 0,
41             );
42              
43              
44             has 'per_page' => (
45             clearer => 'clear_per_page',
46             is => 'rw',
47             predicate => 'has_per_page',
48             default => 100,
49             required => 0,
50             );
51              
52              
53             has 'prepare_request' => (
54             clearer => 'clear_prepare_request',
55             is => 'rw',
56             predicate => 'has_prepare_request',
57             required => 0,
58             );
59              
60              
61             has 'repo' => (
62             clearer => 'clear_repo',
63             is => 'rw',
64             predicate => 'has_repo',
65             required => 0,
66             );
67              
68              
69             has 'token' => (
70             clearer => 'clear_token',
71             is => 'rw',
72             predicate => '_has_token',
73             required => 0,
74             );
75              
76              
77             has 'ua' => (
78             builder => '_build_ua',
79             is => 'ro',
80             lazy => 1,
81             );
82              
83              
84             has 'user' => (
85             clearer => 'clear_user',
86             is => 'rw',
87             predicate => 'has_user',
88             required => 0,
89             );
90              
91              
92             has 'utf8' => (
93             is => 'ro',
94             default => 1,
95             );
96              
97             has '_json' => (
98             builder => '_build__json',
99             is => 'ro',
100             lazy => 1,
101             );
102              
103             my @TOKEN_REQUIRED = (
104             'DELETE /user/emails',
105             'GET /user',
106             'GET /user/emails',
107             'GET /user/followers',
108             'GET /user/following',
109             'GET /user/keys',
110             'GET /user/repos',
111             'PATCH /user',
112             'POST /user/emails',
113             'POST /user/keys',
114             'POST /user/repos',
115             );
116              
117             my @TOKEN_REQUIRED_REGEXP = (
118             qr{^DELETE },
119             qr{^GET /gists/starred$},
120             qr{^GET /gists/[^/]+/star$},
121             qr{^GET /issues$},
122             qr{^GET /orgs/[^/]+/members/.*$},
123             qr{^GET /orgs/[^/]+/teams$},
124             qr{^GET /repos/[^/]+/[^/]+/collaborators$},
125             qr{^GET /repos/[^/]+/[^/]+/collaborators/.*$},
126             qr{^GET /repos/[^/]+/[^/]+/hooks$},
127             qr{^GET /repos/[^/]+/[^/]+/hooks/.*$},
128             qr{^GET /repos/[^/]+/[^/]+/keys$},
129             qr{^GET /repos/[^/]+/[^/]+/keys/.*$},
130             qr{^GET /teams/.*$},
131             qr{^GET /teams/[^/]+/members$},
132             qr{^GET /teams/[^/]+/members/.*$},
133             qr{^GET /teams/[^/]+/repos$},
134             qr{^GET /teams/[^/]+/repos/.*$},
135             qr{^GET /user/following/.*$},
136             qr{^GET /user/keys/.*$},
137             qr{^GET /user/orgs$},
138             qr{^GET /user/starred/[^/]+/.*$},
139             qr{^GET /user/watched$},
140             qr{^GET /user/watched/[^/]+/.*$},
141             qr{^GET /users/[^/]+/events/orgs/.*$},
142             qr{^PATCH /gists/.*$},
143             qr{^PATCH /gists/[^/]+/comments/.*$},
144             qr{^PATCH /orgs/.*$},
145             qr{^PATCH /repos/[^/]+/.*$},
146             qr{^PATCH /repos/[^/]+/[^/]+/comments/.*$},
147             qr{^PATCH /repos/[^/]+/[^/]+/git/refs/.*$},
148             qr{^PATCH /repos/[^/]+/[^/]+/hooks/.*$},
149             qr{^PATCH /repos/[^/]+/[^/]+/issues/.*$},
150             qr{^PATCH /repos/[^/]+/[^/]+/issues/comments/.*$},
151             qr{^PATCH /repos/[^/]+/[^/]+/keys/.*$},
152             qr{^PATCH /repos/[^/]+/[^/]+/labels/.*$},
153             qr{^PATCH /repos/[^/]+/[^/]+/milestones/.*$},
154             qr{^PATCH /repos/[^/]+/[^/]+/pulls/.*$},
155             qr{^PATCH /repos/[^/]+/[^/]+/releases/.*$},
156             qr{^PATCH /repos/[^/]+/[^/]+/pulls/comments/.*$},
157             qr{^PATCH /teams/.*$},
158             qr{^PATCH /user/keys/.*$},
159             qr{^PATCH /user/repos/.*$},
160             qr{^POST /repos/[^/]+/[^/]+/releases/[^/]+/assets.*$},
161             qr{^POST /gists/[^/]+/comments$},
162             qr{^POST /orgs/[^/]+/repos$},
163             qr{^POST /orgs/[^/]+/teams$},
164             qr{^POST /repos/[^/]+/[^/]+/commits/[^/]+/comments$},
165             qr{^POST /repos/[^/]+/[^/]+/downloads$},
166             qr{^POST /repos/[^/]+/[^/]+/forks},
167             qr{^POST /repos/[^/]+/[^/]+/git/blobs$},
168             qr{^POST /repos/[^/]+/[^/]+/git/commits$},
169             qr{^POST /repos/[^/]+/[^/]+/git/refs},
170             qr{^POST /repos/[^/]+/[^/]+/git/tags$},
171             qr{^POST /repos/[^/]+/[^/]+/git/trees$},
172             qr{^POST /repos/[^/]+/[^/]+/hooks$},
173             qr{^POST /repos/[^/]+/[^/]+/hooks/[^/]+/test$},
174             qr{^POST /repos/[^/]+/[^/]+/issues$},
175             qr{^POST /repos/[^/]+/[^/]+/issues/[^/]+/comments},
176             qr{^POST /repos/[^/]+/[^/]+/issues/[^/]+/labels$},
177             qr{^POST /repos/[^/]+/[^/]+/keys$},
178             qr{^POST /repos/[^/]+/[^/]+/labels$},
179             qr{^POST /repos/[^/]+/[^/]+/milestones$},
180             qr{^POST /repos/[^/]+/[^/]+/pulls$},
181             qr{^POST /repos/[^/]+/[^/]+/releases$},
182             qr{^POST /repos/[^/]+/[^/]+/pulls/[^/]+/comments$},
183             qr{^POST /repos/[^/]+/[^/]+/pulls/[^/]+/requested_reviewers$},
184             qr{^PUT /gists/[^/]+/star$},
185             qr{^PUT /orgs/[^/]+/public_members/.*$},
186             qr{^PUT /repos/[^/]+/[^/]+/collaborators/.*$},
187             qr{^PUT /repos/[^/]+/[^/]+/issues/[^/]+/labels$},
188             qr{^PUT /repos/[^/]+/[^/]+/pulls/[^/]+/merge$},
189             qr{^PUT /teams/[^/]+/members/.*$},
190             qr{^PUT /teams/[^/]+/memberships/.*$},
191             qr{^PUT /teams/[^/]+/repos/.*$},
192             qr{^PUT /user/following/.*$},
193             qr{^PUT /user/starred/[^/]+/.*$},
194             qr{^PUT /user/watched/[^/]+/.*$},
195             );
196              
197              
198             sub request {
199 686     686 1 15100 my ( $self, %args ) = @_;
200              
201 686   66     2142 my $method = delete $args{method} || croak 'Missing mandatory key in parameters: method';
202 685   66     1897 my $path = delete $args{path} || croak 'Missing mandatory key in parameters: path';
203 684         1342 my $data = delete $args{data};
204 684         1132 my $options = delete $args{options};
205 684         1270 my $params = delete $args{params};
206              
207 684 100       2732 croak "Invalid method: $method" unless grep $_ eq $method, qw(DELETE GET PATCH POST PUT);
208              
209 683         2327 my $uri = $self->_uri_for($path);
210              
211 683 100       2042 if (my $host = delete $args{host}) {
212 3         32 $uri->host($host);
213             }
214              
215 683 100       2031 if (my $query = delete $args{query}) {
216 6         26 $uri->query_form(%$query);
217             }
218              
219 683         2560 my $request = $self->_request_for( $method, $uri, $data );
220              
221 683 100       1798 if (my $headers = delete $args{headers}) {
222 3         11 foreach my $header (keys %$headers) {
223 3         14 $request->header($header, $headers->{$header});
224             }
225             }
226              
227 683 100 100     2127 if ( $self->_token_required( $method, $path ) && !$self->has_token($request) ) {
228 112         567 croak sprintf 'Access token required for: %s %s (%s)', $method, $path, $uri;
229             }
230              
231 571 100       1580 if ($options) {
232 227 100       937 croak 'The key options must be a hashref' unless ref $options eq 'HASH';
233 226 100 100     1501 croak 'The key prepare_request in the options hashref must be a coderef' if $options->{prepare_request} && ref $options->{prepare_request} ne 'CODE';
234              
235 225 100       764 if ( $options->{prepare_request} ) {
236 224         854 $options->{prepare_request}->($request);
237             }
238             }
239              
240 569 100       12040 if ($params) {
241 9 100       54 croak 'The key params must be a hashref' unless ref $params eq 'HASH';
242 8         30 my %query = ( $request->uri->query_form, %$params );
243 8         432 $request->uri->query_form(%query);
244             }
245              
246 568         2462 my $response = $self->_make_request($request);
247              
248             return Pithub::Result->new(
249             auto_pagination => $self->auto_pagination,
250             response => $response,
251             utf8 => $self->utf8,
252 21     21   406 _request => sub { $self->request(@_) },
253 568         12979 );
254             }
255              
256              
257             sub _make_request {
258 568     568   1226 my($self, $request) = @_;
259              
260 568         1568 my $cache_key = $request->uri->as_string;
261 568 100       8382 if( my $cached_response = $self->shared_cache->get($cache_key) ) {
262             # Add the If-None-Match header from the cache's ETag
263             # and make the request
264 232         20601 $request->header( 'If-None-Match' => $cached_response->header('ETag') );
265 232         19403 my $response = $self->ua->request($request);
266              
267             # Got 304 Not Modified, cache is still valid
268 232 50 100     14488 return $cached_response if ($response->code || 0) == 304;
269              
270             # The response changed, cache it and return it.
271 232         2840 $self->shared_cache->set( $cache_key, $response );
272 232         96792 return $response;
273             }
274              
275 336         32419 my $response = $self->ua->request($request);
276 336         24847 $self->shared_cache->set( $cache_key, $response );
277 336         186033 return $response;
278             }
279              
280              
281              
282             sub has_token {
283 1262     1262 1 2662 my ($self, $request) = @_;
284              
285             # If we have one specified in the object, return true
286 1262 100       5003 return 1 if $self->_has_token;
287             # If no request object here, we don't have a token
288 417 100       1164 return 0 unless $request;
289              
290 113 100       242 return 1 if $request->header('Authorization');
291 112         4115 return 0;
292             }
293              
294              
295             sub rate_limit {
296 0     0 1 0 return shift->request( method => 'GET', path => '/rate_limit' );
297             }
298              
299             sub _build__json {
300 96     96   1159 my ($self) = @_;
301 96         503 return JSON->new->utf8($self->utf8);
302             }
303              
304             sub _build_ua {
305 27     27   666 my ($self) = @_;
306 27         95 return LWP::UserAgent->new;
307             }
308              
309             sub _get_user_repo_args {
310 476     476   942 my ( $self, $args ) = @_;
311 476 100       1950 $args->{user} = $self->user unless defined $args->{user};
312 476 100       1581 $args->{repo} = $self->repo unless defined $args->{repo};
313 476         932 return $args;
314             }
315              
316             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
317             sub _create_instance {
318 222     222   608 my ( $self, $class, @args ) = @_;
319              
320 222         3861 my %args = (
321             api_uri => $self->api_uri,
322             auto_pagination => $self->auto_pagination,
323             ua => $self->ua,
324             utf8 => $self->utf8,
325             @args,
326             );
327              
328 222         14540 for my $attr (qw(repo token user per_page jsonp_callback prepare_request)) {
329             # Allow overrides to set attributes to undef
330 1332 100       2248 next if exists $args{$attr};
331              
332 1329         1827 my $has_attr = "has_$attr";
333 1329 100       4342 $args{$attr} = $self->$attr if $self->$has_attr;
334             }
335              
336 222         4575 return $class->new(%args);
337             }
338             ## use critic
339              
340             sub _request_for {
341 683     683   1962 my ( $self, $method, $uri, $data ) = @_;
342              
343 683         2596 my $headers = HTTP::Headers->new;
344              
345 683 100       6064 if ( $self->has_token ) {
346 451         2554 $headers->header( 'Authorization' => sprintf( 'token %s', $self->token ) );
347             }
348              
349 683         22327 my $request = HTTP::Request->new( $method, $uri, $headers );
350              
351 683 100       72116 if ($data) {
352 304 100       7551 $data = $self->_json->encode($data) if ref $data;
353 304         6335 $request->content($data);
354             }
355              
356 683         8605 $request->header( 'Content-Length' => length $request->content );
357              
358 683 100       40929 if ( $self->has_prepare_request ) {
359 204         1204 $self->prepare_request->($request);
360             }
361              
362 683         2491 return $request;
363             }
364              
365             my %TOKEN_REQUIRED = map { ($_ => 1) } @TOKEN_REQUIRED;
366             sub _token_required {
367 683     683   1498 my ( $self, $method, $path ) = @_;
368              
369 683         1634 my $key = "${method} ${path}";
370              
371 683 100       2070 return 1 if $TOKEN_REQUIRED{$key};
372              
373 653         1834 foreach my $regexp (@TOKEN_REQUIRED_REGEXP) {
374 34159 100       73047 return 1 if $key =~ /$regexp/;
375             }
376              
377 326         1173 return 0;
378             }
379              
380             sub _uri_for {
381 683     683   1500 my ( $self, $path ) = @_;
382              
383 683         15027 my $uri = $self->api_uri->clone;
384 683         11344 my $base_path = $uri->path;
385 683         10332 $path =~ s/^$base_path//;
386 683         1207 my @parts;
387 683         3769 push @parts, split qr{/+}, $uri->path;
388 683         11238 push @parts, split qr{/+}, $path;
389 683         1970 $uri->path( join '/', grep { $_ } @parts );
  3430         6799  
390              
391 683 50       24479 if ( $self->has_per_page ) {
392 683         2750 my %query = ( $uri->query_form, per_page => $self->per_page );
393 683         13290 $uri->query_form(%query);
394             }
395              
396 683 100       43729 if ( $self->has_jsonp_callback ) {
397 203         880 my %query = ( $uri->query_form, callback => $self->jsonp_callback );
398 203         10811 $uri->query_form(%query);
399             }
400              
401 683         14965 return $uri;
402             }
403              
404             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
405             sub _validate_user_repo_args {
406 471     471   1029 my ( $self, $args ) = @_;
407 471         1516 $args = $self->_get_user_repo_args($args);
408 471 100       1283 croak 'Missing key in parameters: user' unless $args->{user};
409 450 100       1542 croak 'Missing key in parameters: repo' unless $args->{repo};
410             }
411             ## use critic
412              
413             1;
414              
415             __END__