File Coverage

blib/lib/RPC/ExtDirect/Client.pm
Criterion Covered Total %
statement 302 311 97.1
branch 82 112 73.2
condition 27 41 65.8
subroutine 48 52 92.3
pod 7 11 63.6
total 466 527 88.4


line stmt bran cond sub pod time code
1             package RPC::ExtDirect::Client;
2              
3 10     10   926991 use strict;
  10         17  
  10         323  
4 10     10   42 use warnings;
  10         11  
  10         253  
5 10     10   38 no warnings 'uninitialized';
  10         11  
  10         294  
6              
7 10     10   38 use Carp;
  10         11  
  10         661  
8 10     10   683 use JSON;
  10         10308  
  10         71  
9              
10 10     10   1227 use File::Spec;
  10         14  
  10         240  
11              
12 10     10   972 use RPC::ExtDirect::Util ();
  10         3024  
  10         155  
13 10     10   1035 use RPC::ExtDirect::Config;
  10         17888  
  10         209  
14 10     10   1262 use RPC::ExtDirect;
  10         25362  
  10         73  
15              
16             #
17             # This module is not compatible with RPC::ExtDirect < 3.0
18             #
19              
20             croak __PACKAGE__." requires RPC::ExtDirect 3.0+"
21             if $RPC::ExtDirect::VERSION lt '3.0';
22              
23             ### PACKAGE GLOBAL VARIABLE ###
24             #
25             # Module version
26             #
27              
28             our $VERSION = '1.20';
29              
30             ### PUBLIC CLASS METHOD (CONSTRUCTOR) ###
31             #
32             # Instantiate a new Client, connect to the specified server
33             # and initialize the Ext.Direct API
34             #
35              
36             sub new {
37 11     11 1 53942 my ($class, %params) = @_;
38            
39 11         87 my $api = delete $params{api};
40 11   66     741 my $config = delete $params{config} || ($api && $api->config) ||
41             RPC::ExtDirect::Config->new();
42            
43 11         14177 my $self = bless {
44             config => $config,
45             api => {},
46             tid => 0,
47             }, $class;
48            
49 11         121 $self->_decorate_config($config);
50 11 100       36 $self->_decorate_api($api) if $api;
51            
52 11         90 my @config_params = qw/
53             api_path router_path poll_path remoting_var polling_var
54             /;
55            
56 11         54 for my $key ( @config_params ) {
57 55 50       116 $config->$key( delete $params{ $key } )
58             if exists $params{ $key };
59             }
60            
61 11         50 my @our_params = qw/ host port cv cookies api_cb /;
62            
63 11         223 @$self{ @our_params } = delete @params{ @our_params };
64            
65             # The rest of parameters apply to the transport
66 11         353 $self->http_params({ %params });
67            
68             # This may die()
69 11         78 eval { $self->_init_api($api) };
  11         68  
70            
71 11 0       51 if ($@) { croak 'ARRAY' eq ref($@) ? $@->[0] : $@ };
  0 50       0  
72            
73 11         106 return $self;
74             }
75              
76             ### PUBLIC INSTANCE METHOD ###
77             #
78             # Call specified Action's Method
79             #
80              
81 32     32 1 33295 sub call { shift->sync_request('call', @_) }
82              
83             ### PUBLIC INSTANCE METHOD ###
84             #
85             # Submit a form to specified Action's Method
86             #
87              
88 9     9 1 12726 sub submit { shift->sync_request('form', @_) }
89              
90             ### PUBLIC INSTANCE METHOD ###
91             #
92             # Upload a file using POST form. Same as submit()
93             #
94              
95             *upload = *submit;
96              
97             ### PUBLIC INSTANCE METHOD ###
98             #
99             # Poll server for Ext.Direct events
100             #
101              
102 11     11 1 6512 sub poll { shift->sync_request('poll', @_) }
103              
104             ### PUBLIC INSTANCE METHOD ###
105             #
106             # Run a specified request type synchronously
107             #
108              
109             sub sync_request {
110 52     52 0 102 my $self = shift;
111 52         136 my $type = shift;
112            
113 52         141 my $tr_class = $self->transaction_class;
114            
115 52         78 my $resp = eval {
116 52         346 my $transaction = $tr_class->new(@_);
117 52         234 $self->_sync_request($type, $transaction);
118             };
119            
120             #
121             # Internally we throw an exception string enclosed in arrayref,
122             # so that die() wouldn't munge it. Easier to do and beats stripping
123             # that \n any time. JSON or other packages could throw a plain string
124             # though, so we need to guard against that.
125             #
126             # Rethrow by croak(), and don't strip the file name and line number
127             # this time -- seeing exactly where the thing blew up in *your*
128             # code is a lot more helpful to a developer than the plain old die()
129             # exception would allow.
130             #
131 52 50       235 if ($@) { croak 'ARRAY' eq ref($@) ? $@->[0] : $@ };
  14 100       1421  
132            
133             # We're only interested in the data, unless it's a poll. In versions
134             # < 1.0, we used to return a scalar value for polls, either a single
135             # event or an arrayref of event hashrefs; that behavior was more or
136             # less closely following the spec and typical server response.
137             # However that was kinda awkward, so we try to DWIM here and adjust
138             # to the caller's expectations.
139 38 100       128 if ( $type eq 'poll') {
140 11 100       106 return wantarray ? @$resp
    100          
141             : @$resp == 1 ? $resp->[0]
142             : $resp
143             ;
144             }
145            
146 27 100       208 return ref($resp) =~ /Exception/ ? $resp : $resp->{result};
147             }
148              
149             ### PUBLIC INSTANCE METHOD ###
150             #
151             # Return next TID (transaction ID)
152             #
153              
154 28     28 0 505 sub next_tid { $_[0]->{tid}++ }
155              
156             ### PUBLIC INSTANCE METHOD ###
157             #
158             # Return API object by its type
159             #
160              
161             sub get_api {
162 81     81 1 1159 my ($self, $type) = @_;
163            
164 81         462 return $self->{api}->{$type};
165             }
166              
167             ### PUBLIC INSTANCE METHOD ###
168             #
169             # Store the passed API object according to its type
170             #
171              
172             sub set_api {
173 22     22 0 39 my ($self, $api, $type) = @_;
174            
175 22   66     691 $type ||= $api->type;
176            
177 22         308 $self->{api}->{$type} = $api;
178             }
179              
180             ### PUBLIC INSTANCE METHODS ###
181             #
182             # Read-only accessor delegates
183             #
184              
185 0     0 1 0 sub remoting_var { $_[0]->config->remoting_var }
186 0     0 1 0 sub polling_var { $_[0]->config->polling_var }
187              
188             ### PUBLIC INSTANCE METHOD ###
189             #
190             # Return the name of the Transaction class. This was not made
191             # a Config option since the only case when somebody would want
192             # to change that is in a subclass.
193             #
194              
195 52     52 0 126 sub transaction_class { 'RPC::ExtDirect::Client::Transaction' }
196              
197             ### PUBLIC INSTANCE METHODS ###
198             #
199             # Read-write accessors
200             #
201              
202             RPC::ExtDirect::Util::Accessor->mk_accessor(
203             simple => [qw/ config host port cv cookies http_params api_cb /],
204             );
205              
206             ############## PRIVATE METHODS BELOW ##############
207              
208             ### PRIVATE INSTANCE METHOD ###
209             #
210             # Create a new Exception object
211             #
212              
213             sub _exception {
214 1     1   3 my ($self, $ex) = @_;
215            
216 1         30 my $config = $self->config;
217 1         24 my $exclass = $config->exception_class;
218            
219 1         63 eval "require $exclass";
220            
221 1         1629 return $exclass->new($ex);
222             }
223              
224             ### PRIVATE INSTANCE METHOD ###
225             #
226             # Add the Client-specific accessors to a Config instance
227             # and set defaults
228             #
229              
230             my %std_config = (
231             api_class_client => 'RPC::ExtDirect::Client::API',
232             transport_class => 'HTTP::Tiny',
233             );
234              
235             sub _decorate_config {
236 11     11   38 my ($self, $config) = @_;
237            
238 11         201 $config->add_accessors(
239             overwrite => 1,
240             simple => [ keys %std_config ],
241             );
242            
243 11         7110 for my $key ( keys %std_config ) {
244 22         166289 my $predicate = "has_${key}";
245            
246 22 50       739 $config->$key( $std_config{ $key } )
247             unless $config->$predicate;
248            
249             # This is the best place to load the classes, too
250             # since we only want to do this once.
251 22         1330 eval "require " . $config->$key;
252             }
253            
254 11         118630 my $std_m_class = 'RPC::ExtDirect::Client::API::Method';
255            
256 11 50       160 $config->api_method_class($std_m_class)
257             if $config->_is_default('api_method_class');
258            
259             # Client uses a flavor of API::Method with disabled
260             # metadata arg checks; since the user might have set
261             # the config value we want to make sure the class
262             # has relevant overrides.
263 11         1082 my $actual_m_class = $config->api_method_class;
264            
265 11 50 33     106 croak __PACKAGE__ . " is configured to use API Method class ".
266             "$actual_m_class that is not a subclass of $std_m_class"
267             unless $actual_m_class eq $std_m_class ||
268             $actual_m_class->isa($std_m_class);
269             }
270              
271             ### PRIVATE INSTANCE METHOD ###
272             #
273             # Make sure that the API instance passed to us is a subclass
274             # of RPC::ExtDirect::Client::API
275             #
276              
277             sub _decorate_api {
278 1     1   2 my ($self, $api) = @_;
279            
280 1         19 my $api_class = $self->config->api_class_client;
281            
282 1 50       42 bless $api, $api_class unless $api->isa($api_class);
283             }
284              
285             ### PRIVATE INSTANCE METHOD ###
286             #
287             # Initialize API declaration.
288             #
289             # The two-step between _init_api and _import_api is to allow
290             # async API retrieval and processing in Client::Async without
291             # duplicating more code than is necessary
292             #
293              
294             sub _init_api {
295 11     11   22 my ($self, $api) = @_;
296            
297 11 100       56 if ( $api ) {
298 1         3 $self->_assign_api($api);
299             }
300             else {
301 10         50 my $api_js = $self->_get_api();
302            
303 10         72 $self->_import_api($api_js);
304             }
305             }
306              
307             ### PRIVATE INSTANCE METHOD ###
308             #
309             # Assign API object to the corresponding slots
310             #
311              
312             sub _assign_api {
313 1     1   2 my ($self, $api) = @_;
314            
315 1         3 $self->set_api($api, 'remoting');
316            
317 1 50       7 if ( $api->get_poll_handlers ) {
318 1         100 $self->set_api($api, 'polling');
319             }
320             }
321              
322             ### PRIVATE INSTANCE METHOD ###
323             #
324             # Receive API declaration from specified server,
325             # parse it and return Client::API object
326             #
327              
328             sub _get_api {
329 10     10   15 my ($self) = @_;
330              
331 10         72 my $uri = $self->_get_uri('api');
332 10         202 my $params = $self->http_params;
333            
334 10         219 my $transport_class = $self->config->transport_class;
335              
336 10         325 my $resp = $transport_class->new(%$params)->get($uri);
337              
338 10 50       241463 die ["Can't download API declaration: $resp->{status} $resp->{content}"]
339             unless $resp->{success};
340              
341 10 50       60 die ["Empty API declaration"] unless length $resp->{content};
342              
343 10         93 return $resp->{content};
344             }
345              
346             ### PRIVATE INSTANCE METHOD ###
347             #
348             # Import specified API into global namespace
349             #
350              
351             sub _import_api {
352 10     10   28 my ($self, $api_js) = @_;
353            
354 10         511 my $config = $self->config;
355 10         393 my $remoting_var = $config->remoting_var;
356 10         326 my $polling_var = $config->polling_var;
357 10         315 my $api_class = $config->api_class_client;
358            
359 10         857 eval "require $api_class";
360            
361 10         2363 $api_js =~ s/\s*//gms;
362            
363 10         105 my @parts = split /;\s*/, $api_js;
364            
365 10         256 my $api_regex = qr/^\Q$remoting_var\E|\Q$polling_var\E/;
366            
367 10         40 for my $part ( @parts ) {
368 20 50       142 next unless $part =~ $api_regex;
369            
370 20         155 my $api = $api_class->new_from_js(
371             config => $config,
372             js => $part,
373             );
374            
375 20         119 $self->set_api($api);
376             }
377             }
378              
379             ### PRIVATE INSTANCE METHOD ###
380             #
381             # Return URI for specified type of call
382             #
383              
384             sub _get_uri {
385 48     48   142 my ($self, $type) = @_;
386            
387 48         1349 my $config = $self->config;
388            
389 48         314 my $api;
390            
391 48 100 100     269 if ( $type eq 'remoting' || $type eq 'polling' ) {
392 38         111 $api = $self->get_api($type);
393            
394 38 50       115 die ["Don't have API definition for type $type"]
395             unless $api;
396             }
397            
398 48         1094 my $host = $self->host;
399 48         1237 my $port = $self->port;
400              
401 48 50 66     1432 my $path = $type eq 'api' ? $config->api_path
    100 66        
    100          
402             : $type eq 'remoting' ? $api->url || $config->router_path
403             : $type eq 'polling' ? $api->url || $config->poll_path
404             : die ["Unknown type $type"]
405             ;
406              
407 48         615 $path =~ s{^/}{};
408              
409 48 50       203 my $uri = $port ? "http://$host:$port/$path"
410             : "http://$host/$path"
411             ;
412              
413 48         104 return $uri;
414             }
415              
416             ### PRIVATE INSTANCE METHOD ###
417             #
418             # Normalize passed arguments to conform to Method's spec
419             #
420              
421             sub _normalize_arg {
422 30     30   36 my ($self, $method, $trans) = @_;
423            
424 30         696 my $arg = $trans->arg;
425            
426             # This could die with a message that has \n at the end to prevent
427             # file and line being appended. Catch and rethrow in a format
428             # more compatible with what Client does in other places.
429 30         162 eval { $method->check_method_arguments($arg) };
  30         163  
430            
431 30 100       2262 if ( my $xcpt = $@ ) {
432 6         26 $xcpt =~ s/\n$//;
433 6         34 die [$xcpt];
434             }
435              
436 24         138 my $result = $method->prepare_method_arguments( input => $arg );
437              
438 24         2900 return $result;
439             }
440              
441             ### PRIVATE INSTANCE METHOD ###
442             #
443             # Normalize passed metadata to conform to Method's spec
444             #
445              
446             sub _normalize_metadata {
447 32     32   46 my ($self, $method, $trans) = @_;
448            
449 32         690 my $meta = $trans->metadata;
450            
451             # See _normalize_arg above
452 32         192 eval { $method->check_method_metadata($meta) };
  32         123  
453            
454 32 100       1915 if ( my $xcpt = $@ ) {
455 5         16 $xcpt =~ s/\n$//;
456 5         26 die [$xcpt];
457             }
458            
459 27         115 my $result = $method->prepare_method_metadata( metadata => $meta );
460            
461 27         1402 return $result;
462             }
463              
464             ### PRIVATE INSTANCE METHOD ###
465             #
466             # Normalize passed arguments to submit as form POST
467             #
468              
469             sub _formalize_arg {
470 9     9   17 my ($self, $method, $trans) = @_;
471            
472 9         228 my $arg = $trans->arg;
473 9         243 my $upload = $trans->upload;
474            
475             # formHandler method require arguments in a hashref and will die
476             # with an error if the arguments are missing. However it is often
477             # convenient to call Client->upload() with empty arg but with a
478             # list of file names to upload; it doesn't make a lot of sense to
479             # insist on providing an empty argument hashref just for the sake
480             # of being strict.
481 9 100 50     80 $arg = $arg || {} if $upload;
482            
483             # This could die with a message that has \n at the end to prevent
484             # file and line being appended. Catch and rethrow in a format
485             # more compatible with what Client does in other places.
486 9         15 eval { $method->check_method_arguments($arg) };
  9         96  
487            
488 9 50       477 if ( my $xcpt = $@ ) {
489 0         0 $xcpt =~ s/\n$//;
490 0         0 die [$xcpt];
491             }
492            
493 9         238 my $fields = {
494             extAction => $method->action,
495             extMethod => $method->name,
496             extType => 'rpc',
497             extTID => $self->next_tid,
498             };
499              
500             # Go over the uploads and check if they're readable; die if not
501 9         50 for my $file ( @$upload ) {
502 4 100       82 die ["Upload entry '$file' is not readable"] unless -r $file;
503             }
504            
505 8 50       33 $fields->{extUpload} = 'true' if $upload;
506              
507 8         54 my $actual_arg = $method->prepare_method_arguments( input => $arg );
508            
509 8         1168 @$fields{ keys %$actual_arg } = values %$actual_arg;
510              
511             # This will die in approved format, so no outer eval
512 8         31 my $meta_json = $self->_formalize_metadata($method, $trans);
513            
514 8 100       25 $fields->{metadata} = $meta_json if $meta_json;
515              
516 8         26 return $fields;
517             }
518              
519             ### PRIVATE INSTANCE METHOD ###
520             #
521             # Normalize passed metadata to conform to Method's spec
522             # and encode in JSON to be submitted in a form POST
523             #
524              
525             sub _formalize_metadata {
526 8     8   15 my ($self, $method, $transaction) = @_;
527            
528 8         11 my $meta_json;
529            
530             # This will die according to plan so no outer eval
531 8         39 my $metadata = $self->_normalize_metadata($method, $transaction);
532            
533 8 100       31 if ( $metadata ) {
534             # This won't die according to plan :(
535 2         4 $meta_json = eval { JSON::to_json($metadata) };
  2         10  
536            
537 2 50       61 if ( $@ ) {
538 0         0 my $xcpt = RPC::ExtDirect::Util::clean_error_message($@);
539 0         0 die [$xcpt];
540             }
541             }
542            
543 8         18 return $meta_json;
544             }
545              
546             ### PRIVATE INSTANCE METHOD ###
547             #
548             # Make an HTTP request in synchronous fashion. Note that we do not
549             # guard against exceptions here, they should be propagated upwards
550             # to be caught in public sync_request() that calls this one.
551             #
552              
553             sub _sync_request {
554 52     52   82 my ($self, $type, $transaction) = @_;
555            
556 52         134 my $prepare = "_prepare_${type}_request";
557 52         95 my $handle = "_handle_${type}_response";
558 52 100       159 my $method = $type eq 'poll' ? 'GET' : 'POST';
559            
560 52         217 my ($uri, $request_content, $http_params, $request_options)
561             = $self->$prepare($transaction);
562            
563 38         92 $request_options->{content} = $request_content;
564            
565 38         790 my $transport_class = $self->config->transport_class;
566            
567 38         1445 my $transport = $transport_class->new(%$http_params);
568 38         3483 my $response = $transport->request($method, $uri, $request_options);
569            
570 38         369077 return $self->$handle($response, $transaction);
571             }
572              
573             ### PRIVATE INSTANCE METHOD ###
574             #
575             # Prepare the POST body, headers, request options and other
576             # data necessary to make an HTTP request for a non-form call
577             #
578              
579             sub _prepare_call_request {
580 32     32   37 my ($self, $transaction) = @_;
581            
582 32         915 my $action_name = $transaction->action;
583 32         861 my $method_name = $transaction->method;
584            
585 32         209 my $api = $self->get_api('remoting');
586 32         190 my $action = $api->get_action_by_name($action_name);
587            
588 32 100       198 die ["Action $action_name is not found"] unless $action;
589            
590 31         120 my $method = $action->method($method_name);
591            
592 31 100       171 die ["Method $method_name is not found in Action $action_name"]
593             unless $method;
594            
595 30         76 my $actual_arg = $self->_normalize_arg($method, $transaction);
596 24         63 my $metadata = $self->_normalize_metadata($method, $transaction);
597            
598 19         85 my $post_body = $self->_encode_post_body(
599             action => $action_name,
600             method => $method_name,
601             data => $actual_arg,
602             metadata => $metadata,
603             );
604            
605             # HTTP params is a union between transaction params and client params.
606 19         115 my $http_params = $self->_merge_params($transaction);
607              
608 19         86 my $request_options = {
609             headers => { 'Content-Type' => 'application/json', }
610             };
611              
612 19         71 $self->_parse_cookies($request_options, $http_params);
613              
614 19         55 my $uri = $self->_get_uri('remoting');
615            
616             return (
617 19         68 $uri,
618             $post_body,
619             $http_params,
620             $request_options,
621             );
622             }
623              
624             ### PRIVATE INSTANCE METHOD ###
625             #
626             # Prepare the POST body, headers, request options and other
627             # data necessary to make an HTTP request for a form call
628             #
629              
630             sub _prepare_form_request {
631 9     9   16 my ($self, $transaction) = @_;
632            
633 9         346 my $action_name = $transaction->action;
634 9         285 my $method_name = $transaction->method;
635            
636 9         85 my $api = $self->get_api('remoting');
637 9         105 my $action = $api->get_action_by_name($action_name);
638            
639 9 50       78 die ["Action $action_name is not found"] unless $action;
640            
641 9         50 my $method = $action->method($method_name);
642            
643 9 50       62 die ["Method $method_name is not found in Action $action_name"]
644             unless $method;
645            
646 9         38 my $fields = $self->_formalize_arg($method, $transaction);
647 8         189 my $upload = $transaction->upload;
648            
649 8 100       99 my $form_body
650             = $upload ? $self->_www_form_multipart($fields, $upload)
651             : $self->_www_form_urlencode($fields)
652             ;
653            
654 8 100       1700 my $ct
655             = $upload ? 'multipart/form-data; boundary='.$self->_get_boundary
656             : 'application/x-www-form-urlencoded; charset=utf-8'
657             ;
658            
659 8         40 my $request_options = {
660             headers => { 'Content-Type' => $ct, },
661             };
662            
663 8         30 my $http_params = $self->_merge_params($transaction);
664            
665 8         42 $self->_parse_cookies($request_options, $http_params);
666            
667 8         33 my $uri = $self->_get_uri('remoting');
668            
669             return (
670 8         45 $uri,
671             $form_body,
672             $http_params,
673             $request_options,
674             );
675             }
676              
677             ### PRIVATE INSTANCE METHOD ###
678             #
679             # Prepare the POST body, headers, request options and other
680             # data necessary to make an HTTP request for an event poll
681             #
682              
683             sub _prepare_poll_request {
684 11     11   23 my ($self, $transaction) = @_;
685            
686 11         39 my $uri = $self->_get_uri('polling');
687            
688 11         71 my $http_params = $self->_merge_params($transaction);
689            
690 11         57 my $request_options = {
691             headers => { 'Content-Type' => 'application/json' },
692             };
693            
694 11         60 $self->_parse_cookies($request_options, $http_params);
695            
696             return (
697 11         27 $uri,
698             undef,
699             $http_params,
700             $request_options,
701             );
702             }
703              
704             ### PRIVATE INSTANCE METHOD ###
705             #
706             # Create POST payload body
707             #
708              
709             sub _create_post_payload {
710 19     19   71 my ($self, %arg) = @_;
711            
712 19         49 my $href = {
713             type => 'rpc',
714             tid => $self->next_tid,
715             action => $arg{action},
716             method => $arg{method},
717             data => $arg{data},
718             };
719            
720 19 50       147 $href->{metadata} = $arg{metadata}
721             if exists $arg{metadata};
722            
723 19         41 return $href;
724             }
725              
726             ### PRIVATE INSTANCE METHOD ###
727             #
728             # Encode post payload body
729             #
730              
731             sub _encode_post_body {
732 19     19   19 my $self = shift;
733            
734 19         51 my $payload = $self->_create_post_payload(@_);
735              
736 19         327 return JSON->new->utf8(1)->encode($payload);
737             }
738              
739             ### PRIVATE INSTANCE METHOD ###
740             #
741             # Encode form fields as multipart/form-data
742             #
743              
744             sub _www_form_multipart {
745 1     1   3 my ($self, $arg, $uploads) = @_;
746              
747             # This code is shamelessly "adapted" from CGI::Test::Input::Multipart
748 1         3 my $CRLF = "\015\012";
749 1         10 my $boundary = '--' . $self->_get_boundary();
750 1         2 my $format = 'Content-Disposition: form-data; name="%s"';
751              
752 1         2 my $result;
753              
754 1         7 while ( my ($field, $value) = each %$arg ) {
755 5         10 $result .= $boundary . $CRLF;
756 5         16 $result .= sprintf($format, $field) . $CRLF.$CRLF;
757 5         24 $result .= $value . $CRLF;
758             };
759              
760 1   66     17 while ( $uploads && @$uploads ) {
761 3         6 my $filename = shift @$uploads;
762 3         55 my $basename = (File::Spec->splitpath($filename))[2];
763              
764 3         12 $result .= $boundary . $CRLF;
765 3         12 $result .= sprintf $format, 'upload';
766 3         9 $result .= sprintf('; filename="%s"', $basename) . $CRLF;
767 3         8 $result .= "Content-Type: application/octet-stream" . $CRLF.$CRLF;
768              
769 3 50       113 if ( open my $fh, '<', $filename ) {
770 3         9 binmode $fh;
771 3         14 local $/;
772              
773 3         110 $result .= <$fh> . $CRLF;
774             };
775             }
776              
777 1 50       6 $result .= $boundary . '--' if $result;
778              
779 1         4 return $result;
780             }
781              
782             ### PRIVATE INSTANCE METHOD ###
783             #
784             # Generate multipart/form-data boundary
785             #
786              
787             my $boundary;
788              
789             sub _get_boundary {
790 2 100   2   10 return $boundary if $boundary;
791            
792 1         2 my $rand;
793              
794 1         4 for ( 0..19 ) {
795 20         68 $rand .= (0..9, 'A'..'Z')[$_] for int rand 36;
796             };
797              
798 1         5 return $boundary = $rand;
799             }
800              
801             ### PRIVATE INSTANCE METHOD ###
802             #
803             # Encode form fields as application/x-www-form-urlencoded
804             #
805              
806             sub _www_form_urlencode {
807 7     7   14 my ($self, $arg) = @_;
808            
809 7         167 my $transport_class = $self->config->transport_class;
810              
811 7         321 return $transport_class->new->www_form_urlencode($arg);
812             }
813              
814             ### PRIVATE INSTANCE METHOD ###
815             #
816             # Produce a union of transaction HTTP parameters
817             # with client HTTP parameters
818             #
819              
820             sub _merge_params {
821 38     38   66 my ($self, $trans) = @_;
822            
823 38         54 my %client_params = %{ $self->http_params };
  38         907  
824 38         281 my %trans_params = %{ $trans->http_params };
  38         801  
825            
826             # Transaction parameters trump client's
827 38         310 @client_params{ keys %trans_params } = values %trans_params;
828            
829             # Cookies from transaction trump client's as well,
830             # but replace them entirely instead of combining
831 38   100     793 $client_params{cookies} = $trans->cookies || $self->cookies;
832            
833 38         1217 return \%client_params;
834             }
835              
836             ### PRIVATE INSTANCE METHOD ###
837             #
838             # Process Ext.Direct response and return either data or exception
839             #
840              
841             sub _handle_call_response {
842 27     27   56 my ($self, $resp) = @_;
843            
844             # By Ext.Direct spec that shouldn't even happen, but then again
845 27 50       99 die ["Ext.Direct request unsuccessful: $resp->{status}"]
846             unless $resp->{success};
847            
848 27 50       96 die [$resp->{content}] if $resp->{status} > 500;
849            
850 27         96 my $content = $self->_decode_response_body( $resp->{content} );
851            
852 27 100 66     328 return $self->_exception($content)
853             if 'HASH' eq ref $content and $content->{type} eq 'exception';
854            
855 26         1700 return $content;
856             }
857              
858             *_handle_form_response = *_handle_call_response;
859              
860             ### PRIVATE INSTANCE METHOD ###
861             #
862             # Handle poll response
863             #
864              
865             sub _handle_poll_response {
866 11     11   30 my ($self, $resp) = @_;
867              
868 11 50       59 die ["Ext.Direct request unsuccessful: $resp->{status}"]
869             unless $resp->{success};
870              
871             # JSON->decode can die()
872 11         51 my $ev = $self->_decode_response_body( $resp->{content} );
873              
874             # Poll provider has to return a null event if there are no events
875             # because returning empty response would break JavaScript client
876             # in certain (now outdated) Ext JS versions. The server has to keep
877             # the compatible behavior but we don't have to follow that
878             # broken implementation here.
879 11 50 66     393 return []
      66        
      66        
      33        
880             if ('HASH' ne ref $ev and 'ARRAY' ne ref $ev) or
881             ('HASH' eq ref $ev and
882             ($ev->{name} eq '__NONE__' or $ev->{name} eq '' or
883             $ev->{type} ne 'event')
884             )
885             ;
886            
887             # Server side can return either a single event, or an array
888             # of events. This is how the spec goes. :/ Normalize the output
889             # here so that we could sanitize it upstream.
890 10 100       287 $ev = 'ARRAY' eq ref($ev) ? $ev : [ $ev ];
891              
892 10         67 delete $_->{type} for @$ev;
893              
894 10         795 return $ev;
895             }
896              
897             ### PRIVATE INSTANCE METHOD ###
898             #
899             # Decode Ext.Direct response body
900             #
901              
902             sub _decode_response_body {
903 38     38   85 my ($self, $body) = @_;
904              
905 38         111 my $json_text = $body;
906              
907             # Form POSTs require this additional handling
908 38         195 my $re = qr{^$}msi;
909              
910 38 100       754 if ( $body =~ $re ) {
911 8         25 $json_text = $1;
912 8         21 $json_text =~ s{\\"}{"}g;
913             };
914              
915 38         922 return JSON->new->utf8(1)->decode($json_text);
916             }
917              
918             ### PRIVATE INSTANCE METHOD ###
919             #
920             # Parse cookies if provided, creating Cookie header
921             #
922              
923             sub _parse_cookies {
924 40     40   18201 my ($self, $to, $from) = @_;
925              
926 40         80 my $cookie_jar = $from->{cookies};
927              
928 40 100       142 return unless $cookie_jar;
929              
930 14         19 my $cookies;
931              
932 14 100       46 if ( 'HTTP::Cookies' eq ref $cookie_jar ) {
933 4         15 $cookies = $self->_parse_http_cookies($cookie_jar);
934             }
935             else {
936 10         32 $cookies = $self->_parse_raw_cookies($cookie_jar);
937             }
938              
939 14 50       73 $to->{headers}->{Cookie} = $cookies if $cookies;
940             }
941              
942             ### PRIVATE INSTANCE METHOD ###
943             #
944             # Parse cookies from HTTP::Cookies object
945             #
946              
947             sub _parse_http_cookies {
948 4     4   8 my ($self, $cookie_jar) = @_;
949              
950 4         7 my @cookies;
951              
952             $cookie_jar->scan(sub {
953 8     8   123 my ($v, $key, $value) = @_;
954              
955 8         32 push @cookies, "$key=$value";
956 4         81 });
957              
958 4         35 return \@cookies;
959             }
960              
961             ### PRIVATE INSTANCE METHOD ###
962             #
963             # Parse (or rather, normalize) cookies passed as a hashref
964             #
965              
966             sub _parse_raw_cookies {
967 10     10   15 my ($self, $cookie_jar) = @_;
968              
969 10 50       37 return [] unless 'HASH' eq ref $cookie_jar;
970              
971 10         31 return [ map { join '=', $_ => $cookie_jar->{$_} } keys %$cookie_jar ];
  20         71  
972             }
973              
974             package
975             RPC::ExtDirect::Client::Transaction;
976              
977             my @fields = qw/ action method arg upload cookies metadata /;
978              
979             sub new {
980 52     52   207 my ($class, %params) = @_;
981            
982 52         114 my %self_params = map { $_ => delete $params{$_} } @fields;
  312         660  
983            
984 52         500 return bless {
985             http_params => { %params },
986             %self_params,
987             }, $class;
988             }
989              
990 0     0     sub start {}
991 0     0     sub finish {}
992              
993             RPC::ExtDirect::Util::Accessor->mk_accessors(
994             simple => ['http_params', @fields],
995             );
996              
997             1;