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.26';
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. This may
13             change, and the interface may change, so documentation is lacking in order
14             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   610 use Carp qw();
  1         3  
  1         28  
41 1     1   500 use HTTP::Tiny::Multipart;
  1         45607  
  1         39  
42 1     1   7 use HTTP::Tiny;
  1         2  
  1         16  
43 1     1   5 use JSON;
  1         2  
  1         21  
44 1     1   628 use Log::Any qw( $log );
  1         8382  
  1         5  
45 1     1   3048 use Path::Tiny;
  1         11093  
  1         54  
46 1     1   7 use Try::Tiny;
  1         3  
  1         58  
47 1     1   484 use Types::Common::Numeric -types;
  1         95259  
  1         8  
48 1     1   1882 use Types::Common::String -types;
  1         23200  
  1         13  
49 1     1   1521 use Types::Standard -types;
  1         2  
  1         6  
50 1     1   4653 use URI::Escape;
  1         9  
  1         61  
51 1     1   6 use URI;
  1         2  
  1         19  
52              
53 1     1   5 use Moo;
  1         2  
  1         7  
54 1     1   487 use strictures 2;
  1         9  
  1         42  
55 1     1   219 use namespace::clean;
  1         2  
  1         9  
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   23 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         8 $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();
106             }
107              
108             has json => (
109             is => 'lazy',
110             isa => HasMethods[ 'encode', 'decode' ],
111             );
112             sub _build_json {
113 2     2   105 return JSON->new->utf8->allow_nonref();
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 3107 my ($self, $verb, $raw_path, $path_vars, $options) = @_;
139              
140 11         177 $self->_clear_request();
141 11         224 $self->_clear_response();
142              
143 11 50       58 $options = { %{ $options || {} } };
  11         49  
144 11         27 my $query = delete $options->{query};
145 11         32 my $content = delete $options->{content};
146 11 50       18 my $headers = $options->{headers} = { %{ $options->{headers} || {} } };
  11         36  
147              
148             # Convert foo/:bar/baz into foo/%s/baz.
149 11         20 my $path = $raw_path;
150 11         26 $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       29 $path = sprintf($path, (map { uri_escape($_) } @$path_vars)) if @$path_vars;
  2         7  
155              
156 11         106 $log->tracef( 'Making %s request against %s', $verb, $path );
157              
158 11         216 my $url = $self->_clean_base_url->clone();
159 11         9609 $url->path( $url->path() . '/' . $path );
160 11 50       459 $url->query_form( $query ) if defined $query;
161 11         38 $url = "$url"; # No more changes to the url from this point forward.
162              
163 11         71 my $req_method = 'request';
164 11         27 my $req = [ $verb, $url, $options ];
165              
166 11 50 66     45 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       27 if (ref $content) {
196 4         87 $content = $self->json->encode( $content );
197 4         52 $headers->{'content-type'} = 'application/json';
198 4         9 $headers->{'content-length'} = length( $content );
199             }
200              
201 11 100       24 $options->{content} = $content if defined $content;
202              
203 11         35 $self->_set_request( $req );
204              
205 11         18 my $res;
206 11         26 my $tries_left = $self->retries();
207 11         15 do {
208 11         33 $res = $self->_http_tiny_request( $req_method, $req );
209 11 50       31 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     31 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     28 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       21 if ($res->{success}) {
231 11 100       37 return undef if $res->{status} eq '204';
232              
233 6         9 my $decode = $options->{decode};
234 6 50       14 $decode = 1 if !defined $decode;
235 6 50       11 return $res->{content} if !$decode;
236              
237             return try{
238 6     6   458 $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         49 };
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__