File Coverage

lib/Web/DataService/Node.pm
Criterion Covered Total %
statement 128 256 50.0
branch 66 222 29.7
condition 20 135 14.8
subroutine 15 21 71.4
pod 0 7 0.0
total 229 641 35.7


line stmt bran cond sub pod time code
1             #
2             # Web::DataService::Node
3             #
4             # This module provides a role that is used by 'Web::DataService'. It implements
5             # routines for defining and querying data service nodes.
6             #
7             # Author: Michael McClennen
8              
9 2     2   17 use strict;
  2         4  
  2         89  
10              
11             package Web::DataService::Node;
12              
13 2     2   11 use Carp 'croak';
  2         10  
  2         103  
14 2     2   11 use Scalar::Util 'reftype';
  2         4  
  2         84  
15              
16 2     2   1094 use Moo::Role;
  2         28665  
  2         12  
17              
18              
19             our (%NODE_DEF) = ( path => 'ignore',
20             disabled => 'single',
21             undocumented => 'single',
22             place => 'single',
23             list => 'single',
24             title => 'single',
25             usage => 'single',
26             file_dir => 'single',
27             file_path => 'single',
28             role => 'single',
29             method => 'single',
30             arg => 'single',
31             node_tag => 'set',
32             node_data => 'single',
33             ruleset => 'single',
34             output => 'list',
35             output_label => 'single',
36             optional_output => 'single',
37             summary => 'single',
38             public_access => 'single',
39             default_format => 'single',
40             default_limit => 'single',
41             default_header => 'single',
42             default_datainfo => 'single',
43             default_count => 'single',
44             default_linebreak => 'single',
45             default_save_filename => 'single',
46             stream_theshold => 'single',
47             invalid_request_hook => 'hook',
48             before_execute_hook => 'hook',
49             before_config_hook => 'hook',
50             before_setup_hook => 'hook',
51             before_operation_hook => 'hook',
52             before_output_hook => 'hook',
53             before_record_hook => 'hook',
54             after_serialize_hook => 'hook',
55             post_configure_hook => 'hook', # deprecated
56             use_cache => 'single',
57             allow_method => 'set',
58             allow_format => 'set',
59             allow_vocab => 'set',
60             doc_string => 'single',
61             doc_template => 'single',
62             doc_default_template => 'single',
63             doc_default_op_template => 'single',
64             doc_defs => 'single',
65             doc_header => 'single',
66             doc_footer => 'single',
67             );
68              
69              
70             our (%NODE_NONHERITABLE) = ( title => 1,
71             doc_string => 1,
72             doc_template => 1,
73             place => 1,
74             usage => 1,
75             );
76              
77             our (%NODE_ATTR_DEFAULT) = ( default_header => 1 );
78              
79             our (%EXTENDED_DEF) = ( path => 1,
80             type => 1,
81             name => 1,
82             disp => 1,
83             );
84              
85             # define_node ( attrs... )
86             #
87             # Set up a "path" entry, representing a complete or partial URL path. This
88             # path should have a documentation page, but if one is not defined a template
89             # page will be used along with any documentation strings given in this call.
90             # Any path which represents an operation must be given an 'op' attribute.
91             #
92             # An error will be signalled unless the "parent" path is already defined. In
93             # other words, you cannot define 'a/b/c' unless 'a/b' is defined first.
94              
95             sub define_node {
96            
97 1     1 0 13 my $ds = shift;
98            
99 1         5 my ($package, $filename, $line) = caller;
100            
101 1         2 my ($last_node);
102            
103             # Now we go through the rest of the arguments. Hashrefs define new
104             # nodes, while strings add to the documentation of the node
105             # whose definition they follow.
106            
107 1         3 foreach my $item (@_)
108             {
109             # A hashref defines a new directory.
110            
111 2 50       7 if ( ref $item eq 'HASH' )
    0          
112             {
113             croak "define_node: each definition must include a non-empty value for 'path'\n"
114 2 50 33     11 unless defined $item->{path} && $item->{path} ne '';
115            
116             croak "define_node: invalid path '$item->{path}'\n" if $item->{path} ne '/' &&
117 2 50 66     18 $item->{path} =~ qr{ ^ / | / $ | // | [?#] }xs;
118            
119 2         8 $last_node = $ds->_create_path_node($item, $filename, $line);
120             }
121            
122             elsif ( not ref $item )
123             {
124 0         0 $ds->add_node_doc($last_node, $item);
125             }
126            
127             else
128             {
129 0         0 croak "define_node: the arguments must be a list of hashrefs and strings\n";
130             }
131             }
132            
133 1 50       5 croak "define_node: arguments must include at least one hashref of attributes\n"
134             unless $last_node;
135             }
136              
137              
138              
139             # _create_path_node ( attrs, filename, line )
140             #
141             # Create a new node representing the specified path. Attributes are
142             # inherited, as follows: 'a/b/c' inherits from 'a/b', which inherits from 'a',
143             # which inherits from '/'. If 'a/b' does not exist, then 'a/b/c' inherits
144             # directly from 'a'.
145              
146             sub _create_path_node {
147              
148 2     2   6 my ($ds, $new_attrs, $filename, $line) = @_;
149            
150 2         5 my $path = $new_attrs->{path};
151            
152             # Make sure this path was not already defined by a previous call.
153            
154 2 50       5 if ( defined $ds->{path_defs}{$path} )
155             {
156 0         0 my $filename = $ds->{path_defs}{$path}{filename};
157 0         0 my $line = $ds->{path_defs}{$path}{line};
158 0         0 croak "define_node: '$path' was already defined at line $line of $filename\n";
159             }
160            
161             else
162             {
163 2         9 $ds->{path_defs}{$path} = { filename => $filename, line => $line };
164             }
165            
166             # Create a new node to hold the path attributes.
167            
168 2         5 my $node_attrs = { disabled => 0 };
169            
170             # Then apply the newly specified attributes, checking any list or set
171             # values.
172            
173             KEY:
174 2         7 foreach my $key ( keys %$new_attrs )
175             {
176             croak "define_node '$path': unknown attribute '$key'\n"
177 6 50       18 unless $NODE_DEF{$key};
178            
179 6         11 my $value = $new_attrs->{$key};
180            
181             # If the value is undefined or the empty string, store it and go on to
182             # the next. This means that the value should be considered unset.
183            
184 6 50 33     44 if ( ! defined $value || $value eq '' )
    100          
    50          
    50          
    100          
185             {
186 0         0 $node_attrs->{$key} = $value;
187             }
188            
189             # If the attribute takes a single value, then set the value as
190             # specified.
191            
192             elsif ( $NODE_DEF{$key} eq 'single' )
193             {
194 3         7 $node_attrs->{$key} = $value;
195             }
196            
197             # If it takes a hook value, then the value can be either a list or a
198             # singleton. In either case, each value must be either a code ref or
199             # a string.
200            
201             elsif ( $NODE_DEF{$key} eq 'hook' )
202             {
203 0 0       0 if ( ref $value eq 'ARRAY' )
204             {
205 0         0 foreach my $v ( @$value )
206             {
207 0 0 0     0 croak "define_node '$path': $key has invalid value '$v', must be a code ref or string\n"
208             unless ref $v eq 'CODE' || ! ref $v;
209             }
210             }
211            
212             else
213             {
214 0 0 0     0 croak "define_node '$path': $key has invalid value '$value', must be a code ref or string\n"
215             unless ref $value eq 'CODE' || ! ref $value;
216            
217 0         0 $value = [ $value ];
218             }
219            
220 0         0 $node_attrs->{$key} = $value;
221 0         0 $ds->{hook_enabled}{$key} = 1;
222             }
223            
224             # If the attribute takes a set value, then check that it is
225             # either a single value or a comma-separated list. If any of the
226             # values begin with + or -, then all must.
227            
228             elsif ( $NODE_DEF{$key} eq 'set' )
229             {
230 0 0 0     0 unless ( $value =~ qr{ ^ (?> [\w.:][\w.:-]* | \s*,\s* )* $ }xs ||
231             $value =~ qr{ ^ (?> [+-][\w.:][\w.:-]* | \s*,\s* )* $ }xs )
232             {
233 0         0 croak "define_node '$path': $key has invalid value '$value'\n";
234             }
235            
236 0         0 $node_attrs->{$key} = $value;
237 0 0       0 $ds->{path_compose}{$path}{$key} = 1 if $value =~ qr{ ^ (?> \s*,\s* )* [+-] }xs;
238             }
239            
240             # If the attribute takes a list value, then check that it is either a
241             # single value or a comma-separated list.
242            
243             elsif ( $NODE_DEF{$key} eq 'list' )
244             {
245 1 50       15 unless ( $value =~ qr{ ^ (?> [\w.:-]+ | \s*,\s* )+ $ }xs )
246             {
247 0         0 croak "define_node '$path': $key has invalid value '$value'\n";
248             }
249            
250 1         5 $node_attrs->{$key} = $value;
251             }
252            
253             # Otherwise this attribute is ignored
254            
255             else
256             {
257             }
258             }
259            
260             # Install the node.
261            
262 2         6 $ds->{node_attrs}{$path} = $node_attrs;
263            
264 2         4 my $place = $node_attrs->{place};
265            
266 2 50       6 if ( defined $place )
267             {
268 0   0     0 my $list = $node_attrs->{list} // $ds->path_parent($path);
269            
270 2     2   3284 no warnings;
  2         6  
  2         522  
271 0 0 0     0 if ( $place > 0 && defined $list && $list ne '' )
    0 0        
272             {
273 0         0 push @{$ds->{node_list}{$list}{$place}}, { path => $path };
  0         0  
274             }
275            
276             elsif ( $place ne '0' )
277             {
278 0         0 croak "define_node '$path': invalid value for 'place' - must be a number\n";
279             }
280             }
281            
282             # Now check the attributes to make sure they are consistent:
283            
284 2         8 $ds->_check_path_node($path);
285            
286             # If one of the attributes is 'role', create a new request execution class
287             # for this role unless we are in "one request" mode.
288            
289 2         5 my $role = $ds->node_attr($path, 'role');
290            
291 2 50 33     6 if ( $role and not $Web::DataService::ONE_REQUEST )
292             {
293 0         0 $ds->execution_class($role);
294 0         0 $ds->documentation_class($role);
295             }
296            
297             # Now return the new node.
298            
299 2         7 return $node_attrs;
300             }
301              
302              
303             sub _check_path_node {
304            
305 2     2   5 my ($ds, $path) = @_;
306            
307             # Throw an error if 'role' doesn't specify an existing module.
308            
309 2         9 my $role = $ds->node_attr($path, 'role');
310            
311 2 50       6 if ( $role )
312             {
313 2     2   18 no strict 'refs';
  2         4  
  2         797  
314            
315 0 0       0 croak "define_node '$path': the value of 'role' should be a package name, not a file name\n"
316             if $role =~ qr { [.] pm $ }xs;
317            
318             croak "define_node '$path': you must load the module '$role' before using it as the value of 'role'\n"
319 0 0       0 unless %{ "${role}::" };
  0         0  
320             }
321            
322             # Throw an error if 'method' doesn't specify an existing method
323             # implemented by this role.
324            
325 2         5 my $method = $ds->node_attr($path, 'method');
326            
327 2 50       7 if ( $method )
328             {
329 0 0       0 croak "define_node '$path': method '$method' is not valid unless you also specify its package using 'role'\n"
330             unless defined $role;
331            
332 0 0       0 croak "define_node '$path': '$method' must be a method implemented by '$role'\n"
333             unless $role->can($method);
334             }
335            
336             # Throw an error if more than one of 'file_path', 'file_dir', 'method' are
337             # set.
338            
339 2         5 my $attr_count = 0;
340            
341 2 50       4 $attr_count++ if $method;
342 2 50       5 $attr_count++ if $ds->node_attr($path, 'file_dir');
343 2 50       6 $attr_count++ if $ds->node_attr($path, 'file_path');
344            
345 2 50 33     32 if ( $method && $attr_count > 1 )
    50          
346             {
347 0         0 croak "define_node '$path': you may only specify one of 'method', 'file_dir', 'file_path'\n";
348             }
349            
350             elsif ( $attr_count > 1 )
351             {
352 0         0 croak "define_node '$path': you may only specify one of 'file_dir' and 'file_path'\n";
353             }
354            
355             # Throw an error if any of the specified formats fails to match an
356             # existing format. If any of the formats has a default vocabulary, add it
357             # to the vocabulary list.
358            
359 2         7 my $allow_format = $ds->node_attr($path, 'allow_format');
360            
361 2 50 33     14 if ( ref $allow_format && reftype $allow_format eq 'HASH' )
362             {
363 2         8 foreach my $f ( keys %$allow_format )
364             {
365             croak "define_node '$path': invalid value '$f' for format, no such format has been defined for this data service\n"
366 4 50       12 unless ref $ds->{format}{$f};
367            
368             #my $dv = $ds->{format}{$f}{default_vocab};
369             #$node_attrs->{allow_vocab}{$dv} = 1 if $dv;
370             }
371             }
372            
373             # Throw an error if any of the specified vocabularies fails to match an
374             # existing vocabulary.
375            
376 2         6 my $allow_vocab = $ds->node_attr($path, 'allow_vocab');
377            
378 2 50 33     29 if ( ref $allow_vocab && reftype $allow_vocab eq 'HASH' )
379             {
380 2         9 foreach my $v ( keys %$allow_vocab )
381             {
382             croak "define_node '$path': invalid value '$v' for vocab, no such vocabulary has been defined for this data service\n"
383 4 50       13 unless ref $ds->{vocab}{$v};
384             }
385             }
386            
387             # Throw an error if 'place' is not greater than zero.
388            
389 2         7 my $place = $ds->node_attr($path, 'place');
390            
391 2     2   16 no warnings;
  2         6  
  2         5328  
392            
393 2 50 33     7 if ( defined $place && $place !~ qr{^[0-9]+$} )
394             {
395 0         0 croak "define_node '$path': the value of 'place' must be an integer";
396             }
397            
398 2         5 my $a = 1; # we can stop here when debugging;
399             }
400              
401              
402             our (%LIST_DEF) = ( path => 'single',
403             place => 'single',
404             list => 'single',
405             title => 'single',
406             usage => 'single',
407             doc_string => 'single' );
408              
409             # list_node ( attrs... )
410             #
411             # Add an entry to a node list.
412              
413             sub list_node {
414              
415 0     0 0 0 my $ds = shift;
416            
417 0         0 my ($last_node);
418            
419             # Now we go through the rest of the arguments. Hashrefs define new
420             # list entries, while strings add to the documentation of the entry
421             # whose definition they follow.
422            
423 0         0 foreach my $item (@_)
424             {
425             # A hashref defines a new directory.
426            
427 0 0       0 if ( ref $item eq 'HASH' )
    0          
428             {
429             croak "list_node: each definition must include a non-empty value for 'path'\n"
430 0 0 0     0 unless defined $item->{path} && $item->{path} ne '';
431            
432             croak "list_node: invalid path '$item->{path}'\n" if $item->{path} ne '/' &&
433 0 0 0     0 $item->{path} =~ qr{ ^ / | / $ | // | [?#] }xs;
434            
435 0         0 $last_node = $ds->_create_list_entry($item);
436             }
437            
438             elsif ( not ref $item )
439             {
440 0         0 $ds->add_node_doc($last_node, $item);
441             }
442            
443             else
444             {
445 0         0 croak "list_node: the arguments must be a list of hashrefs and strings\n";
446             }
447             }
448            
449 0 0       0 croak "list_node: arguments must include at least one hashref of attributes\n"
450             unless $last_node;
451             }
452              
453              
454             sub _create_list_entry {
455              
456 0     0   0 my ($ds, $item) = @_;
457            
458             # Start by checking the attributes.
459            
460 0         0 my $path = $item->{path};
461            
462             KEY:
463 0         0 foreach my $key ( keys %$item )
464             {
465             croak "list_node '$path': unknown attribute '$key'\n"
466 0 0       0 unless $NODE_DEF{$key};
467             }
468            
469 0         0 my $place = $item->{place};
470 0         0 my $list = $item->{list};
471            
472 0 0 0     0 croak "list_node '$path': you must specify a numeric value for 'place'\n"
473             unless defined $place && $place =~ qr{^[0-9]+$};
474            
475 0 0 0     0 croak "list_node '$path': you must specify a non-empty value for 'list'\n"
476             unless defined $list && $list ne '';
477            
478             # Then install the item.
479            
480 0 0       0 push @{$ds->{node_list}{$list}{$place}}, $item if $place;
  0         0  
481            
482 0         0 return $item;
483             }
484              
485              
486             # extended_doc ( attrs ... )
487             #
488             # Add extended documentation to one or more nodes. The documentation strings
489             # defined by this call will be used to extend the documentation provided in
490             # the original node definitions. By default, this extended documentation will
491             # be appended to the documentation string (if any) specified in the calls to
492             # 'define_node', for display at the top of the documentation page for each
493             # node. The original documentation strings will be used to document lists of
494             # nodes.
495              
496             sub extended_doc {
497            
498 0     0 0 0 my $ds = shift;
499            
500 0         0 my ($last_node);
501            
502             # Now we go through the rest of the arguments. Hashrefs select or other
503             # elements to be documented, while strings add to the documentation of the
504             # selected element.
505            
506 0         0 foreach my $item (@_)
507             {
508             # A hashref selects a node to be documented.
509            
510 0 0       0 if ( ref $item eq 'HASH' )
    0          
511             {
512             croak "extended_doc: each definition must include a non-empty value for either 'path' or 'type'\n"
513             unless (defined $item->{path} && $item->{path} ne '' ||
514 0 0 0     0 defined $item->{type} && $item->{type} ne '');
      0        
      0        
515            
516             croak "define_node: invalid path '$item->{path}'\n" if $item->{path} ne '/' &&
517 0 0 0     0 $item->{path} =~ qr{ ^ / | / $ | // | [?#] }xs;
518            
519 0         0 $last_node = $ds->_select_extended_doc($item);
520             }
521            
522             elsif ( not ref $item )
523             {
524 0         0 $ds->_add_extended_doc($last_node, $item);
525             }
526            
527             else
528             {
529 0         0 croak "extended_doc: the arguments must be a list of hashrefs and strings\n";
530             }
531             }
532            
533 0 0       0 croak "extended_doc: arguments must include at least one hashref of attributes\n"
534             unless $last_node;
535             }
536              
537              
538             # _select_extended_doc ( attrs )
539             #
540             # Return a reference to the extended documentation record corresponding to the
541             # specified attributes. Create the record if it does not already exist.
542              
543             sub _select_extended_doc {
544            
545 0     0   0 my ($ds, $item) = @_;
546            
547 0   0     0 my $disp = $item->{disp} || '';
548 0   0     0 my $type = $item->{type} || 'node';
549 0         0 my $path = $item->{path};
550 0   0     0 my $name = $path || $item->{name};
551              
552 0 0       0 croak "extended_doc: you must specify either 'name' or 'path' in each set of attributes\n"
553             unless $name;
554            
555             KEY:
556 0         0 foreach my $key ( keys %$item )
557             {
558             croak "extended_doc '$name': unknown attribute '$key'\n"
559 0 0       0 unless $EXTENDED_DEF{$key};
560             }
561            
562 0 0 0     0 croak "extended_doc '$name': value of disp must be either 'replace', 'add' or 'para'\n"
      0        
      0        
563             unless $disp eq '' || $disp eq 'replace' || $disp eq 'add' || $disp eq 'para';
564            
565 0 0       0 if ( $path )
    0          
    0          
566             {
567             croak "extended_doc '$path': you may not specify both 'path' and 'name'\n"
568 0 0       0 if $item->{name};
569            
570 0 0       0 croak "extended_doc '$path': type must be 'node' if you also specify 'path'\n"
571             if $type ne 'node';
572            
573             croak "extended_node '$path': no such node has been defined\n"
574 0 0       0 unless ref $ds->{node_attrs}{$path} eq 'HASH';
575            
576 0   0     0 $ds->{extdoc_node}{$path} ||= { path => $path, disp => 'para', type => 'node' };
577 0 0       0 $ds->{extdoc_node}{$path}{disp} = $disp if $disp;
578 0         0 return $ds->{extdoc_node}{$path};
579             }
580            
581             elsif ( $type eq 'format' )
582             {
583 0 0       0 croak "extended_doc: you must specify either a path or a name for every record\n"
584             unless $name;
585            
586             croak "extended_doc '$name': no such format has been defined\n"
587 0 0       0 unless ref $ds->{format}{$name} eq 'Web::DataService::Format';
588            
589 0   0     0 $ds->{extdoc_format}{$name} ||= { name => $name, disp => 'para', type => 'format' };
590 0 0       0 $ds->{extdoc_format}{$name}{disp} = $disp if $disp;
591 0         0 return $ds->{extdoc_format}{$name};
592             }
593            
594             elsif ( $type eq 'vocab' )
595             {
596 0 0       0 croak "extended_doc: you must specify either a path or a name for every record\n"
597             unless $name;
598            
599             croak "extended_doc '$name': no such vocabulary has been defined\n"
600 0 0       0 unless ref $ds->{format}{$name} eq 'Web::DataService::Vocab';
601            
602 0   0     0 $ds->{extdoc_vocab}{$name} ||= { name => $name, disp => $disp, type => 'vocab' };
603 0 0       0 $ds->{extdoc_vocab}{$name}{disp} = $disp if $disp;
604 0         0 return $ds->{extdoc_vocab}{$name};
605             }
606            
607             else
608             {
609 0 0       0 croak "extended_doc '$name': you must specify an element type, i.e. 'vocab' or 'format'\n"
610             unless $type;
611            
612 0 0       0 croak "extended_doc '$type': you must specify a node path\n"
613             if $type eq 'node';
614            
615 0 0 0     0 croak "extended_doc '$name': invalid type '$type', must be either 'node', 'format' or 'vocab'\n"
      0        
616             unless $type eq 'node' || $type eq 'format' || $type eq 'vocab';
617            
618 0         0 croak "extended_doc '$name': invalid attributes";
619             }
620             }
621              
622              
623             sub _add_extended_doc {
624            
625 0     0   0 my ($ds, $item, $doc) = @_;
626            
627 0 0       0 return unless defined $doc;
628            
629 0   0     0 my $name = $item->{path} || $item->{name};
630            
631 0 0       0 croak "extended_doc '$name': only strings may be added to documentation: $doc is not valid"
632             if ref $doc;
633            
634             # If the string starts with either '>' or '>>', add an extra blank line so
635             # that it becomes a new paragraph. We ignore an initial '!'. If you wish
636             # to mark a node as undocumented, do so in the 'define_node' call.
637            
638 0         0 $doc =~ s{^>>?}{\n}xs;
639 0         0 $doc =~ s{^[!]}{}xs;
640            
641             # Now add the documentation string.
642            
643 0 0       0 $item->{doc_string} = '' unless defined $item->{doc_string};
644 0 0       0 $item->{doc_string} .= "\n" if $item->{doc_string} ne '';
645 0         0 $item->{doc_string} .= $doc;
646             }
647              
648              
649             # node_defined ( path )
650             #
651             # Return true if the specified path has been defined, false otherwise.
652              
653             sub node_defined {
654              
655 0     0 0 0 my ($ds, $path) = @_;
656            
657 0 0       0 return unless defined $path;
658 0 0       0 $path = '/' if $path eq '';
659            
660 0   0     0 return $ds->{node_attrs}{$path} && ! $ds->{node_attrs}{$path}{disabled};
661             }
662              
663              
664             # node_attr ( path, key )
665             #
666             # Return the specified attribute for the given path. These are computed
667             # lazily; if the specified attribute is already in the attribute cache, then
668             # return it. Otherwise, we must look it up.
669              
670             sub node_attr {
671            
672 19     19 0 3779 my ($ds, $path, $key) = @_;
673            
674             # If we are given an object as the value of $path, pull out its
675             # 'node_path' attribute, or else default to the root path '/'.
676            
677 19 50 33     48 if ( ref $path && reftype $path eq 'HASH' )
678             {
679 0   0     0 $path = $path->{node_path} || '/';
680             }
681            
682             # If the specified attribute is in the attribute cache for this path, just
683             # return it. Even if the value is undefined. We need to turn off warnings
684             # for this block, because either of $path or $key may be undefined. The
685             # behavior is correct in any case, we just don't want the warning.
686            
687             {
688 2     2   43 no warnings;
  2         6  
  2         2482  
  19         26  
689 19 100       47 if ( exists $ds->{attr_cache}{$path}{$key} )
690             {
691 2         5 return $ds->{attr_cache}{$path}{$key};
692             #return ref $ds->{attr_cache}{$path}{$key} eq 'ARRAY' ?
693             # @{$ds->{attr_cache}{$path}{$key}} : $ds->{attr_cache}{$path}{$key};
694             }
695             }
696            
697             # If no key is given, or an invalid key is given, then return undefined.
698             # If no path is given, return undefined. If the empty string is given for
699             # the path, return the root attribute.
700            
701 17 50 33     64 return unless $key && defined $NODE_DEF{$key};
702 17 50 33     53 return unless defined $path && $path ne '';
703            
704 17 50       31 $path = '/' if $path eq '';
705            
706 17 50       33 return unless exists $ds->{node_attrs}{$path};
707            
708             # Otherwise, look up what the value should be and store it in the cache.
709            
710 17         37 return $ds->_lookup_node_attr($path, $key);
711             }
712              
713              
714             # _lookup_node_attr ( path, key )
715             #
716             # Look up the specified attribute for the given path. If it is not defined
717             # for the specified path, look for a parent path. If it is not defined for
718             # any of the parents, see if the data service has the specified attribute.
719             # Because this is an internal routine, we skip the 'defined' checks.
720              
721             sub _lookup_node_attr {
722            
723 24     24   43 my ($ds, $path, $key) = @_;
724            
725             # First create an attribute cache for this path if one does not already exist.
726            
727 24   50     45 $ds->{attr_cache}{$path} //= {};
728            
729             # If the attribute is non-heritable, then just cache and return whatever
730             # is defined for this node.
731            
732 24 100       53 if ( $NODE_NONHERITABLE{$key} )
733             {
734 3         18 return $ds->{attr_cache}{$path}{$key} = $ds->{node_attrs}{$path}{$key};
735             }
736            
737             # Otherwise check if the path actually has a value for this attribute.
738             # If it does not, or if the corresponding path_compose entry is set, then
739             # look up the value for the parent node if there is one.
740            
741 21         30 my $inherited_value;
742            
743 21 100 66     54 if ( ! exists $ds->{node_attrs}{$path}{$key} || $ds->{path_compose}{$path}{$key} )
744             {
745 19         36 my $parent = $ds->path_parent($path);
746            
747             # If we have a parent, look up the attribute there and put the value
748             # in the cache for the current path.
749            
750 19 100       31 if ( defined $parent )
751             {
752 7         19 $inherited_value = $ds->_lookup_node_attr($parent, $key);
753             }
754            
755             # Otherwise, if the attribute is defined in the configuration file
756             # then look it up there.
757            
758             else
759             {
760 12         30 my $config_value = $ds->config_value($key);
761            
762 12 50       46 if ( defined $config_value )
    50          
    50          
    100          
    100          
763             {
764 0         0 $inherited_value = $config_value;
765             }
766            
767             # If it is not defined in the configuration file, see if we have a
768             # universal default.
769            
770             elsif ( defined $NODE_ATTR_DEFAULT{$key} )
771             {
772 0         0 $inherited_value = $NODE_ATTR_DEFAULT{$key};
773             }
774            
775             # Otherwise, if this is one of the following attributes, use the
776             # indicated default.
777            
778             elsif ( $key eq 'allow_method' )
779             {
780 0         0 my %default_methods = map { $_ => 1 } @Web::DataService::DEFAULT_METHODS;
  0         0  
781 0         0 $inherited_value = \%default_methods;
782             }
783            
784             elsif ( $key eq 'allow_format' )
785             {
786 2         4 my %default_formats = map { $_ => 1 } @{$ds->{format_list}};
  4         12  
  2         6  
787 2         5 $inherited_value = \%default_formats;
788             }
789            
790             elsif ( $key eq 'allow_vocab' )
791             {
792 2         4 my %default_vocab = map { $_ => 1 } @{$ds->{vocab_list}};
  4         12  
  2         5  
793 2         4 $inherited_value = \%default_vocab;
794             }
795             }
796            
797             # If no value exists for the current path, cache and return the value we
798             # just looked up. Or undef if we didn't find any value.
799            
800 19 50       45 if ( ! exists $ds->{node_attrs}{$path}{$key} )
801             {
802 19         32 $ds->{attr_cache}{$path}{$key} = $inherited_value;
803 19         48 return $ds->{attr_cache}{$path}{$key};
804             }
805             }
806            
807             # If we get here then we need to compose the inherited value with the
808             # value from the current node.
809            
810 2         5 my $new_value;
811            
812             # If the attribute type is 'set', then separate the value by commas. If
813             # we have an inherited value, start with it and add or delete sub-values
814             # as indicated.
815            
816 2 50       12 if ( $NODE_DEF{$key} eq 'set' )
    100          
    50          
817             {
818 0 0       0 $new_value = ref $inherited_value eq 'HASH' ? { %$inherited_value } : { };
819 0   0     0 my $string_value = $ds->{node_attrs}{$path}{$key} // '';
820            
821 0         0 foreach my $v ( split( /\s*,\s*/, $string_value ) )
822             {
823 0 0       0 next unless $v =~ /^([+-])?(.*)/;
824            
825 0 0 0     0 if ( defined $1 && $1 eq '-' )
826             {
827 0         0 delete $new_value->{$2};
828             }
829            
830             else
831             {
832 0         0 $new_value->{$2} = 1;
833             }
834             }
835             }
836            
837             # If the attribute type is 'list', then separate the value by commas and
838             # create a list.
839            
840             elsif ( $NODE_DEF{$key} eq 'list' )
841             {
842 1         4 $new_value = [ ];
843 1   50     6 my $string_value = $ds->{node_attrs}{$path}{$key} // '';
844            
845 1         5 foreach my $v ( split( /\s*,\s*/, $string_value ) )
846             {
847 1 50 33     9 push @$new_value, $v if defined $v && $v ne '';
848             }
849             }
850              
851             # If the attribute type is 'hook', then add the new value to the end of the previous list.
852              
853             elsif ( $NODE_DEF{$key} eq 'hook' )
854             {
855 0 0 0     0 if ( ref $inherited_value eq 'ARRAY' && @$inherited_value )
856             {
857 0         0 $new_value = [ @$inherited_value, @{$ds->{node_attrs}{$path}{$key}} ];
  0         0  
858             }
859            
860             else
861             {
862 0         0 $new_value = $ds->{node_attrs}{$path}{$key};
863             }
864             }
865            
866             # Otherwise, the new value simply overrides any inherited value. This code
867             # path is only here in case path_compose is set mistakenly for some attribute
868             # of type 'single'.
869            
870             else
871             {
872 1         3 $new_value = $ds->{node_attrs}{$path}{$key};
873             }
874            
875             # Stuff the new value into the cache and return it.
876            
877 2         15 return $ds->{attr_cache}{$path}{$key} = $new_value;
878             }
879              
880              
881             # path_parent ( path )
882             #
883             # Return the parent path of the given path. For example, the parent of "a/b"
884             # is "a". The parent of "a" is "/". The parent of "/" or is undefined. So
885             # is the parent of "", though that is not a valid path.
886              
887             sub path_parent {
888            
889 19     19 0 29 my ($ds, $path) = @_;
890            
891             # If $path is defined, we cache the lookup values undef 'path_parent'.
892            
893 19 50       34 return undef unless defined $path;
894 19 100       53 return $ds->{path_parent}{$path} if exists $ds->{path_parent}{$path};
895            
896             # If not found, add it to the cache and return it.
897            
898 2 100 66     23 if ( $path eq '/' || $path eq '' )
    50          
    0          
899             {
900 1         4 return $ds->{path_parent}{$path} = undef;
901             }
902            
903             elsif ( $path =~ qr{ ^ [^/]+ $ }xs )
904             {
905 1         7 return $ds->{path_parent}{$path} = '/';
906             }
907            
908             elsif ( $path =~ qr{ ^ (.+) / [^/]+ }xs )
909             {
910 0         0 return $ds->{path_parent}{$path} = $1;
911             }
912            
913             else
914             {
915 0         0 return $ds->{path_parent}{$path} = undef;
916             }
917             }
918              
919              
920             # add_node_doc ( node, doc_string )
921             #
922             # Add the specified documentation string to the specified node.
923              
924             sub add_node_doc {
925            
926 3     3 0 9 my ($ds, $node, $doc) = @_;
927            
928 3 50       9 return unless defined $doc;
929            
930 3 50       13 croak "only strings may be added to documentation: '$doc' is not valid"
931             if ref $doc;
932            
933             # If the first documentation string starts with !, mark the node as
934             # undocumented and remove the '!'.
935            
936 3 50       9 unless ( $node->{doc_string} )
937             {
938 3 50       25 if ( $doc =~ qr{ ^ ! (.*) }xs )
939             {
940 0         0 $doc = $1;
941 0         0 $node->{undocumented} = 1;
942             }
943             }
944            
945             # Change any initial > or >> into a blank line, to indicate a new
946             # paragraph.
947            
948 3         9 $doc =~ s{^>>?}{\n}xs;
949            
950             # Now add the documentation string.
951            
952 3 50       13 $node->{doc_string} = '' unless defined $node->{doc_string};
953 3 50 33     11 $node->{doc_string} .= "\n" if $node->{doc_string} ne '' && $doc ne '';
954 3         14 $node->{doc_string} .= $doc;
955             }
956              
957              
958             1;