File Coverage

blib/lib/Stash/REST.pm
Criterion Covered Total %
statement 199 207 96.1
branch 86 122 70.4
condition 43 78 55.1
subroutine 21 21 100.0
pod 8 8 100.0
total 357 436 81.8


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