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   208779 use strict;
  1         2  
  1         26  
3 1     1   18 use 5.008_005;
  1         3  
4             our $VERSION = '0.10';
5              
6 1     1   3 use warnings;
  1         1  
  1         22  
7 1     1   557 use utf8;
  1         9  
  1         4  
8 1     1   27 use URI;
  1         1  
  1         16  
9 1     1   3 use URI::QueryParam;
  1         0  
  1         20  
10 1     1   476 use HTTP::Request::Common qw(GET POST DELETE HEAD);
  1         1859  
  1         63  
11 1     1   5 use Carp qw/confess cluck/;
  1         1  
  1         45  
12              
13 1     1   524 use Moo;
  1         11108  
  1         6  
14 1     1   1666 use namespace::clean;
  1         7673  
  1         3  
15              
16 1     1   688 use Class::Trigger;
  1         1036  
  1         5  
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   64 my ( $method, $self, @params ) = @_;
62 20         21 my ( $uri, $data, %conf );
63              
64 20 100       183 confess 'invalid number of params' if @params < 1;
65              
66 19         31 $uri = shift @params;
67 19 100 100     206 confess 'invalid uri param' if ref $uri ne '' && ref $uri ne 'ARRAY';
68              
69 18 100       36 $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       50 if ( scalar @params % 2 == 0 ) {
73 12         45 %conf = @params;
74 12 100       34 $data = exists $conf{data} ? $conf{data} : undef;
75             }
76             else {
77 6         9 $data = pop @params;
78 6         28 %conf = @params;
79             }
80              
81 18 50 66     64 $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     46 if exists $conf{files} && ( exists $conf{headers} && grep { /Content-Type/i } @{ $conf{headers} } );
  0   66     0  
  0         0  
