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