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              
4             # ABSTRACT: Github v3 base class for all Pithub modules
5              
6 24     24   14441 use Moo;
  24         57  
  24         121  
7              
8             our $VERSION = '0.01041';
9              
10 24     24   7588 use Carp qw( croak );
  24         67  
  24         1274  
11 24     24   7236 use HTTP::Headers ();
  24         89996  
  24         583  
12 24     24   11433 use HTTP::Request ();
  24         212071  
  24         653  
13 24     24   6418 use JSON::MaybeXS qw( JSON );
  24         76919  
  24         1264  
14 24     24   17973 use LWP::UserAgent ();
  24         654810  
  24         613  
15 24     24   12810 use Pithub::Result ();
  24         105  
  24         673  
16 24     24   203 use URI ();
  24         48  
  24         80036  
17              
18             with 'Pithub::Result::SharedCache';
19              
20              
21             has 'auto_pagination' => (
22             default => sub { 0 },
23             is => 'rw',
24             );
25              
26              
27             has 'api_uri' => (
28             default => sub { URI->new('https://api.github.com') },
29             is => 'rw',
30             trigger => sub {
31             my ( $self, $uri ) = @_;
32             $self->{api_uri} = URI->new("$uri");
33             },
34             );
35              
36              
37             has 'jsonp_callback' => (
38             clearer => 'clear_jsonp_callback',
39             is => 'rw',
40             predicate => 'has_jsonp_callback',
41             required => 0,
42             );
43              
44              
45             has 'per_page' => (
46             clearer => 'clear_per_page',
47             is => 'rw',
48             predicate => 'has_per_page',
49             default => 100,
50             required => 0,
51             );
52              
53              
54             has 'prepare_request' => (
55             clearer => 'clear_prepare_request',
56             is => 'rw',
57             predicate => 'has_prepare_request',
58             required => 0,
59             );
60              
61              
62             has 'repo' => (
63             clearer => 'clear_repo',
64             is => 'rw',
65             predicate => 'has_repo',
66             required => 0,
67             );
68              
69              
70             has 'token' => (
71             clearer => 'clear_token',
72             is => 'rw',
73             predicate => '_has_token',
74             required => 0,
75             );
76              
77              
78             has 'ua' => (
79             builder => '_build_ua',
80             is => 'ro',
81             lazy => 1,
82             );
83              
84              
85             has 'user' => (
86             clearer => 'clear_user',
87             is => 'rw',
88             predicate => 'has_user',
89             required => 0,
90             );
91              
92              
93             has 'utf8' => (
94             is => 'ro',
95             default => 1,
96             );
97              
98             has '_json' => (
99             builder => '_build__json',
100             is => 'ro',
101             lazy => 1,
102             );
103              
104             my @TOKEN_REQUIRED = (
105             'DELETE /user/emails',
106             'GET /user',
107             'GET /user/emails',
108             'GET /user/followers',
109             'GET /user/following',
110             'GET /user/keys',
111             'GET /user/repos',
112             'PATCH /user',
113             'POST /user/emails',
114             'POST /user/keys',
115             'POST /user/repos',
116             );
117              
118             my @TOKEN_REQUIRED_REGEXP = (
119             qr{^DELETE },
120             qr{^GET /gists/starred$},
121             qr{^GET /gists/[^/]+/star$},
122             qr{^GET /issues$},
123             qr{^GET /orgs/[^/]+/members/.*$},
124             qr{^GET /orgs/[^/]+/teams$},
125             qr{^GET /repos/[^/]+/[^/]+/collaborators$},
126             qr{^GET /repos/[^/]+/[^/]+/collaborators/.*$},
127             qr{^GET /repos/[^/]+/[^/]+/hooks$},
128             qr{^GET /repos/[^/]+/[^/]+/hooks/.*$},
129             qr{^GET /repos/[^/]+/[^/]+/keys$},
130             qr{^GET /repos/[^/]+/[^/]+/keys/.*$},
131             qr{^GET /teams/.*$},
132             qr{^GET /teams/[^/]+/members$},
133             qr{^GET /teams/[^/]+/members/.*$},
134             qr{^GET /teams/[^/]+/repos$},
135             qr{^GET /teams/[^/]+/repos/.*$},
136             qr{^GET /user/following/.*$},
137             qr{^GET /user/keys/.*$},
138             qr{^GET /user/orgs$},
139             qr{^GET /user/starred/[^/]+/.*$},
140             qr{^GET /user/watched$},
141             qr{^GET /user/watched/[^/]+/.*$},
142             qr{^GET /users/[^/]+/events/orgs/.*$},
143             qr{^PATCH /gists/.*$},
144             qr{^PATCH /gists/[^/]+/comments/.*$},
145             qr{^PATCH /orgs/.*$},
146             qr{^PATCH /repos/[^/]+/.*$},
147             qr{^PATCH /repos/[^/]+/[^/]+/comments/.*$},
148             qr{^PATCH /repos/[^/]+/[^/]+/git/refs/.*$},
149             qr{^PATCH /repos/[^/]+/[^/]+/hooks/.*$},
150             qr{^PATCH /repos/[^/]+/[^/]+/issues/.*$},
151             qr{^PATCH /repos/[^/]+/[^/]+/issues/comments/.*$},
152             qr{^PATCH /repos/[^/]+/[^/]+/keys/.*$},
153             qr{^PATCH /repos/[^/]+/[^/]+/labels/.*$},
154             qr{^PATCH /repos/[^/]+/[^/]+/milestones/.*$},
155             qr{^PATCH /repos/[^/]+/[^/]+/pulls/.*$},
156             qr{^PATCH /repos/[^/]+/[^/]+/releases/.*$},
157             qr{^PATCH /repos/[^/]+/[^/]+/pulls/comments/.*$},
158             qr{^PATCH /teams/.*$},
159             qr{^PATCH /user/keys/.*$},
160             qr{^PATCH /user/repos/.*$},
161             qr{^POST /repos/[^/]+/[^/]+/releases/[^/]+/assets.*$},
162             qr{^POST /gists/[^/]+/comments$},
163             qr{^POST /orgs/[^/]+/repos$},
164             qr{^POST /orgs/[^/]+/teams$},
165             qr{^POST /repos/[^/]+/[^/]+/commits/[^/]+/comments$},
166             qr{^POST /repos/[^/]+/[^/]+/downloads$},
167             qr{^POST /repos/[^/]+/[^/]+/forks},
168             qr{^POST /repos/[^/]+/[^/]+/git/blobs$},
169             qr{^POST /repos/[^/]+/[^/]+/git/commits$},
170             qr{^POST /repos/[^/]+/[^/]+/git/refs},
171             qr{^POST /repos/[^/]+/[^/]+/git/tags$},
172             qr{^POST /repos/[^/]+/[^/]+/git/trees$},
173             qr{^POST /repos/[^/]+/[^/]+/hooks$},
174             qr{^POST /repos/[^/]+/[^/]+/hooks/[^/]+/test$},
175             qr{^POST /repos/[^/]+/[^/]+/issues$},
176             qr{^POST /repos/[^/]+/[^/]+/issues/[^/]+/comments},
177             qr{^POST /repos/[^/]+/[^/]+/issues/[^/]+/labels$},
178             qr{^POST /repos/[^/]+/[^/]+/keys$},
179             qr{^POST /repos/[^/]+/[^/]+/labels$},
180             qr{^POST /repos/[^/]+/[^/]+/milestones$},
181             qr{^POST /repos/[^/]+/[^/]+/pulls$},
182             qr{^POST /repos/[^/]+/[^/]+/releases$},
183             qr{^POST /repos/[^/]+/[^/]+/pulls/[^/]+/comments$},
184             qr{^POST /repos/[^/]+/[^/]+/pulls/[^/]+/requested_reviewers$},
185             qr{^PUT /gists/[^/]+/star$},
186             qr{^PUT /orgs/[^/]+/public_members/.*$},
187             qr{^PUT /repos/[^/]+/[^/]+/collaborators/.*$},
188             qr{^PUT /repos/[^/]+/[^/]+/issues/[^/]+/labels$},
189             qr{^PUT /repos/[^/]+/[^/]+/pulls/[^/]+/merge$},
190             qr{^PUT /teams/[^/]+/members/.*$},
191             qr{^PUT /teams/[^/]+/memberships/.*$},
192             qr{^PUT /teams/[^/]+/repos/.*$},
193             qr{^PUT /user/following/.*$},
194             qr{^PUT /user/starred/[^/]+/.*$},
195             qr{^PUT /user/watched/[^/]+/.*$},
196             );
197              
198              
199             sub request {
200 686     686 1 15569 my ( $self, %args ) = @_;
201              
202             my $method = delete $args{method}
203 686   66     2018 || croak 'Missing mandatory key in parameters: method';
204             my $path = delete $args{path}
205 685   66     1738 || croak 'Missing mandatory key in parameters: path';
206 684         1179 my $data = delete $args{data};
207 684         1031 my $options = delete $args{options};
208 684         975 my $params = delete $args{params};
209              
210 684 100       2746 croak "Invalid method: $method"
211             unless grep $_ eq $method, qw(DELETE GET PATCH POST PUT);
212              
213 683         1793 my $uri = $self->_uri_for($path);
214              
215 683 100       1922 if ( my $host = delete $args{host} ) {
216 3         50 $uri->host($host);
217             }
218              
219 683 100       1822 if ( my $query = delete $args{query} ) {
220 6         23 $uri->query_form(%$query);
221             }
222              
223 683         2626 my $request = $self->_request_for( $method, $uri, $data );
224              
225 683 100       1783 if ( my $headers = delete $args{headers} ) {
226 3         17 foreach my $header ( keys %$headers ) {
227 3         12 $request->header( $header, $headers->{$header} );
228             }
229             }
230              
231 683 100 100     1750 if ( $self->_token_required( $method, $path )
232             && !$self->has_token($request) ) {
233 112         671 croak sprintf 'Access token required for: %s %s (%s)', $method,
234             $path, $uri;
235             }
236              
237 571 100       1314 if ($options) {
238 227 100       644 croak 'The key options must be a hashref'
239             unless ref $options eq 'HASH';
240             croak
241             'The key prepare_request in the options hashref must be a coderef'
242             if $options->{prepare_request}
243 226 100 100     991 && ref $options->{prepare_request} ne 'CODE';
244              
245 225 100       463 if ( $options->{prepare_request} ) {
246 224         576 $options->{prepare_request}->($request);
247             }
248             }
249              
250 569 100       13221 if ($params) {
251 9 100       78 croak 'The key params must be a hashref' unless ref $params eq 'HASH';
252 8         38 my %query = ( $request->uri->query_form, %$params );
253 8         488 $request->uri->query_form(%query);
254             }
255              
256 568         2140 my $response = $self->_make_request($request);
257              
258             return Pithub::Result->new(
259             auto_pagination => $self->auto_pagination,
260             response => $response,
261             utf8 => $self->utf8,
262 21     21   297 _request => sub { $self->request(@_) },
263 568         13554 );
264             }
265              
266             sub _make_request {
267 568     568   1034 my ( $self, $request ) = @_;
268              
269 568         1469 my $cache_key = $request->uri->as_string;
270 568 100       7748 if ( my $cached_response = $self->shared_cache->get($cache_key) ) {
271              
272             # Add the If-None-Match header from the cache's ETag
273             # and make the request
274 228         19748 $request->header(
275             'If-None-Match' => $cached_response->header('ETag') );
276 228         22859 my $response = $self->ua->request($request);
277              
278             # Got 304 Not Modified, cache is still valid
279 228 50 100     15295 return $cached_response if ( $response->code || 0 ) == 304;
280              
281             # The response changed, cache it and return it.
282 228         3204 $self->shared_cache->set( $cache_key, $response );
283 228         104516 return $response;
284             }
285              
286 340         28480 my $response = $self->ua->request($request);
287 340         23567 $self->shared_cache->set( $cache_key, $response );
288 340         197176 return $response;
289             }
290              
291              
292             sub has_token {
293 1264     1264 1 2537 my ( $self, $request ) = @_;
294              
295             # If we have one specified in the object, return true
296 1264 100       4652 return 1 if $self->_has_token;
297              
298             # If no request object here, we don't have a token
299 419 100       1168 return 0 unless $request;
300              
301 113 100       296 return 1 if $request->header('Authorization');
302 112         5229 return 0;
303             }
304              
305              
306             sub rate_limit {
307 0     0 1 0 return shift->request( method => 'GET', path => '/rate_limit' );
308             }
309              
310             sub _build__json {
311 96     96   1259 my ($self) = @_;
312 96         391 return JSON->new->utf8( $self->utf8 );
313             }
314              
315             sub _build_ua {
316 27     27   3018 my ($self) = @_;
317 27         104 return LWP::UserAgent->new;
318             }
319              
320             sub _get_user_repo_args {
321 476     476   802 my ( $self, $args ) = @_;
322 476 100       1745 $args->{user} = $self->user unless defined $args->{user};
323 476 100       1394 $args->{repo} = $self->repo unless defined $args->{repo};
324 476         950 return $args;
325             }
326              
327             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
328             sub _create_instance {
329 224     224   619 my ( $self, $class, @args ) = @_;
330              
331 224         4404 my %args = (
332             api_uri => $self->api_uri,
333             auto_pagination => $self->auto_pagination,
334             ua => $self->ua,
335             utf8 => $self->utf8,
336             @args,
337             );
338              
339 224         16111 for my $attr (qw(repo token user per_page jsonp_callback prepare_request))
340             {
341             # Allow overrides to set attributes to undef
342 1344 100       2555 next if exists $args{$attr};
343              
344 1341         2114 my $has_attr = "has_$attr";
345 1341 100       4423 $args{$attr} = $self->$attr if $self->$has_attr;
346             }
347              
348 224         5195 return $class->new(%args);
349             }
350             ## use critic
351              
352             sub _request_for {
353 683     683   1581 my ( $self, $method, $uri, $data ) = @_;
354              
355 683         2232 my $headers = HTTP::Headers->new;
356              
357 683 100       6005 if ( $self->has_token ) {
358 451         2374 $headers->header(
359             'Authorization' => sprintf( 'token %s', $self->token ) );
360             }
361              
362 683         22660 my $request = HTTP::Request->new( $method, $uri, $headers );
363              
364 683 100       44326 if ($data) {
365 304 100       7764 $data = $self->_json->encode($data) if ref $data;
366 304         5280 $request->content($data);
367             }
368              
369 683         7444 $request->header( 'Content-Length' => length $request->content );
370              
371 683 100       46010 if ( $self->has_prepare_request ) {
372 204         754 $self->prepare_request->($request);
373             }
374              
375 683         2350 return $request;
376             }
377              
378             my %TOKEN_REQUIRED = map { ( $_ => 1 ) } @TOKEN_REQUIRED;
379              
380             sub _token_required {
381 683     683   1450 my ( $self, $method, $path ) = @_;
382              
383 683         1766 my $key = "${method} ${path}";
384              
385 683 100       1813 return 1 if $TOKEN_REQUIRED{$key};
386              
387 653         1321 foreach my $regexp (@TOKEN_REQUIRED_REGEXP) {
388 34159 100       79255 return 1 if $key =~ /$regexp/;
389             }
390              
391 326         1031 return 0;
392             }
393              
394             sub _uri_for {
395 683     683   1287 my ( $self, $path ) = @_;
396              
397 683         16118 my $uri = $self->api_uri->clone;
398 683         10511 my $base_path = $uri->path;
399 683         10248 $path =~ s/^$base_path//;
400 683         1251 my @parts;
401 683         3273 push @parts, split qr{/+}, $uri->path;
402 683         11792 push @parts, split qr{/+}, $path;
403 683         2034 $uri->path( join '/', grep { $_ } @parts );
  3430         7831  
404              
405 683 50       25749 if ( $self->has_per_page ) {
406 683         2036 my %query = ( $uri->query_form, per_page => $self->per_page );
407 683         13288 $uri->query_form(%query);
408             }
409              
410 683 100       46711 if ( $self->has_jsonp_callback ) {
411 203         522 my %query = ( $uri->query_form, callback => $self->jsonp_callback );
412 203         9247 $uri->query_form(%query);
413             }
414              
415 683         17972 return $uri;
416             }
417              
418             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
419             sub _validate_user_repo_args {
420 471     471   950 my ( $self, $args ) = @_;
421 471         1291 $args = $self->_get_user_repo_args($args);
422 471 100       1270 croak 'Missing key in parameters: user' unless $args->{user};
423 450 100       1371 croak 'Missing key in parameters: repo' unless $args->{repo};
424             }
425             ## use critic
426              
427             1;
428              
429             __END__