File Coverage

blib/lib/HTTP/API/Client.pm
Criterion Covered Total %
statement 39 181 21.5
branch 1 58 1.7
condition 0 39 0.0
subroutine 13 42 30.9
pod 0 8 0.0
total 53 328 16.1


line stmt bran cond sub pod time code
1             package HTTP::API::Client;
2             $HTTP::API::Client::VERSION = '0.05';
3 5     5   90295 use strict;
  5         11  
  5         125  
4 5     5   25 use warnings;
  5         9  
  5         194  
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 ) );
16             my $ua3 = HTTP::API::Client->new(base_url => URI->new( $url ) );
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 5     5   3289 use URI;
  5         30323  
  5         128  
72 5     5   3591 use Mouse;
  5         171558  
  5         26  
73 5     5   7011 use JSON::XS;
  5         28980  
  5         316  
74 5     5   5000 use Try::Tiny;
  5         7328  
  5         292  
75 5     5   2842 use HTTP::Request;
  5         68283  
  5         146  
76 5     5   33 use HTTP::Headers;
  5         9  
  5         130  
77 5     5   5023 use LWP::UserAgent;
  5         118759  
  5         4129  
78              
79             has username => (
80             is => "rw",
81             isa => "Str",
82             lazy_build => 1,
83             );
84              
85 0 0   0   0 sub _build_username { $ENV{HTTP_USERNAME} || qq{} }
86              
87             has password => (
88             is => "rw",
89             isa => "Str",
90             lazy_build => 1,
91             );
92              
93 0 0   0   0 sub _build_password { $ENV{HTTP_PASSWORD} || qq{} }
94              
95             has auth_token => (
96             is => "rw",
97             isa => "Str",
98             lazy_build => 1,
99             );
100              
101 0 0   0   0 sub _build_auth_token { $ENV{HTTP_AUTH_TOKEN} || qq{} }
102              
103             has base_url => (
104             is => "rw",
105             isa => "URI",
106             );
107              
108             has last_response => (
109             is => "rw",
110             isa => "HTTP::Response",
111             );
112              
113             has charset => (
114             is => "rw",
115             isa => "Str",
116             lazy_build => 1,
117             );
118              
119 0 0   0   0 sub _build_charset { $ENV{HTTP_CHARSET} || "utf8" }
120              
121             has browser_id => (
122             is => "rw",
123             isa => "Str",
124             lazy_build => 1,
125             );
126              
127             sub _build_browser_id {
128 0     0   0 my $self = shift;
129 0   0     0 my $ver = $HTTP::API::Client::VERSION || -1;
130 0         0 return "HTTP API Client v$ver";
131             }
132              
133             has content_type => (
134             is => "rw",
135             isa => "Str",
136             lazy_build => 1,
137             );
138              
139             sub _build_content_type {
140 0     0   0 my $self = shift;
141 0         0 my $charset = $self->charset;
142 0         0 return "application/json; charset=$charset";
143             }
144              
145             has ua => (
146             is => "rw",
147             isa => "LWP::UserAgent",
148             lazy_build => 1,
149             );
150              
151             sub _build_ua {
152 0     0   0 my $self = shift;
153 0         0 my $ssl_verify = $self->ssl_verify;
154 0         0 my $ua =
155             LWP::UserAgent->new( ssl_opts => { verify_hostname => $ssl_verify } );
156 0         0 $ua->agent( $self->browser_id );
157 0         0 $ua->timeout( $self->timeout );
158 0         0 return $ua;
159             }
160              
161             has ssl_verify => (
162             is => "rw",
163             isa => "Bool",
164             lazy_build => 1,
165             );
166              
167             sub _build_ssl_verify {
168 0     0   0 return _smart_or( $ENV{SSL_VERIFY}, 0 );
169             }
170              
171             has retry => (
172             is => "rw",
173             isa => "HashRef",
174             lazy_build => 1,
175             );
176              
177             sub _build_retry {
178 0     0   0 my $self = shift;
179 0 0       0 my %retry = %{ $self->retry_config || {} };
  0         0  
180 0         0 my $count = $retry{fail_response};
181 0         0 my %status = map { $_ => 1 } split /,/, $retry{fail_status};
  0         0  
182              
183 0         0 my $delay = $retry{delay};
184              
185             return {
186 0         0 count => $count,
187             status => \%status,
188             delay => $delay,
189             };
190             }
191              
192             has retry_config => (
193             is => "rw",
194             isa => "HashRef",
195             lazy_build => 1,
196             );
197              
198             sub _build_retry_config {
199             return {
200             fail_response => _smart_or( $ENV{RETRY_FAIL_RESPONSE}, 0 ),
201             fail_status => $ENV{RETRY_FAIL_STATUS} || q{},
202 0   0 0   0 delay => _smart_or( $ENV{RETRY_DELAY}, 5 ),
203             };
204             }
205              
206             has timeout => (
207             is => "rw",
208             isa => "Int",
209             lazy_build => 1,
210             );
211              
212 0   0 0   0 sub _build_timeout { return $ENV{HTTP_TIMEOUT} || 60 }
213              
214             has json => (
215             is => "rw",
216             isa => "JSON::XS",
217             lazy_build => 1,
218             );
219              
220             sub _build_json {
221 0     0   0 my $self = shift;
222 0         0 my $json = JSON::XS->new->canonical(1);
223 0         0 my $charset = $self->charset;
224 0         0 eval { $json->$charset };
  0         0  
225 0         0 return $json;
226             }
227              
228             has debug_flags => (
229             is => "rw",
230             isa => "HashRef",
231             lazy_build => 1,
232             );
233              
234             sub _build_debug_flags {
235             return {
236             in_out => $ENV{DEBUG_IN_OUT},
237             send_out => $ENV{DEBUG_SEND_OUT},
238             response => $ENV{DEBUG_RESPONSE},
239             response_header_only => $ENV{DEBUG_RESPONSE_HEADER_ONLY},
240             response_if_fail => $ENV{DEBUG_RESPONSE_IF_FAIL},
241 0     0   0 };
242             }
243              
244 5     5   82 no Mouse;
  5         9  
  5         51  
