File Coverage

blib/lib/GitLab/API/v4/RESTClient.pm
Criterion Covered Total %
statement 93 125 74.4
branch 17 36 47.2
condition 7 22 31.8
subroutine 19 24 79.1
pod 0 3 0.0
total 136 210 64.7


line stmt bran cond sub pod time code
1             package GitLab::API::v4::RESTClient;
2             our $VERSION = '0.27';
3              
4             =encoding utf8
5              
6             =head1 NAME
7              
8             GitLab::API::v4::RESTClient - The HTTP client that does the heavy lifting.
9              
10             =head1 DESCRIPTION
11              
12             Currently this class uses L and L to do its job.
13             This may change, and the interface may change, so documentation is lacking in
14             order to not mislead people.
15              
16             If you do want to customize how this class works then take a look at the
17             source.
18              
19             =head1 ATTRIBUTES
20              
21             =head1 http_tiny_request
22              
23             my $req = $api->rest_client->http_tiny_request();
24              
25             The most recent request arrayref as passed to L.
26              
27             If this is C then no request has been made.
28              
29             =head1 http_tiny_response
30              
31             my $res = $api->rest_client->http_tiny_response();
32              
33             The most recent response hashref as passed back from L.
34              
35             If this is C and L is defined then no response was received
36             and you will have encountered an error when making the request
37              
38             =cut
39              
40 1     1   517 use Carp qw();
  1         2  
  1         22  
41 1     1   376 use HTTP::Tiny::Multipart;
  1         38283  
  1         34  
42 1     1   7 use HTTP::Tiny;
  1         2  
  1         15  
43 1     1   5 use JSON::MaybeXS;
  1         2  
  1         51  
44 1     1   399 use Log::Any qw( $log );
  1         6792  
  1         5  
45 1     1   2435 use Path::Tiny;
  1         10259  
  1         47  
46 1     1   6 use Try::Tiny;
  1         2  
  1         49  
47 1     1   388 use Types::Common::Numeric -types;
  1         94762  
  1         9  
48 1     1   1589 use Types::Common::String -types;
  1         21344  
  1         25  
49 1     1   1334 use Types::Standard -types;
  1         2  
  1         6  
50 1     1   3750 use URI::Escape;
  1         8  
  1         58  
51 1     1   5 use URI;
  1         2  
  1         15  
52              
53 1     1   5 use Moo;
  1         2  
  1         6  
54 1     1   419 use strictures 2;
  1         7  
  1         35  
55 1     1   188 use namespace::clean;
  1         2  
  1         8  
