File Coverage

blib/lib/AnyEvent/Net/Curl/Queued/Easy.pm
Criterion Covered Total %
statement 40 42 95.2
branch n/a
condition n/a
subroutine 14 14 100.0
pod n/a
total 54 56 96.4


line stmt bran cond sub pod time code
1             package AnyEvent::Net::Curl::Queued::Easy;
2             # ABSTRACT: Net::Curl::Easy wrapped by Moo
3              
4              
5 9     9   17988 use feature qw(switch);
  9         21  
  9         868  
6 9     9   53 use strict;
  9         21  
  9         282  
7 9     9   49 use utf8;
  9         22  
  9         108  
8 9     9   286 use warnings qw(all);
  9         19  
  9         381  
9              
10 9     9   46 use Carp qw(carp confess);
  9         22  
  9         612  
11 9     9   7717 use Digest::SHA;
  9         33105  
  9         555  
12 9     9   12164 use Encode;
  9         112680  
  9         961  
13 9     9   5321 use HTTP::Response;
  9         184667  
  9         301  
14 9     9   2256 use JSON;
  9         29870  
  9         87  
15 9     9   1484 use Moo;
  9         22  
  9         86  
16 9         1145 use MooX::Types::MooseLike::Base qw(
17             AnyOf
18             Bool
19             CodeRef
20             HashRef
21             InstanceOf
22             Int
23             Object
24             ScalarRef
25             Str
26 9     9   3384 );
  9         18  
27 9     9   147 use Scalar::Util qw(set_prototype);
  9         14  
  9         448  
28 9     9   47 use URI;
  9         16  
  9         841  
29              
30             # kill Net::Curl::Easy prototypes as they wreck around/before/after method modifiers
31             set_prototype \&Net::Curl::Easy::new => undef;
32             set_prototype \&Net::Curl::Easy::getinfo => undef;
33             set_prototype \&Net::Curl::Easy::setopt => undef;
34              
35             extends 'Net::Curl::Easy';
36              
37 9     9   7256 use AnyEvent::Net::Curl::Const;
  0            
  0            
