File Coverage

lib/Web/DataService/Format.pm
Criterion Covered Total %
statement 47 90 52.2
branch 15 64 23.4
condition 16 56 28.5
subroutine 6 8 75.0
pod 0 4 0.0
total 84 222 37.8


line stmt bran cond sub pod time code
1             #
2             # Web::DataService::Format
3             #
4             # This module provides a role that is used by 'Web::DataService'. It implements
5             # routines for defining and documenting output formats.
6             #
7             # Author: Michael McClennen
8              
9 2     2   15 use strict;
  2         4  
  2         88  
10              
11             package Web::DataService::Format;
12              
13 2     2   11 use Carp 'croak';
  2         4  
  2         89  
14 2     2   13 use Data::Dumper;
  2         4  
  2         98  
15              
16 2     2   15 use Moo::Role;
  2         4  
  2         14  
17              
18              
19             our (%FORMAT_DEF) = (name => 'ignore',
20             suffix => 'single',
21             title => 'single',
22             content_type => 'single',
23             disposition => 'single',
24             uses_header => 'single',
25             is_text => 'single',
26             encode_as_text => 'single',
27             default_vocab => 'single',
28             doc_node => 'single',
29             module => 'single',
30             package => 'single',
31             doc_string => 'single',
32             undocumented => 'single',
33             disabled => 'single');
34              
35             our (%FORMAT_CT) = (json => 'application/json',
36             txt => 'text/plain',
37             tsv => 'text/tab-separated-values',
38             csv => 'text/csv',
39             xml => 'text/xml');
40              
41             our (%FORMAT_CLASS) = (json => 'Web::DataService::Plugin::JSON',
42             txt => 'Web::DataService::Plugin::Text',
43             tsv => 'Web::DataService::Plugin::Text',
44             csv => 'Web::DataService::Plugin::Text',
45             xml => 'Web::DataService::Plugin::XML');
46              
47              
48             # define_format ( attrs... )
49             #
50             # Define one or more formats for data service responses.
51              
52             sub define_format {
53              
54 1     1 0 12 my $ds = shift;
55            
56 1         2 my ($last_node);
57            
58             # Now we go through the rest of the arguments. Hashrefs define new
59             # vocabularies, while strings add to the documentation of the vocabulary
60             # whose definition they follow.
61            
62 1         2 foreach my $item (@_)
63             {
64             # A hashref defines a new vocabulary.
65            
66 4 100       17 if ( ref $item eq 'HASH' )
    50          
67             {
68             # Make sure the attributes include 'name'.
69            
70 2         5 my $name = $item->{name};
71            
72 2 50       4 croak "define_format: the attributes must include 'name'" unless defined $name;
73            
74             # Make sure this format was not already defined by a previous call.
75            
76 2 50       7 croak "define_format: '$name' was already defined" if defined $ds->{format}{$name};
77            
78             # Create a new record to represent this format and check the attributes.
79            
80 2         10 my $record = bless { name => $name }, 'Web::DataService::Format';
81            
82 2         9 foreach my $k ( keys %$item )
83             {
84 8 50       19 croak "define_format: invalid attribute '$k'" unless $FORMAT_DEF{$k};
85            
86 8         12 my $v = $item->{$k};
87            
88 8 0 33     19 if ( $k eq 'default_vocab' && defined $v && $v ne '' )
      33        
89             {
90             croak "define_format: unknown vocabulary '$v'"
91 0 0       0 unless ref $ds->{vocab}{$v};
92            
93             croak "define_format: cannot default to disabled vocabulary '$v'"
94 0 0 0     0 if $ds->{vocab}{$v}{disabled} and not $item->{disabled};
95             }
96            
97 8         19 $record->{$k} = $item->{$k};
98             }
99            
100             # Set defaults and check values.
101            
102 2   66     11 $record->{content_type} ||= $FORMAT_CT{$name};
103 2 50 0     22 $record->{uses_header} //= 1 if $name eq 'txt' || $name eq 'tsv' || $name eq 'csv';
      33        
      33        
104             $record->{is_text} //= 1 if $record->{content_type} =~ /(x(?:ht)?ml|text|json|javascript)/
105 2 100 50     24 || $record->{encode_as_text};
      66        
106            
107             croak "define_format: you must specify an HTTP content type for format '$name' using the attribute 'content_type'"
108 2 50       7 unless $record->{content_type};
109            
110 2   66     11 $record->{package} //= $record->{module};
111 2   66     8 $record->{package} //= $FORMAT_CLASS{$name};
112            
113             croak "define_format: you must specify a package to implement format '$name' using the attribute 'module'"
114 2 50       5 unless defined $record->{package};
115            
116 2   66     8 $record->{module} //= $record->{package};
117            
118             # Make sure that the module is loaded, unless the format is disabled.
119            
120 2 50 33     8 if ( $record->{module} && ! $record->{disabled} )
121             {
122 2         5 my $filename = $record->{module};
123 2         9 $filename =~ s{::}{/}g;
124 2 50       15 $filename .= '.pm' unless $filename =~ /\.pm$/;
125            
126 2         900 require $filename;
127             }
128            
129             # Now store the record as a response format for this data service.
130            
131 2         11 $ds->{format}{$name} = $record;
132 2 50       16 push @{$ds->{format_list}}, $name unless $record->{disabled};
  2         7  
133 2         7 $last_node = $record;
134             }
135            
136             # A scalar is taken to be a documentation string.
137            
138             elsif ( not ref $item )
139             {
140 2         9 $ds->add_node_doc($last_node, $item);
141             }
142            
143             else
144             {
145 0         0 croak "define_format: the arguments to this routine must be hashrefs and strings";
146             }
147             }
148            
149 1 50       7 croak "define_format: you must include at least one hashref of attributes"
150             unless $last_node;
151             }
152              
153              
154             # list_formats ( )
155             #
156             # Return the list of names of all the formats that have been defined for this
157             # data service.
158              
159             sub list_formats {
160            
161 1     1 0 10 my ($ds) = @_;
162 1         3 return @{$ds->{format_list}};
  1         4  
163             }
164              
165              
166             # valid_format ( )
167             #
168             # Return a code reference (actually a reference to a closure) that can be used
169             # in a parameter rule to validate a format-selecting parameter. All
170             # non-disabled formats are included.
171              
172             sub format_validator {
173            
174 0     0 0   my ($self) = @_;
175            
176             # The ENUM_VALUE subroutine is defined by HTTP::Validate.pm.
177            
178 0           return ENUM_VALUE(@{$self->{format_list}});
  0            
179             }
180              
181              
182             # document_formats ( path, options )
183             #
184             # Return a string containing POD documentation of the response formats that
185             # are allowed for the request path. If the root path '/' is specified, then
186             # document all of the formats enabled for this data service regardless of
187             # whether they are actually allowed for that path. But formats marked as
188             # undocumented are never shown. If the option 'extended' is specified, then
189             # include the text description of each format.
190              
191             sub document_formats {
192              
193 0     0 0   my ($ds, $path, $options) = @_;
194            
195 0   0       $options ||= {};
196 0   0       $path ||= '/';
197            
198             # If no formats have been defined, return a note to that effect.
199            
200             return "MSG_FORMAT_NONE_DEFINED"
201 0 0         unless ref $ds->{format_list} eq 'ARRAY';
202            
203             # Now figure out which formats to document. If the path is '/', then
204             # document all of them. Otherwise, go thorugh the list of defined formats
205             # in order, filtering out those which are not allowed for this path. The
206             # reason for doing it this way is so that the formats will always be
207             # listed in the order defined, instead of the arbitrary hash order.
208            
209 0           my @formats;
210            
211 0 0         if ( $path eq '/' )
212             {
213 0           @formats = grep { ! $ds->{format}{$_}{undocumented} } @{$ds->{format_list}};
  0            
  0            
214 0 0         return "MSG_FORMAT_NONE_DEFINED" unless @formats;
215             }
216            
217             else
218             {
219 0           my $allowed = $ds->node_attr($path, 'allow_format');
220            
221 0 0         return "MSG_FORMAT_NONE_ALLOWED"
222             unless ref $allowed eq 'HASH';
223            
224 0 0         @formats = grep { $allowed->{$_} && ! $ds->{format}{$_}{undocumented} } @{$ds->{format_list}};
  0            
  0            
225 0 0         return "MSG_FORMAT_NONE_ALLOWED" unless @formats;
226             }
227            
228             # Go through the list of defined formats in order,
229            
230 0           my @paths = grep { $ds->{format}{$_}{doc_node} } @formats;
  0            
231            
232 0 0         my $name_header = $ds->has_feature('format_suffix') ? 'Suffix' : 'Name';
233 0 0 0       my $ext_header = $options->{extended} || ! @paths ? "| Description" : '';
234 0 0         my $doc_header = @paths ? "| Documentation" : '';
235            
236 0           my $doc = "=for wds_table_header Format* | $name_header $doc_header $ext_header\n\n";
237 0           $doc .= "=over 4\n\n";
238            
239             FORMAT:
240 0           foreach my $name (@formats)
241             {
242 0           my $frec = $ds->{format}{$name};
243 0   0       my $title = $frec->{title} || $frec->{name};
244 0 0         my $doc_link = $ds->node_link($frec->{doc_node}) if $frec->{doc_node};
245 0 0         my $name_or_suffix = $ds->has_feature('format_suffix') ? ".$frec->{name}" : $frec->{name};
246            
247 0 0         next FORMAT if $frec->{undocumented};
248            
249 0           $doc .= "=item $title | C<$name_or_suffix>";
250 0 0 0       $doc .= " | $doc_link" if $doc_link && @paths && $options->{extended};
      0        
251 0           $doc .= "\n\n";
252            
253 0 0 0       if ( $options->{extended} || ! @paths )
    0          
254             {
255 0 0         $doc .= "$frec->{doc_string}\n\n" if $frec->{doc_string};
256             }
257            
258             elsif ( $doc_link )
259             {
260 0           $doc .= "$doc_link\n\n";
261             }
262             }
263            
264 0           $doc .= "=back";
265            
266 0           return $doc;
267             }
268              
269              
270             1;