56              
57             sub croak {
58 0     0 0 0 local $Carp::Internal{ 'GitLab::API::v4' } = 1;
59 0         0 local $Carp::Internal{ 'GitLab::API::v4::RESTClient' } = 1;
60              
61 0         0 return Carp::croak( @_ );
62             }
63              
64             sub croakf {
65 0     0 0 0 my $msg = shift;
66 0         0 $msg = sprintf( $msg, @_ );
67 0         0 return croak( $msg );
68             }
69              
70             has _clean_base_url => (
71             is => 'lazy',
72             init_arg => undef,
73             builder => '_build_clean_base_url',
74             );
75             sub _build_clean_base_url {
76 2     2   19 my ($self) = @_;
77 2         9 my $url = $self->base_url();
78              
79             # Remove any leading slash so that request() does not build URLs
80             # with double slashes when joining the base_url with the path.
81             # If double slashes were allowed then extra unecessary redirects
82             # could happen.
83 2         7 $url =~ s{/+$}{};
84              
85 2         10 return URI->new( $url )->canonical();
86             }
87              
88             has base_url => (
89             is => 'ro',
90             isa => NonEmptySimpleStr,
91             required => 1,
92             );
93              
94             has retries => (
95             is => 'ro',
96             isa => PositiveOrZeroInt,
97             default => 0,
98             );
99              
100             has http_tiny => (
101             is => 'lazy',
102             isa => InstanceOf[ 'HTTP::Tiny' ],
103             );
104             sub _build_http_tiny {
105 0     0   0 return HTTP::Tiny->new( verify_SSL => 1 );
106             }
107              
108             has json => (
109             is => 'lazy',
110             isa => HasMethods[ 'encode', 'decode' ],
111             );
112             sub _build_json {
113 2     2   33 return JSON::MaybeXS->new(utf8 => 1, allow_nonref => 1);
114             }
115              
116             has http_tiny_request => (
117             is => 'ro',
118             writer => '_set_request',
119             clearer => '_clear_request',
120             init_arg => undef,
121             );
122              
123             has http_tiny_response => (
124             is => 'ro',
125             writer => '_set_response',
126             clearer => '_clear_response',
127             init_arg => undef,
128             );
129              
130             # The purpose of this method is for tests to have a place to inject themselves.
131             sub _http_tiny_request {
132 0     0   0 my ($self, $req_method, $req) = @_;
133              
134 0         0 return $self->http_tiny->$req_method( @$req );
135             }
136              
137             sub request {
138 11     11 0 2821 my ($self, $verb, $raw_path, $path_vars, $options) = @_;
139              
140 11         150 $self->_clear_request();
141 11         182 $self->_clear_response();
142              
143 11 50       45 $options = { %{ $options || {} } };
  11         42  
144 11         22 my $query = delete $options->{query};
145 11         18 my $content = delete $options->{content};
146 11 50       12 my $headers = $options->{headers} = { %{ $options->{headers} || {} } };
  11         35  
147              
148             # Convert foo/:bar/baz into foo/%s/baz.
149 11         16 my $path = $raw_path;
150 11         25 $path =~ s{:[^/]+}{%s}g;
151             # sprintf will throw if the number of %s doesn't match the size of @$path_vars.
152             # Might be nice to catch that and provide a better error message, but that should
153             # never happen as the API methods verify the argument size before we get here.
154 11 100       22 $path = sprintf($path, (map { uri_escape($_) } @$path_vars)) if @$path_vars;
  2         7  
155              
156 11         90 $log->tracef( 'Making %s request against %s', $verb, $path );
157              
158 11         188 my $url = $self->_clean_base_url->clone();
159 11         10473 $url->path( $url->path() . '/' . $path );
160 11 50       369 $url->query_form( $query ) if defined $query;
161 11         33 $url = "$url"; # No more changes to the url from this point forward.
162              
163 11         60 my $req_method = 'request';
164 11         23 my $req = [ $verb, $url, $options ];
165              
166 11 50 66     51 if ($verb eq 'POST' and ref($content) eq 'HASH' and $content->{file}) {
      66        
167 0         0 $content = { %$content };
168 0         0 my $file = path( delete $content->{file} );
169              
170 0 0 0     0 unless (-f $file and -r $file) {
171 0         0 local $Carp::Internal{ 'GitLab::API::v4' } = 1;
172 0         0 local $Carp::Internal{ 'GitLab::API::v4::RESTClient' } = 1;
173 0         0 croak "File $file is not readable";
174             }
175              
176             # Might as well mask the filename, but leave the extension.
177 0         0 my $filename = $file->basename(); # foo/bar.txt => bar.txt
178              
179 0         0 my $data = {
180             file => {
181             filename => $filename,
182             content => $file->slurp(),
183             },
184             };
185              
186 0         0 $req->[0] = $req->[1]; # Replace method with url.
187 0         0 $req->[1] = $data; # Put data where url was.
188             # So, req went from [$verb,$url,$options] to [$url,$data,$options],
189             # per the post_multipart interface.
190              
191 0         0 $req_method = 'post_multipart';
192 0 0       0 $content = undef if ! %$content;
193             }
194              
195 11 100       21 if (ref $content) {
196 4         62 $content = $self->json->encode( $content );
197 4         38 $headers->{'content-type'} = 'application/json';
198 4         7 $headers->{'content-length'} = length( $content );
199             }
200              
201 11 100       21 $options->{content} = $content if defined $content;
202              
203 11         38 $self->_set_request( $req );
204              
205 11         12 my $res;
206 11         23 my $tries_left = $self->retries();
207 11         12 do {
208 11         28 $res = $self->_http_tiny_request( $req_method, $req );
209 11 50       28 if ($res->{status} =~ m{^5}) {
210 0         0 $tries_left--;
211 0 0       0 $log->warn('Request failed; retrying...') if $tries_left > 0;
212             }
213             else {
214 11         36 $tries_left = 0
215             }
216             } while $tries_left > 0;
217              
218 11         29 $self->_set_response( $res );
219              
220 11 50 33     29 if ($res->{status} eq '404' and $verb eq 'GET') {
221 0         0 return undef;
222             }
223              
224             # Special case for:
225             # https://github.com/bluefeet/GitLab-API-v4/issues/35#issuecomment-515533017
226 11 0 33     23 if ($res->{status} eq '403' and $verb eq 'GET' and $raw_path eq 'projects/:project_id/releases/:tag_name') {
      33        
227 0         0 return undef;
228             }
229              
230 11 50       20 if ($res->{success}) {
231 11 100       29 return undef if $res->{status} eq '204';
232              
233 6         8 my $decode = $options->{decode};
234 6 50       14 $decode = 1 if !defined $decode;
235 6 50       10 return $res->{content} if !$decode;
236              
237             return try{
238 6     6   365 $self->json->decode( $res->{content} );
239             }
240             catch {
241             croakf(
242             'Error decoding JSON (%s %s %s): ',
243 0     0     $verb, $url, $res->{status}, $_,
244             );
245 6         40 };
246             }
247              
248 0   0       my $glimpse = $res->{content} || '';
249 0           $glimpse =~ s{\s+}{ }g;
250 0 0         if ( length($glimpse) > 50 ) {
251 0           $glimpse = substr( $glimpse, 0, 50 );
252 0           $glimpse .= '...';
253             }
254              
255             croakf(
256             'Error %sing %s (HTTP %s): %s %s',
257             $verb, $url,
258 0   0       $res->{status}, ($res->{reason} || 'Unknown'),
259             $glimpse,
260             );
261             }
262              
263             1;
264             __END__