File Coverage

blib/lib/HTTP/API/Client.pm
Criterion Covered Total %
statement 33 188 17.5
branch 0 62 0.0
condition 0 39 0.0
subroutine 11 43 25.5
pod 0 8 0.0
total 44 340 12.9


line stmt bran cond sub pod time code
1             package HTTP::API::Client;
2             $HTTP::API::Client::VERSION = '0.06';
3 4     4   139092 use strict;
  4         6  
  4         98  
4 4     4   17 use warnings;
  4         8  
  4         108  
5              
6             =head1 NAME
7              
8             HTTP::API::Client - API Client
9              
10             =head1 USAGE
11              
12             use HTTP::API::Client;
13              
14             my $ua1 = HTTP::API::Client->new;
15             my $ua2 = HTTP::API::Client->new(base_url => URI->new( $url ), pre_deinfed_headers => { X_COMPANY => 'ABC LTD' } );
16             my $ua3 = HTTP::API::Client->new(base_url => URI->new( $url ), pre_defined_data => { api_key => 123 } );
17              
18             $ua->send( $method, $url, \%data, \%header );
19              
20             Send short hand methods - get, post, head, put and delete
21              
22             Example:
23              
24             $ua->get( $url ) same as $ua->send( GET, $url );
25             $ua->post( $url, \%data, \%headers ) same as $ua->send( GET, $url, \%data, \%headers );
26              
27             Get Json Data - grab the content body from the response and json decode
28              
29             $ua = HTTP::API::Client->new(base_url => URI->new("http://google.com"));
30             $ua->get("/search" => { q => "something" });
31             my $hashref_from_decoded_json_string = $ua->json_response;
32             ## ps. this is just an example to get json from a rest api
33              
34             Send a query string to server
35              
36             $ua = HTTP::API::Client->new( content_type => "application/x-www-form-urlencoded" );
37             $ua->post("http://google.com", { q => "something" });
38             my $response = $ua->last_response; ## is a HTTP::Response object
39              
40             At the moment, only support query string and json data in and out
41              
42             =head1 ENVIRONMENT VARIABLES
43              
44             These enviornment variables expose the controls without changing the existing code.
45              
46             HTTP VARIABLES
47              
48             HTTP_USERNAME - basic auth username
49             HTTP_PASSWORD - basic auth password
50             HTTP_AUTH_TOKEN - basic auth token string
51             HTTP_CHARSET - content type charset. default utf8
52             HTTP_TIMEOUT - timeout the request for ??? seconds. default 60 seconds.
53             SSL_VERIFY - verify ssl url. default is off
54              
55             DEBUG VARIABLES
56              
57             DEBUG_IN_OUT - print out request and response in string to STDERR
58             DEBUG_SEND_OUT - print out request in string to STDERR
59             DEBUG_RESPONSE - print out response in string to STDERR
60             DEBUG_RESPONSE_HEADER_ONLY - print out response header only without the body
61             DEBUG_RESPONSE_IF_FAIL - only print out response in string if fail.
62              
63             RETRY VARIABLES
64              
65             RETRY_FAIL_RESPONSE - number of time to retry if resposne comes back is failed. default 0 retry
66             RETRY_FAIL_STATUS - only retry if specified status code. e.g. 500,404
67             RETRY_DELAY - retry with wait time of ??? seconds in between
68              
69             =cut
70              
71 4     4   1227 use Encode;
  4         28160  
  4         230  
72 4     4   1234 use HTTP::Headers;
  4         21414  
  4         110  
73 4     4   985 use HTTP::Request;
  4         40497  
  4         99  
74 4     4   1606 use JSON::XS;
  4         12007  
  4         198  
75 4     4   1480 use LWP::UserAgent;
  4         68716  
  4         115  
76 4     4   1091 use Mouse;
  4         69524  
  4         17  
77 4     4   1325 use Try::Tiny;
  4         9  
  4         217  
78 4     4   29 use URI;
  4         6  
  4         2519  
