File Coverage

blib/lib/Stash/REST.pm
Criterion Covered Total %
statement 197 207 95.1
branch 87 128 67.9
condition 46 81 56.7
subroutine 21 21 100.0
pod 8 8 100.0
total 359 445 80.6


line stmt bran cond sub pod time code
1             package Stash::REST;
2 1     1   126499 use strict;
  1         1  
  1         22  
3 1     1   27 use 5.008_005;
  1         2  
4             our $VERSION = '0.11';
5              
6 1     1   3 use warnings;
  1         2  
  1         19  
7 1     1   559 use utf8;
  1         8  
  1         3  
8 1     1   23 use URI;
  1         2  
  1         14  
9 1     1   3 use URI::QueryParam;
  1         1  
  1         20  
10 1     1   397 use HTTP::Request::Common qw(GET POST DELETE HEAD);
  1         1581  
  1         55  
11 1     1   5 use Carp qw/confess cluck/;
  1         1  
  1         41  
12              
13 1     1   420 use Moo;
  1         8592  
  1         5  
14 1     1   1446 use namespace::clean;
  1         6872  
  1         5  
15              
16 1     1   686 use Class::Trigger;
  1         950  
  1         4  
17              
18             has 'do_request' => (
19             is => 'rw',
20             isa => sub { die "$_[0] is not a CodeRef" unless ref $_[0] eq 'CODE' },
21             required => 1
22             );
23              
24             has 'decode_response' => (
25             is => 'rw',
26             isa => sub { die "$_[0] is not a CodeRef" unless ref $_[0] eq 'CODE' },
27             required => 1
28             );
29              
30             has 'stash' => (
31             is => 'rw',
32             isa => sub { die "$_[0] is not a HashRef" unless ref $_[0] eq 'HASH' },
33             default => sub { {} }
34             );
35              
36             has 'fixed_headers' => (
37             is => 'rw',
38             isa => sub { die "$_[0] is not a ArrayRef" unless ref $_[0] eq 'ARRAY' },
39             default => sub { [] }
40             );
41              
42             around 'stash' => sub {
43             my $orig = shift;
44             my $c = shift;
45             my $stash = $orig->($c);
46              
47             if (@_) {
48             return $stash->{ $_[0] } if ( @_ == 1 && ref $_[0] eq '' );
49              
50             my $new_stash = @_ > 1 ? {@_} : $_[0];
51             die('stash takes a hash or hashref') unless ref $new_stash;
52             foreach my $key ( keys %$new_stash ) {
53             $stash->{$key} = $new_stash->{$key};
54             }
55             }
56              
57             return $stash;
58             };
59              
60             sub _capture_args {
61 20     20   91 my ( $method, $self, @params ) = @_;
62 20         21 my ( $uri, $data, %conf );
63              
64 20 100       144 confess 'invalid number of params' if @params < 1;
65              
66 19         24 $uri = shift @params;
67 19 100 100     213 confess 'invalid uri param' if ref $uri ne '' && ref $uri ne 'ARRAY';
68              
69 18 100       35 $uri = join '/', @$uri if ref $uri eq 'ARRAY';
70              
71             # if number of params is odd, then, the last item is defined as $data
72 18 100       53 if ( scalar @params % 2 == 0 ) {
73 12         36 %conf = @params;
74 12 100       29 $data = exists $conf{data} ? $conf{data} : undef;
75             }
76             else {
77 6         9 $data = pop @params;
78 6         31 %conf = @params;
79             }
80              
81 18 50 66     49 $conf{headers} = [ %{ $conf{headers} } ] if exists $conf{headers} && ref $conf{headers} eq 'HASH';
  0         0  
82              
83             confess "Can't use ->{files} helper with custom Content-Type."
84 18 50 33     42 if exists $conf{files} && ( exists $conf{headers} && grep { /Content-Type/i } @{ $conf{headers} } );
  0   66     0  
  0         0  
85              
86 18         90 my $can_have_body = $method =~ /POST|PUT|DELETE/;
87              
88 18 100 100     167 if ( !$can_have_body && $data && exists $conf{params} ) {
    100 66        
    50 100        
      66        
      66        
89             confess "You are using {data} and {params} in $method. Please, use only {params} instead"
90 1 50       176 unless $conf{allow_get_with_body};
91 0         0 $conf{data} = $data;
92             }
93             elsif ( !$can_have_body && $data && !exists $conf{params} ) {
94 3         6 $conf{params} = $data;
95             }
96             elsif ( !$can_have_body && $data ) {
97             confess "$method does not allow body. You may have problems with proxy. Consider removing it"
98 0 0       0 unless $conf{allow_get_with_body};
99 0         0 $conf{data} = $data;
100             }
101             else {
102 14         27 $conf{data} = $data;
103             }
104              
105 17         84 return ( $self, $uri, %conf );
106             }
107              
108             sub rest_put {
109 1     1 1 5 my ( $self, $url, %conf ) = &_capture_args( 'PUT', @_ );
110              
111 1         5 $self->call_trigger( 'before_rest_put', { url => $url, conf => \%conf } );
112             $self->_rest_request(
113             $url,
114 1 50       51 code => ( exists $conf{is_fail} ? 400 : 202 ),
115             %conf,
116             method => 'PUT'
117             );
118             }
119              
120             sub rest_head {
121 1     1 1 3 my ( $self, $url, %conf ) = &_capture_args( 'HEAD', @_ );
122              
123 1         7 $self->call_trigger( 'before_rest_head', { url => $url, conf => \%conf } );
124 1         68 $self->_rest_request(
125             $url,
126             code => 200,
127             %conf,
128             method => 'HEAD'
129             );
130             }
131              
132             sub rest_delete {
133 1     1 1 3 my ( $self, $url, %conf ) = &_capture_args( 'DELETE', @_ );
134              
135 1         5 $self->call_trigger( 'before_rest_delete', { url => $url, conf => \%conf } );
136 1         47 $self->_rest_request(
137             $url,
138             code => 204,
139             %conf,
140             method => 'DELETE'
141             );
142             }
143              
144             sub rest_get {
145 10     10 1 9048 my ( $self, $url, %conf ) = &_capture_args( 'GET', @_ );
146              
147 7         33 $self->call_trigger( 'before_rest_get', { url => $url, conf => \%conf } );
148 7         186 $self->_rest_request(
149             $url,
150             code => 200,
151             %conf,
152             method => 'GET'
153             );
154             }
155              
156             sub rest_post {
157 7     7 1 1922 my ( $self, $url, %conf ) = &_capture_args( 'POST', @_ );
158 7         39 $self->call_trigger( 'before_rest_post', { url => $url, conf => \%conf } );
159              
160 7         230 $self->_rest_request( $url, %conf, method => 'POST' );
161             }
162              
163             sub _rest_request {
164 17     17   55 my ( $self, $url, %conf ) = @_;
165              
166 17 100       35 my $data = exists $conf{data} ? $conf{data} : undef;
167              
168 17 50       44 $conf{automatic_load_item} = 1 unless exists $conf{automatic_load_item};
169              
170 17   66     40 my $is_fail = exists $conf{is_fail} && $conf{is_fail};
171              
172 17         16 my $code = $conf{code};
173 17 50 66     42 $code ||= $is_fail ? 400 : 201;
174 17         20 $conf{code} = $code;
175              
176 17         79 my $uri = URI->new($url);
177 17 100       4113 if ( $conf{params} ) {
178 6 50       15 my @old = ref $conf{params} eq 'ARRAY' ? @{ $conf{params} } : %{ $conf{params} };
  6         14  
  0         0  
179 6         18 while ( my ( $k, $v ) = splice( @old, 0, 2 ) ) {
180 8         165 $uri->query_param_append( $k, $v );
181             }
182             }
183 17         476 $url = $uri->as_string;
184              
185 17 100       102 my $stashkey = exists $conf{stash} ? $conf{stash} : undef;
186              
187 17 100       16 my @headers = ( @{ $self->fixed_headers() }, @{ $conf{headers} || [] } );
  17         320  
  17         125  
188              
189 17         17 my $req;
190              
191 17 100       27 if ( !exists $conf{files} ) {
192 16 100       29 if ( defined $data ) {
193 9         35 $req = POST $url, @headers, Content => $data;
194             }
195             else {
196 7         19 $req = GET $url, @headers;
197             }
198             }
199             else {
200 1         1 $conf{files}{$_} = [ $conf{files}{$_} ] for keys %{ $conf{files} };
  1         6  
201              
202             $req = POST $url, @headers,
203             'Content-Type' => 'form-data',
204 1 50 33     13 Content => [ ( $data && ref $data eq 'ARRAY' ? @$data : () ), %{ $conf{files} } ];
  1         5  
205             }
206              
207             $conf{process_request}->( { req => $req, conf => \%conf } )
208 17 50 33     7279 if ( exists $conf{process_request} && ref $conf{process_request} eq 'CODE' );
209              
210 17         60 $self->call_trigger( 'process_request', { req => $req, conf => \%conf } );
211              
212             # change to correct method.
213 17         382 $req->method( $conf{method} );
214              
215 17         99 my $res = eval { $self->do_request()->($req) };
  17         309  
216 17 50       105930 confess "request died: $@" if $@;
217              
218             $conf{process_response}->( { req => $req, res => $res, conf => \%conf } )
219 17 50 33     53 if ( exists $conf{process_response} && ref $conf{process_response} eq 'CODE' );
220              
221 17         90 $self->call_trigger( 'process_response', { req => $req, res => $res, conf => \%conf } );
222              
223             #is( $res->code, $code, $name . ' status code is ' . $code );
224 17 50       485 if ( !exists $conf{skip_response_tests} ) {
225 17 100 100     45 confess 'response expected fail and it is successed' if $is_fail && $res->is_success;
226 16 100 66     84 confess 'response expected success and it is failed' if !$is_fail && !$res->is_success;
227              
228 15 100       125 confess 'response code [', $res->code, '] diverge expected [', $code, ']' if $code != $res->code;
229             }
230              
231             $conf{process_response_success}->( { req => $req, res => $res, conf => \%conf } )
232 14 50 33     111 if ( exists $conf{process_response_success} && ref $conf{process_response_success} eq 'CODE' );
233              
234 14         50 $self->call_trigger( 'process_response_success', { req => $req, res => $res, conf => \%conf } );
235              
236 14 100       281 return '' if $code == 204;
237 13 100 66     89 return $res if exists $conf{method} && $conf{method} eq 'HEAD';
238              
239 12         13 my $obj = eval { $self->decode_response()->($res) };
  12         292  
240 12 50       229 confess("decode_response failed: $@") if $@;
241              
242             $conf{response_decoded}->( { req => $req, res => $res, decoded => $obj, conf => \%conf } )
243 12 50 33     35 if ( exists $conf{response_decoded} && ref $conf{response_decoded} eq 'CODE' );
244              
245 12         43 $self->call_trigger( 'response_decoded', { req => $req, res => $res, decoded => $obj, conf => \%conf } );
246              
247 12 100       244 if ($stashkey) {
248 11         193 $self->stash->{$stashkey} = $obj;
249              
250 11 100       89 $self->stash( $stashkey . '.prepare_request' => $conf{prepare_request} ) if exists $conf{prepare_request};
251              
252 11 100       30 if ( $code == 201 ) {
253 4 50 33     80 $self->stash( $stashkey . '.id' => $obj->{id} ) if ref $obj eq 'HASH' && exists $obj->{id};
254              
255 4         12 my $item_url = $res->header('Location');
256              
257 4 50 33     123 if ( $item_url && $conf{automatic_load_item} ) {
    0          
258 4         68 $self->stash->{ $stashkey . '.url' } = $item_url;
259              
260 4         15 $self->rest_reload($stashkey);
261              
262             $conf{item_loaded}->( { stash => $stashkey, conf => \%conf } )
263 3 50 33     13 if ( exists $conf{item_loaded} && ref $conf{item_loaded} eq 'CODE' );
264              
265 3         14 $self->call_trigger( 'item_loaded', { stash => $stashkey, conf => \%conf } );
266             }
267             elsif ( $conf{automatic_load_item} ) {
268 0         0 confess 'requests with response code 201 should contain header Location';
269             }
270              
271             $conf{stash_added}->( { stash => $stashkey, conf => \%conf } )
272 3 50 33     89 if ( exists $conf{stash_added} && ref $conf{stash_added} eq 'CODE' );
273 3         12 $self->call_trigger( 'stash_added', { stash => $stashkey, conf => \%conf } );
274             }
275             }
276              
277 11 100 66     109 if ( $stashkey && exists $conf{list} ) {
278              
279 2         46 $self->stash( $stashkey . '.list-url' => $url );
280              
281 2         8 $self->rest_reload_list($stashkey);
282              
283             $conf{list_loaded}->( { stash => $stashkey, conf => \%conf } )
284 2 50 33     7 if ( exists $conf{list_loaded} && ref $conf{list_loaded} eq 'CODE' );
285              
286 2         9 $self->call_trigger( 'list_loaded', { stash => $stashkey, conf => \%conf } );
287              
288             }
289              
290 11         171 return $obj;
291             }
292              
293             sub rest_reload {
294 6     6 1 1139 my $self = shift;
295 6         6 my $stashkey = shift;
296              
297 6         10 my %conf = @_;
298              
299 6 100       11 my $code = exists $conf{code} ? $conf{code} : 200;
300 6         8 $conf{code} = $code;
301              
302 6 50       8 my @headers = ( @{ $self->fixed_headers() }, @{ $conf{headers} || [] } );
  6         94  
  6         57  
303 6         89 my $item_url = $self->stash->{ $stashkey . '.url' };
304              
305 6 50       14 confess "can't stash $stashkey.url is not valid" unless $item_url;
306              
307             my $prepare_request =
308             exists $self->stash->{ $stashkey . '.prepare_request' }
309 6 50       81 ? $self->stash->{ $stashkey . '.prepare_request' }
310             : undef;
311              
312 6 100 66     159 confess 'prepare_request must be a coderef'
313             if $prepare_request && ref $prepare_request ne 'CODE';
314              
315 5         16 my $req = GET $item_url, @headers;
316 5         408 $req->method('GET');
317 5 50       39 $prepare_request->($req) if $prepare_request;
318              
319 5         6140 $self->call_trigger( 'process_request', { req => $req, conf => \%conf } );
320 5         208 my $res = $self->do_request()->($req);
321              
322 5         17793 $self->call_trigger( 'process_response', { req => $req, res => $res, conf => \%conf } );
323              
324 5 50       129 confess 'request code diverge expected' if $code != $res->code;
325              
326 5         50 $self->call_trigger( 'process_response_success', { req => $req, res => $res, conf => \%conf } );
327              
328 5         84 my $obj;
329 5 100       10 if ( $res->code == 200 ) {
    50          
330 4         27 my $obj = eval { $self->decode_response()->($res) };
  4         94  
331 4 50       77 confess("decode_response failed: $@") if $@;
332              
333 4         21 $self->call_trigger( 'response_decoded', { req => $req, res => $res, decoded => $obj, conf => \%conf } );
334              
335 4         144 $self->stash( $stashkey . '.get' => $obj );
336             }
337             elsif ( $res->code == 404 ) {
338              
339 1         14 $self->call_trigger( 'stash_removed', { stash => $stashkey, conf => \%conf } );
340              
341             # $self->stash->{ $stashkey . '.get' };
342 1         75 delete $self->stash->{ $stashkey . '.id' };
343 1         15 delete $self->stash->{ $stashkey . '.url' };
344 1         15 delete $self->stash->{$stashkey};
345              
346             }
347             else {
348 0         0 confess 'response code ' . $res->code . ' is not valid for rest_reload';
349             }
350              
351 5         40 return $obj;
352             }
353              
354             sub rest_reload_list {
355 3     3 1 5 my $self = shift;
356 3         4 my $stashkey = shift;
357              
358 3         5 my %conf = @_;
359              
360 3 50       9 my $code = exists $conf{code} ? $conf{code} : 200;
361 3         6 $conf{code} = $code;
362              
363 3 50       3 my @headers = ( @{ $self->fixed_headers() }, @{ $conf{headers} || [] } );
  3         41  
  3         28  
364 3         44 my $item_url = $self->stash->{ $stashkey . '.list-url' };
365              
366 3 50       9 confess "can't stash $stashkey.list-url is not valid" unless $item_url;
367              
368             my $prepare_request =
369             exists $self->stash->{ $stashkey . '.prepare_request' }
370 3 50       45 ? $self->stash->{ $stashkey . '.prepare_request' }
371             : undef;
372 3 50 33     22 confess 'prepare_request must be a coderef'
373             if $prepare_request && ref $prepare_request ne 'CODE';
374              
375 3         10 my $req = GET $item_url, @headers;
376 3 50       240 $prepare_request->($req) if $prepare_request;
377              
378 3         3471 $self->call_trigger( 'process_request', { req => $req, conf => \%conf } );
379              
380 3         132 my $res = $self->do_request()->($req);
381              
382 3         10464 $self->call_trigger( 'process_response', { req => $req, res => $res, conf => \%conf } );
383              
384 3 50       70 confess 'request code diverge expected' if $code != $res->code;
385              
386 3         28 $self->call_trigger( 'process_response_success', { req => $req, res => $res, conf => \%conf } );
387              
388 3         59 my $obj;
389 3 50       6 if ( $res->code == 200 ) {
390 3         21 my $obj = eval { $self->decode_response()->($res) };
  3         68  
391 3 50       57 confess("decode_response failed: $@") if $@;
392              
393 3         57 $self->call_trigger( 'response_decoded', { req => $req, res => $res, decoded => $obj, conf => \%conf } );
394              
395 3         119 $self->stash( $stashkey . '.list' => $obj );
396             }
397             else {
398 0         0 confess 'response code ' . $res->code . ' is not valid for rest_reload';
399             }
400              
401 3         18 return $obj;
402             }
403              
404             sub stash_ctx {
405 10     10 1 105 my ( $self, $staname, $sub ) = @_;
406              
407 10         30 $self->call_trigger( 'before_stash_ctx', { stash => $staname } );
408              
409 10         395 my @ret = $sub->( $self->stash->{$staname} );
410              
411 10         16849 $self->call_trigger( 'after_stash_ctx', { stash => $staname, results => \@ret } );
412 10         239 return @ret;
413             }
414              
415             1;
416              
417             __END__