File Coverage

blib/lib/HTTP/AnyUA.pm
Criterion Covered Total %
statement 125 165 75.7
branch 41 90 45.5
condition 26 82 31.7
subroutine 27 32 84.3
pod 14 14 100.0
total 233 383 60.8


line stmt bran cond sub pod time code
1             package HTTP::AnyUA;
2             # ABSTRACT: An HTTP user agent programming interface unification layer
3              
4              
5 17     17   30366 use 5.010;
  17         156  
6 17     17   87 use warnings;
  17         30  
  17         489  
7 17     17   87 use strict;
  17         32  
  17         819  
8              
9             our $VERSION = '0.902'; # VERSION
10              
11 17     17   7303 use HTTP::AnyUA::Util;
  17         41  
  17         912  
12 17     17   7136 use Module::Loader;
  17         308711  
  17         544  
13 17     17   122 use Scalar::Util;
  17         31  
  17         1837  
14              
15              
16             our $BACKEND_NAMESPACE;
17             our $MIDDLEWARE_NAMESPACE;
18             our @BACKENDS;
19             our %REGISTERED_BACKENDS;
20              
21             BEGIN {
22 17     17   65 $BACKEND_NAMESPACE = __PACKAGE__ . '::Backend';
23 17         36326 $MIDDLEWARE_NAMESPACE = __PACKAGE__ . '::Middleware';
24             }
25              
26              
27 19 50   19   76 sub _debug_log { print STDERR join(' ', @_), "\n" if $ENV{PERL_HTTP_ANYUA_DEBUG} }
28              
29 1     1   6 sub _croak { require Carp; Carp::croak(@_) }
  1         19  