79              
80             has username => (
81             is => "rw",
82             isa => "Str",
83             lazy_build => 1,
84             );
85              
86 0 0   0     sub _build_username { $ENV{HTTP_USERNAME} || qq{} }
87              
88             has password => (
89             is => "rw",
90             isa => "Str",
91             lazy_build => 1,
92             );
93              
94 0 0   0     sub _build_password { $ENV{HTTP_PASSWORD} || qq{} }
95              
96             has auth_token => (
97             is => "rw",
98             isa => "Str",
99             lazy_build => 1,
100             );
101              
102 0 0   0     sub _build_auth_token { $ENV{HTTP_AUTH_TOKEN} || qq{} }
103              
104             has base_url => (
105             is => "rw",
106             isa => "URI",
107             );
108              
109             has last_response => (
110             is => "rw",
111             isa => "HTTP::Response",
112             );
113              
114             has charset => (
115             is => "rw",
116             isa => "Str",
117             lazy_build => 1,
118             );
119              
120 0 0   0     sub _build_charset { $ENV{HTTP_CHARSET} || "utf8" }
121              
122             has browser_id => (
123             is => "rw",
124             isa => "Str",
125             lazy_build => 1,
126             );
127              
128             sub _build_browser_id {
129 0     0     my $self = shift;
130 0   0       my $ver = $HTTP::API::Client::VERSION || -1;
131 0           return "HTTP API Client v$ver";
132             }
133              
134             has content_type => (
135             is => "rw",
136             isa => "Str",
137             lazy_build => 1,
138             );
139              
140             sub _build_content_type {
141 0     0     my $self = shift;
142 0           my $charset = $self->charset;
143 0           return "application/json; charset=$charset";
144             }
145              
146             has ua => (
147             is => "rw",
148             isa => "LWP::UserAgent",
149             lazy_build => 1,
150             );
151              
152             sub _build_ua {
153 0     0     my $self = shift;
154 0           my $ssl_verify = $self->ssl_verify;
155 0           my $ua =
156             LWP::UserAgent->new( ssl_opts => { verify_hostname => $ssl_verify } );
157 0           $ua->agent( $self->browser_id );
158 0           $ua->timeout( $self->timeout );
159 0           return $ua;
160             }
161              
162             has ssl_verify => (
163             is => "rw",
164             isa => "Bool",
165             lazy_build => 1,
166             );
167              
168             sub _build_ssl_verify {
169 0     0     return _smart_or( $ENV{SSL_VERIFY}, 0 );
170             }
171              
172             has retry => (
173             is => "rw",
174             isa => "HashRef",
175             lazy_build => 1,
176             );
177              
178             sub _build_retry {
179 0     0     my $self = shift;
180 0 0         my %retry = %{ $self->retry_config || {} };
  0            
181 0           my $count = $retry{fail_response};
182 0           my %status = map { $_ => 1 } split /,/, $retry{fail_status};
  0            
183              
184 0           my $delay = $retry{delay};
185              
186             return {
187 0           count => $count,
188             status => \%status,
189             delay => $delay,
190             };
191             }
192              
193             has retry_config => (
194             is => "rw",
195             isa => "HashRef",
196             lazy_build => 1,
197             );
198              
199             sub _build_retry_config {
200             return {
201             fail_response => _smart_or( $ENV{RETRY_FAIL_RESPONSE}, 0 ),
202             fail_status => $ENV{RETRY_FAIL_STATUS} || q{},
203 0   0 0     delay => _smart_or( $ENV{RETRY_DELAY}, 5 ),
204             };
205             }
206              
207             has timeout => (
208             is => "rw",
209             isa => "Int",
210             lazy_build => 1,
211             );
212              
213 0   0 0     sub _build_timeout { return $ENV{HTTP_TIMEOUT} || 60 }
214              
215             has json => (
216             is => "rw",
217             isa => "JSON::XS",
218             lazy_build => 1,
219             );
220              
221             sub _build_json {
222 0     0     my $self = shift;
223 0           my $json = JSON::XS->new->canonical(1);
224 0           my $charset = $self->charset;
225 0           eval { $json->$charset };
  0            
226 0           return $json;
227             }
228              
229             has debug_flags => (
230             is => "rw",
231             isa => "HashRef",
232             lazy_build => 1,
233             );
234              
235             sub _build_debug_flags {
236             return {
237             in_out => $ENV{DEBUG_IN_OUT},
238             send_out => $ENV{DEBUG_SEND_OUT},
239             response => $ENV{DEBUG_RESPONSE},
240             response_header_only => $ENV{DEBUG_RESPONSE_HEADER_ONLY},
241             response_if_fail => $ENV{DEBUG_RESPONSE_IF_FAIL},
242 0     0     };
243             }
244              
245             has pre_defined_data => (
246             is => "rw",
247             isa => "HashRef",
248             );
249              
250             has pre_defined_headers => (
251             is => "rw",
252             isa => "HashRef",
253             );
254              
255 4     4   27 no Mouse;
  4         7  
  4         21  