245              
246             sub get {
247 0     0 0 0 my $self = shift;
248 0         0 return $self->send( GET => @_ );
249             }
250              
251             sub post {
252 0     0 0 0 my $self = shift;
253 0         0 return $self->send( POST => @_ );
254             }
255              
256             sub put {
257 0     0 0 0 my $self = shift;
258 0         0 return $self->send( PUT => @_ );
259             }
260              
261             sub head {
262 0     0 0 0 my $self = shift;
263 0         0 return $self->send( HEAD => @_ );
264             }
265              
266             sub delete {
267 0     0 0 0 my $self = shift;
268 0         0 return $self->send( DELETE => @_ );
269             }
270              
271             sub send {
272 0     0 0 0 my $self = shift;
273 0   0     0 my $method = shift || "GET";
274 0         0 my $path = shift;
275 0   0     0 my $data = shift || {};
276 0   0     0 my $headers = shift || {};
277 0         0 my $ua = $self->ua;
278 0         0 my $base_url = $self->base_url;
279 0 0       0 my $url = $base_url ? $base_url . $path : $path;
280 0         0 my $req = $self->_request( $method, $url, $data, $headers );
281 0         0 my $retry_count = _smart_or( $self->retry->{count}, 1 );
282 0 0       0 my %retry_status = %{ $self->retry->{status} || {} };
  0         0  
283 0         0 my $retry_delay = _smart_or( $self->retry->{delay}, 5 );
284 0 0       0 my %debug = %{ $self->debug_flags || {} };
  0         0  
285              
286 0         0 my $response;
287              
288             RETRY:
289 0         0 foreach my $retry ( 0 .. $retry_count ) {
290 0         0 my $started_time = time;
291              
292 0         0 $response = $ua->request($req);
293              
294 0 0 0     0 if ( $debug{in_out} || $debug{send_out} ) {
295 0         0 print STDERR "-- REQUEST --\n";
296 0 0 0     0 if ( $retry_count && $retry ) {
297 0         0 print STDERR "-- RETRY $retry of $retry_count\n";
298             }
299 0         0 print STDERR $response->request->as_string;
300 0         0 print STDERR "\n";
301             }
302              
303 0   0     0 my $debug_response = $debug{in_out} || $debug{response};
304              
305             $debug_response = 0
306 0 0 0     0 if $debug{response_if_fail} && $response->is_success;
307              
308 0 0       0 if ($debug_response) {
309 0         0 my $used_time = time - $started_time;
310              
311 0         0 print STDERR "-- RESPONSE $used_time sec(s) --\n";
312              
313             print STDERR $debug{response_header_only}
314 0 0       0 ? $response->headers->as_string
315             : $response->as_string;
316              
317 0         0 print STDERR ( "-" x 80 ) . "\n";
318             }
319              
320             last RETRY ## request is success, not further for retry
321 0 0       0 if $response->is_success;
322              
323 0 0       0 if ( !%retry_status ) {
324 0         0 sleep $retry_delay;
325             ## no retry pattern at all then just retry
326 0         0 next RETRY;
327             }
328              
329 0 0       0 my $pattern = $retry_status{ $response->code }
330             or
331             last RETRY; ## no retry pattern for this status code, just stop retry
332              
333             ## retry if pattern is match otherwise, just stop retry
334 0 0       0 if ( $response->decode_content =~ /$pattern/ ) {
335 0         0 sleep $retry_delay;
336 0         0 next RETRY;
337             }
338              
339 0         0 last RETRY;
340             }
341              
342 0         0 return $self->last_response($response);
343             }
344              
345             sub json_response {
346 0     0 0 0 my $self = shift;
347 0         0 my $response = shift;
348             try {
349 0     0   0 my $last_response = $self->last_response->decoded_content;
350 0   0     0 $response = $self->json->decode( $last_response || "{}" );
351             }
352             catch {
353 0     0   0 my $error = $_;
354 0         0 $response = { status => "error", error => $error };
355 0         0 };
356 0         0 return $response;
357             }
358              
359             sub value_pair_response {
360 0     0 0 0 my $self = shift;
361 0   0     0 my @pairs = split /&/, $self->last_response->decoded_content || q{};
362             my $data =
363 0         0 { map { my ( $k, $v ) = split /=/, $_, 2; ( $k => $v ) } @pairs };
  0         0  
  0         0  
364 0 0       0 if ( my $error = "$@" ) {
365 0         0 $data = { status => "error", error => $error };
366             }
367 0         0 return $data;
368             }
369              
370             sub _request {
371 0     0   0 my $self = shift;
372 0         0 my $method = uc shift;
373 0         0 my $url = shift;
374 0         0 my $data = shift;
375 0   0     0 my $headers = shift || {};
376              
377             my $create_req = sub {
378 0     0   0 my $uri = shift;
379 0         0 my $req = HTTP::Request->new( $method => $uri );
380 0 0       0 $req->content_type( $self->content_type )
381             if $method !~ /get/i;
382 0 0 0     0 if ( $self->username || $self->password ) {
    0          
383 0         0 _basic_authenticator( $req, $self->username, $self->password );
384             }
385             elsif ( $self->auth_token ) {
386 0   0     0 $headers->{authorization} ||= $self->auth_token;
387             }
388 0         0 return $req;
389 0         0 };
390              
391 0         0 my $req = $create_req->($url);
392              
393 0         0 my $content = _tune_utf8( $self->_convert_data( $req, $data ) );
394              
395 0 0       0 if ( $method =~ /get/i ) {
396 0 0       0 $req = $create_req->( $content ? "$url?$content" : $url );
397             }
398              
399 0         0 foreach my $field ( keys %$headers ) {
400 0         0 $req->header( $field => $headers->{$field} );
401             }
402              
403 0 0       0 if ( $method !~ /get/i ) {
404 0         0 $req->content($content);
405             }
406              
407 0         0 return $req;
408             }
409              
410             sub _tune_utf8 {
411 1     1   9597 my $content = shift;
412 1         7 my $req = HTTP::Request->new( POST => "http://find-encoding.com" );
413             try {
414 1     1   44 $req->content($content);
415             }
416             catch {
417 1     1   190 my $error = $_;
418 1 50       5 if ( $error =~ /content must be bytes/ ) {
419 1         2 eval { $content = Encode::encode( utf8 => $content ); };
  1         17  
420             }
421 1         103 };
422 1         29 return $content;
423             }
424              
425             sub _convert_data {
426 0     0     my $self = shift;
427 0           my $req = shift;
428 0           my $data = shift;
429              
430 0 0         return $data
431             if !ref $data;
432              
433 0 0         my $ct = $req->content_type
434             or return _hash_to_query_string(%$data);
435              
436 0 0         return $ct =~ /json/
437             ? $self->json->encode($data)
438             : _hash_to_query_string(%$data);
439             }
440              
441             sub _hash_to_query_string {
442 0     0     my %hash = @_;
443 0           my $uri = URI->new("http://parser.com");
444 0           $uri->query_form( \%hash );
445 0           my ( undef, $params ) = split /\?/, $uri->as_string;
446 0           return $params;
447             }
448              
449             sub _basic_authenticator {
450 0     0     my $req = shift;
451 0           my $username = shift;
452 0           my $password = shift;
453 0           $req->headers->authorization_basic( $username, $password );
454             }
455              
456             sub _smart_or {
457 0     0     my $default_value = shift;
458 0           my $or_value = shift;
459             return
460 0 0 0       defined($default_value)
461             && length($default_value) ? $default_value : $or_value;
462             }
463              
464             1;