30 1     1   8 sub _usage { _croak("Usage: @_\n") }
31              
32              
33              
34             sub new {
35 10     10 1 56184 my $class = shift;
36 10 100       50 unshift @_, 'ua' if @_ % 2;
37 10         41 my %args = @_;
38 10 100       41 $args{ua} or _usage(q{HTTP::AnyUA->new(ua => $user_agent, %attr)});
39              
40 9         19 my $self;
41 9         29 my @attr = qw(ua backend response_is_future);
42              
43 9         26 for my $attr (@attr) {
44 27 100       88 $self->{$attr} = $args{$attr} if defined $args{$attr};
45             }
46              
47 9         22 bless $self, $class;
48              
49 9         33 $self->_debug_log('Created with user agent', $self->ua);
50              
51             # call accessors to get the checks to run
52 9         27 $self->ua;
53 9 100       32 $self->response_is_future($args{response_is_future}) if defined $args{response_is_future};
54              
55 9         46 return $self;
56             }
57              
58              
59 32 50   32 1 205 sub ua { shift->{ua} or _croak 'User agent is required' }
60              
61              
62             sub response_is_future {
63 26     26 1 49 my $self = shift;
64 26         41 my $val = shift;
65              
66 26 100 66     143 if (defined $val) {
    100          
67 1 50       4 $self->_debug_log('Set response_is_future to', $val ? 'ON' : 'OFF');
68              
69 1         4 $self->_check_response_is_future($val);
70 1         2 $self->{response_is_future} = $val;
71              
72 1 50       6 $self->_module_loader->load('Future') if $self->{response_is_future};
73             }
74             elsif (!defined $self->{response_is_future} && $self->{backend}) {
75 6         27 $self->{response_is_future} = $self->backend->response_is_future;
76              
77 6 50       24 $self->_module_loader->load('Future') if $self->{response_is_future};
78             }
79              
80 26   100     2414 return $self->{response_is_future} || '';
81             }
82              
83              
84             sub backend {
85 41     41 1 85 my $self = shift;
86              
87 41 100       278 return $self->{backend} if defined $self->{backend};
88              
89 7         28 $self->{backend} = $self->_build_backend;
90 7         30 $self->_check_response_is_future($self->response_is_future);
91              
92 7         33 return $self->{backend};
93             }
94              
95              
96             sub request {
97 14     14 1 43 my ($self, $method, $url, $args) = @_;
98 14   100     65 $args ||= {};
99 14 50 33     95 @_ == 3 || (@_ == 4 && ref $args eq 'HASH')
      33        
100             or _usage(q{$any_ua->request($method, $url, \%options)});
101              
102 14         27 my $resp = eval { $self->backend->request(uc($method) => $url, $args) };
  14         49  
103 14 50       414 if (my $err = $@) {
104 0         0 return $self->_wrap_internal_exception($err);
105             }
106              
107 14         45 return $self->_wrap_response($resp);
108             }
109              
110              
111             # adapted from HTTP/Tiny.pm
112             for my $sub_name (qw{get head put post delete}) {
113             my %swap = (SUBNAME => $sub_name, METHOD => uc($sub_name));
114             my $code = q[
115             sub {{SUBNAME}} {
116             my ($self, $url, $args) = @_;
117             @_ == 2 || (@_ == 3 && ref $args eq 'HASH')
118             or _usage(q{$any_ua->{{SUBNAME}}($url, \%options)});
119             return $self->request('{{METHOD}}', $url, $args);
120             }
121             ];
122             $code =~ s/\{\{([A-Z_]+)\}\}/$swap{$1}/ge;
123 1 0 0 1 1 1134 eval $code; ## no critic
  1 50 33 6 1 6  
  1 0 33 1 1 4  
  6 50 66 4 1 2164  
  6 0 0 1 1 40  
  6   33     25  
  1   33     1482  
  1   66     6  
  1   0     5  
  4   33     2392  
  4         28  
  4         15  
  1         1057  
  1         5  
  1         5  
124             }
125              
126              
127             # adapted from HTTP/Tiny.pm
128             sub post_form {
129 1     1 1 14 my ($self, $url, $data, $args) = @_;
130 1 0 0     3 (@_ == 3 || @_ == 4 && ref $args eq 'HASH')
      33        
131             or _usage(q{$any_ua->post_form($url, $formdata, \%options)});
132              
133 1         7 my $headers = HTTP::AnyUA::Util::normalize_headers($args->{headers});
134 1         2 delete $args->{headers};
135              
136 1         8 return $self->request(POST => $url, {
137             %$args,
138             content => HTTP::AnyUA::Util::www_form_urlencode($data),
139             headers => {
140             %$headers,
141             'content-type' => 'application/x-www-form-urlencoded',
142             },
143             });
144             }
145              
146              
147             # adapted from HTTP/Tiny.pm
148             sub mirror {
149 0     0 1 0 my ($self, $url, $file, $args) = @_;
150 0 0 0     0 @_ == 3 || (@_ == 4 && ref $args eq 'HASH')
      0        
151             or _usage(q{$any_ua->mirror($url, $filepath, \%options)});
152              
153 0         0 $args->{headers} = HTTP::AnyUA::Util::normalize_headers($args->{headers});
154              
155 0 0 0     0 if (-e $file and my $mtime = (stat($file))[9]) {
156 0   0     0 $args->{headers}{'if-modified-since'} ||= HTTP::AnyUA::Util::http_date($mtime);
157             }
158 0         0 my $tempfile = $file . int(rand(2**31));
159              
160             # set up the response body to be written to the file
161 0         0 require Fcntl;
162 0 0       0 sysopen(my $fh, $tempfile, Fcntl::O_CREAT()|Fcntl::O_EXCL()|Fcntl::O_WRONLY())
163             or return $self->_wrap_internal_exception(qq/Error: Could not create temporary file $tempfile for downloading: $!\n/);
164 0         0 binmode $fh;
165 0     0   0 $args->{data_callback} = sub { print $fh $_[0] };
  0         0  
166              
167 0         0 my $resp = $self->request(GET => $url, $args);
168              
169             my $finish = sub {
170 0     0   0 my $resp = shift;
171              
172 0 0       0 close $fh
173             or return HTTP::AnyUA::Util::internal_exception(qq/Error: Caught error closing temporary file $tempfile: $!\n/);
174              
175 0 0       0 if ($resp->{success}) {
176 0 0       0 rename($tempfile, $file)
177             or return HTTP::AnyUA::Util::internal_exception(qq/Error replacing $file with $tempfile: $!\n/);
178 0         0 my $lm = $resp->{headers}{'last-modified'};
179 0 0 0     0 if ($lm and my $mtime = HTTP::AnyUA::Util::parse_http_date($lm)) {
180 0         0 utime($mtime, $mtime, $file);
181             }
182             }
183 0         0 unlink($tempfile);
184              
185 0   0     0 $resp->{success} ||= $resp->{status} eq '304';
186              
187 0         0 return $resp;
188 0         0 };
189              
190 0 0       0 if ($self->response_is_future) {
191             return $resp->followed_by(sub {
192 0     0   0 my $future = shift;
193 0 0       0 my @resp = $future->is_done ? $future->get : $future->failure;
194 0         0 my $resp = $finish->(@resp);
195 0 0       0 if ($resp->{success}) {
196 0         0 return Future->done(@resp);
197             }
198             else {
199 0         0 return Future->fail(@resp);
200             }
201 0         0 });
202             }
203             else {
204 0         0 return $finish->($resp);
205             }
206             }
207              
208              
209             sub apply_middleware {
210 4     4 1 29 my $self = shift;
211 4         8 my $class = shift;
212              
213 4 50       13 if (!ref $class) {
214 4 50       31 $class = "${MIDDLEWARE_NAMESPACE}::${class}" unless $class =~ s/^\+//;
215 4         13 $self->_module_loader->load($class);
216             }
217              
218 4         57 $self->{backend} = $class->wrap($self->backend, @_);
219 4         13 $self->_check_response_is_future($self->response_is_future);
220              
221 4         51 return $self;
222             }
223              
224              
225             sub register_backend {
226 5     5 1 469 my ($class, $ua_type, $backend_class) = @_;
227 5 50       26 @_ == 3 or _usage(q{HTTP::AnyUA->register_backend($ua_type, $backend_package)});
228              
229 5 50       22 if ($backend_class) {
230 5 50       35 $backend_class = "${BACKEND_NAMESPACE}::${backend_class}" unless $backend_class =~ s/^\+//;
231 5         30 $REGISTERED_BACKENDS{$ua_type} = $backend_class;
232             }
233             else {
234 0         0 delete $REGISTERED_BACKENDS{$ua_type};
235             }
236             }
237              
238              
239             # turn a response into a Future if it needs to be
240             sub _wrap_response {
241 14     14   24 my $self = shift;
242 14         18 my $resp = shift;
243              
244 14 100 66     43 if ($self->response_is_future && !$self->backend->response_is_future) {
245             # wrap the response in a Future
246 1 50       3 if ($resp->{success}) {
247 0         0 $self->_debug_log('Wrapped successful response in a Future');
248 0         0 $resp = Future->done($resp);
249             }
250             else {
251 1         4 $self->_debug_log('Wrapped failed response in a Future');
252 1         8 $resp = Future->fail($resp);
253             }
254             }
255              
256 14         83 return $resp;
257             }
258              
259 0     0   0 sub _wrap_internal_exception { shift->_wrap_response(HTTP::AnyUA::Util::internal_exception(@_)) }
260              
261             # get a module loader object
262 13   66 13   108 sub _module_loader { shift->{_module_loader} ||= Module::Loader->new }
263              
264             # get a list of potential backends that may be able to handle the user agent
265             sub _build_backend {
266 7     7   16 my $self = shift;
267 7 50 33     30 my $ua = shift || $self->ua or _croak 'User agent is required';
268              
269 7         32 my $ua_type = Scalar::Util::blessed($ua);
270              
271 7         15 my @classes;
272              
273 7 100       22 if ($ua_type) {
274 1 50       4 push @classes, $REGISTERED_BACKENDS{$ua_type} if $REGISTERED_BACKENDS{$ua_type};
275              
276 1         4 push @classes, "${BACKEND_NAMESPACE}::${ua_type}";
277              
278 1 50       3 if (!@BACKENDS) {
279             # search for some backends to try
280 1         3 @BACKENDS = sort $self->_module_loader->find_modules($BACKEND_NAMESPACE);
281 1         8341 $self->_debug_log('Found backends to try (' . join(', ', @BACKENDS) . ')');
282             }
283              
284 1         4 for my $backend_type (@BACKENDS) {
285 7         11 my $plugin = $backend_type;
286 7         35 $plugin =~ s/^\Q${BACKEND_NAMESPACE}\E:://;
287 7 100       46 push @classes, $backend_type if $ua->isa($plugin);
288             }
289             }
290             else {
291 6 50       30 push @classes, $REGISTERED_BACKENDS{$ua} if $REGISTERED_BACKENDS{$ua};
292 6         28 push @classes, "${BACKEND_NAMESPACE}::${ua}";
293             }
294              
295 7         22 for my $class (@classes) {
296 7 50       14 if (eval { $self->_module_loader->load($class); 1 }) {
  7         28  
  7         906  
297 7         46 $self->_debug_log("Found usable backend (${class})");
298 7         24 return $class->new($self->ua);
299             }
300             else {
301 0         0 $self->_debug_log($@);
302             }
303             }
304              
305 0         0 _croak 'Cannot find a usable backend that supports the given user agent';
306             }
307              
308             # make sure the response_is_future setting is compatible with the backend
309             sub _check_response_is_future {
310 12     12   24 my $self = shift;
311 12         24 my $val = shift;
312              
313             # make sure the user agent is not non-blocking
314 12 50 66     72 if (!$val && $self->{backend} && $self->backend->response_is_future) {
      33        
315 0           _croak 'Cannot disable response_is_future with a non-blocking user agent';
316             }
317             }
318              
319             1;
320              
321             __END__