line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package CatalystX::QueryModel; |
2
|
|
|
|
|
|
|
|
3
|
6
|
|
|
6
|
|
85402
|
use Class::Method::Modifiers; |
|
6
|
|
|
|
|
50
|
|
|
6
|
|
|
|
|
456
|
|
4
|
6
|
|
|
6
|
|
52
|
use Scalar::Util; |
|
6
|
|
|
|
|
12
|
|
|
6
|
|
|
|
|
203
|
|
5
|
6
|
|
|
6
|
|
34
|
use Moo::_Utils; |
|
6
|
|
|
|
|
15
|
|
|
6
|
|
|
|
|
268
|
|
6
|
6
|
|
|
6
|
|
37
|
use Module::Pluggable::Object; |
|
6
|
|
|
|
|
15
|
|
|
6
|
|
|
|
|
148
|
|
7
|
6
|
|
|
6
|
|
30
|
use Module::Runtime (); |
|
6
|
|
|
|
|
16
|
|
|
6
|
|
|
|
|
116
|
|
8
|
6
|
|
|
6
|
|
61
|
use CatalystX::RequestModel::Utils::InvalidContentType; |
|
6
|
|
|
|
|
14
|
|
|
6
|
|
|
|
|
1892
|
|
9
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
require Moo::Role; |
11
|
|
|
|
|
|
|
require Sub::Util; |
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
our @DEFAULT_ROLES = (qw(CatalystX::QueryModel::DoesQueryModel)); |
14
|
|
|
|
|
|
|
our @DEFAULT_EXPORTS = (qw(property properties namespace content_type)); |
15
|
|
|
|
|
|
|
our %Meta_Data = (); |
16
|
|
|
|
|
|
|
|
17
|
24
|
|
|
24
|
0
|
91
|
sub default_roles { return @DEFAULT_ROLES } |
18
|
48
|
|
|
48
|
0
|
164
|
sub default_exports { return @DEFAULT_EXPORTS } |
19
|
0
|
|
|
0
|
0
|
0
|
sub request_model_metadata { return %Meta_Data } |
20
|
0
|
|
|
0
|
0
|
0
|
sub request_model_metadata_for { return $Meta_Data{shift} } |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
sub import { |
23
|
24
|
|
|
24
|
|
245133
|
my $class = shift; |
24
|
24
|
|
|
|
|
96
|
my $target = caller; |
25
|
|
|
|
|
|
|
|
26
|
24
|
50
|
|
|
|
591
|
unless (Moo::Role->is_role($target)) { |
27
|
24
|
|
|
|
|
1323
|
my $orig = $target->can('with'); |
28
|
|
|
|
|
|
|
Moo::_Utils::_install_tracked($target, 'with', sub { |
29
|
0
|
0
|
|
0
|
|
0
|
unless ($target->can('request_metadata')) { |
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
30
|
0
|
|
|
|
|
0
|
$Meta_Data{$target}{'request'} = \my @data; |
31
|
0
|
|
|
0
|
|
0
|
my $method = Sub::Util::set_subname "${target}::request_metadata" => sub { @data }; |
|
0
|
|
|
|
|
0
|
|
32
|
6
|
|
|
6
|
|
62
|
no strict 'refs'; |
|
6
|
|
|
|
|
35
|
|
|
6
|
|
|
|
|
4301
|
|
33
|
0
|
|
|
|
|
0
|
*{"${target}::request_metadata"} = $method; |
|
0
|
|
|
|
|
0
|
|
34
|
|
|
|
|
|
|
} |
35
|
0
|
|
|
|
|
0
|
&$orig; |
36
|
24
|
|
|
|
|
218
|
}); |
37
|
|
|
|
|
|
|
} |
38
|
|
|
|
|
|
|
|
39
|
24
|
|
|
|
|
1514
|
foreach my $default_role ($class->default_roles) { |
40
|
24
|
50
|
|
|
|
97
|
next if Role::Tiny::does_role($target, $default_role); |
41
|
24
|
|
|
|
|
530
|
Moo::Role->apply_roles_to_package($target, $default_role); |
42
|
24
|
|
|
|
|
18460
|
foreach my $export ($class->default_exports) { |
43
|
96
|
|
|
|
|
2957
|
Moo::_Utils::_install_tracked($target, "__${export}_for_exporter", \&{"${target}::${export}"}); |
|
96
|
|
|
|
|
367
|
|
44
|
|
|
|
|
|
|
} |
45
|
|
|
|
|
|
|
} |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
my %cb = map { |
48
|
24
|
|
|
|
|
927
|
$_ => $target->can("__${_}_for_exporter"); |
|
96
|
|
|
|
|
436
|
|
49
|
|
|
|
|
|
|
} $class->default_exports; |
50
|
|
|
|
|
|
|
|
51
|
24
|
|
|
|
|
126
|
foreach my $exported_method (keys %cb) { |
52
|
|
|
|
|
|
|
my $sub = sub { |
53
|
102
|
100
|
|
102
|
|
10778
|
if(Scalar::Util::blessed($_[0])) { |
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
|
102
|
|
|
|
54
|
36
|
|
|
|
|
154
|
return $cb{$exported_method}->(@_); |
55
|
|
|
|
|
|
|
} else { |
56
|
66
|
|
|
|
|
247
|
return $cb{$exported_method}->($target, @_); |
57
|
|
|
|
|
|
|
} |
58
|
96
|
|
|
|
|
3049
|
}; |
59
|
96
|
|
|
|
|
252
|
Moo::_Utils::_install_tracked($target, $exported_method, $sub); |
60
|
|
|
|
|
|
|
} |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
Class::Method::Modifiers::install_modifier $target, 'around', 'has', sub { |
63
|
60
|
|
|
60
|
|
41740
|
my $orig = shift; |
64
|
60
|
|
|
|
|
259
|
my ($attr, %opts) = @_; |
65
|
|
|
|
|
|
|
|
66
|
60
|
|
|
|
|
114
|
my $predicate; |
67
|
60
|
100
|
|
|
|
208
|
unless($opts{required}) { |
68
|
48
|
50
|
|
|
|
146
|
if(exists $opts{predicate}) { |
69
|
0
|
|
|
|
|
0
|
$predicate = $opts{predicate}; |
70
|
|
|
|
|
|
|
} else { |
71
|
48
|
|
|
|
|
167
|
$predicate = "__cx_q_model_has_${attr}"; |
72
|
48
|
|
|
|
|
114
|
$opts{predicate} = $predicate; |
73
|
|
|
|
|
|
|
} |
74
|
|
|
|
|
|
|
} |
75
|
|
|
|
|
|
|
|
76
|
60
|
50
|
|
|
|
199
|
if(my $info = delete $opts{property}) { |
77
|
60
|
50
|
50
|
|
|
379
|
$info = +{ name=>$attr } unless (ref($info)||'') eq 'HASH'; |
78
|
60
|
100
|
|
|
|
205
|
$info->{attr_predicate} = $predicate if defined($predicate); |
79
|
60
|
50
|
|
|
|
187
|
$info->{omit_empty} = 1 unless exists($info->{omit_empty}); |
80
|
60
|
|
|
|
|
118
|
my $method = \&{"${target}::property"}; |
|
60
|
|
|
|
|
319
|
|
81
|
60
|
|
|
|
|
169
|
$method->($attr, $info, \%opts); |
82
|
|
|
|
|
|
|
} |
83
|
|
|
|
|
|
|
|
84
|
60
|
|
|
|
|
256
|
return $orig->($attr, %opts); |
85
|
24
|
50
|
|
|
|
1222
|
} if $target->can('has'); |
86
|
|
|
|
|
|
|
} |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
sub _add_metadata { |
89
|
66
|
|
|
66
|
|
165
|
my ($target, $type, @add) = @_; |
90
|
66
|
|
66
|
|
|
252
|
my $store = $Meta_Data{$target}{$type} ||= do { |
91
|
30
|
|
|
|
|
51
|
my @data; |
92
|
30
|
50
|
33
|
|
|
123
|
if (Moo::Role->is_role($target) or $target->can("${type}_metadata")) { |
93
|
|
|
|
|
|
|
$target->can('around')->("${type}_metadata", sub { |
94
|
0
|
|
|
0
|
|
0
|
my ($orig, $self) = (shift, shift); |
95
|
0
|
|
|
|
|
0
|
($self->$orig(@_), @data); |
96
|
0
|
|
|
|
|
0
|
}); |
97
|
|
|
|
|
|
|
} else { |
98
|
30
|
|
|
|
|
1492
|
require Sub::Util; |
99
|
30
|
|
|
26
|
|
281
|
my $method = Sub::Util::set_subname "${target}::${type}_metadata" => sub { @data }; |
|
26
|
|
|
26
|
|
128
|
|
|
|
|
|
26
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
|
26
|
|
|
|
100
|
6
|
|
|
6
|
|
66
|
no strict 'refs'; |
|
6
|
|
|
|
|
14
|
|
|
6
|
|
|
|
|
943
|
|
101
|
30
|
|
|
|
|
85
|
*{"${target}::${type}_metadata"} = $method; |
|
30
|
|
|
|
|
145
|
|
102
|
|
|
|
|
|
|
} |
103
|
30
|
|
|
|
|
117
|
\@data; |
104
|
|
|
|
|
|
|
}; |
105
|
|
|
|
|
|
|
|
106
|
66
|
|
|
|
|
154
|
push @$store, @add; |
107
|
66
|
|
|
|
|
211
|
return; |
108
|
|
|
|
|
|
|
} |
109
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
1; |
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
=head1 NAME |
113
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
CatalystX::QueryModel - Inflate Models from a Request Content Body or from URL Query Parameters |
115
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
=head1 SYNOPSIS |
117
|
|
|
|
|
|
|
|
118
|
|
|
|
|
|
|
An example Catalyst Request Model: |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
package Example::Model::PagingQuery; |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
use Moose; |
123
|
|
|
|
|
|
|
use CatalystX::QueryModel; |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
extends 'Catalyst::Model'; |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
namespace 'user'; |
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
has status => (is=>'ro', property=>1); |
130
|
|
|
|
|
|
|
has page => (is=>'ro', property=>1); |
131
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable(); |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
Using it in a controller: |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
package Example::Controller::User; |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
use Moose; |
139
|
|
|
|
|
|
|
use MooseX::MethodAttributes; |
140
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
extends 'Catalyst::Controller'; |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
sub root :Chained(/root) PathPart('user') CaptureArgs(0) { } |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
sub list :GET Chained('root') PathPart('') Args(0) Does(QueryModel) QueryModel(PagingQuery) { |
146
|
|
|
|
|
|
|
my ($self, $c, $query_model) = @_; |
147
|
|
|
|
|
|
|
} |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable; |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
Now if the incoming GET looks like this: |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
[debug] Query Parameters are: |
154
|
|
|
|
|
|
|
.-------------------------------------+--------------------------------------. |
155
|
|
|
|
|
|
|
| Parameter | Value | |
156
|
|
|
|
|
|
|
+-------------------------------------+--------------------------------------+ |
157
|
|
|
|
|
|
|
| user.page | 2 | |
158
|
|
|
|
|
|
|
| user.status | active | |
159
|
|
|
|
|
|
|
'-------------------------------------+--------------------------------------' |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
The object instance C<$query_model> would look like: |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
say $query_model->page; # 2 |
164
|
|
|
|
|
|
|
say $query_model->status; # 'active' |
165
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
And C<$query_model> has additional helper public methods to query attributes marked as request |
167
|
|
|
|
|
|
|
fields (via the C<property> attribute field) which you can read about below. |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
=head1 DESCRIPTION |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
This is very similiar to <CatalystX::RequestModel> but for query parameters that are part of the request |
172
|
|
|
|
|
|
|
URL. Basically we are mapping the query params hash to a object which makes it more robust to access |
173
|
|
|
|
|
|
|
and gives you a place to do any sort of query parameter logic. Can neaten up your controllers and give |
174
|
|
|
|
|
|
|
you more reusable code. |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
=head2 Query options |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
When you include "use CatalystX::QueryModel" we apply the role L<CatalystX::QueryModel::DoesQueryModel> |
179
|
|
|
|
|
|
|
to you model, which gives you some useful methods as well as the ability to store the meta data needed |
180
|
|
|
|
|
|
|
to properly mapped parsed query parameters to your model. You also get some imported subroutines and a |
181
|
|
|
|
|
|
|
new field on your attribute declarations: |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
C<namespace>: This is an optional imported subroutine which allows you to declare the namespace under which |
184
|
|
|
|
|
|
|
we expect to find the attribute mappings. This can be useful if your fields are not top level in your |
185
|
|
|
|
|
|
|
request content body (as in the example given above). This is optional and if you leave it off we just |
186
|
|
|
|
|
|
|
assume all fields are in the top level of the parsed data hash that you content parser builds based on whatever |
187
|
|
|
|
|
|
|
is in the content body. |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
If you declare a namespace in a query model by default we don't throw an error if the namespace is missing |
190
|
|
|
|
|
|
|
(unlike in request models) because I think for query parameters this is the common case where the query |
191
|
|
|
|
|
|
|
is not required (for example in a paged list screen when you default to page 1 when a page is not given). |
192
|
|
|
|
|
|
|
If you want the namespace required you can declare it so like this |
193
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
namespace paging => (required=>1); |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
C<content_type>: This is the request content type which this model is designed to handle. For now you can |
197
|
|
|
|
|
|
|
only declare one content type per model (if your endpoint can handle more than one content type you'll need |
198
|
|
|
|
|
|
|
for now to define a request model for each one; I'm open to changing this to allow one than one content type |
199
|
|
|
|
|
|
|
per request model, but I need to see your use cases for this before I paint myself into a corner codewise). |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
This is also an optional check for query parameters. |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
C<property>: This is a new field allowed on your attribute declarations. Setting its value to C<1> (as in |
204
|
|
|
|
|
|
|
the example above) just means to use all the default settings for the declared content_type but you can declare |
205
|
|
|
|
|
|
|
this as a hashref instead if you have special handling needs. For example: |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
has notes => (is=>'ro', property=>+{ expand=>'JSON' }); |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
Here's the current list of property settings and what they do. You can also request the test cases for more |
210
|
|
|
|
|
|
|
examples: |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
=over 4 |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
=item name |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
The name of the field in the request body we are mapping to the request model. The default is to just use |
217
|
|
|
|
|
|
|
the name of the attribute. |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
=item omit_empty |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
Defaults to true. If there's no matching field in the request body we leave the request model attribute |
222
|
|
|
|
|
|
|
empty (we don't stick an undef in there). If for some reason you don't want that, setting this to false |
223
|
|
|
|
|
|
|
will put an undef into a scalar fields, and an empty array into an indexed one. If has no effect on |
224
|
|
|
|
|
|
|
attributes that map to a submodel since I have no idea what that should be (your use cases welcomed). |
225
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
=item flatten |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
If the value associated with a field is an array, flatten it to a single value. The default is based on |
229
|
|
|
|
|
|
|
the body content parser. Its really a hack to deal with HTML form POST and Query parameters since the |
230
|
|
|
|
|
|
|
way those formats work you can't be sure if a value is flat or an array. This isn't a problem with |
231
|
|
|
|
|
|
|
JSON encoded request bodies. You'll need to check the docs for the Content Body Parser you are using to |
232
|
|
|
|
|
|
|
see what this does. |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
=item always_array |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
Similar to C<flatten> but opposite, it forces a value into an array even if there's just one value. Again |
237
|
|
|
|
|
|
|
mostly useful to deal with ideosyncracies of HTML form post. |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
B<NOTE>: The attribute property settings C<flatten> and C<always_array> are currently exclusive (only one of |
240
|
|
|
|
|
|
|
the two will apply if you supply both. The C<always_array> property always takes precedence. At some point |
241
|
|
|
|
|
|
|
in the future supplying both might generate an exception so its best not to do that. I'm only leaving it |
242
|
|
|
|
|
|
|
allowed for now since I'm not sure there's a use case for both. |
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
=item boolean |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
Defaults to false. If true will convert value to the common Perl convention 0 is false, 1 is true. The way |
247
|
|
|
|
|
|
|
this is converted is partly dependent on your content body parser. |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
=item expand |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
Example the value into a data structure by parsing it. Right now there's only one value this will take, |
252
|
|
|
|
|
|
|
which is C<JSON> and will then parse the value into a structure using a JSON parser. Again this is mostly |
253
|
|
|
|
|
|
|
useful for HTML form posting and coping with some limitations you have in classic HTML form input types. |
254
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
=back |
256
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
=head2 Setting a required attribute |
258
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
Generally it's best to not mark attributes which map to request properties as required and to handled anything |
260
|
|
|
|
|
|
|
like thia via your validation layer so that you can provide more useful feedback to your application users. |
261
|
|
|
|
|
|
|
If you do need to mark something required in order for your request model to be valid, please note that we |
262
|
|
|
|
|
|
|
capture the exception created by Moo/se and throw L<CatalystX::RequestModel::Utils::BadRequest>. If you are |
263
|
|
|
|
|
|
|
using L<CatalystX::Errors> this will get rendered as a HTTP 400 Bad Request; otherwise you just get the |
264
|
|
|
|
|
|
|
generic L<Catalyst> HTTP 500 Server Error or as you might have written in your custom error handling code. |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
=head2 Nested and Indexed attributes |
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
These work the same as in L<CatalystX::RequestModel> |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
=head1 METHODS |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
Please see L<CatalystX::QueryModel::DoesQueryModel> for the public API details. |
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
=head1 EXCEPTIONS |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
This class can throw the following exceptions. Please note all exceptions are compatible with |
277
|
|
|
|
|
|
|
L<CatalystX::Errors> to make it easy and consistent to convert errors to actual error responses. |
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
=head2 Bad Request |
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
If your request generates an exception when trying to instantiate your model (basically when calling ->new |
282
|
|
|
|
|
|
|
on it) we capture that error, log the error and throw a L<CatalystX::RequestModel::Utils::BadRequest> |
283
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
=head2 Invalid Request Content Type |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
If the incoming content body doesn't have a content type header that matches one of the available |
287
|
|
|
|
|
|
|
content body parsers then we throw an L<CatalystX::RequestModel::Utils::InvalidContentType>. This |
288
|
|
|
|
|
|
|
will get interpretated as an HTTP 415 status client error if you are using L<CatalystX::Errors>. |
289
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
=head1 AUTHOR |
291
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
John Napiorkowski <jjnapiork@cpan.org> |
293
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
=head1 COPYRIGHT |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
2022 |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
=head1 LICENSE |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
=cut |
303
|
|
|
|
|
|
|
|