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   10328 use strict;
  21         61  
  21         693  
40 21     21   407 use warnings;
  21         45  
  21         754  
41 21     21   134 use feature "state";
  21         45  
  21         2046  
42              
43 21     21   155 use App::CELL qw( $CELL $log $core $meta $site );
  21         59  
  21         3203  
44 21     21   164 use Data::Dumper;
  21         51  
  21         1234  
45 21     21   138 use Exporter qw( import );
  21         51  
  21         792  
46 21     21   139 use Module::Runtime qw( use_module );
  21         68  
  21         214  
47 21     21   1265 use Params::Validate qw( :all );
  21         50  
  21         3978  
48 21     21   5822 use Web::MREST::InitRouter qw( $router $resources );
  21         78  
  21         2906  
49 21     21   6043 use Web::MREST::Util qw( pod_to_html pod_to_text );
  21         64  
  21         1345  
50              
51 21     21   144 use parent 'Web::MREST::Entity';
  21         45  
  21         178  
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 166 $log->debug("Entering " . __PACKAGE__. "::init_router");
309             #
310             # initialize Path::Router singleton
311             #
312 20 50 33     3338 $router = Path::Router->new unless ref( $router ) and $router->can( 'match' );
313             #
314             # load resource definitions
315             #
316 20         78222 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   107 my ( $self, $pass ) = @_;
329              
330 42 100       142 if ( $pass == 1 ) {
331 21         123 $log->debug( "Resource handler first pass, resource always exists" );
332 21         3040 return 1;
333             }
334 21         63 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 18 my ( $self, $pass ) = @_;
346 6         42 $log->debug( "Entering " . __PACKAGE__ . "::handler_bugreport, pass number $pass" );
347              
348             # first pass
349 6 100       887 return 1 if $self->_first_pass_always_exists( $pass );
350              
351             # second pass
352 3         33 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 12 my ( $self, $pass ) = @_;
384 4         26 $log->debug( "Entering " . __PACKAGE__ . "::handler_docu, pass number $pass" );
385              
386             # first pass
387 4 100       528 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         8 my $docu_resource = $self->context->{'request_entity'};
393 2 50       7 if ( $docu_resource ) {
394 2         12 $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         259 my $def = $resources->{$docu_resource};
402 2         8 $log->debug( "handler_docu: resource definition is " . Dumper( $def ) );
403 2 50       654 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         9 my $method = $self->context->{'method'};
411 2         7 my $resource_name = $self->context->{'resource_name'};
412 2         9 my $pl = {
413             'resource' => $docu_resource,
414             };
415 2   50     9 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       9 if ( $resource_name eq 'docu/pod' ) {
    50          
422 1         3 $pl->{'format'} = 'POD';
423 1         3 $pl->{'documentation'} = $docs;
424             } elsif ( $resource_name eq 'docu/html' ) {
425 1         5 $pl->{'format'} = 'HTML';
426 1         7 $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         15 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 15 my ( $self, $pass ) = @_;
444 6         35 $log->debug( "Entering " . __PACKAGE__ . "::handler_echo, pass number $pass" );
445            
446 6 100       751 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         12 $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 62 my ( $self, $pass ) = @_;
462 18         138 $log->debug( "Entering " . __PACKAGE__ . "::handler_param, pass number $pass" );
463              
464             # get parameters
465 18         2828 my $method = $self->context->{'method'};
466 18         54 my $mapping = $self->context->{'mapping'};
467 18         41 my ( $type, $param );
468 18 50       58 if ( $mapping ) {
469 18         46 $type = $self->context->{'mapping'}->{'type'};
470 18         45 $param = $self->context->{'mapping'}->{'param'};
471             } else {
472 0         0 die "AAAHAHAHAAHAAHAAAAAAAA! no mapping?? in handler_param_get";
473             }
474 18         44 my $resource_name = $self->context->{'resource_name'};
475              
476 18         39 my ( $bool, $param_obj );
477 18 100       59 if ( $type eq 'meta' ) {
    50          
    0          
478 16         74 $param_obj = $meta;
479             } elsif ( $type eq 'core' ) {
480 2         5 $param_obj = $core;
481             } elsif ( $type eq 'site' ) {
482 0         0 $param_obj = $site;
483             }
484 18 50       60 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       75 if ( $pass == 1 ) {
491 10         53 $bool = $param_obj->exists( $param );
492 10 100       176 $bool = $bool ? 1 : 0;
493 10 100       52 $self->context->{'stash'}->{'param_value'} = $param_obj->get( $param ) if $bool;
494 10         60 return $bool;
495             }
496              
497             # second pass
498 8 50 66     48 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       40 if ( $method eq 'GET' ) {
    100          
    50          
504             return $CELL->status_ok( 'MREST_PARAMETER_VALUE', payload => {
505 4         16 $param => $self->context->{'stash'}->{'param_value'},
506             } );
507             } elsif ( $method eq 'PUT' ) {
508 3         12 $log->debug( "Request entity: " . Dumper( $self->context->{'request_entity'} ) );
509 3         692 return $param_obj->set( $param, $self->context->{'request_entity'} );
510             } elsif ( $method eq 'DELETE' ) {
511 1         4 delete $param_obj->{$param};
512 1         9 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 62 my ( $self, $pass ) = @_;
528 24         147 $log->debug( "Entering " . __PACKAGE__ . "::noop" );
529              
530             # pass one
531 24 100       3786 return 1 if $self->_first_pass_always_exists( $pass );
532              
533             # pass two
534 12         43 my $method = $self->context->{'method'};
535 12         36 my $resource_name = $self->context->{'resource_name'};
536 12         37 my $def = $resources->{$resource_name};
537             my $pl = {
538             'resource_name' => $resource_name,
539             'description' => $def->{$method}->{'description'},
540             'parent' => $def->{'parent'},
541 12         104 'children' => $def->{'children'},
542             };
543 12         75 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 39 my ( $self, $pass ) = @_;
558              
559 13         40 my $method = $self->context->{'method'};
560 13         37 my $mapping = $self->context->{'mapping'};
561 13 50       47 my $specs = $self->context->{'mapping'}->{'specs'} if $mapping;
562              
563             # first pass
564 13 100       38 if ( $pass == 1 ) {
565 8         27 my $re = 0;
566 8 100       43 if ( not defined $specs ) {
    100          
567 2         12 $log->debug( "handler_test: \$specs is missing and the resource exists" );
568 2         310 $re = 1;
569             } elsif ( $specs eq '0' ) {
570 2         16 $log->debug( "handler_test: \$specs is ->$specs<- and the resource does not exist" );
571             } else {
572 4         54 $log->debug( "handler_test: \$specs is ->$specs<- and the resource exists" );
573 4         643 $re = 1;
574 4 100       19 if ( $method eq 'POST' ) {
575 3 100       15 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         281 return $re;
582             }
583              
584             # second pass
585 5 100       27 if ( $method eq 'GET' ) {
    50          
    0          
    0          
586 1         7 return $self->_test_get( $specs );
587             } elsif ( $method eq 'POST' ) {
588 4         18 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   4 my ( $self, $specs ) = @_;
600              
601 1         9 my $status = $CELL->status_ok( 'TEST_GET_RESOURCE' );
602 1         605 $status->payload( 'DUMMY' );
603 1         11 return $status;
604             }
605              
606             sub _test_post {
607 4     4   10 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         8 my $status;
612 4 100 100     26 if ( not defined $specs or $specs eq '1' ) {
    50          
613             # this post does not create a new resource
614 3         21 $status = $CELL->status_ok( 'TEST_POST_OK' );
615 3         1535 $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         7 $status = $CELL->status_ok( 'TEST_POST_IS_CREATE' );
622             }
623 4         449 $status->payload( 'DUMMY' );
624 4         44 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 6 my ( $self, $pass ) = @_;
671 2         13 $log->debug( "Entering " . __PACKAGE__ . "::handler_version, pass number $pass" );
672              
673             # first pass
674 2 100       248 return 1 if $self->_first_pass_always_exists( $pass );
675              
676             # second pass
677 1         13 my $param = $site->MREST_APPLICATION_MODULE;
678 1         31 my $version = use_module( $param )->version;
679 1 50       11 my $payload = ( $version )
680             ? {
681             'application' => $param,
682             'version' => $version,
683             }
684             : "BUBBA did not find nothin";
685              
686 1         8 return $CELL->status_ok( 'MREST_DISPATCH_VERSION', payload => $payload );
687             }
688              
689              
690             1;
691