File Coverage

blib/lib/Web/MREST/Dispatch.pm
Criterion Covered Total %
statement 141 179 78.7
branch 53 82 64.6
condition 7 11 63.6
subroutine 22 25 88.0
pod 9 9 100.0
total 232 306 75.8


line stmt bran cond sub pod time code
1             # *************************************************************************
2             # Copyright (c) 2014-2015-2015, SUSE LLC
3             #
4             # All rights reserved.
5             #
6             # Redistribution and use in source and binary forms, with or without
7             # modification, are permitted provided that the following conditions are met:
8             #
9             # 1. Redistributions of source code must retain the above copyright notice,
10             # this list of conditions and the following disclaimer.
11             #
12             # 2. Redistributions in binary form must reproduce the above copyright
13             # notice, this list of conditions and the following disclaimer in the
14             # documentation and/or other materials provided with the distribution.
15             #
16             # 3. Neither the name of SUSE LLC nor the names of its contributors may be
17             # used to endorse or promote products derived from this software without
18             # specific prior written permission.
19             #
20             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30             # POSSIBILITY OF SUCH DAMAGE.
31             # *************************************************************************
32              
33             # ------------------------
34             # This package contains handlers.
35             # ------------------------
36              
37             package Web::MREST::Dispatch;
38              
39 21     21   8063 use strict;
  21         28  
  21         489  
40 21     21   70 use warnings;
  21         23  
  21         440  
41 21     21   71 use feature "state";
  21         21  
  21         1629  
42              
43 21     21   112 use App::CELL qw( $CELL $log $core $meta $site );
  21         23  
  21         2213  
44 21     21   81 use Data::Dumper;
  21         22  
  21         846  
45 21     21   68 use Exporter qw( import );
  21         29  
  21         533  
46 21     21   73 use Module::Runtime qw( use_module );
  21         27  
  21         136  
47 21     21   793 use Params::Validate qw( :all );
  21         30  
  21         2881  
48 21     21   5677 use Web::MREST::InitRouter qw( $router $resources );
  21         40  
  21         2441  
49 21     21   6089 use Web::MREST::Util qw( pod_to_html pod_to_text );
  21         41  
  21         1035  
50              
51 21     21   92 use parent 'Web::MREST::Entity';
  21         23  
  21         150  
