File Coverage

lib/Web/DataService/Document.pm
Criterion Covered Total %
statement 12 184 6.5
branch 0 98 0.0
condition 0 65 0.0
subroutine 4 14 28.5
pod 0 8 0.0
total 16 369 4.3


line stmt bran cond sub pod time code
1             #
2             # Web::DataService::Document
3             #
4             # This module provides a role that is used by 'Web::DataService'. It implements
5             # routines for executing documentation requests.
6             #
7             # Author: Michael McClennen
8              
9 2     2   16 use strict;
  2         6  
  2         98  
10              
11             package Web::DataService::Document;
12              
13 2     2   31 use Carp 'croak';
  2         5  
  2         111  
14 2     2   14 use Scalar::Util qw(reftype weaken);
  2         5  
  2         180  
15              
16 2     2   19 use Moo::Role;
  2         5  
  2         15  
17              
18              
19             # generate_doc ( request )
20             #
21             # Generate and return a documentation page for this request. The accepted
22             # formats, one of which was selected when the request was created, are 'html'
23             # and 'pod'.
24             #
25             # If a documentation template corresponding to the specified path is found, it
26             # will be used. Otherwise, a default template will be used.
27              
28             sub generate_doc {
29            
30 0     0 0   my ($ds, $request) = @_;
31            
32 0           my $path = $request->node_path;
33 0           my $format = $request->output_format;
34            
35             # If this is not a valid request, then return a 404 error.
36            
37             die "404\n" if $request->{is_invalid_request} ||
38 0 0 0       $ds->node_attr($path, 'undocumented') ||
      0        
39             $ds->node_attr($path, 'disabled');
40            
41             # If we are in 'one request' mode, initialize this request's primary
42             # role. If we are not in this mode, then all of the roles will have
43             # been previously initialized.
44            
45 0 0         if ( $Web::DataService::ONE_REQUEST )
46             {
47 0           my $role = $ds->node_attr($path, 'role');
48 0 0         $ds->initialize_role($role) if $role;
49             }
50            
51             # If the output format is not already set, then try to determine what
52             # it should be.
53            
54 0 0         unless ( $format )
55             {
56             # If the special parameter 'format' is enabled, check to see if a
57             # value for that parameter was given.
58              
59 0   0       $request->{raw_params} //= $Web::DataService::FOUNDATION->get_params($request);
60            
61 0   0       $format ||= $request->special_value('format');
62            
63             # Default to HTML.
64            
65 0   0       $format ||= 'html';
66            
67 0           $request->output_format($format);
68             }
69            
70             # We start by determining the values necessary to fill in the documentation
71             # template. This may include one or more of: a title, parameters,
72             # response fields, etc.
73            
74 0   0       my $doc_title = $ds->node_attr($path, 'title') // $path;
75            
76 0           my $vars = { ds => $ds,
77             request => $request,
78             doc_title => $doc_title };
79            
80             # All documentation is public, so set the maximally permissive CORS header.
81            
82 0           $ds->_set_cors_header($request, "*");
83            
84             # Now determine the class that corresponds to this request's primary role
85             # and bless the request into that class.
86            
87 0           my $role = $ds->node_attr($request, 'role');
88 0           bless $request, $ds->documentation_class($role);
89            
90             # Now determine the location of the template for generating this
91             # documentation page. If one has not been specified, we try the path
92             # appended with "/index.tt", and if that does not exist we try the
93             # path appended with "_doc.tt". Or with whatever suffix has been
94             # specified for template files. If none of these template files are
95             # present, we try the documentation error template as a backup.
96            
97 0   0       my $doc_suffix = $ds->{template_suffix} // "";
98            
99 0   0       my $doc_defs = $ds->node_attr($path, 'doc_defs') // $ds->check_doc("doc_defs${doc_suffix}");
100 0   0       my $doc_header = $ds->node_attr($path, 'doc_header') // $ds->check_doc("doc_header${doc_suffix}");
101 0   0       my $doc_footer = $ds->node_attr($path, 'doc_footer') // $ds->check_doc("doc_footer${doc_suffix}");
102            
103             # Now see if we can find a template for this documentation page. If one
104             # was explicitly specified, we try that first. Otherwise, try the node
105             # path suffixed by '_doc' with the template suffix added, and then
106             # '/index' with the template suffix.
107            
108 0           my $doc_template = $ds->node_attr($path, 'doc_template');
109            
110 0 0         if ( defined $doc_template )
111             {
112 0 0         die "404\n" if $doc_template eq '';
113 0 0         croak "template $doc_template: not found\n" unless $ds->check_doc($doc_template);
114             }
115            
116             else
117             {
118 0           my @try_template;
119            
120 0 0         if ( $path eq '/' )
121             {
122 0           push @try_template, 'index' . $doc_suffix;
123             }
124            
125             else
126             {
127 0           push @try_template, $path . '_doc' . $doc_suffix;
128 0           push @try_template, $path . '/index' . $doc_suffix;
129 0 0         push @try_template, $ds->node_attr($path, 'doc_default_op_template')
130             if $ds->node_has_operation($path);
131 0           push @try_template, $ds->node_attr($path, 'doc_default_template');
132             }
133            
134 0           foreach my $t ( @try_template )
135             {
136 0 0         next unless defined $t;
137            
138 0 0         $doc_template = $t, last if $ds->check_doc($t);
139             }
140             }
141            
142 0 0         if ( $ds->debug )
143             {
144 0           print STDERR "---------------\nDocumentation '$path'\n";
145             }
146            
147             # Record this request's URL base so that we have it in order to generate
148             # documentation if necessary.
149            
150 0           $ds->{base_url} = $request->base_url;
151            
152             # Now, if we have found a template that works then render it.
153            
154 0 0         if ( $doc_template )
155             {
156 0           my $doc_string = $ds->render_doc($doc_template, $doc_defs, $doc_header, $doc_footer, $vars);
157            
158             my $url_formatter = sub {
159 0 0   0     if ( $_[0] =~ qr{ ^ (node|op|path) (abs|rel|site)? [:] ( [^#?]* ) (?: [?] ( [^#]* ) )? (?: [#] (.*) )? }xs )
160             {
161 0           my $arg = $1;
162 0   0       my $type = $2 || 'site';
163 0   0       my $path = $3 || '/';
164 0           my $params = $4;
165 0           my $frag = $5;
166 0           my $format;
167            
168 0 0 0       if ( $arg ne 'path' && $path =~ qr{ (.*) [.] ([^.]+) $ }x )
169             {
170 0           $path = $1; $format = $2;
  0            
171             }
172            
173 0           return $request->generate_url({ $arg => $path, type => $type, format => $format,
174             params => $params, fragment => $frag });
175             }
176             else
177             {
178 0           return $_[0];
179             }
180 0           };
181            
182             # If Pod format was requested, return the documentation as is. The
183             # only change we need to make is to convert our special link syntax to
184             # standard Pod syntax.
185            
186 0 0 0       if ( defined $format && $format eq 'pod' )
187             {
188 0           $ds->_set_content_type($request, 'text/plain');
189 0           return $ds->convert_pod_links($doc_string, $url_formatter);
190             }
191            
192             # Otherwise, convert the POD to HTML using the PodParser and return the result.
193            
194             else
195             {
196 0   0       my $stylesheet = $ds->node_attr($path, 'doc_stylesheet') ||
197             $ds->generate_site_url({ path => 'css/dsdoc.css' });
198            
199 0           my $parser = Web::DataService::PodParser->new({ target => 'html', css => $stylesheet,
200             url_formatter => $url_formatter,
201             page_title => $doc_title });
202            
203 0           $parser->parse_string_document($doc_string);
204            
205 0           $ds->_set_content_type($request, 'text/html');
206 0           return $parser->output;
207             }
208             }
209            
210             # If no valid template file was found, we return an error result.
211            
212             else
213             {
214 0           die "404\n";
215             }
216             }
217              
218              
219             # check_for_template ( path )
220             #
221             # Return true if a documentation template exists for the specified node path.
222             # Return false if not. Throw an exception if the file exists but is not
223             # readable.
224              
225             sub check_for_template {
226              
227 0     0 0   my ($ds, $path) = @_;
228            
229 0   0       my $doc_suffix = $ds->{template_suffix} // "";
230            
231 0           my $check1 = $path . '_doc' . $doc_suffix;
232            
233 0 0         return $check1 if $ds->check_doc( $check1 );
234            
235 0           my $check2 = $path . '/index' . $doc_suffix;
236            
237 0 0         return $check2 if $ds->check_doc( $check2 );
238            
239 0           return; # otherwise
240             }
241              
242              
243             # make_doc_node ( path, doc_path )
244             #
245             # Create a documentation node for the specified path, reading the title from
246             # the template file. The second method parameter is the actual (relative)
247             # path of the file on disk.
248              
249             sub make_doc_node {
250            
251 0     0 0   my ($ds, $path, $doc_path) = @_;
252            
253 0           my $new_attrs = { path => $path, title => 'NULL' };
254            
255 0           my $partial_contents = $ds->read_doc_partial($doc_path);
256            
257 0           while ( $partial_contents =~ m{ ^ =for \s+ wds_node \s* (.*) $ }gxmi )
258             {
259 0           my $expr = $1;
260            
261 0           while ( $expr )
262             {
263 0 0         if ( $expr =~ qr{ ^ (\w+) \s* = \s* " ( (?: [^"] | \\{2} | \\" )+ ) " \s* (.*) }xs )
    0          
    0          
264             {
265 0           $expr = $3;
266 0           my $attr = $1;
267 0           my $value = $2;
268 0           $value =~ s{\\{2}}{\\}g;
269            
270 0 0         unless ( $Web::DataService::Node::NODE_DEF{$attr} )
271             {
272 0           die "500 Invalid attribute '$attr' for wds_node\n";
273             }
274            
275 0           $new_attrs->{$attr} = $value;
276             }
277            
278             elsif ( $expr =~ qr{ ^ (\w+) \s* = \s* ( (?: [^;] | \\{2} | \\; )+ ) \s* (.*) }xs )
279             {
280 0           $expr = $3;
281 0           my $attr = $1;
282 0           my $value = $2;
283 0           $value =~ s{\\{2}}{\\}g;
284            
285 0 0         unless ( $Web::DataService::Node::NODE_DEF{$attr} )
286             {
287 0           die "500 Invalid attribute '$attr' for wds_node\n";
288             }
289            
290 0           $new_attrs->{$attr} = $value;
291             }
292            
293             elsif ( $expr =~ qr{ ^ ; \s* (.*) }xs )
294             {
295 0           $expr = $1;
296             }
297            
298             else
299             {
300 0           die "500 Invalid syntax for wds_node: '$expr'\n";
301             }
302             }
303             }
304            
305 0           $ds->_create_path_node($new_attrs, '', '');
306             }
307              
308              
309             # get_nodelist ( )
310             #
311             # Return a list of sub-nodes of the current one. This will include all
312             # sub-nodes with a value for the node attribute 'place', in order by the value
313             # of that attribute.
314              
315             sub get_nodelist {
316              
317 0     0 0   my ($ds, $path) = @_;
318            
319 0           my $node_hash = $ds->{node_list}{$path};
320            
321 0 0         return unless ref $node_hash eq 'HASH';
322            
323 0           return map { @{$node_hash->{$_}} } sort { $a <=> $b } keys %$node_hash;
  0            
  0            
  0            
324             }
325              
326              
327             # document_nodelist ( )
328             #
329             # Return a documentation string in Pod format listing the subnodes (if any)
330             # given for this node. See &list_subnodes above.
331              
332             sub document_nodelist {
333            
334 0     0 0   my ($ds, $path, $options) = @_;
335            
336 0   0       $options ||= {};
337            
338 0           my @list = $ds->get_nodelist($path);
339            
340 0 0         return '' unless @list;
341            
342 0           my $documentation = "=over\n\n";
343            
344 0           foreach my $n ( @list )
345             {
346 0           my $path = $n->{path};
347 0   0       my $title = $n->{title} // $ds->node_attr($path, 'title') // $path;
      0        
348 0   0       my $body = $n->{doc_string} // $ds->node_attr($path, 'doc_string');
349            
350 0           $documentation .= "=item L<$title|node:$path>\n\n";
351            
352 0 0 0       if ( defined $body && $body ne '' )
353             {
354 0           $documentation .= $body;
355             }
356            
357 0 0         if ( $options->{usage} )
358             {
359 0   0       my $usage = $n->{usage} // $ds->node_attr($path,'usage');
360 0 0         my @usage_list = ref $usage eq 'ARRAY' ? @$usage : $usage;
361            
362 0           my $usage_doc = $ds->_make_usage_doc($path, @usage_list);
363            
364 0 0         if ( $usage_doc )
365             {
366 0           $documentation .= "\n" . $options->{usage};
367 0           $documentation .= "\n\n";
368 0           $documentation .= $usage_doc;
369             }
370             }
371            
372 0           $documentation .= "\n\n";
373             }
374            
375 0           $documentation .= "=back\n\n";
376            
377 0           return $documentation;
378             }
379              
380              
381             # document_usage ( )
382             #
383             # Return a documentation string in Pod format describing the usage examples
384             # (if any) given for this node.
385              
386             sub document_usage {
387            
388 0     0 0   my ($ds, $path, $options) = @_;
389            
390 0   0       $options ||= {};
391            
392 0           my $usage = $ds->node_attr($path, 'usage');
393            
394 0 0         my @usage_list = ref $usage eq 'ARRAY' ? @$usage : $usage;
395            
396 0           return $ds->_make_usage_doc($path, @usage_list);
397             }
398              
399              
400             sub _make_usage_doc {
401            
402 0     0     my ($ds, $path, @usage_list) = @_;
403            
404 0           my @urls;
405            
406 0           foreach my $example ( @usage_list )
407             {
408 0 0         next unless defined $example;
409            
410 0 0         if ( $example =~ qr{ ^ html: | ^ text: }xs )
    0          
    0          
411             {
412 0           push @urls, $example;
413             }
414            
415             elsif ( $example =~ qr{ ( / | http:/+ )? ( [^?.#]+ ) (?: [.] ([^?.#]+) ) (?: [?] ( [^#]+ ) )? (?: [#] (.*) )? }xs )
416             {
417 0           my $args = { op => $2 };
418 0 0         $args->{format} = $3 if $3;
419 0 0         $args->{params} = $4 if $4;
420 0 0         $args->{fragment} = $5 if $5;
421 0 0 0       $args->{type} = 'abs' if defined $1 && $1 =~ qr{ ^h }x;
422            
423 0           my $url = $ds->generate_site_url($args);
424 0 0         push @urls, $url if $url;
425             }
426            
427             elsif ( ref $example eq 'HASH' )
428             {
429 0           my $args = { op => $path };
430 0 0         $args->{format} = $example->{format} if $example->{format};
431 0 0         $args->{params} = $example->{params} if $example->{params};
432 0 0         $args->{fragment} = $example->{fragment} if $example->{fragment};
433 0 0         $args->{type} = $example->{type} if $example->{type};
434            
435 0           my $url = $ds->generate_site_url($args);
436 0 0         push @urls, $url if $url;
437             }
438             }
439            
440 0 0         return '' unless @urls;
441            
442 0           my $doc_string = "=over\n\n";
443            
444 0           foreach my $url ( @urls )
445             {
446 0 0         if ( $url =~ qr{ ^ (\w+): (.+) }xs )
447             {
448 0 0         if ( $1 eq 'html' )
    0          
449             {
450 0           $doc_string .= "=for html $2\n\n";
451             }
452            
453             elsif ( $1 eq 'text' )
454             {
455 0           my $rest = $2;
456 0           $doc_string =~ s/\n$//;
457 0           $doc_string .= "$rest\n\n";
458             }
459             }
460            
461             else
462             {
463 0           $doc_string .= "=item *\n\nL<$url>\n\n";
464             }
465             }
466            
467 0           $doc_string .= "=back\n";
468            
469 0           return $doc_string;
470             }
471              
472              
473             # convert_pod_links ( doc_string )
474             #
475             # Convert the contents of all L<...>, L<<...>>, etc. elements to proper links.
476              
477             sub convert_pod_links {
478            
479 0     0 0   my ($ds, $doc_string, $urlgen) = @_;
480            
481 0           $doc_string =~ s{L<<<(.*?)>>>}{'L<<<' . $ds->convert_pod_link($1, $urlgen) . '>>>'}ge;
  0            
482 0           $doc_string =~ s{L<<(.*?)>>}{'L<<' . $ds->convert_pod_link($1, $urlgen) . '>>'}ge;
  0            
483 0           $doc_string =~ s{L<(.*?)>}{'L<' . $ds->convert_pod_link($1, $urlgen). '>'}ge;
  0            
484            
485 0           return $doc_string;
486             }
487              
488              
489             sub convert_pod_link {
490            
491 0     0 0   my ($ds, $target, $urlgen) = @_;
492            
493 0 0         if ( $target =~ qr{ ^ (.*?) \| (.*) }xs )
494             {
495 0           return "$1|" . $urlgen->($2);
496             }
497            
498             else
499             {
500 0           return $urlgen->($target);
501             }
502             }
503              
504             1;