File Coverage

blib/lib/RPC/ExtDirect/API.pm
Criterion Covered Total %
statement 185 193 95.8
branch 58 76 76.3
condition 18 34 52.9
subroutine 25 25 100.0
pod 14 14 100.0
total 300 342 87.7


line stmt bran cond sub pod time code
1             package RPC::ExtDirect::API;
2              
3 27     27   2785 use strict;
  27         29  
  27         618  
4 27     27   79 use warnings;
  27         34  
  27         573  
5 27     27   78 no warnings 'uninitialized'; ## no critic
  27         28  
  27         612  
6              
7 27     27   81 use Carp;
  27         27  
  27         1354  
8              
9 27     27   5158 use RPC::ExtDirect::Config;
  27         40  
  27         561  
10 27     27   10032 use RPC::ExtDirect::Serializer;
  27         44  
  27         496  
11 27     27   99 use RPC::ExtDirect::Util::Accessor;
  27         23  
  27         41197  
12              
13             ### PACKAGE GLOBAL VARIABLE ###
14             #
15             # Turn this on for debugging
16             #
17             # DEPRECATED. Use `debug_api` or `debug` Config options instead.
18             #
19              
20             our $DEBUG;
21              
22             ### PUBLIC PACKAGE SUBROUTINE ###
23             #
24             # Does not import anything to caller namespace but accepts
25             # configuration parameters. This method always operates on
26             # the "default" API object stored in RPC::ExtDirect
27             #
28              
29             sub import {
30 38     38   885 my ($class, @args) = @_;
31              
32             # Nothing to do
33 38 100       5041 return unless @args;
34              
35             # Only hash-like arguments are supported
36 8 50       42 croak 'Odd number of arguments in RPC::ExtDirect::API::import()'
37             unless (@args % 2) == 0;
38              
39 8         30 my %arg = @args;
40 8         24 %arg = map { lc $_ => delete $arg{ $_ } } keys %arg;
  28         65  
41            
42             # In most cases that's a formality since RPC::ExtDirect
43             # should be already required elsewhere; some test scripts
44             # may not load it on purpose so we guard against that
45             # just in case. We don't want to `use` RPC::ExtDirect above,
46             # because that would create a circular dependency.
47 8         38 require RPC::ExtDirect;
48              
49 8         29 my $api = RPC::ExtDirect->get_api;
50            
51 8         21 for my $type ( $class->HOOK_TYPES ) {
52 24         28 my $code = delete $arg{ $type };
53            
54 24 100       64 $api->add_hook( type => $type, code => $code )
55             if $code;
56             };
57            
58 8         184 my $api_config = $api->config;
59            
60 8         137 for my $option ( keys %arg ) {
61 18         17 my $value = $arg{$option};
62            
63 18         488 $api_config->$option($value);
64             }
65             }
66              
67             ### PUBLIC CLASS METHOD (ACCESSOR) ###
68             #
69             # Return the hook types supported by the API
70             #
71              
72 83     83 1 208 sub HOOK_TYPES { qw/ before instead after/ }
73              
74             ### PUBLIC CLASS METHOD (CONSTRUCTOR) ###
75             #
76             # Init a new API object
77             #
78              
79             sub new {
80 26     26 1 36 my $class = shift;
81            
82 26 50 33     175 my %arg = @_ == 1 && 'HASH' eq ref($_[0]) ? %{ $_[0] } : @_;
  0         0  
83            
84 26   66     164 $arg{config} ||= RPC::ExtDirect::Config->new();
85            
86 26         178 return bless {
87             %arg,
88             actions => {},
89             }, $class;
90             }
91              
92             ### PUBLIC CLASS METHOD (CONSTRUCTOR) ###
93             #
94             # Init a new API object and populate it from the supplied hashref
95             #
96              
97             sub new_from_hashref {
98 4     4 1 41 my ($class, %arg) = @_;
99            
100 4         7 my $api_href = delete $arg{api_href};
101            
102 4         13 my $self = $class->new(%arg);
103            
104 4         11 $self->init_from_hashref($api_href);
105            
106 4         15 return $self;
107             }
108              
109             ### PUBLIC INSTANCE METHOD ###
110             #
111             # Initialize the API from a hashref
112             #
113              
114             sub init_from_hashref {
115 4     4 1 6 my ($self, $api_href) = @_;
116            
117             # Global hooks go first
118 4         10 for my $type ( $self->HOOK_TYPES ) {
119             $self->add_hook( type => $type, code => delete $api_href->{$type} )
120 12 100       26 if exists $api_href->{$type};
121             }
122            
123 4         13 for my $key ( keys %$api_href ) {
124 14         18 my $action_def = $api_href->{ $key };
125 14         21 my $remote = $action_def->{remote};
126 14 100       20 my $package = $remote ? undef : $key;
127 14 100       24 my $action_name = $remote ? $key : $action_def->{action};
128            
129 14         32 my $action = $self->add_action(
130             action => $action_name,
131             package => $package,
132             no_overwrite => 1,
133             );
134            
135 14 100       50 for my $hook_type ( $remote ? () : $self->HOOK_TYPES ) {
136 18         18 my $hook_code = $action_def->{$hook_type};
137            
138 18 50       26 if ( $hook_code ) {
139 0         0 $self->add_hook(
140             package => $package,
141             type => $hook_type,
142             code => $hook_code,
143             );
144             }
145             }
146            
147 14         18 my $methods = $action_def->{methods};
148            
149 14         34 for my $method_name ( keys %$methods ) {
150 56         61 my $method_def = $methods->{ $method_name };
151            
152 56         148 $self->add_method(
153             action => $action_name,
154             package => $package,
155             method => $method_name,
156             %$method_def
157             );
158             }
159             }
160             }
161              
162             ### PUBLIC INSTANCE METHOD ###
163             #
164             # Returns the JavaScript chunk for REMOTING_API
165             #
166              
167             sub get_remoting_api {
168 8     8 1 3451 my ($class, %arg) = @_;
169            
170 8         12 my ($self, $config);
171            
172             # There is an option to pass config externally; mainly for testing
173 8         13 $config = $arg{config};
174            
175             # Environment object is optional
176 8         12 my $env = $arg{env};
177            
178             # Backwards compatibility: if called as a class method, operate on
179             # the "global" API object instead, and create a new Config instance
180             # as well to take care of possibly-modified-since global variables
181 8 100       23 if ( ref $class ) {
182 5         6 $self = $class;
183 5   33     127 $config ||= $self->config;
184             }
185             else {
186 3         15 require RPC::ExtDirect;
187              
188 3         16 $self = RPC::ExtDirect->get_api();
189 3   66     34 $config ||= $self->config->clone();
190            
191 3         32 $config->read_global_vars();
192             }
193            
194             # Get REMOTING_API hashref
195 8         31 my $remoting_api = $self->_get_remoting_api($config, $env);
196              
197             # Get POLLING_API hashref
198 8         23 my $polling_api = $self->_get_polling_api($config, $env);
199              
200             # Return empty string if we got nothing to declare
201 8 0 33     23 return '' if !$remoting_api && !$polling_api;
202              
203             # Shortcuts
204 8         187 my $remoting_var = $config->remoting_var;
205 8         185 my $polling_var = $config->polling_var;
206 8         163 my $auto_connect = $config->auto_connect;
207 8         173 my $no_polling = $config->no_polling;
208 8         159 my $s_class = $config->serializer_class_api;
209 8         164 my $debug_api = $config->debug_api;
210            
211 8         58 my $serializer = $s_class->new( config => $config );
212            
213 8         33 my $api_json = $serializer->serialize(
214             mute_exceptions => 1,
215             debug => $debug_api,
216             data => [$remoting_api],
217             );
218              
219             # Compile JavaScript for REMOTING_API
220 8   50     64 my $js_chunk = "$remoting_var = " . ($api_json || '{}') . ";\n";
221              
222             # If auto_connect is on, add client side initialization code
223 8 100       32 $js_chunk .= "Ext.direct.Manager.addProvider($remoting_var);\n"
224             if $auto_connect;
225              
226             # POLLING_API is added only when there's something in it
227 8 100 66     34 if ( $polling_api && !$no_polling ) {
228 5         18 $api_json = $serializer->serialize(
229             mute_exceptions => 1,
230             debug => $debug_api,
231             data => [$polling_api],
232             );
233            
234 5   50     25 $js_chunk .= "$polling_var = " . ($api_json || '{}' ) . ";\n";
235              
236             # Same initialization code for POLLING_API if auto connect is on
237 5 100       12 $js_chunk .= "Ext.direct.Manager.addProvider($polling_var);\n"
238             if $auto_connect;
239             };
240              
241 8         130 return $js_chunk;
242             }
243              
244             ### PUBLIC INSTANCE METHOD ###
245             #
246             # Get the list of all defined Actions' names
247             #
248              
249 3     3 1 4 sub actions { keys %{ $_[0]->{actions} } }
  3         16  
