line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Perinci::Sub::Property::result::table; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
our $DATE = '2015-03-10'; # DATE |
4
|
|
|
|
|
|
|
our $VERSION = '0.06'; # VERSION |
5
|
|
|
|
|
|
|
|
6
|
1
|
|
|
1
|
|
3008
|
use 5.010001; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
32
|
|
7
|
1
|
|
|
1
|
|
3
|
use strict; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
23
|
|
8
|
1
|
|
|
1
|
|
3
|
use warnings; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
29
|
|
9
|
|
|
|
|
|
|
#use Log::Any '$log'; |
10
|
|
|
|
|
|
|
|
11
|
1
|
|
|
1
|
|
430
|
use Locale::TextDomain::UTF8 'Perinci-Sub-Property-result-table'; |
|
1
|
|
|
|
|
15222
|
|
|
1
|
|
|
|
|
7
|
|
12
|
1
|
|
|
1
|
|
8984
|
use Perinci::Object::Metadata; |
|
1
|
|
|
|
|
1711
|
|
|
1
|
|
|
|
|
30
|
|
13
|
1
|
|
|
1
|
|
472
|
use Perinci::Sub::PropertyUtil qw(declare_property); |
|
1
|
|
|
|
|
647
|
|
|
1
|
|
|
|
|
476
|
|
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
declare_property( |
16
|
|
|
|
|
|
|
name => 'result/table', |
17
|
|
|
|
|
|
|
type => 'function', |
18
|
|
|
|
|
|
|
schema => ['hash*'], |
19
|
|
|
|
|
|
|
wrapper => { |
20
|
|
|
|
|
|
|
meta => { |
21
|
|
|
|
|
|
|
v => 2, |
22
|
|
|
|
|
|
|
prio => 50, |
23
|
|
|
|
|
|
|
}, |
24
|
|
|
|
|
|
|
handler => sub { |
25
|
2
|
|
|
2
|
|
25927
|
my ($self, %args) = @_; |
26
|
2
|
|
33
|
|
|
18
|
my $v = $args{new} // $args{value} // {}; |
|
|
|
50
|
|
|
|
|
27
|
2
|
|
|
|
|
4
|
my $meta = $args{meta}; |
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
# add format_options |
30
|
|
|
|
|
|
|
{ |
31
|
2
|
50
|
|
|
|
3
|
last if $meta->{result_naked}; |
|
2
|
|
|
|
|
8
|
|
32
|
2
|
|
|
|
|
8
|
$self->select_section('after_call_after_res_validation'); |
33
|
2
|
|
|
|
|
22
|
$self->push_lines('# add format_options from result/table hints'); |
34
|
2
|
|
|
|
|
46
|
$self->push_lines('{'); |
35
|
2
|
|
|
|
|
19
|
$self->indent; |
36
|
2
|
|
|
|
|
21
|
$self->push_lines( |
37
|
|
|
|
|
|
|
# we are in a local block, so no need to use _w_ prefixes |
38
|
|
|
|
|
|
|
# for vars or even use add_var() |
39
|
|
|
|
|
|
|
'last unless ref($_w_res->[2]) eq "ARRAY";', |
40
|
|
|
|
|
|
|
'my $firstrow = $_w_res->[2][0] or last;', # deduce type from first row |
41
|
|
|
|
|
|
|
'my $tablespec = '.$self->{_args}{meta_name}.'->{result}{table}{spec} or last;', |
42
|
|
|
|
|
|
|
'my $tct = {};', |
43
|
|
|
|
|
|
|
'my $tco;', |
44
|
|
|
|
|
|
|
'if (ref($firstrow) eq "ARRAY" && $_w_res->[3]{"table.fields"}) {', |
45
|
|
|
|
|
|
|
' my $field_names = $_w_res->[3]{"table.fields"};', # map column\d to field names |
46
|
|
|
|
|
|
|
' for (0..@$field_names-1) {', |
47
|
|
|
|
|
|
|
' next if defined($tct->{$_});', |
48
|
|
|
|
|
|
|
' my $sch = $tablespec->{fields}{$field_names->[$_]}{schema} or next;', # field is unknown in table spec |
49
|
|
|
|
|
|
|
' my $type = ref($sch) eq "ARRAY" ? $sch->[0] : $sch;', |
50
|
|
|
|
|
|
|
' $type =~ s/\\*$//;', |
51
|
|
|
|
|
|
|
' $tct->{"column$_"} = $type;', |
52
|
|
|
|
|
|
|
' }', |
53
|
|
|
|
|
|
|
'} elsif (ref($firstrow) eq "HASH") {', |
54
|
|
|
|
|
|
|
' my $fields = [keys %$firstrow];', # XXX should we check from several/all rows to collect more complete keys? |
55
|
|
|
|
|
|
|
' $tco = [sort {($tablespec->{fields}{$a}{pos} // $tablespec->{fields}{$a}{index} // 9999) <=> ($tablespec->{fields}{$b}{pos} // $tablespec->{fields}{$b}{index} // 9999)} @$fields];', |
56
|
|
|
|
|
|
|
' for (@$fields) {', |
57
|
|
|
|
|
|
|
' my $sch = $tablespec->{fields}{$_}{schema} or next;', # field is unknown in table spec |
58
|
|
|
|
|
|
|
' my $type = ref($sch) eq "ARRAY" ? $sch->[0] : $sch;', |
59
|
|
|
|
|
|
|
' $type =~ s/\\*$//;', |
60
|
|
|
|
|
|
|
' $tct->{$_} = $type;', |
61
|
|
|
|
|
|
|
' }', |
62
|
|
|
|
|
|
|
'} else {', |
63
|
|
|
|
|
|
|
' last;', |
64
|
|
|
|
|
|
|
'}', |
65
|
|
|
|
|
|
|
'my $rfo = {};', |
66
|
|
|
|
|
|
|
'$rfo->{table_column_types} = [$tct] if $tct;', |
67
|
|
|
|
|
|
|
'$rfo->{table_column_orders} = [$tco] if $tco;', |
68
|
|
|
|
|
|
|
'$_w_res->[3]{format_options} //= {};', |
69
|
|
|
|
|
|
|
'$_w_res->[3]{format_options}{any} //= $rfo;', |
70
|
|
|
|
|
|
|
); |
71
|
2
|
|
|
|
|
101
|
$self->unindent; |
72
|
2
|
|
|
|
|
15
|
$self->push_lines('}'); |
73
|
|
|
|
|
|
|
} |
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
# TODO validate table data, if requested |
76
|
|
|
|
|
|
|
}, |
77
|
|
|
|
|
|
|
}, |
78
|
|
|
|
|
|
|
cmdline_help => { |
79
|
|
|
|
|
|
|
meta => { |
80
|
|
|
|
|
|
|
prio => 50, |
81
|
|
|
|
|
|
|
}, |
82
|
|
|
|
|
|
|
handler => sub { |
83
|
0
|
|
|
0
|
|
|
my ($self, $r) = @_; |
84
|
0
|
|
|
|
|
|
my $meta = $r->{_help_meta}; |
85
|
0
|
0
|
|
|
|
|
my $table_spec = $meta->{result}{table}{spec} |
86
|
|
|
|
|
|
|
or return undef; |
87
|
0
|
|
|
|
|
|
my $text = __("Returns table data. Table fields are as follow:"); |
88
|
0
|
|
|
|
|
|
$text .= "\n\n"; |
89
|
0
|
|
|
|
|
|
my $ff = $table_spec->{fields}; |
90
|
|
|
|
|
|
|
# reminder: index property is for older spec, will be removed |
91
|
|
|
|
|
|
|
# someday |
92
|
0
|
|
0
|
|
|
|
for my $fn (sort {($ff->{$a}{pos}//$ff->{$a}{index}//0) <=> |
|
0
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
93
|
|
|
|
|
|
|
($ff->{$b}{pos}//$ff->{$b}{index}//0)} |
94
|
|
|
|
|
|
|
keys %$ff) { |
95
|
0
|
|
|
|
|
|
my $f = $ff->{$fn}; |
96
|
0
|
|
|
|
|
|
my $fo = Perinci::Object::Metadata->new($f); |
97
|
0
|
|
|
|
|
|
my $sum = $fo->langprop("summary"); |
98
|
0
|
|
|
|
|
|
my $type; |
99
|
0
|
0
|
|
|
|
|
if ($f->{schema}) { |
100
|
0
|
0
|
|
|
|
|
$type = ref($f->{schema}) eq 'ARRAY' ? |
101
|
|
|
|
|
|
|
$f->{schema}[0] : $f->{schema}; |
102
|
0
|
|
|
|
|
|
$type =~ s/\*$//; |
103
|
|
|
|
|
|
|
} |
104
|
|
|
|
|
|
|
$text .= |
105
|
0
|
0
|
|
|
|
|
join("", |
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
106
|
|
|
|
|
|
|
" - *$fn*", |
107
|
|
|
|
|
|
|
($type ? " ($type)" : ""), |
108
|
|
|
|
|
|
|
$table_spec->{pk} eq $fn ? |
109
|
|
|
|
|
|
|
" (".__x("ID field").")":"", |
110
|
|
|
|
|
|
|
$sum ? ": $sum" : "", |
111
|
|
|
|
|
|
|
"\n\n"); |
112
|
0
|
|
|
|
|
|
my $desc = $fo->langprop("description"); |
113
|
0
|
0
|
|
|
|
|
if ($desc) { |
114
|
0
|
|
|
|
|
|
$desc =~ s/(\r?\n)+\z//; |
115
|
0
|
|
|
|
|
|
$desc =~ s/^/ /mg; |
116
|
0
|
|
|
|
|
|
$text .= "$desc\n\n"; |
117
|
|
|
|
|
|
|
} |
118
|
|
|
|
|
|
|
} |
119
|
0
|
|
|
|
|
|
$text; |
120
|
|
|
|
|
|
|
}, |
121
|
|
|
|
|
|
|
}, # cmdline_help |
122
|
|
|
|
|
|
|
); |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
1; |
126
|
|
|
|
|
|
|
# ABSTRACT: Specify table data in result |
127
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
__END__ |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
=pod |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
=encoding UTF-8 |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
=head1 NAME |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
Perinci::Sub::Property::result::table - Specify table data in result |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
=head1 VERSION |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
This document describes version 0.06 of Perinci::Sub::Property::result::table (from Perl distribution Perinci-Sub-Property-result-table), released on 2015-03-10. |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
=head1 SYNOPSIS |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
In function L<Rinci> metadata: |
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
result => { |
147
|
|
|
|
|
|
|
table => { |
148
|
|
|
|
|
|
|
spec => { |
149
|
|
|
|
|
|
|
summary => "Employee's' current salary", |
150
|
|
|
|
|
|
|
fields => { |
151
|
|
|
|
|
|
|
name => { |
152
|
|
|
|
|
|
|
summary => "Employee's name", |
153
|
|
|
|
|
|
|
schema => 'str*', |
154
|
|
|
|
|
|
|
pos => 0, |
155
|
|
|
|
|
|
|
}, |
156
|
|
|
|
|
|
|
position => { |
157
|
|
|
|
|
|
|
summary => "Employee's current position", |
158
|
|
|
|
|
|
|
schema => 'str*', |
159
|
|
|
|
|
|
|
pos => 1, |
160
|
|
|
|
|
|
|
}, |
161
|
|
|
|
|
|
|
salary => { |
162
|
|
|
|
|
|
|
summary => "Employee's current monthly salary", |
163
|
|
|
|
|
|
|
schema => 'float*', |
164
|
|
|
|
|
|
|
pos => 2, |
165
|
|
|
|
|
|
|
}, |
166
|
|
|
|
|
|
|
}, |
167
|
|
|
|
|
|
|
pk => 'name', |
168
|
|
|
|
|
|
|
}, |
169
|
|
|
|
|
|
|
# allow_extra_fields => 0, |
170
|
|
|
|
|
|
|
# allow_underscore_fields => 0, |
171
|
|
|
|
|
|
|
}, |
172
|
|
|
|
|
|
|
... |
173
|
|
|
|
|
|
|
} |
174
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
=head1 DESCRIPTION |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
If your function returns table data, either in the form of array (single-column |
178
|
|
|
|
|
|
|
rows): |
179
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
["andi", "budi", "cinta", ...] |
181
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
or array of arrays (CSV-like): |
183
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
[ |
185
|
|
|
|
|
|
|
["andi" , "manager", 12_000_000], |
186
|
|
|
|
|
|
|
["budi" , "staff", 5_000_000], |
187
|
|
|
|
|
|
|
["cinta", "junior manager", 7_500_000], |
188
|
|
|
|
|
|
|
# ... |
189
|
|
|
|
|
|
|
] |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
or array of hashes (with field names): |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
[ |
194
|
|
|
|
|
|
|
{name=>"andi" , position=>"manager", salary=>12_000_000}, |
195
|
|
|
|
|
|
|
{name=>"budi" , position=>"staff", salary=> 5_000_000}, |
196
|
|
|
|
|
|
|
{name=>"cinta", position=>"junior manager", salary=> 7_500_000}, |
197
|
|
|
|
|
|
|
# ... |
198
|
|
|
|
|
|
|
] |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
then you might want to add a C<table> property inside your C<result> property of |
201
|
|
|
|
|
|
|
your function metadata. This module offers several things: |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
=over |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
=item * |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
When your function is run under L<Perinci::CmdLine>, your tables will look |
208
|
|
|
|
|
|
|
prettier. This is done via adding C<format_options> property to your function |
209
|
|
|
|
|
|
|
result metadata, giving hints to the L<Data::Format::Pretty> formatter. |
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
Also when you use --help (--verbose), the table structure is described in the |
212
|
|
|
|
|
|
|
Result section. |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
=item * |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
(NOT YET IMPLEMENTED) When you generate documentation, the table specification |
217
|
|
|
|
|
|
|
is also included in the documentation. |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
=item * |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
(NOT YET IMPLEMENTED, IDEA) The user can also perhaps request the table |
222
|
|
|
|
|
|
|
specification, e.g. C<yourfunc --help=result-table-spec>, C<yourfunc |
223
|
|
|
|
|
|
|
--result-table-spec>. |
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
=item * |
226
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
(NOT YET IMPLEMENTED) The wrapper code can optionally validate your function |
228
|
|
|
|
|
|
|
result, making sure that your resulting table conforms to the table |
229
|
|
|
|
|
|
|
specification. |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
=item * |
232
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
(NOT YET IMPLEMENTED, IDEA) The wrapper code can optionally filter, summarize, |
234
|
|
|
|
|
|
|
or sort the table on the fly before returning the final result to the user. |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
(Alternatively, you can pipe the output to another tool like B<jq>, just like a |
237
|
|
|
|
|
|
|
la Unix toolbox philosophy). |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
=back |
240
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
=head1 SPECIFICATION |
242
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
The value of the C<table> property should be a L<DefHash>. Known properties: |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
=over |
246
|
|
|
|
|
|
|
|
247
|
|
|
|
|
|
|
=item * spec => DEFHASH |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
Required. Table data specification, specified using L<TableDef>. |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
=item * allow_extra_fields => BOOL (default: 0) |
252
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
Whether to allow the function to return extra fields other than the ones |
254
|
|
|
|
|
|
|
specified in C<spec>. This is only relevant when function returns array of |
255
|
|
|
|
|
|
|
hashes (i.e. when the field names are present). And this is only relevant when |
256
|
|
|
|
|
|
|
validating the table data. |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
=item * allow_underscore_fields => BOOL (default: 0) |
259
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
Like C<allow_extra_fields>, but regulates whether to allow any extra fields |
261
|
|
|
|
|
|
|
prefixed by an underscore. Underscore-prefixed keys is the DefHash's convention |
262
|
|
|
|
|
|
|
of extra keys that can be ignored. |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
=back |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
=head1 NOTES |
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
If you return an array or array of arrays (i.e. no field names), you might want |
269
|
|
|
|
|
|
|
to add C<table.fields> result metadata so the wrapper code can know which |
270
|
|
|
|
|
|
|
element belongs to which field. Example: |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
my $table = []; |
273
|
|
|
|
|
|
|
push @$table, ["andi", 1]; |
274
|
|
|
|
|
|
|
push @$table, ["budi", 2]; |
275
|
|
|
|
|
|
|
return [200, "OK", $table, {"table.fields"=>[qw/name id/]}]; |
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
This is not needed if you return array of hashes, since the field names are |
278
|
|
|
|
|
|
|
present as hash keys: |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
my $table = []; |
281
|
|
|
|
|
|
|
push @$table, {name=>"andi", id=>1}; |
282
|
|
|
|
|
|
|
push @$table, {name=>"budi", id=>2}; |
283
|
|
|
|
|
|
|
return [200, "OK", $table]; |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
=head1 RESULT METADATA |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
=over |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
=item * attribute: table.fields => ARRAY OF STR |
290
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
=back |
292
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
=head1 FAQ |
294
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
=head2 Why not use the C<schema> property in the C<result> property? |
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
That is, in your function metadata: |
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
result => { |
300
|
|
|
|
|
|
|
schema => ['array*', of => ['hash*' => keys => { |
301
|
|
|
|
|
|
|
name => 'str*', |
302
|
|
|
|
|
|
|
position => 'str', |
303
|
|
|
|
|
|
|
salary => ['float*', min => 0], |
304
|
|
|
|
|
|
|
... |
305
|
|
|
|
|
|
|
}]], |
306
|
|
|
|
|
|
|
}, |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
First of all, table data can come in several forms, either a 1-dimensional |
309
|
|
|
|
|
|
|
array, an array of arrays, or an array of hashes. Moreover, when returning an |
310
|
|
|
|
|
|
|
array of arrays, the order of fields can sometimes be changed. The above schema |
311
|
|
|
|
|
|
|
will become more complex if it has to handle all those cases. |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
With the C<table> property, the intent becomes clearer that we want to return |
314
|
|
|
|
|
|
|
table data. We can also specify more aspects aside from just the schema. |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
=head1 HOMEPAGE |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Sub-Property-result-table>. |
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
=head1 SOURCE |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
Source repository is at L<https://github.com/perlancar/perl-Perinci-Sub-Property-result-table>. |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
=head1 BUGS |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Sub-Property-result-table> |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
When submitting a bug or request, please include a test-file or a |
329
|
|
|
|
|
|
|
patch to an existing test-file that illustrates the bug or desired |
330
|
|
|
|
|
|
|
feature. |
331
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
=head1 AUTHOR |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
perlancar <perlancar@cpan.org> |
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
This software is copyright (c) 2015 by perlancar@cpan.org. |
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
341
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
=cut |