85              
86 18         87 my $can_have_body = $method =~ /POST|PUT|DELETE/;
87              
88 18 100 100     185 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       190 unless $conf{allow_get_with_body};
91 0         0 $conf{data} = $data;
92             }
93             elsif ( !$can_have_body && $data && !exists $conf{params} ) {
94 3         8 $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         33 $conf{data} = $data;
103             }
104              
105 17         94 return ( $self, $uri, %conf );
106             }
107              
108             sub rest_put {
109 1     1 1 4 my ( $self, $url, %conf ) = &_capture_args( 'PUT', @_ );
110              
111 1         7 $self->call_trigger( 'before_rest_put', { url => $url, conf => \%conf } );
112             $self->_rest_request(
113             $url,
114 1 50       54 code => ( exists $conf{is_fail} ? 400 : 202 ),
115             %conf,
116             method => 'PUT'
117             );
118             }
119              
120             sub rest_head {
121 1     1 1 4 my ( $self, $url, %conf ) = &_capture_args( 'HEAD', @_ );
122              
123 1         7 $self->call_trigger( 'before_rest_head', { url => $url, conf => \%conf } );
124 1         71 $self->_rest_request(
125             $url,
126             code => 200,
127             %conf,
128             method => 'HEAD'
129             );
130             }
131              
132             sub rest_delete {
133 1     1 1 4 my ( $self, $url, %conf ) = &_capture_args( 'DELETE', @_ );
134              
135 1         5 $self->call_trigger( 'before_rest_delete', { url => $url, conf => \%conf } );
136 1         102 $self->_rest_request(
137             $url,
138             code => 204,
139             %conf,
140             method => 'DELETE'
141             );
142             }
143              
144             sub rest_get {
145 10     10 1 10781 my ( $self, $url, %conf ) = &_capture_args( 'GET', @_ );
146              
147 7         37 $self->call_trigger( 'before_rest_get', { url => $url, conf => \%conf } );
148 7         228 $self->_rest_request(
149             $url,
150             code => 200,
151             %conf,
152             method => 'GET'
153             );
154             }
155              
156             sub rest_post {
157 7     7 1 2122 my ( $self, $url, %conf ) = &_capture_args( 'POST', @_ );
158 7         53 $self->call_trigger( 'before_rest_post', { url => $url, conf => \%conf } );
159              
160 7         275 $self->_rest_request( $url, %conf, method => 'POST' );
161             }
162              
163             sub _rest_request {
164 17     17   58 my ( $self, $url, %conf ) = @_;
165              
166 17 100       39 my $data = exists $conf{data} ? $conf{data} : undef;
167              
168 17 50       52 $conf{automatic_load_item} = 1 unless exists $conf{automatic_load_item};
169              
170 17   66     36 my $is_fail = exists $conf{is_fail} && $conf{is_fail};
171              
172 17         25 my $code = $conf{code};
173 17 50 66     52 $code ||= $is_fail ? 400 : 201;
174 17         20 $conf{code} = $code;
175              
176 17         86 my $uri = URI->new($url);
177 17 100       4639 if ( $conf{params} ) {
178 6 50       19 my @old = ref $conf{params} eq 'ARRAY' ? @{ $conf{params} } : %{ $conf{params} };
  6         14  
  0         0  
179 6         22 while ( my ( $k, $v ) = splice( @old, 0, 2 ) ) {
180 8         170 $uri->query_param_append( $k, $v );
181             }
182             }
183 17         576 $url = $uri->as_string;
184              
185 17 100       129 my $stashkey = exists $conf{stash} ? $conf{stash} : undef;
186              
187 17 100       20 my @headers = ( @{ $self->fixed_headers() }, @{ $conf{headers} || [] } );
  17         363  
  17         636  
188              
189 17         28 my $req;
190              
191 17 100       32 if ( !exists $conf{files} ) {
192 16 100       27 if ( defined $data ) {
193 9         39 $req = POST $url, @headers, Content => $data;
194             }
195             else {
196 7         23 $req = GET $url, @headers;
197             }
198             }
199             else {
200 1         3 $conf{files}{$_} = [ $conf{files}{$_} ] for keys %{ $conf{files} };
  1         7  
201              
202             $req = POST $url, @headers,
203             'Content-Type' => 'form-data',
204 1 50 33     15 Content => [ ( $data && ref $data eq 'ARRAY' ? @$data : () ), %{ $conf{files} } ];
  1         7  
205             }
206              
207             $conf{process_request}->( { req => $req, conf => \%conf } )
208 17 50 33     8613 if ( exists $conf{process_request} && ref $conf{process_request} eq 'CODE' );
209              
210 17         85 $self->call_trigger( 'process_request', { req => $req, conf => \%conf } );
211              
212             # change to correct method.
213 17         481 $req->method( $conf{method} );
214              
215 17         127 my $res = eval { $self->do_request()->($req) };
  17         357  
216 17 50       122892 confess "request died: $@" if $@;
217              
218             $conf{process_response}->( { req => $req, res => $res, conf => \%conf } )
219 17 50 33     64 if ( exists $conf{process_response} && ref $conf{process_response} eq 'CODE' );
220              
221 17         112 $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       557 if ( !exists $conf{skip_response_tests} ) {
225 17 100 100     61 confess 'response expected fail and it is successed' if $is_fail && $res->is_success;
226 16 100 66     98 confess 'response expected success and it is failed' if !$is_fail && !$res->is_success;
227              
228 15 100       137 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     137 if ( exists $conf{process_response_success} && ref $conf{process_response_success} eq 'CODE' );
233              
234 14         60 $self->call_trigger( 'process_response_success', { req => $req, res => $res, conf => \%conf } );
235              
236 14 100       380 return '' if $code == 204;
237 13 100 66     81 return $res if exists $conf{method} && $conf{method} eq 'HEAD';
238              
239 12         17 my $obj = eval { $self->decode_response()->($res) };
  12         321  
240 12 50       874 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         60 $self->call_trigger( 'response_decoded', { req => $req, res => $res, decoded => $obj, conf => \%conf } );
246              
247 12 100       338 if ($stashkey) {
248 11         269 $self->stash->{$stashkey} = $obj;
249              
250 11 100       119 $self->stash( $stashkey . '.prepare_request' => $conf{prepare_request} ) if exists $conf{prepare_request};
251              
252 11 100       25 if ( $code == 201 ) {
253 4 50 33     118 $self->stash( $stashkey . '.id' => $obj->{id} ) if ref $obj eq 'HASH' && exists $obj->{id};
254              
255 4         13 my $item_url = $res->header('Location');
256              
257 4 50 33     151 if ( $item_url && $conf{automatic_load_item} ) {
    0          
258 4         82 $self->stash->{ $stashkey . '.url' } = $item_url;
259              
260 4         17 $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         19 $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     115 if ( exists $conf{stash_added} && ref $conf{stash_added} eq 'CODE' );
273 3         13 $self->call_trigger( 'stash_added', { stash => $stashkey, conf => \%conf } );
274             }
275             }
276              
277 11 100 66     125 if ( $stashkey && exists $conf{list} ) {
278              
279 2         52 $self->stash( $stashkey . '.list-url' => $url );
280              
281 2         6 $self->rest_reload_list($stashkey);
282              
283             $conf{list_loaded}->( { stash => $stashkey, conf => \%conf } )
284 2 50 33     8 if ( exists $conf{list_loaded} && ref $conf{list_loaded} eq 'CODE' );
285              
286 2         8 $self->call_trigger( 'list_loaded', { stash => $stashkey, conf => \%conf } );
287              
288             }
289              
290 11         197 return $obj;
291             }
292              
293             sub rest_reload {
294 6     6 1 1274 my $self = shift;
295 6         11 my $stashkey = shift;
296              
297 6         10 my %conf = @_;
298              
299 6 100       21 my $code = exists $conf{code} ? $conf{code} : 200;
300 6         10 $conf{code} = $code;
301              
302 6 50       8 my @headers = ( @{ $self->fixed_headers() }, @{ $conf{headers} || [] } );
  6         114  
  6         66  
303 6         111 my $item_url = $self->stash->{ $stashkey . '.url' };
304              
305 6 50       12 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       107 ? $self->stash->{ $stashkey . '.prepare_request' }
310             : undef;
311              
312 6 100 66     163 confess 'prepare_request must be a coderef'
313             if $prepare_request && ref $prepare_request ne 'CODE';
314              
315 5         15 my $req = GET $item_url, @headers;
316 5         442 $req->method('GET');
317 5 50       49 $prepare_request->($req) if $prepare_request;
318              
319 5         6825 $self->call_trigger( 'process_request', { req => $req, conf => \%conf } );
320 5         268 my $res = $self->do_request()->($req);
321              
322 5         20122 $self->call_trigger( 'process_response', { req => $req, res => $res, conf => \%conf } );
323              
324 5 50       136 confess 'request code diverge expected' if $code != $res->code;
325              
326 5         56 $self->call_trigger( 'process_response_success', { req => $req, res => $res, conf => \%conf } );
327              
328 5         102 my $obj;
329 5 100       13 if ( $res->code == 200 ) {
    50          
330 4         28 my $obj = eval { $self->decode_response()->($res) };
  4         103  
331 4 50       81 confess("decode_response failed: $@") if $@;
332              
333 4         19 $self->call_trigger( 'response_decoded', { req => $req, res => $res, decoded => $obj, conf => \%conf } );
334              
335 4         178 $self->stash( $stashkey . '.get' => $obj );
336             }
337             elsif ( $res->code == 404 ) {
338              
339 1         19 $self->call_trigger( 'stash_removed', { stash => $stashkey, conf => \%conf } );
340              
341             # $self->stash->{ $stashkey . '.get' };
342 1         73 delete $self->stash->{ $stashkey . '.id' };
343 1         18 delete $self->stash->{ $stashkey . '.url' };
344 1         18 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         44 return $obj;
352             }
353              
354             sub rest_reload_list {
355 3     3 1 7 my $self = shift;
356 3         5 my $stashkey = shift;
357              
358 3         5 my %conf = @_;
359              
360 3 50       11 my $code = exists $conf{code} ? $conf{code} : 200;
361 3         4 $conf{code} = $code;
362              
363 3 50       6 my @headers = ( @{ $self->fixed_headers() }, @{ $conf{headers} || [] } );
  3         50  
  3         38  
364 3         59 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       52 ? $self->stash->{ $stashkey . '.prepare_request' }
371             : undef;
372 3 50 33     46 confess 'prepare_request must be a coderef'
373             if $prepare_request && ref $prepare_request ne 'CODE';
374              
375 3         9 my $req = GET $item_url, @headers;
376 3 50       257 $prepare_request->($req) if $prepare_request;
377              
378 3         3743 $self->call_trigger( 'process_request', { req => $req, conf => \%conf } );
379              
380 3         165 my $res = $self->do_request()->($req);
381              
382 3         11544 $self->call_trigger( 'process_response', { req => $req, res => $res, conf => \%conf } );
383              
384 3 50       83 confess 'request code diverge expected' if $code != $res->code;
385              
386 3         34 $self->call_trigger( 'process_response_success', { req => $req, res => $res, conf => \%conf } );
387              
388 3         71 my $obj;
389 3 50       9 if ( $res->code == 200 ) {
390 3         23 my $obj = eval { $self->decode_response()->($res) };
  3         78  
391 3 50       64 confess("decode_response failed: $@") if $@;
392              
393 3         48 $self->call_trigger( 'response_decoded', { req => $req, res => $res, decoded => $obj, conf => \%conf } );
394              
395 3         135 $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         21 return $obj;
402             }
403              
404             sub stash_ctx {
405 10     10 1 86 my ( $self, $staname, $sub ) = @_;
406              
407 10         36 $self->call_trigger( 'before_stash_ctx', { stash => $staname } );
408              
409 10         493 my @ret = $sub->( $self->stash->{$staname} );
410              
411 10         20657 $self->call_trigger( 'after_stash_ctx', { stash => $staname, results => \@ret } );
412 10         322 return @ret;
413             }
414              
415             1;
416              
417             __END__