250              
251             ### PUBLIC INSTANCE METHOD ###
252             #
253             # Add an Action (class), or update if it exists
254             #
255              
256             sub add_action {
257 85     85 1 215 my ($self, %arg) = @_;
258            
259             $arg{action} = $self->_get_action_name( $arg{package} )
260 85 100       274 unless defined $arg{action};
261            
262 85         106 my $action_name = $arg{action};
263            
264             return $self->{actions}->{ $action_name }
265 85 100 66     292 if $arg{no_overwrite} && exists $self->{actions}->{ $action_name };
266            
267 84         1736 my $config = $self->config;
268 84         1604 my $a_class = $config->api_action_class();
269            
270             # This is to avoid hard binding on the Action class
271 84         3863 eval "require $a_class";
272            
273 84         445 my $action_obj = $a_class->new(
274             config => $config,
275             %arg,
276             );
277            
278 84         182 $self->{actions}->{ $action_name } = $action_obj;
279            
280 84         180 return $action_obj;
281             }
282              
283             ### PUBLIC INSTANCE METHOD ###
284             #
285             # Return Action object by its name
286             #
287              
288             sub get_action_by_name {
289 539     539 1 430 my ($self, $action_name) = @_;
290            
291 539         737 return $self->{actions}->{ $action_name };
292             }
293              
294             ### PUBLIC INSTANCE METHOD ###
295             #
296             # Return Action object by package name
297             #
298              
299             sub get_action_by_package {
300 396     396 1 361 my ($self, $package) = @_;
301            
302 396         330 for my $action ( values %{ $self->{actions} } ) {
  396         946  
303 636 100       12358 return $action if $action->package eq $package;
304             }
305            
306 33         52 return;
307             }
308              
309             ### PUBLIC INSTANCE METHOD ###
310             #
311             # Add a Method, or update if it exists.
312             # Also create the Method's Action if it doesn't exist yet
313             #
314              
315             sub add_method {
316 364     364 1 790 my ($self, %arg) = @_;
317            
318 364         450 my $package = delete $arg{package};
319 364         378 my $action_name = delete $arg{action};
320 364         323 my $method_name = $arg{method};
321            
322             # Try to find the Action by the package name
323 364 100       759 my $action = $action_name ? $self->get_action_by_name($action_name)
324             : $self->get_action_by_package($package)
325             ;
326            
327             # If Action is not found, create a new one
328 364 100       664 if ( !$action ) {
329 33 50       125 $action_name = $self->_get_action_name($package)
330             unless $action_name;
331            
332 33         110 $action = $self->add_action(
333             action => $action_name,
334             package => $package,
335             );
336             }
337            
338             # Usually redefining a Method means a typo or something
339 364 0       1771 croak "Attempting to redefine Method '$method_name' ".
    50          
340             ($package ? "in package $package" : "in Action '$action_name'")
341             if $action->can($method_name);
342            
343 364         808 $action->add_method(\%arg);
344             }
345              
346             ### PUBLIC INSTANCE METHOD ###
347             #
348             # Return the Method object by Action and Method name
349             #
350              
351             sub get_method_by_name {
352 93     93 1 130 my ($self, $action_name, $method_name) = @_;
353            
354 93         150 my $action = $self->get_action_by_name($action_name);
355            
356 93 100       170 return unless $action;
357            
358 90         216 return $action->method($method_name);
359             }
360              
361             ### PUBLIC INSTANCE METHOD ###
362             #
363             # Add a hook instance
364             #
365              
366             sub add_hook {
367 52     52 1 121 my ($self, %arg) = @_;
368            
369 52         67 my $package = $arg{package};
370 52         56 my $action_name = $arg{action};
371 52         56 my $method_name = $arg{method};
372 52         51 my $type = $arg{type};
373 52         70 my $code = $arg{code};
374            
375 52         1095 my $hook_class = $self->config->api_hook_class;
376            
377             # This is to avoid hard binding on RPC::ExtDirect::API::Hook
378 52         57 { local $@; eval "require $hook_class"; }
  52         54  
  52         2060  
379            
380 52         207 my $hook = $hook_class->new( type => $type, code => $code );
381            
382 52 100 66     168 if ( $package || $action_name ) {
383 40         44 my $action;
384            
385 40 50       66 if ( $package ) {
386 40         126 $action = $self->get_action_by_package($package);
387            
388 40 50       134 croak "Can't find the Action for package '$package'"
389             unless $action;
390             }
391             else {
392 0         0 $action = $self->get_action_by_name($action_name);
393            
394 0 0       0 croak "Can't find the '$action_name' Action"
395             unless $action;
396             }
397            
398 40 50       78 if ( $method_name ) {
399 0         0 my $method = $action->method($method_name);
400            
401 0 0       0 croak "Can't find Method '$method_name'"
402             unless $method;
403            
404 0         0 $method->$type($hook);
405             }
406             else {
407 40         845 $action->$type($hook);
408             }
409             }
410             else {
411 12         282 $self->$type($hook);
412             }
413            
414 52         511 return $hook;
415             }
416              
417             ### PUBLIC INSTANCE METHOD ###
418             #
419             # Return the hook object by Method name, Action or package, and type
420             #
421              
422             sub get_hook {
423 411     411 1 770 my ($self, %arg) = @_;
424            
425             my ($action_name, $package, $method_name, $type)
426 411         569 = @arg{qw/ action package method type/};
427            
428 411 100       662 my $action = $action_name ? $self->get_action_by_name($action_name)
429             : $self->get_action_by_package($package)
430             ;
431            
432 411 50 0     610 croak "Can't find action '", ($action_name || $package),
433             "' for Method $method_name"
434             unless $action;
435            
436 411         675 my $method = $action->method($method_name);
437            
438 411   100     7590 my $hook = $method->$type || $action->$type || $self->$type;
439            
440 411         924 return $hook;
441             }
442              
443             ### PUBLIC INSTANCE METHOD ###
444             #
445             # Return the list of all installed poll handlers
446             #
447              
448             sub get_poll_handlers {
449 9     9 1 6 my ($self) = @_;
450            
451 9         8 my @handlers;
452            
453             ACTION:
454 9         9 for my $action ( values %{ $self->{actions} } ) {
  9         21  
455 17         42 my @methods = map { $action->method($_) }
  9         14  
456             $action->polling_methods();
457            
458 17         24 push @handlers, @methods;
459             }
460            
461 9         20 return @handlers;
462             }
463              
464             ### PUBLIC INSTANCE METHODS ###
465             #
466             # Simple read-write accessors
467             #
468              
469             my $accessors = [qw/
470             config
471             /,
472             __PACKAGE__->HOOK_TYPES,
473             ];
474              
475             RPC::ExtDirect::Util::Accessor::mk_accessors(
476             simple => $accessors,
477             );
478              
479             ############## PRIVATE METHODS BELOW ##############
480              
481             ### PRIVATE CLASS METHOD ###
482             #
483             # Prepare REMOTING_API hashref
484             #
485              
486             sub _get_remoting_api {
487 8     8   16 my ($self, $config, $env) = @_;
488              
489 8         7 my %api;
490            
491 8         15 my %actions = %{ $self->{actions} };
  8         45  
492            
493             ACTION:
494 8         35 while ( my ($name, $action) = each %actions ) {
495             # Get the list of methods for Action
496 33         67 my @methods = $action->remoting_api($env);
497              
498 33 100       77 next ACTION unless @methods;
499            
500 26         110 $api{ $name } = [ @methods ];
501             };
502              
503             # Compile hashref
504 8         186 my $remoting_api = {
505             url => $config->router_path,
506             type => 'remoting',
507             actions => { %api },
508             };
509              
510             # Add timeout if it's defined
511 8 100       162 $remoting_api->{timeout} = $config->timeout
512             if $config->timeout;
513              
514             # Add maxRetries if it's defined
515 8 100       150 $remoting_api->{maxRetries} = $config->max_retries
516             if $config->max_retries;
517              
518             # Add namespace if it's defined
519 8 100       157 $remoting_api->{namespace} = $config->namespace
520             if $config->namespace;
521              
522 8         24 return $remoting_api;
523             }
524              
525             ### PRIVATE CLASS METHOD ###
526             #
527             # Returns POLLING_API definition hashref
528             #
529              
530             sub _get_polling_api {
531 8     8   14 my ($self, $config, $env) = @_;
532            
533             # Check if we have any poll handlers in our definitions
534 8         6 my $has_poll_handlers;
535            
536 8         10 my %actions = %{ $self->{actions} };
  8         29  
537            
538             ACTION:
539 8         151 while ( my ($name, $action) = each %actions ) {
540 24         62 $has_poll_handlers = $action->has_pollHandlers($env);
541              
542 24 100       82 last ACTION if $has_poll_handlers;
543             };
544              
545             # No sense in setting up polling if there ain't no Event providers
546 8 100       29 return undef unless $has_poll_handlers; ## no critic
547            
548             # Got poll handlers, return definition hashref
549             return {
550 5         94 type => 'polling',
551             url => $config->poll_path,
552             };
553             }
554              
555             ### PRIVATE INSTANCE METHOD ###
556             #
557             # Make an Action name from a package name (strip namespace)
558             #
559              
560             sub _get_action_name {
561 60     60   76 my ($self, $action_name) = @_;
562            
563 60 50       1337 if ( $self->config->api_full_action_names ) {
564 0         0 $action_name =~ s/::/./g;
565             }
566             else {
567 60         212 $action_name =~ s/^.*:://;
568             }
569            
570 60         121 return $action_name;
571             }
572              
573             1;