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; |