File Coverage

blib/lib/RPC/ExtDirect/Request.pm
Criterion Covered Total %
statement 145 149 97.3
branch 46 50 92.0
condition 27 41 65.8
subroutine 20 21 95.2
pod 0 6 0.0
total 238 267 89.1


line stmt bran cond sub pod time code
1             package RPC::ExtDirect::Request;
2              
3 8     8   2977 use strict;
  8         9  
  8         189  
4 8     8   25 use warnings;
  8         8  
  8         180  
5 8     8   32 no warnings 'uninitialized'; ## no critic
  8         10  
  8         208  
6              
7 8     8   25 use Carp;
  8         7  
  8         398  
8              
9 8     8   671 use RPC::ExtDirect::Config;
  8         8  
  8         153  
10 8     8   22 use RPC::ExtDirect::Util::Accessor;
  8         8  
  8         145  
11 8     8   22 use RPC::ExtDirect::Util qw/ clean_error_message /;
  8         6  
  8         10929  
12              
13             ### PACKAGE GLOBAL VARIABLE ###
14             #
15             # Turn on for debugging
16             #
17             # DEPRECATED. Use `debug_request` or `debug` Config options instead.
18             #
19              
20             our $DEBUG;
21              
22             ### PACKAGE GLOBAL VARIABLE ###
23             #
24             # Set Exception class name so it could be configured
25             #
26             # DEPRECATED. Use `exception_class_request` or
27             # `exception_class` Config options instead.
28             #
29              
30             our $EXCEPTION_CLASS;
31              
32             ### PUBLIC CLASS METHOD (ACCESSOR) ###
33             #
34             # Return the list of supported hook types
35             #
36              
37 68     68 0 142 sub HOOK_TYPES { qw/ before instead after / }
38              
39             ### PUBLIC CLASS METHOD (CONSTRUCTOR) ###
40             #
41             # Initializes new instance of RPC::ExtDirect::Request
42             #
43              
44             sub new {
45 71     71 0 153 my ($class, $arg) = @_;
46            
47 71   33     199 my $api = delete $arg->{api} || RPC::ExtDirect->get_api();
48 71   33     153 my $config = delete $arg->{config} || RPC::ExtDirect::Config->new();
49            
50             my $debug = exists $arg->{debug} ? !!(delete $arg->{debug})
51 71 100       865 : $config->debug_request
52             ;
53              
54             # Need blessed object to call private methods
55 71         187 my $self = bless {
56             api => $api,
57             config => $config,
58             debug => $debug,
59             }, $class;
60              
61             # Unpack and validate arguments
62             my ($action_name, $method_name, $tid, $data, $type, $upload, $meta, $aux)
63 71         78 = eval { $self->_unpack_arguments($arg) };
  71         138  
64            
65 71 100       167 return $self->_exception({
66             action => $action_name,
67             method => $method_name,
68             tid => $tid,
69             message => $@->[0],
70             }) if $@;
71              
72             # Look up the Method
73 69         194 my $method_ref = $api->get_method_by_name($action_name, $method_name);
74            
75 69 100       156 return $self->_exception({
76             action => $action_name,
77             method => $method_name,
78             tid => $tid,
79             message => 'ExtDirect action or method not found'
80             }) unless $method_ref;
81              
82             # Check if arguments passed in $data are of right kind
83 65         132 my $exception = $self->check_arguments(
84             action_name => $action_name,
85             method_name => $method_name,
86             method_ref => $method_ref,
87             tid => $tid,
88             data => $data,
89             metadata => $meta,
90             );
91            
92 65 100       158 return $exception if defined $exception;
93            
94             # Bulk assignment for brevity
95 60         285 @$self{ qw/ tid type data metadata upload method_ref run_count aux / }
96             = ($tid, $type, $data, $meta, $upload, $method_ref, 0, $aux);
97            
98             # Finally, resolve the hooks; it's easier to do that upfront
99             # since it involves API lookup
100 60         165 for my $hook_type ( $class->HOOK_TYPES ) {
101 180         371 my $hook = $api->get_hook(
102             action => $action_name,
103             method => $method_name,
104             type => $hook_type,
105             );
106            
107 180 100       1292 $self->$hook_type($hook) if $hook;
108             }
109              
110 60         280 return $self;
111             }
112              
113             ### PUBLIC INSTANCE METHOD ###
114             #
115             # Checks if method arguments are in order
116             #
117              
118             my @checkers = qw/ check_method_arguments check_method_metadata /;
119              
120             my %checker_property = (
121             check_method_arguments => 'data',
122             check_method_metadata => 'metadata',
123             );
124              
125             sub check_arguments {
126 56     56 0 171 my ($self, %arg) = @_;
127            
128 56         68 my $action_name = $arg{action_name};
129 56         48 my $method_name = $arg{method_name};
130 56         53 my $method_ref = $arg{method_ref};
131 56         96 my $tid = $arg{tid};
132              
133             # Event poll handlers return Event objects instead of plain data;
134             # there is no sense in calling them directly
135 56 100       1312 if ( $method_ref->pollHandler ) {
136 1         8 return $self->_exception({
137             action => $action_name,
138             method => $method_name,
139             tid => $tid,
140             message => "ExtDirect pollHandler method ".
141             "$action_name.$method_name should not ".
142             "be called directly"
143             });
144             }
145              
146             else {
147             # One extra check for formHandlers
148 55 100       1039 if ( $method_ref->formHandler ) {
149 14         23 my $data = $arg{data};
150            
151 14 100 66     130 if ( 'HASH' ne ref($data) || !exists $data->{extAction} ||
      66        
152             !exists $data->{extMethod} )
153             {
154 2         22 return $self->_exception({
155             action => $action_name,
156             method => $method_name,
157             tid => $tid,
158             message => "ExtDirect formHandler method ".
159             "$action_name.$method_name should only ".
160             "be called with form submits"
161             })
162             }
163             }
164            
165             # The actual heavy lifting happens in the Method itself
166 53         86 for my $checker ( @checkers ) {
167 105         129 my $what = $checker_property{ $checker };
168 105         99 my $have = $arg{ $what };
169            
170 105         102 local $@;
171            
172 105         105 eval { $method_ref->$checker($have) };
  105         279  
173            
174 105 100       246 if ( my $error = $@ ) {
175 2         7 $error =~ s/\n$//;
176            
177 2         15 return $self->_exception({
178             action => $action_name,
179             method => $method_name,
180             tid => $tid,
181             message => $error,
182             where => ref($method_ref) ."->${checker}",
183             });
184             }
185             }
186             }
187              
188             # undef means no exception
189 51         129 return undef; ## no critic
190             }
191              
192             ### PUBLIC INSTANCE METHOD ###
193             #
194             # Runs the request; returns false value if method died on us,
195             # true otherwise
196             #
197              
198             sub run {
199 61     61 0 8683 my ($self, $env) = @_;
200              
201             # Ensure run() is not called twice
202 61 100       1279 return $self->_set_error("ExtDirect request can't run more than once per batch")
203             if $self->run_count > 0;
204            
205             # Set the flag
206 60         1109 $self->run_count(1);
207            
208 60         1072 my $method_ref = $self->method_ref;
209              
210             # Prepare the arguments
211             my @method_arg = $method_ref->prepare_method_arguments(
212             env => $env,
213             input => $self->{data},
214 60         1094 upload => $self->upload,
215             metadata => $self->metadata,
216             );
217            
218 60         1170 my %params = (
219             api => $self->api,
220             method_ref => $method_ref,
221             env => $env,
222             arg => \@method_arg,
223             metadata => $self->metadata,
224             aux_data => $self->aux,
225             );
226              
227 60         82 my ($run_method, $callee, $result, $exception) = (1);
228              
229             # Run "before" hook if we got one
230 60 100 100     1073 ($result, $exception, $run_method) = $self->_run_before_hook(%params)
231             if $self->before && $self->before->runnable;
232              
233             # If there is "instead" hook, call it instead of the method
234 60 100       248 ($result, $exception, $callee) = $self->_run_method(%params)
235             if $run_method;
236              
237             # Finally, run "after" hook if we got one
238 60 100 66     1149 $self->_run_after_hook(
239             %params,
240             result => $result,
241             exception => $exception,
242             callee => $callee
243             ) if $self->after && $self->after->runnable;
244              
245             # Fail gracefully if method call was unsuccessful
246 60 100       265 return $self->_process_exception($env, $exception)
247             if $exception;
248              
249             # Else stash the results
250 55         79 $self->{result} = $result;
251              
252 55         168 return 1;
253             }
254              
255             ### PUBLIC INSTANCE METHOD ###
256             #
257             # If method call was successful, returns result hashref.
258             # If an error occured, returns exception hashref. It will contain
259             # error-specific message only if we're debugging. This is somewhat weird
260             # requirement in ExtDirect specification. If the debug config option
261             # is not set, the exception hashref will contain generic error message.
262             #
263              
264             sub result {
265 47     47 0 10043 my ($self) = @_;
266              
267 47         84 return $self->_get_result_hashref();
268             }
269              
270             ### PUBLIC INSTANCE METHOD ###
271             #
272             # Return the data represented as a list
273             #
274              
275             sub data {
276 0     0 0 0 my ($self) = @_;
277              
278 0         0 return 'HASH' eq ref $self->{data} ? %{ $self->{data} }
279 0 0       0 : 'ARRAY' eq ref $self->{data} ? @{ $self->{data} }
  0 0       0  
280             : ()
281             ;
282             }
283              
284             ### PUBLIC INSTANCE METHODS ###
285             #
286             # Simple read-write accessors.
287             #
288              
289             my $accessors = [qw/
290             config
291             api
292             debug
293             method_ref
294             type
295             tid
296             state
297             where
298             message
299             upload
300             run_count
301             metadata
302             aux
303             /,
304             __PACKAGE__->HOOK_TYPES,
305             ];
306              
307             RPC::ExtDirect::Util::Accessor::mk_accessors(
308             simple => $accessors,
309             );
310              
311             ############## PRIVATE METHODS BELOW ##############
312              
313             ### PRIVATE INSTANCE METHOD ###
314             #
315             # Return new Exception object
316             #
317              
318             sub _exception {
319 15     15   23 my ($self, $arg) = @_;
320            
321 15         323 my $config = $self->config;
322 15         283 my $ex_class = $config->exception_class_request;
323            
324 15         699 eval "require $ex_class";
325            
326 15         45 my $where = $arg->{where};
327              
328 15 100       38 if ( !$where ) {
329 9         91 my ($package, $sub)
330             = (caller 1)[3] =~ / \A (.*) :: (.*?) \z /xms;
331 9         30 $arg->{where} = $package . '->' . $sub;
332             };
333            
334 15         315 return $ex_class->new({
335             config => $config,
336             debug => $self->debug,
337             verbose => $config->verbose_exceptions,
338             %$arg
339             });
340             }
341              
342             ### PRIVATE INSTANCE METHOD ###
343             #
344             # Replaces Request object with Exception object
345             #
346              
347             sub _set_error {
348 4     4   6 my ($self, $msg, $where) = @_;
349              
350             # Munge $where to avoid it being '_set_error' all the time
351 4 100       10 if ( !defined $where ) {
352 1         8 my ($package, $sub) = (caller 1)[3] =~ / \A (.*) :: (.*?) \z /xms;
353 1         4 $where = $package . '->' . $sub;
354             };
355            
356 4         67 my $method_ref = $self->method_ref;
357              
358             # We need newborn Exception object to tear its guts out
359 4         70 my $ex = $self->_exception({
360             action => $method_ref->action,
361             method => $method_ref->name,
362             tid => $self->tid,
363             message => $msg,
364             where => $where,
365             debug => $self->debug,
366             });
367              
368             # Now the black voodoo magiKC part, live on stage
369 4         40 delete @$self{ keys %$self };
370 4         21 @$self{ keys %$ex } = values %$ex;
371              
372             # Finally, cover our sins with a blessing and we've been born again!
373 4         10 bless $self, ref $ex;
374              
375             # Humbly return failure to be propagated upwards
376 4         28 return !1;
377             }
378              
379             ### PRIVATE INSTANCE METHOD ###
380             #
381             # Unpacks arguments into a list and validates them
382             #
383              
384             my @std_keys = qw/
385             extAction action extMethod method extTID tid data metadata
386             extType type extUpload _uploads
387             /;
388              
389             sub _unpack_arguments {
390 71     71   81 my ($self, $arg) = @_;
391              
392             # Unpack and normalize arguments
393 71   66     199 my $action = $arg->{extAction} || $arg->{action};
394 71   66     425 my $method = $arg->{extMethod} || $arg->{method};
395 71   66     147 my $tid = $arg->{extTID} || $arg->{tid}; # can't be 0
396 71   100     164 my $type = $arg->{type} || 'rpc';
397            
398             # For a formHandler, the "data" field is the form itself;
399             # the arguments are fields in the form-encoded POST body
400 71   66     154 my $data = $arg->{data} || $arg;
401 71         71 my $meta = $arg->{metadata};
402             my $upload = $arg->{extUpload} eq 'true' ? $arg->{_uploads}
403             : undef
404 71 100       138 ;
405              
406             # Throwing arrayref so that die() wouldn't add file/line to the string
407 71 100 66     290 die [ "ExtDirect action (class name) required" ]
408             unless defined $action && length $action > 0;
409              
410 70 100 66     242 die [ "ExtDirect method name required" ]
411             unless defined $method && length $method > 0;
412              
413 69         183 my %arg_keys = map { $_ => 1, } keys %$arg;
  372         504  
414 69         270 delete @arg_keys{ @std_keys };
415              
416             # Collect ancillary data that might be passed in the packet
417             # and make it available to the Hooks. This might be used e.g.
418             # for passing CSRF protection tokens, etc.
419 69         98 my %aux = map { $_ => $arg->{$_} } keys %arg_keys;
  24         52  
420            
421 69 100       154 my $aux_ref = %aux ? { %aux } : undef;
422              
423             return (
424 69         241 $action, $method, $tid, $data, $type, $upload, $meta, $aux_ref
425             );
426             }
427              
428             ### PRIVATE INSTANCE METHOD ###
429             #
430             # Run "before" hook
431             #
432              
433             sub _run_before_hook {
434 26     26   77 my ($self, %arg) = @_;
435            
436 26         35 my ($run_method, $result, $exception) = (1);
437            
438             # This hook may die() with an Exception
439 26         34 local $@;
440 26         34 my $hook_result = eval { $self->before->run(%arg) };
  26         516  
441              
442             # If "before" hook died, cancel Method call
443 26 100       324 if ( $@ ) {
444 3         3 $exception = $@;
445 3         5 $run_method = !1;
446             };
447              
448             # If "before" hook returns anything but number 1,
449             # treat it as an Ext.Direct response and do not call
450             # the actual method
451 26 100       56 if ( $hook_result ne '1' ) {
452 5         6 $result = $hook_result;
453 5         5 $run_method = !1;
454             };
455            
456 26         79 return ($result, $exception, $run_method);
457             }
458              
459             ### PRIVATE INSTANCE METHOD ###
460             #
461             # Runs "instead" hook if it exists, or the method itself
462             #
463              
464             sub _run_method {
465 55     55   127 my ($self, %arg) = @_;
466            
467             # We call methods by code reference
468 55         1100 my $hook = $self->instead;
469 55   66     128 my $run_hook = $hook && $hook->runnable;
470 55 100       1038 my $callee = $run_hook ? $hook : $self->method_ref;
471            
472 55         63 local $@;
473 55         67 my $result = eval { $callee->run(%arg) };
  55         181  
474 55         84 my $exception = $@;
475            
476 55         138 return ($result, $exception, $callee->code);
477             }
478              
479             ### PRIVATE INSTANCE METHOD ###
480             #
481             # Run "after" hook
482             #
483              
484             sub _run_after_hook {
485 24     24   91 my ($self, %arg) = @_;
486            
487             # Localize so that we don't clobber the $@
488 24         26 local $@;
489            
490             # Return value and exceptions are ignored
491 24         27 eval { $self->after->run(%arg) };
  24         452  
492             }
493              
494             ### PRIVATE INSTANCE METHOD ###
495             #
496             # Return result hashref
497             #
498              
499             sub _get_result_hashref {
500 47     47   43 my ($self) = @_;
501            
502 47         993 my $method_ref = $self->method_ref;
503              
504             my $result_ref = {
505             type => 'rpc',
506             tid => $self->tid,
507             action => $method_ref->action,
508             method => $method_ref->name,
509             result => $self->{result}, # To avoid collisions
510 47         1522 };
511              
512 47         138 return $result_ref;
513             }
514              
515             ### PRIVATE INSTANCE METHOD ###
516             #
517             # Process exception message returned by die() in method or hooks
518             #
519              
520             sub _process_exception {
521 5     5   11 my ($self, $env, $exception) = @_;
522              
523             # Stringify exception and treat it as error message
524 5         19 my $msg = clean_error_message("$exception");
525            
526             # Report actual package and method in case we're debugging
527 5         109 my $method_ref = $self->method_ref;
528 5         89 my $where = $method_ref->package .'->'. $method_ref->name;
529              
530 5         17 return $self->_set_error($msg, $where);
531             }
532              
533             1;