256              
257             sub get {
258 0     0 0   my $self = shift;
259 0           return $self->send( GET => @_ );
260             }
261              
262             sub post {
263 0     0 0   my $self = shift;
264 0           return $self->send( POST => @_ );
265             }
266              
267             sub put {
268 0     0 0   my $self = shift;
269 0           return $self->send( PUT => @_ );
270             }
271              
272             sub head {
273 0     0 0   my $self = shift;
274 0           return $self->send( HEAD => @_ );
275             }
276              
277             sub delete {
278 0     0 0   my $self = shift;
279 0           return $self->send( DELETE => @_ );
280             }
281              
282             sub send {
283 0     0 0   my $self = shift;
284 0   0       my $method = shift || "GET";
285 0           my $path = shift;
286 0   0       my $data = shift || {};
287 0   0       my $headers = shift || {};
288 0           my $ua = $self->ua;
289 0           my $base_url = $self->base_url;
290 0 0         my $url = $base_url ? $base_url . $path : $path;
291 0           my $req = $self->_request( $method, $url, $data, $headers );
292 0           my $retry_count = _smart_or( $self->retry->{count}, 1 );
293 0 0         my %retry_status = %{ $self->retry->{status} || {} };
  0            
294 0           my $retry_delay = _smart_or( $self->retry->{delay}, 5 );
295 0 0         my %debug = %{ $self->debug_flags || {} };
  0            
296              
297 0 0         if ( my $pd = $self->pre_defined_data ) {
298 0           %$data = ( %$pd, %$data );
299             }
300 0 0         if ( my $ph = $self->pre_defined_headers ) {
301 0           %$headers = ( %$ph, %$headers );
302             }
303              
304 0           my $response;
305              
306             RETRY:
307 0           foreach my $retry ( 0 .. $retry_count ) {
308 0           my $started_time = time;
309              
310 0           $response = $ua->request($req);
311              
312 0 0 0       if ( $debug{in_out} || $debug{send_out} ) {
313 0           print STDERR "-- REQUEST --\n";
314 0 0 0       if ( $retry_count && $retry ) {
315 0           print STDERR "-- RETRY $retry of $retry_count\n";
316             }
317 0           print STDERR $response->request->as_string;
318 0           print STDERR "\n";
319             }
320              
321 0   0       my $debug_response = $debug{in_out} || $debug{response};
322              
323             $debug_response = 0
324 0 0 0       if $debug{response_if_fail} && $response->is_success;
325              
326 0 0         if ($debug_response) {
327 0           my $used_time = time - $started_time;
328              
329 0           print STDERR "-- RESPONSE $used_time sec(s) --\n";
330              
331             print STDERR $debug{response_header_only}
332 0 0         ? $response->headers->as_string
333             : $response->as_string;
334              
335 0           print STDERR ( "-" x 80 ) . "\n";
336             }
337              
338             last RETRY ## request is success, not further for retry
339 0 0         if $response->is_success;
340              
341 0 0         if ( !%retry_status ) {
342 0           sleep $retry_delay;
343             ## no retry pattern at all then just retry
344 0           next RETRY;
345             }
346              
347 0 0         my $pattern = $retry_status{ $response->code }
348             or
349             last RETRY; ## no retry pattern for this status code, just stop retry
350              
351             ## retry if pattern is match otherwise, just stop retry
352 0 0         if ( $response->decode_content =~ /$pattern/ ) {
353 0           sleep $retry_delay;
354 0           next RETRY;
355             }
356              
357 0           last RETRY;
358             }
359              
360 0           return $self->last_response($response);
361             }
362              
363             sub json_response {
364 0     0 0   my $self = shift;
365 0           my $response = shift;
366             try {
367 0     0     my $last_response = $self->last_response->decoded_content;
368 0   0       $response = $self->json->decode( $last_response || "{}" );
369             }
370             catch {
371 0     0     my $error = $_;
372 0           $response = { status => "error", error => $error };
373 0           };
374 0           return $response;
375             }
376              
377             sub value_pair_response {
378 0     0 0   my $self = shift;
379 0   0       my @pairs = split /&/, $self->last_response->decoded_content || q{};
380             my $data =
381 0           { map { my ( $k, $v ) = split /=/, $_, 2; ( $k => $v ) } @pairs };
  0            
  0            
382 0 0         if ( my $error = "$@" ) {
383 0           $data = { status => "error", error => $error };
384             }
385 0           return $data;
386             }
387              
388             sub _request {
389 0     0     my $self = shift;
390 0           my $method = uc shift;
391 0           my $url = shift;
392 0           my $data = shift;
393 0   0       my $headers = shift || {};
394              
395             my $create_req = sub {
396 0     0     my $uri = shift;
397 0           my $req = HTTP::Request->new( $method => $uri );
398 0 0         $req->content_type( $self->content_type )
399             if $method !~ /get/i;
400 0 0 0       if ( $self->username || $self->password ) {
    0          
401 0           _basic_authenticator( $req, $self->username, $self->password );
402             }
403             elsif ( $self->auth_token ) {
404 0   0       $headers->{authorization} ||= $self->auth_token;
405             }
406 0           return $req;
407 0           };
408              
409 0           my $req = $create_req->($url);
410              
411 0           my $content = _tune_utf8( $self->_convert_data( $req, $data ) );
412              
413 0 0         if ( $method =~ /get/i ) {
414 0 0         $req = $create_req->( $content ? "$url?$content" : $url );
415             }
416              
417 0           foreach my $field ( keys %$headers ) {
418 0           $req->header( $field => $headers->{$field} );
419             }
420              
421 0 0         if ( $method !~ /get/i ) {
422 0           $req->content($content);
423             }
424              
425 0           return $req;
426             }
427              
428             sub _tune_utf8 {
429 0     0     my $content = shift;
430 0           my $req = HTTP::Request->new( POST => "http://find-encoding.com" );
431             try {
432 0     0     $req->content($content);
433             }
434             catch {
435 0     0     my $error = $_;
436 0 0         if ( $error =~ /content must be bytes/ ) {
437 0           eval { $content = Encode::encode( utf8 => $content ); };
  0            
438             }
439 0           };
440 0           return $content;
441             }
442              
443             sub _convert_data {
444 0     0     my $self = shift;
445 0           my $req = shift;
446 0           my $data = shift;
447              
448 0 0         return $data
449             if !ref $data;
450              
451 0 0         my $ct = $req->content_type
452             or return _hash_to_query_string(%$data);
453              
454 0 0         return $ct =~ /json/
455             ? $self->json->encode($data)
456             : _hash_to_query_string(%$data);
457             }
458              
459             sub _hash_to_query_string {
460 0     0     my %hash = @_;
461 0           my $uri = URI->new("http://parser.com");
462 0           $uri->query_form( \%hash );
463 0           my ( undef, $params ) = split /\?/, $uri->as_string;
464 0           return $params;
465             }
466              
467             sub _basic_authenticator {
468 0     0     my $req = shift;
469 0           my $username = shift;
470 0           my $password = shift;
471 0           $req->headers->authorization_basic( $username, $password );
472             }
473              
474             sub _smart_or {
475 0     0     my $default_value = shift;
476 0           my $or_value = shift;
477             return
478 0 0 0       defined($default_value)
479             && length($default_value) ? $default_value : $or_value;
480             }
481              
482             1;