File Coverage

blib/lib/HTTP/Thin/UserAgent.pm
Criterion Covered Total %
statement 62 128 48.4
branch 0 14 0.0
condition 1 2 50.0
subroutine 21 36 58.3
pod 2 10 20.0
total 86 190 45.2


line stmt bran cond sub pod time code
1             package HTTP::Thin::UserAgent;
2             $HTTP::Thin::UserAgent::VERSION = '0.015';
3 3     3   61280 use 5.12.1;
  3         11  
4 3     3   15 use warnings;
  3         7  
  3         130  
5              
6             # ABSTRACT: A Thin UserAgent around some useful modules.
7              
8              
9             {
10             package
11             HTTP::Thin::UserAgent::HTTPExceptionWithResponse;
12 3     3   2285 use Moo::Role;
  3         65237  
  3         17  
13             has response => ( is => 'ro' );
14             }
15              
16             {
17             package
18             HTTP::Thin::UserAgent::Error::UnexpectedResponse;
19              
20 3     3   3550 use Moo;
  3         6167  
  3         18  
21             extends qw(Throwable::Error);
22             with qw(HTTP::Thin::UserAgent::HTTPExceptionWithResponse);
23              
24             }
25              
26             {
27             package
28             HTTP::Thin::UserAgent::HTTP::Throwable::Factory;
29 3     3   6150 use Moo;
  3         5  
  3         14  
30              
31             extends qw(HTTP::Throwable::Factory);
32              
33             sub extra_roles {
34 0     0 1 0 return qw(
35             HTTP::Throwable::Role::TextBody
36             HTTP::Thin::UserAgent::HTTPExceptionWithResponse
37             );
38             }
39              
40             # A PR for this has been sent to HTTP::Throwable when that ships we can bump the dep
41             # and remove this method
42             sub ident_for_status_code {
43 0     0 0 0 my ($self, $code) = @_;
44              
45 0         0 my %lookup = (
46             300 => 'MultipleChoices',
47             301 => 'MovedPermanently',
48             302 => 'Found',
49             303 => 'SeeOther',
50             304 => 'NotModified',
51             305 => 'UseProxy',
52             307 => 'TemporaryRedirect',
53              
54             400 => 'BadRequest',
55             401 => 'Unauthorized',
56             403 => 'Forbidden',
57             404 => 'NotFound',
58             405 => 'MethodNotAllowed',
59             406 => 'NotAcceptable',
60             407 => 'ProxyAuthenticationRequired',
61             408 => 'RequestTimeout',
62             409 => 'Conflict',
63             410 => 'Gone',
64             411 => 'LengthRequired',
65             412 => 'PreconditionFailed',
66             413 => 'RequestEntityTooLarge',
67             414 => 'RequestURITooLong',
68             415 => 'UnsupportedMediaType',
69             416 => 'RequestedRangeNotSatisfiable',
70             417 => 'ExpectationFailed',
71              
72             500 => 'InternalServerError',
73             501 => 'NotImplemented',
74             502 => 'BadGateway',
75             503 => 'Status::ServiceUnavailable',
76             504 => 'GatewayTimeout',
77             505 => 'HTTPVersionNotSupported',
78             );
79              
80 0         0 return $lookup{$code};
81             }
82             }
83              
84             {
85              
86             package
87             HTTP::Thin::UserAgent::Client;
88              
89 3     3   1266 use Moo;
  3         4  
  3         12  
90 3     3   2881 use MooX::late;
  3         72873  
  3         18  
91 3     3   2555 use MooX::ChainedAttributes;
  3         15698  
  3         18  
92 3     3   3576 use HTTP::Thin;
  3         272556  
  3         103  
93 3     3   2599 use JSON::Any;
  3         10525  
  3         13  
94 3     3   17717 use Try::Tiny;
  3         7324  
  3         187  
95 3     3   17 use Scalar::Util qw/weaken/;
  3         6  
  3         134  
96 3     3   15 use Carp qw(confess);
  3         4  
  3         140  
97              
98 3   50 3   14 use constant TRACE => $ENV{TRACE} // 0;
  3         5  
  3         184  
99 3     3   14 use constant UnexpectedResponse => 'HTTP::Thin::UserAgent::Error::UnexpectedResponse';
  3         7  
  3         135  
100 3     3   15 use constant HTTPException => 'HTTP::Thin::UserAgent::HTTP::Throwable::Factory';
  3         5  
  3         3308  
101              
102             has ua => (
103             is => 'ro',
104             default => sub { HTTP::Thin->new() },
105             );
106              
107             has request => (
108             is => 'ro',
109             required => 1,
110             );
111              
112             has on_error => (
113             is => 'rw',
114             default => sub { sub { confess $_->message } },
115             chained => 1,
116             );
117              
118             has decoder => (
119             is => 'rw',
120             chained => 1,
121             default => sub {
122             sub { shift->decoded_content }
123             },
124             );
125              
126             sub decoded_content {
127 0     0 0 0 my $self = shift;
128 0         0 return $self->decoder->( $self->response );
129             }
130              
131 0     0 0 0 sub decode { warn 'decode is deprecated, please call decoded_content instead'; shift->decoded_content }
  0         0  
132              
133             has response => (
134             is => 'ro',
135             lazy => 1,
136             builder => '_build_response',
137             handles => { 'content' => 'decoded_content' },
138             );
139              
140             sub _build_response {
141 0     0   0 my $self = shift;
142 0         0 my $ua = $self->ua;
143 0         0 my $request = $self->request;
144              
145 0         0 warn $request->dump if TRACE;
146 0         0 my $res = $ua->request($request);
147 0         0 warn $res->dump if TRACE;
148              
149 0 0       0 if ( $res->is_error ) {
150 0         0 my $e;
151 0 0       0 if ( my $id = HTTPException->ident_for_status_code( $res->code ) ) {
152 0         0 $e = HTTPException->new_exception(
153             $id => {
154             additional_headers => [ $res->headers->flatten() ],
155             response => $res,
156             }
157             );
158             }
159             else {
160 0         0 $e = HTTPException->new_exception(
161             {
162             status_code => $res->code,
163             reason => $res->message,
164             additional_headers => [ $res->headers->flatten(), ],
165             response => $res,
166             }
167             );
168             }
169              
170 0         0 for ($e) { $self->on_error->($e) }
  0         0  
171             }
172              
173 0         0 return $res;
174             }
175              
176             sub as_json {
177 0     0 0 0 my $self = shift;
178              
179 0         0 my $request = $self->request;
180              
181 0         0 $request->header(
182             'Accept' => 'application/json',
183             'Content-Type' => 'application/json',
184             );
185              
186 0 0       0 if ( my $data = shift ) {
187 0         0 $request->content( JSON::Any->encode($data) );
188             }
189              
190 0         0 weaken($self);
191             $self->decoder(
192             sub {
193 0     0   0 my $res = shift;
194 0         0 my $content_type = $res->header('Content-Type');
195             my $data = try {
196 0 0       0 die "Content-Type was $content_type not application/json"
197             unless $content_type =~ m'application/json';
198 0         0 JSON::Any->decode( $res->decoded_content );
199             }
200             catch {
201 0         0 my $error = UnexpectedResponse->new(
202             message => $_,
203             response => $res,
204             );
205 0         0 for ($error) {
206 0         0 $self->on_error->($error);
207             }
208 0         0 };
209             }
210 0         0 );
211 0         0 return $self;
212             }
213              
214 0     0 0 0 sub dump { require Data::Dumper; return Data::Dumper::Dumper(shift) }
  0         0  
215              
216             sub scraper {
217 0     0 0 0 my ( $self, $scraper ) = @_;
218              
219 0         0 weaken($self);
220             $self->decoder(
221             sub {
222 0     0   0 my $res = shift;
223 0         0 my $data = try { $scraper->scrape( $res->decoded_content ) }
224             catch {
225 0         0 my $error = UnexpectedResponse->new(
226             message => $_,
227             response => $res
228             );
229 0         0 for ($error) { $self->on_error->($error); }
  0         0  
230 0         0 };
231 0         0 return $data;
232             }
233 0         0 );
234 0         0 return $self;
235             }
236              
237             sub tree {
238 0     0 0 0 my ($self) = @_;
239 0         0 my $t = HTML::TreeBuilder::XPath->new;
240 0 0       0 $t->store_comments(1) if ( $t->can('store_comments') );
241 0         0 $t->ignore_unknown(0);
242 0         0 $t->parse( $self->content );
243 0         0 return $t;
244             }
245              
246             sub find {
247 0     0 0 0 my ( $self, $exp ) = @_;
248              
249 0 0       0 my $xpath =
250             $exp =~ m!^(?:/|id\()!
251             ? $exp
252             : HTML::Selector::XPath::selector_to_xpath($exp);
253              
254 0     0   0 my @nodes = try { $self->tree->findnodes($xpath) }
255             catch {
256 0     0   0 for ($_) { $self->on_error($_) }
  0         0  
257 0         0 };
258 0 0       0 return unless @nodes;
259 0         0 return \@nodes;
260             }
261              
262             }
263              
264 3     3   17 use parent qw(Exporter);
  3         5  
  3         21  
265 3     3   2352 use Import::Into;
  3         1500  
  3         85  
266 3     3   2459 use HTTP::Request::Common;
  3         11934  
  3         198  
267 3     3   2174 use Web::Scraper;
  3         245685  
  3         21  
268              
269             our @EXPORT = qw(http);
270              
271             sub import {
272 3     3   300 shift->export_to_level(1);
273 3         36 HTTP::Request::Common->import::into( scalar caller );
274 3         712 Web::Scraper->import::into( scalar caller );
275             }
276              
277 0     0 1   sub http { HTTP::Thin::UserAgent::Client->new( request => shift ) }
278              
279             1;
280              
281             __END__