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