38             use AnyEvent::Net::Curl::Queued::Stats;
39              
40             no if ($] >= 5.017010), warnings => q(experimental);
41              
42             our $VERSION = '0.047'; # VERSION
43              
44             has json => (
45             is => 'ro',
46             isa => InstanceOf['JSON'],
47             default => sub { JSON->new->utf8->allow_blessed->convert_blessed },
48             lazy => 1,
49             );
50              
51              
52             has curl_result => (is => 'ro', isa => Object, writer => 'set_curl_result');
53              
54              
55             has data => (is => 'ro', isa => ScalarRef, writer => 'set_data');
56              
57              
58             has force => (is => 'ro', isa => Bool, default => sub { 0 });
59              
60              
61             has header => (is => 'ro', isa => ScalarRef, writer => 'set_header');
62              
63              
64             has _autodecoded => (is => 'rw', isa => Bool, default => sub { 0 });
65             has http_response => (is => 'ro', isa => Bool, default => sub { 0 }, writer => 'set_http_response');
66              
67              
68             has post_content => (is => 'ro', isa => Str, default => sub { '' }, writer => 'set_post_content');
69              
70              
71             sub _URI_type {
72             my $uri = shift;
73             return $uri->isa('URI')
74             ? $uri
75             : URI->new(q...$uri)
76             }
77              
78             has initial_url => (is => 'ro', isa => InstanceOf['URI'], coerce => \&_URI_type, required => 1);
79              
80              
81             has final_url => (is => 'ro', isa => InstanceOf['URI'], coerce => \&_URI_type, writer => 'set_final_url');
82              
83              
84             has opts => (is => 'ro', isa => HashRef, default => sub { {} });
85              
86              
87             has queue => (
88             is => 'rw',
89             isa => AnyOf[
90             InstanceOf['AnyEvent::Net::Curl::Queued'],
91             InstanceOf['YADA'],
92             ],
93             weak_ref => 1,
94             );
95              
96              
97             has sha => (is => 'ro', isa => InstanceOf['Digest::SHA'], default => sub { Digest::SHA->new(256) }, lazy => 1);
98              
99              
100             has response => (is => 'ro', isa => InstanceOf['HTTP::Response'], writer => 'set_response');
101             sub res { my ($self, @args) = @_; return $self->response(@args) }
102              
103              
104             has retry => (is => 'ro', isa => Int, default => sub { 10 });
105              
106              
107             has stats => (is => 'ro', isa => InstanceOf['AnyEvent::Net::Curl::Queued::Stats'], default => sub { AnyEvent::Net::Curl::Queued::Stats->new }, lazy => 1);
108             has use_stats => (is => 'ro', isa => Bool, default => sub { 0 });
109              
110              
111             has [qw(on_init on_finish)] => (is => 'ro', isa => CodeRef);
112              
113              
114             ## no critic (RequireArgUnpacking)
115              
116             sub BUILDARGS {
117             return ($_[0] eq ref $_[-1])
118             ? $_[-1]
119             : FOREIGNBUILDARGS(@_);
120             }
121              
122              
123             sub FOREIGNBUILDARGS {
124             my $class = shift;
125             if (@_ == 1 and q(HASH) eq ref $_[0]) {
126             return shift;
127             } elsif (@_ == 1) {
128             return { initial_url => shift };
129             } elsif (@_ % 2 == 0) {
130             return { @_ };
131             } else {
132             confess 'Should be initialized as ' . $class . '->new(Hash|HashRef|URL)';
133             }
134             }
135              
136              
137             sub unique {
138             my ($self) = @_;
139              
140             # make URL-friendly Base64
141             my $digest = $self->sha->clone->b64digest;
142             $digest =~ tr{+/}{-_};
143              
144             # return the signature
145             return $digest;
146             }
147              
148              
149             sub sign {
150             my ($self, $str) = @_;
151              
152             # add entropy to the signature
153             ## no critic (ProtectPrivateSubs)
154             Encode::_utf8_off($str);
155             return $self->sha->add($str);
156             }
157              
158              
159             sub init {
160             my ($self) = @_;
161              
162             # buffers
163             my $data = '';
164             $self->set_data(\$data);
165             my $header = '';
166             $self->set_header(\$header);
167              
168             # fragment mangling
169             my $url = $self->initial_url->clone;
170             $url->fragment(undef);
171             $self->setopt(
172             Net::Curl::Easy::CURLOPT_URL, $url->as_string,
173             Net::Curl::Easy::CURLOPT_WRITEDATA, \$data,
174             Net::Curl::Easy::CURLOPT_WRITEHEADER, \$header,
175             );
176              
177             # common parameters
178             if (defined($self->queue)) {
179             $self->setopt(
180             Net::Curl::Easy::CURLOPT_SHARE, $self->queue->share,
181             Net::Curl::Easy::CURLOPT_TIMEOUT, $self->queue->timeout,
182             );
183             $self->setopt($self->queue->common_opts);
184             $self->set_http_response($self->queue->http_response)
185             if $self->queue->http_response;
186             }
187              
188             # salt
189             $self->sign(ref($self));
190             # URL; GET parameters included
191             $self->sign($url->as_string);
192              
193             # set default options
194             $self->setopt($self->opts);
195              
196             # call the optional callback
197             $self->on_init->(@_) if ref($self->on_init) eq 'CODE';
198              
199             return;
200             }
201              
202              
203             sub has_error {
204             # very bad error
205             return 0 + $_[0]->curl_result != Net::Curl::Easy::CURLE_OK;
206             }
207              
208              
209             ## no critic (ProhibitUnusedPrivateSubroutines)
210             sub _finish {
211             my ($self, $result) = @_;
212              
213             # populate results
214             $self->set_curl_result($result);
215             $self->set_final_url($self->getinfo(Net::Curl::Easy::CURLINFO_EFFECTIVE_URL));
216              
217             # optionally encapsulate with HTTP::Response
218             if ($self->http_response and $self->final_url->scheme =~ m{^https?$}ix) {
219             # libcurl concatenates headers of redirections!
220             my $header = ${$self->header};
221             $header =~ s/^.*(?:\015\012?|\012\015){2}(?!$)//sx;
222             $self->set_response(
223             HTTP::Response->parse(
224             $header
225             . ${$self->data}
226             )
227             );
228              
229             $self->response->headers->header(content_encoding => 'identity')
230             if $self->_autodecoded;
231              
232             my $msg = $self->response->message // '';
233             $msg =~ s/^\s+|\s+$//gsx;
234             $self->response->message($msg);
235             }
236              
237             # wrap around the extendible interface
238             $self->finish($result);
239              
240             # re-enqueue the request
241             if ($self->has_error and $self->retry > 1) {
242             $self->queue->queue_push($self->clone);
243             }
244              
245             # update stats
246             if ($self->use_stats) {
247             $self->stats->sum($self);
248             $self->queue->stats->sum($self);
249             }
250              
251             # request completed (even if returned error!)
252             $self->queue->inc_completed;
253              
254             # move queue
255             $self->queue->start;
256              
257             return;
258             }
259              
260             sub finish {
261             my ($self, $result) = @_;
262              
263             # call the optional callback
264             $self->on_finish->($self, $result) if ref($self->on_finish) eq 'CODE';
265              
266             return;
267             }
268              
269              
270             sub clone {
271             my ($self, $param) = @_;
272              
273             # silently ignore unsupported parameters
274             $param = {} unless 'HASH' eq ref $param;
275              
276             my $class = ref($self);
277             $param->{$_} = $self->$_()
278             for qw(
279             http_response
280             initial_url
281             retry
282             use_stats
283             );
284             --$param->{retry};
285             $param->{force} = 1;
286              
287             $param->{on_init} = $self->on_init if ref($self->on_init) eq 'CODE';
288             $param->{on_finish} = $self->on_finish if ref($self->on_finish) eq 'CODE';
289              
290             my $post_content = $self->post_content;
291             return ($post_content eq '')
292             ? sub { $class->new($param) }
293             : sub {
294             my $new = $class->new($param);
295             $new->setopt(Net::Curl::Easy::CURLOPT_POSTFIELDS, $post_content);
296             return $new;
297             };
298             }
299              
300              
301             around setopt => sub {
302             my $orig = shift;
303             my $self = shift;
304              
305             if (@_) {
306             my %param;
307             if (scalar @_ % 2 == 0) {
308             %param = @_;
309             } elsif (ref($_[0]) eq 'HASH') {
310             my $param = shift;
311             %param = %{$param};
312             } else {
313             carp "setopt() expects OPTION/VALUE pair, OPTION/VALUE hash or hashref!";
314             }
315              
316             while (my ($key, $val) = each %param) {
317             $key = AnyEvent::Net::Curl::Const::opt($key);
318             if ($key == Net::Curl::Easy::CURLOPT_POSTFIELDS) {
319             my $is_json = 0;
320             ($val, $is_json) = $self->_setopt_postfields($val);
321              
322             $orig->($self =>
323             Net::Curl::Easy::CURLOPT_HTTPHEADER,
324             [ 'Content-Type: application/json; charset=utf-8' ],
325             ) if $is_json;
326             } elsif ($key == Net::Curl::Easy::CURLOPT_ENCODING) {
327             $self->_autodecoded(1);
328             $val = $self->_setopt_encoding($val);
329             }
330             $orig->($self => $key, $val);
331             }
332             } else {
333             carp "Specify at least one OPTION/VALUE pair!";
334             }
335             };
336              
337             sub _setopt_postfields {
338             my ($self, $val) = @_;
339              
340             my $is_json = 0;
341             if ('HASH' eq ref $val) {
342             ++$is_json;
343             $val = $self->json->encode($val);
344             } else {
345             # some DWIMmery here!
346             # application/x-www-form-urlencoded is supposed to have a 7-bit encoding
347             $val = encode_utf8($val)
348             if utf8::is_utf8($val);
349              
350             my $obj;
351             ++$is_json if 'HASH' eq ref($obj = eval { $self->json->decode($val) });
352             }
353              
354             return ($self->set_post_content($val), $is_json);
355             }
356              
357             sub _setopt_encoding {
358             my ($self, $val) = @_;
359              
360             # stolen from LWP::Protocol::Net::Curl
361             my @encoding =
362             map { /^(?:x-)?(deflate|gzip|identity)$/ix ? lc $1 : () }
363             split /\s*,\s*/x, $val;
364              
365             return join q(,) => @encoding;
366             }
367              
368              
369             around getinfo => sub {
370             my $orig = shift;
371             my $self = shift;
372              
373             for (ref($_[0])) {
374             when ('ARRAY') {
375             my @val;
376             for my $name (@{$_[0]}) {
377             my $const = AnyEvent::Net::Curl::Const::info($name);
378             next unless defined $const;
379             push @val, $self->$orig($const);
380             }
381             return @val;
382             } when ('HASH') {
383             my %val;
384             for my $name (keys %{$_[0]}) {
385             my $const = AnyEvent::Net::Curl::Const::info($name);
386             next unless defined $const;
387             $val{$name} = $self->$orig($const);
388             }
389              
390             # write back to HashRef if called under void context
391             unless (defined wantarray) {
392             while (my ($k, $v) = each %val) {
393             $_[0]->{$k} = $v;
394             }
395             return;
396             } else {
397             return \%val;
398             }
399             } when ('') {
400             my $const = AnyEvent::Net::Curl::Const::info($_[0]);
401             return defined $const ? $self->$orig($const) : $const;
402             } default {
403             carp "getinfo() expects array/hash reference or string!";
404             return;
405             }
406             }
407             };
408              
409              
410             1;
411              
412             __END__