52              
53             =head1 NAME
54              
55             App::MREST::Dispatch - Resource handlers
56              
57              
58              
59              
60             =head1 DESCRIPTION
61              
62             Your application should not call any of the routines in this module directly.
63             They are called by L<Web::MREST::Resource> during the course of request processing.
64             What your application can do is provide its own resource handlers.
65              
66             The resource handlers are called as ordinary functions with a sole argument:
67             the MREST context.
68              
69             =cut
70              
71              
72              
73             =head1 INITIALIZATION/RESOURCE DEFINITIONS
74              
75             In this section we provide definitions of all resources handled by this module.
76             These are picked up by L<Web::MREST::InitRouter>.
77              
78             =cut
79              
80             our @EXPORT_OK = qw( init_router );
81             our $resource_defs = {
82              
83             # root resource
84             '/' => {
85             handler => 'handler_noop',
86             description => 'The root resource',
87             documentation => <<'EOH',
88             =pod
89              
90             This resource is the parent of all resources that do not specify
91             a parent in their resource definition.
92             EOH
93             },
94            
95             # bugreport
96             'bugreport' =>
97             {
98             parent => '/',
99             handler => {
100             GET => 'handler_bugreport',
101             },
102             cli => 'bugreport',
103             description => 'Display instructions for reporting bugs in Web::MREST',
104             documentation => <<'EOH',
105             =pod
106              
107             Returns a JSON structure containing instructions for reporting bugs.
108             EOH
109             },
110            
111             # configinfo
112             'configinfo' =>
113             {
114             parent => '/',
115             handler => {
116             GET => 'handler_configinfo',
117             },
118             cli => 'configinfo',
119             description => 'Display information about Web::MREST configuration',
120             documentation => <<'EOH',
121             =pod
122              
123             Returns a list of directories that were scanned for configuration files.
124             EOH
125             },
126            
127             # docu
128             'docu' =>
129             {
130             parent => '/',
131             handler => 'handler_noop',
132             cli => 'docu',
133             description => 'Access on-line documentation (via POST to appropriate subresource)',
134             documentation => <<'EOH',
135             =pod
136              
137             This resource provides access to on-line documentation through its
138             subresources: 'docu/pod', 'docu/html', and 'docu/text'.
139              
140             To get documentation on a resource, send a POST reqeuest for one of
141             these subresources, including the resource name in the request
142             entity as a bare JSON string (i.e. in double quotes).
143             EOH
144             },
145            
146             # docu/pod
147             'docu/pod' =>
148             {
149             parent => 'docu',
150             handler => {
151             POST => 'handler_docu',
152             },
153             cli => 'docu pod $RESOURCE',
154             description => 'Display POD documentation of a resource',
155             documentation => <<'EOH',
156             =pod
157            
158             This resource provides access to on-line help documentation in POD format.
159             It expects to find a resource name (e.g. "employee/eid/:eid" including the
160             double-quotes, and without leading or trailing slash) in the request body. It
161             returns a string containing the POD source code of the resource documentation.
162             EOH
163             },
164            
165             # docu/html
166             'docu/html' =>
167             {
168             parent => 'docu',
169             handler => {
170             POST => 'handler_docu',
171             },
172             cli => 'docu html $RESOURCE',
173             description => 'Display HTML documentation of a resource',
174             documentation => <<'EOH',
175             =pod
176              
177             This resource provides access to on-line help documentation. It expects to find
178             a resource name (e.g. "employee/eid/:eid" including the double-quotes, and without
179             leading or trailing slash) in the request body. It generates HTML from the
180             resource documentation's POD source code.
181             EOH
182             },
183            
184             # docu/text
185             'docu/text' =>
186             {
187             parent => 'docu',
188             handler => {
189             POST => 'handler_docu',
190             },
191             cli => 'docu text $RESOURCE',
192             description => 'Display resource documentation in plain text',
193             documentation => <<'EOH',
194             =pod
195              
196             This resource provides access to on-line help documentation. It expects to find
197             a resource name (e.g. "employee/eid/:eid" including the double-quotes, and without
198             leading or trailing slash) in the request body. It returns a plain text rendering
199             of the POD source of the resource documentation.
200             EOH
201             },
202            
203             # echo
204             'echo' =>
205             {
206             parent => '/',
207             handler => {
208             POST => 'handler_echo',
209             },
210             cli => 'echo [$JSON]',
211             description => 'Echo the request body',
212             documentation => <<'EOH',
213             =pod
214              
215             This resource simply takes whatever content body was sent and echoes it
216             back in the response body.
217             EOH
218             },
219            
220             # noop
221             'noop' =>
222             {
223             parent => '/',
224             handler => 'handler_noop',
225             cli => 'noop',
226             description => 'A resource that does nothing',
227             documentation => <<'EOH',
228             =pod
229              
230             Regardless of anything, this resource does nothing at all.
231             EOH
232             },
233            
234             # param/:type/:param
235             'param/:type/:param' =>
236             {
237             parent => '/',
238             handler => {
239             'GET' => 'handler_param',
240             'PUT' => 'handler_param',
241             'DELETE' => 'handler_param',
242             },
243             cli => {
244             'GET' => 'param $TYPE $PARAM',
245             'PUT' => 'param $TYPE $PARAM $VALUE',
246             'DELETE' => 'param $TYPE $PARAM',
247             },
248             description => {
249             'GET' => 'Display value of a meta/core/site parameter',
250             'PUT' => 'Set value of a parameter (meta only)',
251             'DELETE' => 'Delete a parameter (meta only)',
252             },
253             documentation => <<'EOH',
254             =pod
255              
256             This resource can be used to look up (GET) meta, core, and site parameters,
257             as well as to set (PUT) and delete (DELETE) meta parameters.
258             EOH
259             validations => {
260             'type' => qr/^(meta)|(core)|(site)$/,
261             'param' => qr/^[[:alnum:]_][[:alnum:]_-]+$/,
262             },
263             },
264            
265             # test/?:specs
266             'test/?:specs' =>
267             {
268             parent => '/',
269             handler => 'handler_test',
270             cli => 'test [$SPECS]',
271             description => "Resources for testing resource handling semantics",
272             },
273            
274             # version
275             'version' =>
276             {
277             parent => '/',
278             handler => {
279             GET => 'handler_version',
280             },
281             cli => 'version',
282             description => 'Display application name and version',
283             documentation => <<'EOH',
284             =pod
285              
286             Shows the software version running on the present instance. The version displayed
287             is taken from the C<$VERSION> package variable of the package specified in the
288             C<MREST_APPLICATION_MODULE> site parameter.
289             EOH
290             },
291              
292             };
293              
294              
295              
296             =head1 FUNCTIONS
297              
298             =cut
299              
300             =head2 init_router
301              
302             Initialize (populate) the router. Called from Resource.pm when the first
303             request comes waltzing in.
304              
305             =cut
306              
307             sub init_router {
308 20     20 1 132 $log->debug("Entering " . __PACKAGE__. "::init_router");
309             #
310             # initialize Path::Router singleton
311             #
312 20 50 33     2141 $router = Path::Router->new unless ref( $router ) and $router->can( 'match' );
313             #
314             # load resource definitions
315             #
316 20         45877 Web::MREST::InitRouter::load_resource_defs( $resource_defs );
317             # ... might need to be called multiple times ...
318             }
319              
320              
321             =head2 _first_pass_always_exists
322              
323             Boilerplate code for use in handlers of resources that always exist
324              
325             =cut
326              
327             sub _first_pass_always_exists {
328 42     42   60 my ( $self, $pass ) = @_;
329              
330 42 100       94 if ( $pass == 1 ) {
331 21         89 $log->debug( "Resource handler first pass, resource always exists" );
332 21         1796 return 1;
333             }
334 21         52 return 0;
335             }
336              
337              
338             =head2 handler_bugreport
339              
340             Handler for the C<bugreport> resource.
341              
342             =cut
343              
344             sub handler_bugreport {
345 6     6 1 9 my ( $self, $pass ) = @_;
346 6         29 $log->debug( "Entering " . __PACKAGE__ . "::handler_bugreport, pass number $pass" );
347              
348             # first pass
349 6 100       528 return 1 if $self->_first_pass_always_exists( $pass );
350              
351             # second pass
352 3         23 return $CELL->status_ok( 'MREST_DISPATCH_BUGREPORT',
353             payload => { report_bugs_to => $site->MREST_REPORT_BUGS_TO },
354             );
355             }
356              
357              
358             =head2 handler_configinfo
359              
360             Handler for the C<configinfo> resource.
361              
362             =cut
363              
364             sub handler_configinfo {
365 0     0 1 0 my ( $self, $pass ) = @_;
366 0         0 $log->debug( "Entering " . __PACKAGE__ . "::handler_configinfo, pass number $pass" );
367              
368             # first pass
369 0 0       0 return 1 if $self->_first_pass_always_exists( $pass );
370              
371             # second pass
372 0         0 return $CELL->status_ok( 'MREST_DISPATCH_CONFIGINFO',
373             payload => $meta->CELL_META_SITEDIR_LIST,
374             );
375             }
376              
377              
378             =head2 handler_docu
379              
380             =cut
381              
382             sub handler_docu {
383 4     4 1 5 my ( $self, $pass ) = @_;
384 4         40 $log->debug( "Entering " . __PACKAGE__ . "::handler_docu, pass number $pass" );
385              
386             # first pass
387 4 100       329 return 1 if $self->_first_pass_always_exists( $pass );
388              
389             # '/docu/...' resources only
390              
391             # the resource to be documented should be in the request body - if not, return 400
392 2         6 my $docu_resource = $self->context->{'request_entity'};
393 2 50       5 if ( $docu_resource ) {
394 2         10 $log->debug( "handler_docu: request body is ->$docu_resource<-" );
395             } else {
396 0         0 $self->mrest_declare_status( 'code' => 400, 'explanation' => 'Missing request entity' );
397 0         0 return $CELL->status_not_ok;
398             }
399              
400             # the resource should be defined - if not, return 404
401 2         157 my $def = $resources->{$docu_resource};
402 2         6 $log->debug( "handler_docu: resource definition is " . Dumper( $def ) );
403 2 50       486 if ( ref( $def ) ne 'HASH' ) {
404 0         0 $self->mrest_declare_status( 'code' => 404, 'explanation' => 'Undefined resource' );
405 0         0 $log->debug( "Resource not defined: " . Dumper( $docu_resource ) );
406 0         0 return $CELL->status_not_ok;
407             }
408              
409             # all green - assemble the requested documentation
410 2         7 my $method = $self->context->{'method'};
411 2         4 my $resource_name = $self->context->{'resource_name'};
412 2         5 my $pl = {
413             'resource' => $docu_resource,
414             };
415 2   50     5 my $docs = $def->{'documentation'} || <<"EOH";
416             =pod
417              
418             The definition of resource $docu_resource lacks a 'documentation' property
419             EOH
420             # if they want POD, give them POD; if they want HTML, give them HTML, etc.
421 2 100       8 if ( $resource_name eq 'docu/pod' ) {
    50          
422 1         2 $pl->{'format'} = 'POD';
423 1         3 $pl->{'documentation'} = $docs;
424             } elsif ( $resource_name eq 'docu/html' ) {
425 1         2 $pl->{'format'} = 'HTML';
426 1         5 $pl->{'documentation'} = pod_to_html( $docs );
427             } else {
428             # fall back to plain text
429 0         0 $pl->{'format'} = 'text';
430 0         0 $pl->{'documentation'} = pod_to_text( $docs );
431             }
432 2         6 return $CELL->status_ok( 'MREST_DISPATCH_ONLINE_DOCUMENTATION', payload => $pl );
433             }
434              
435              
436             =head2 handler_echo
437              
438             Echo request body back in the response
439              
440             =cut
441              
442             sub handler_echo {
443 6     6 1 9 my ( $self, $pass ) = @_;
444 6         31 $log->debug( "Entering " . __PACKAGE__ . "::handler_echo, pass number $pass" );
445            
446 6 100       537 return 1 if $self->_first_pass_always_exists( $pass );
447              
448             # second call - just echo, nothing else
449             return $CELL->status_ok( "ECHO_REQUEST_ENTITY", payload =>
450 3         10 $self->context->{'request_entity'} );
451             }
452              
453              
454             =head2 handler_param
455              
456             Handler for 'param/:type/:param' resource.
457              
458             =cut
459              
460             sub handler_param {
461 18     18 1 21 my ( $self, $pass ) = @_;
462 18         73 $log->debug( "Entering " . __PACKAGE__ . "::handler_param, pass number $pass" );
463              
464             # get parameters
465 18         1490 my $method = $self->context->{'method'};
466 18         27 my $mapping = $self->context->{'mapping'};
467 18         16 my ( $type, $param );
468 18 50       26 if ( $mapping ) {
469 18         28 $type = $self->context->{'mapping'}->{'type'};
470 18         27 $param = $self->context->{'mapping'}->{'param'};
471             } else {
472 0         0 die "AAAHAHAHAAHAAHAAAAAAAA! no mapping?? in handler_param_get";
473             }
474 18         26 my $resource_name = $self->context->{'resource_name'};
475              
476 18         12 my ( $bool, $param_obj );
477 18 100       29 if ( $type eq 'meta' ) {
    50          
    0          
478 16         26 $param_obj = $meta;
479             } elsif ( $type eq 'core' ) {
480 2         2 $param_obj = $core;
481             } elsif ( $type eq 'site' ) {
482 0         0 $param_obj = $site;
483             }
484 18 50       25 if ( ! $param_obj) {
485 0         0 $self->mrest_declare_status( code => '500', explanation => 'IMPROPER TYPE' );
486 0         0 return 0;
487             }
488              
489             # first pass
490 18 100       26 if ( $pass == 1 ) {
491 10         22 $bool = $param_obj->exists( $param );
492 10 100       74 $bool = $bool ? 1 : 0;
493 10 100       21 $self->context->{'stash'}->{'param_value'} = $param_obj->get( $param ) if $bool;
494 10         22 return $bool;
495             }
496              
497             # second pass
498 8 50 66     21 if ( $type ne 'meta' and $method =~ m/^(PUT)|(DELETE)$/ ) {
499 0         0 $self->mrest_declare_status( code => 400, explanation =>
500             'PUT and DELETE can be used with meta parameters only' );
501 0         0 return $CELL->status_not_ok;
502             }
503 8 100       59 if ( $method eq 'GET' ) {
    100          
    50          
504             return $CELL->status_ok( 'MREST_PARAMETER_VALUE', payload => {
505 4         8 $param => $self->context->{'stash'}->{'param_value'},
506             } );
507             } elsif ( $method eq 'PUT' ) {
508 3         7 $log->debug( "Request entity: " . Dumper( $self->context->{'request_entity'} ) );
509 3         385 return $param_obj->set( $param, $self->context->{'request_entity'} );
510             } elsif ( $method eq 'DELETE' ) {
511 1         2 delete $param_obj->{$param};
512 1         6 return $CELL->status_ok( 'MREST_PARAMETER_DELETED', payload => {
513             'type' => $type,
514             'param' => $param,
515             } );
516             }
517             }
518              
519              
520             =head2 handler_noop
521              
522             Generalized handler for resources that don't do anything.
523              
524             =cut
525              
526             sub handler_noop {
527 24     24 1 26 my ( $self, $pass ) = @_;
528 24         87 $log->debug( "Entering " . __PACKAGE__ . "::noop" );
529              
530             # pass one
531 24 100       1957 return 1 if $self->_first_pass_always_exists( $pass );
532              
533             # pass two
534 12         36 my $method = $self->context->{'method'};
535 12         20 my $resource_name = $self->context->{'resource_name'};
536 12         19 my $def = $resources->{$resource_name};
537             my $pl = {
538             'resource_name' => $resource_name,
539             'description' => $def->{$method}->{'description'},
540             'parent' => $def->{'parent'},
541 12         50 'children' => $def->{'children'},
542             };
543 12         48 return $CELL->status_ok( 'MREST_DISPATCH_NOOP',
544             payload => $pl
545             );
546             }
547              
548              
549             =head2 handler_test
550              
551             The only purpose of this resource is testing/demonstration of request
552             handling.
553              
554             =cut
555              
556             sub handler_test {
557 13     13 1 17 my ( $self, $pass ) = @_;
558              
559 13         29 my $method = $self->context->{'method'};
560 13         34 my $mapping = $self->context->{'mapping'};
561 13 50       39 my $specs = $self->context->{'mapping'}->{'specs'} if $mapping;
562              
563             # first pass
564 13 100       28 if ( $pass == 1 ) {
565 8         8 my $re = 0;
566 8 100       29 if ( not defined $specs ) {
    100          
567 2         10 $log->debug( "handler_test: \$specs is missing and the resource exists" );
568 2         172 $re = 1;
569             } elsif ( $specs eq '0' ) {
570 2         12 $log->debug( "handler_test: \$specs is ->$specs<- and the resource does not exist" );
571             } else {
572 4         19 $log->debug( "handler_test: \$specs is ->$specs<- and the resource exists" );
573 4         325 $re = 1;
574 4 100       14 if ( $method eq 'POST' ) {
575 3 100       11 if ( $specs ne '1' ) {
576 1         4 $self->context->{'post_is_create'} = 1;
577 1         3 $self->context->{'create_path'} = $self->context->{'uri_path'};
578             }
579             }
580             }
581 8         191 return $re;
582             }
583              
584             # second pass
585 5 100       18 if ( $method eq 'GET' ) {
    50          
    0          
    0          
586 1         3 return $self->_test_get( $specs );
587             } elsif ( $method eq 'POST' ) {
588 4         13 return $self->_test_post( $specs );
589             } elsif ( $method eq 'PUT' ) {
590 0         0 return $self->_test_put( $specs );
591             } elsif ( $method eq 'DELETE' ) {
592 0         0 return $self->_test_delete( $specs );
593             } else {
594 0         0 return $CELL->status_crit( 'ERROR_UNSUPPORTED_METHOD' );
595             }
596             }
597              
598             sub _test_get {
599 1     1   7 my ( $self, $specs ) = @_;
600              
601 1         6 my $status = $CELL->status_ok( 'TEST_GET_RESOURCE' );
602 1         352 $status->payload( 'DUMMY' );
603 1         8 return $status;
604             }
605              
606             sub _test_post {
607 4     4   5 my ( $self, $specs ) = @_;
608             # $specs cannot be 0, but can be anything else, including undef
609             # we interpret the values '1' and undef to mean post_is_create is false
610              
611 4         6 my $status;
612 4 100 100     22 if ( not defined $specs or $specs eq '1' ) {
    50          
613             # this post does not create a new resource
614 3         12 $status = $CELL->status_ok( 'TEST_POST_OK' );
615 3         802 $self->context->{'post_is_create'} = 0;
616             } elsif ( $specs eq '0' ) {
617             # already handled in caller
618 0         0 die "AAAADAHDDAAAAADDDDGGAAAA!";
619             } else {
620             # pretend that this POST creates a new resource
621 1         5 $status = $CELL->status_ok( 'TEST_POST_IS_CREATE' );
622             }
623 4         267 $status->payload( 'DUMMY' );
624 4         30 return $status;
625             }
626              
627             sub _test_put {
628 0     0   0 my ( $self, $specs ) = @_;
629 0 0       0 my $bool = $specs ? 1 : 0;
630              
631 0         0 my $status;
632 0 0       0 if ( $specs ) {
633             # pretend that the resource already existed
634 0         0 $status = $CELL->status_ok( 'TEST_PUT_RESOURCE_EXISTS' );
635             } else {
636             # pretend that a new resource was created
637 0         0 $status = $CELL->status_ok( 'TEST_PUT_NEW_RESOURCE_CREATED' );
638             }
639 0         0 $status->payload( 'DUMMY' );
640 0         0 return $status;
641             }
642              
643             sub _test_delete {
644 0     0   0 my ( $self, $specs ) = @_;
645 0 0       0 my $bool = $specs ? 1 : 0;
646              
647 0         0 my $status;
648 0 0       0 if ( $specs ) {
649             # pretend we deleted something
650 0         0 $status = $CELL->status_ok( 'TEST_RESOURCE_DELETED' );
651             } else {
652             # resource didn't exist
653 0         0 $status = $CELL->status_not_ok( 'TEST_NON_EXISTENT_RESOURCE', args => [ 'DELETE' ], );
654             # we have to force 404 here - due to how Web::Machine handles DELETE
655 0         0 $self->mrest_declare_status( 'code' => 404,
656             explanation => 'Request to delete non-existent resource; nothing to do' );
657             }
658 0         0 $status->payload( 'DUMMY' );
659 0         0 return $status;
660             }
661              
662              
663             =head2 handler_version
664              
665             Handler for the C<version> resource.
666              
667             =cut
668              
669             sub handler_version {
670 2     2 1 3 my ( $self, $pass ) = @_;
671 2         9 $log->debug( "Entering " . __PACKAGE__ . "::handler_version, pass number $pass" );
672              
673             # first pass
674 2 100       171 return 1 if $self->_first_pass_always_exists( $pass );
675              
676             # second pass
677 1         8 my $param = $site->MREST_APPLICATION_MODULE;
678 1         19 my $version = use_module( $param )->version;
679 1 50       6 my $payload = ( $version )
680             ? {
681             'application' => $param,
682             'version' => $version,
683             }
684             : "BUBBA did not find nothin";
685              
686 1         6 return $CELL->status_ok( 'MREST_DISPATCH_VERSION', payload => $payload );
687             }
688              
689              
690             1;
691