File Coverage

blib/lib/Pithub/Base.pm
Criterion Covered Total %
statement 124 125 99.2
branch 58 60 96.6
condition 12 14 85.7
subroutine 20 21 95.2
pod 3 3 100.0
total 217 223 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   14303 use Moo;
  24         86  
  24         152  
7              
8             our $VERSION = '0.01043';
9              
10 24     24   10130 use Carp qw( croak );
  24         75  
  24         1578  
11 24     24   7506 use HTTP::Headers ();
  24         77851  
  24         691  
12 24     24   12244 use HTTP::Request ();
  24         277266  
  24         872  
13 24     24   6689 use JSON::MaybeXS qw( JSON );
  24         153294  
  24         1971  
14 24     24   18247 use LWP::UserAgent ();
  24         822330  
  24         850  
15 24     24   15125 use Pithub::Result ();
  24         138  
  24         904  
16 24     24   290 use URI ();
  24         75  
  24         90748  
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 18865 my ( $self, %args ) = @_;
201              
202             my $method = delete $args{method}
203 686   66     2556 || croak 'Missing mandatory key in parameters: method';
204             my $path = delete $args{path}
205 685   66     2165 || croak 'Missing mandatory key in parameters: path';
206 684         1385 my $data = delete $args{data};
207 684         1295 my $options = delete $args{options};
208 684         1292 my $params = delete $args{params};
209              
210 684 100       3212 croak "Invalid method: $method"
211             unless grep $_ eq $method, qw(DELETE GET PATCH POST PUT);
212              
213 683         2472 my $uri = $self->_uri_for($path);
214              
215 683 100       2298 if ( my $host = delete $args{host} ) {
216 3         35 $uri->host($host);
217             }
218              
219 683 100       3627 if ( my $query = delete $args{query} ) {
220 6         21 my %orig_query = $uri->query_form;
221 6         345 $uri->query_form(%orig_query, %$query);
222             }
223              
224 683         2752 my $request = $self->_request_for( $method, $uri, $data );
225              
226 683 100       1884 if ( my $headers = delete $args{headers} ) {
227 3         15 foreach my $header ( keys %$headers ) {
228 3         13 $request->header( $header, $headers->{$header} );
229             }
230             }
231              
232 683 100 100     2270 if ( $self->_token_required( $method, $path )
233             && !$self->has_token($request) ) {
234 112         875 croak sprintf 'Access token required for: %s %s (%s)', $method,
235             $path, $uri;
236             }
237              
238 571 100       1487 if ($options) {
239 227 100       681 croak 'The key options must be a hashref'
240             unless ref $options eq 'HASH';
241             croak
242             'The key prepare_request in the options hashref must be a coderef'
243             if $options->{prepare_request}
244 226 100 100     1170 && ref $options->{prepare_request} ne 'CODE';
245              
246 225 100       527 if ( $options->{prepare_request} ) {
247 224         749 $options->{prepare_request}->($request);
248             }
249             }
250              
251 569 100       14242 if ($params) {
252 19 100       93 croak 'The key params must be a hashref' unless ref $params eq 'HASH';
253 18         82 my %query = ( $request->uri->query_form, %$params );
254 18         1195 $request->uri->query_form(%query);
255             }
256              
257 568         3731 my $response = $self->_make_request($request);
258              
259             return Pithub::Result->new(
260             auto_pagination => $self->auto_pagination,
261             response => $response,
262             utf8 => $self->utf8,
263 21     21   450 _request => sub { $self->request(@_) },
264 568         16761 );
265             }
266              
267             sub _make_request {
268 568     568   1181 my ( $self, $request ) = @_;
269              
270 568         1891 my $cache_key = $request->uri->as_string;
271 568 100       9069 if ( my $cached_response = $self->shared_cache->get($cache_key) ) {
272              
273             # Add the If-None-Match header from the cache's ETag
274             # and make the request
275 231         26025 $request->header(
276             'If-None-Match' => $cached_response->header('ETag') );
277 231         24324 my $response = $self->ua->request($request);
278              
279             # Got 304 Not Modified, cache is still valid
280 231 50 100     18115 return $cached_response if ( $response->code || 0 ) == 304;
281              
282             # The response changed, cache it and return it.
283 231         3646 $self->shared_cache->set( $cache_key, $response );
284 231         111213 return $response;
285             }
286              
287 337         36385 my $response = $self->ua->request($request);
288 337         29557 $self->shared_cache->set( $cache_key, $response );
289 337         205367 return $response;
290             }
291              
292              
293             sub has_token {
294 1264     1264 1 2670 my ( $self, $request ) = @_;
295              
296             # If we have one specified in the object, return true
297 1264 100       5684 return 1 if $self->_has_token;
298              
299             # If no request object here, we don't have a token
300 419 100       1519 return 0 unless $request;
301              
302 113 100       399 return 1 if $request->header('Authorization');
303 112         5952 return 0;
304             }
305              
306              
307             sub rate_limit {
308 0     0 1 0 return shift->request( method => 'GET', path => '/rate_limit' );
309             }
310              
311             sub _build__json {
312 96     96   1377 my ($self) = @_;
313 96         626 return JSON->new->utf8( $self->utf8 );
314             }
315              
316             sub _build_ua {
317 27     27   1091 my ($self) = @_;
318 27         159 return LWP::UserAgent->new;
319             }
320              
321             sub _get_user_repo_args {
322 476     476   956 my ( $self, $args ) = @_;
323 476 100       2381 $args->{user} = $self->user unless defined $args->{user};
324 476 100       1846 $args->{repo} = $self->repo unless defined $args->{repo};
325 476         975 return $args;
326             }
327              
328             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
329             sub _create_instance {
330 224     224   669 my ( $self, $class, @args ) = @_;
331              
332 224         5083 my %args = (
333             api_uri => $self->api_uri,
334             auto_pagination => $self->auto_pagination,
335             ua => $self->ua,
336             utf8 => $self->utf8,
337             @args,
338             );
339              
340 224         15847 for my $attr (qw(repo token user per_page jsonp_callback prepare_request))
341             {
342             # Allow overrides to set attributes to undef
343 1344 100       2740 next if exists $args{$attr};
344              
345 1341         1818 my $has_attr = "has_$attr";
346 1341 100       5230 $args{$attr} = $self->$attr if $self->$has_attr;
347             }
348              
349 224         5843 return $class->new(%args);
350             }
351             ## use critic
352              
353             sub _request_for {
354 683     683   1812 my ( $self, $method, $uri, $data ) = @_;
355              
356 683         3303 my $headers = HTTP::Headers->new;
357              
358 683 100       6720 if ( $self->has_token ) {
359 451         2686 $headers->header(
360             'Authorization' => sprintf( 'token %s', $self->token ) );
361             }
362              
363 683         25268 my $request = HTTP::Request->new( $method, $uri, $headers );
364              
365 683 100       51313 if ($data) {
366 304 100       9828 $data = $self->_json->encode($data) if ref $data;
367 304         5807 $request->content($data);
368             }
369              
370 683         8602 $request->header( 'Content-Length' => length $request->content );
371              
372 683 100       50281 if ( $self->has_prepare_request ) {
373 204         1055 $self->prepare_request->($request);
374             }
375              
376 683         2773 return $request;
377             }
378              
379             my %TOKEN_REQUIRED = map { ( $_ => 1 ) } @TOKEN_REQUIRED;
380              
381             sub _token_required {
382 683     683   1596 my ( $self, $method, $path ) = @_;
383              
384 683         1578 my $key = "${method} ${path}";
385              
386 683 100       1881 return 1 if $TOKEN_REQUIRED{$key};
387              
388 653         1616 foreach my $regexp (@TOKEN_REQUIRED_REGEXP) {
389 34159 100       97786 return 1 if $key =~ /$regexp/;
390             }
391              
392 326         1356 return 0;
393             }
394              
395             sub _uri_for {
396 683     683   1665 my ( $self, $path ) = @_;
397              
398 683         22597 my $uri = $self->api_uri->clone;
399 683         13197 my $base_path = $uri->path;
400 683         12883 $path =~ s/^$base_path//;
401 683         1411 my @parts;
402 683         4323 push @parts, split qr{/+}, $uri->path;
403 683         13610 push @parts, split qr{/+}, $path;
404 683         2209 $uri->path( join '/', grep { $_ } @parts );
  3430         8127  
405              
406 683 50       30270 if ( $self->has_per_page ) {
407 683         2991 my %query = ( $uri->query_form, per_page => $self->per_page );
408 683         15269 $uri->query_form(%query);
409             }
410              
411 683 100       56769 if ( $self->has_jsonp_callback ) {
412 203         576 my %query = ( $uri->query_form, callback => $self->jsonp_callback );
413 203         9778 $uri->query_form(%query);
414             }
415              
416 683         17842 return $uri;
417             }
418              
419             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
420             sub _validate_user_repo_args {
421 471     471   1198 my ( $self, $args ) = @_;
422 471         2006 $args = $self->_get_user_repo_args($args);
423 471 100       1619 croak 'Missing key in parameters: user' unless $args->{user};
424 450 100       1794 croak 'Missing key in parameters: repo' unless $args->{repo};
425             }
426             ## use critic
427              
428             1;
429              
430             __END__