line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package CatalystX::RequestModel; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
our $VERSION = '0.018'; |
4
|
|
|
|
|
|
|
|
5
|
6
|
|
|
6
|
|
13261065
|
use Class::Method::Modifiers; |
|
6
|
|
|
|
|
10843
|
|
|
6
|
|
|
|
|
454
|
|
6
|
6
|
|
|
6
|
|
53
|
use Scalar::Util; |
|
6
|
|
|
|
|
14
|
|
|
6
|
|
|
|
|
226
|
|
7
|
6
|
|
|
6
|
|
2759
|
use Moo::_Utils; |
|
6
|
|
|
|
|
17102
|
|
|
6
|
|
|
|
|
342
|
|
8
|
6
|
|
|
6
|
|
53
|
use Module::Pluggable::Object; |
|
6
|
|
|
|
|
14
|
|
|
6
|
|
|
|
|
202
|
|
9
|
6
|
|
|
6
|
|
48
|
use Module::Runtime (); |
|
6
|
|
|
|
|
16
|
|
|
6
|
|
|
|
|
98
|
|
10
|
6
|
|
|
6
|
|
2588
|
use CatalystX::RequestModel::Utils::InvalidContentType; |
|
6
|
|
|
|
|
2352
|
|
|
6
|
|
|
|
|
3068
|
|
11
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
require Moo::Role; |
13
|
|
|
|
|
|
|
require Sub::Util; |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
our @DEFAULT_ROLES = (qw(CatalystX::RequestModel::DoesRequestModel)); |
16
|
|
|
|
|
|
|
our @DEFAULT_EXPORTS = (qw(property properties namespace content_type content_in)); |
17
|
|
|
|
|
|
|
our %Meta_Data = (); |
18
|
|
|
|
|
|
|
our %ContentBodyParsers = (); |
19
|
|
|
|
|
|
|
|
20
|
84
|
|
|
84
|
0
|
305
|
sub default_roles { return @DEFAULT_ROLES } |
21
|
168
|
|
|
168
|
0
|
618
|
sub default_exports { return @DEFAULT_EXPORTS } |
22
|
0
|
|
|
0
|
0
|
0
|
sub request_model_metadata { return %Meta_Data } |
23
|
0
|
|
|
0
|
0
|
0
|
sub request_model_metadata_for { return $Meta_Data{shift} } |
24
|
0
|
|
|
0
|
0
|
0
|
sub content_body_parsers { return %ContentBodyParsers } |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
sub content_body_parser_for { |
27
|
33
|
|
|
33
|
0
|
67
|
my $ct = shift; |
28
|
33
|
|
33
|
|
|
194
|
return $ContentBodyParsers{$ct} || CatalystX::RequestModel::Utils::InvalidContentType->throw(ct=>$ct); |
29
|
|
|
|
|
|
|
} |
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
sub load_content_body_parsers { |
32
|
84
|
|
|
84
|
0
|
186
|
my $class = shift; |
33
|
84
|
|
|
|
|
868
|
my @packages = Module::Pluggable::Object->new( |
34
|
|
|
|
|
|
|
search_path => "${class}::ContentBodyParser" |
35
|
|
|
|
|
|
|
)->plugins; |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
%ContentBodyParsers = map { |
38
|
252
|
|
|
|
|
3499
|
$_->content_type => $_; |
39
|
|
|
|
|
|
|
} map { |
40
|
84
|
|
|
|
|
282889
|
Module::Runtime::use_module $_; |
|
252
|
|
|
|
|
5746
|
|
41
|
|
|
|
|
|
|
} @packages; |
42
|
|
|
|
|
|
|
} |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
sub import { |
45
|
84
|
|
|
84
|
|
851485
|
my $class = shift; |
46
|
84
|
|
|
|
|
296
|
my $target = caller; |
47
|
|
|
|
|
|
|
|
48
|
84
|
|
|
|
|
1681
|
$class->load_content_body_parsers; |
49
|
|
|
|
|
|
|
|
50
|
84
|
50
|
|
|
|
673
|
unless (Moo::Role->is_role($target)) { |
51
|
84
|
|
|
|
|
4923
|
my $orig = $target->can('with'); |
52
|
|
|
|
|
|
|
Moo::_Utils::_install_tracked($target, 'with', sub { |
53
|
0
|
0
|
|
0
|
|
0
|
unless ($target->can('request_metadata')) { |
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
54
|
0
|
|
|
|
|
0
|
$Meta_Data{$target}{'request'} = \my @data; |
55
|
0
|
|
|
0
|
|
0
|
my $method = Sub::Util::set_subname "${target}::request_metadata" => sub { @data }; |
|
0
|
|
|
|
|
0
|
|
56
|
6
|
|
|
6
|
|
68
|
no strict 'refs'; |
|
6
|
|
|
|
|
26
|
|
|
6
|
|
|
|
|
4399
|
|
57
|
0
|
|
|
|
|
0
|
*{"${target}::request_metadata"} = $method; |
|
0
|
|
|
|
|
0
|
|
58
|
|
|
|
|
|
|
} |
59
|
0
|
|
|
|
|
0
|
&$orig; |
60
|
84
|
|
|
|
|
773
|
}); |
61
|
|
|
|
|
|
|
} |
62
|
|
|
|
|
|
|
|
63
|
84
|
|
|
|
|
4868
|
foreach my $default_role ($class->default_roles) { |
64
|
84
|
50
|
|
|
|
280
|
next if Role::Tiny::does_role($target, $default_role); |
65
|
84
|
|
|
|
|
1594
|
Moo::Role->apply_roles_to_package($target, $default_role); |
66
|
84
|
|
|
|
|
56956
|
foreach my $export ($class->default_exports) { |
67
|
420
|
|
|
|
|
13653
|
Moo::_Utils::_install_tracked($target, "__${export}_for_exporter", \&{"${target}::${export}"}); |
|
420
|
|
|
|
|
1611
|
|
68
|
|
|
|
|
|
|
} |
69
|
|
|
|
|
|
|
} |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
my %cb = map { |
72
|
84
|
|
|
|
|
3256
|
$_ => $target->can("__${_}_for_exporter"); |
|
420
|
|
|
|
|
1764
|
|
73
|
|
|
|
|
|
|
} $class->default_exports; |
74
|
|
|
|
|
|
|
|
75
|
84
|
|
|
|
|
415
|
foreach my $exported_method (keys %cb) { |
76
|
|
|
|
|
|
|
my $sub = sub { |
77
|
582
|
100
|
|
582
|
|
166719
|
if(Scalar::Util::blessed($_[0])) { |
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
|
582
|
|
|
|
78
|
156
|
|
|
|
|
584
|
return $cb{$exported_method}->(@_); |
79
|
|
|
|
|
|
|
} else { |
80
|
426
|
|
|
|
|
2077
|
return $cb{$exported_method}->($target, @_); |
81
|
|
|
|
|
|
|
} |
82
|
420
|
|
|
|
|
12964
|
}; |
83
|
420
|
|
|
|
|
1396
|
Moo::_Utils::_install_tracked($target, $exported_method, $sub); |
84
|
|
|
|
|
|
|
} |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
Class::Method::Modifiers::install_modifier $target, 'around', 'has', sub { |
87
|
354
|
|
|
354
|
|
414639
|
my $orig = shift; |
88
|
354
|
|
|
|
|
1599
|
my ($attr, %opts) = @_; |
89
|
|
|
|
|
|
|
|
90
|
354
|
|
|
|
|
658
|
my $predicate; |
91
|
354
|
100
|
|
|
|
1115
|
unless($opts{required}) { |
92
|
312
|
50
|
|
|
|
922
|
if(exists $opts{predicate}) { |
93
|
0
|
|
|
|
|
0
|
$predicate = $opts{predicate}; |
94
|
|
|
|
|
|
|
} else { |
95
|
312
|
|
|
|
|
804
|
$predicate = "__cx_req_model_has_${attr}"; |
96
|
312
|
|
|
|
|
707
|
$opts{predicate} = $predicate; |
97
|
|
|
|
|
|
|
} |
98
|
|
|
|
|
|
|
} |
99
|
|
|
|
|
|
|
|
100
|
354
|
50
|
|
|
|
1088
|
if(my $info = delete $opts{property}) { |
101
|
354
|
100
|
100
|
|
|
1938
|
$info = +{ name=>$attr } unless (ref($info)||'') eq 'HASH'; |
102
|
354
|
100
|
|
|
|
1046
|
$info->{attr_predicate} = $predicate if defined($predicate); |
103
|
354
|
100
|
|
|
|
917
|
$info->{omit_empty} = 1 unless exists($info->{omit_empty}); |
104
|
354
|
|
|
|
|
538
|
my $method = \&{"${target}::property"}; |
|
354
|
|
|
|
|
1756
|
|
105
|
354
|
|
|
|
|
972
|
$method->($attr, $info, \%opts); |
106
|
|
|
|
|
|
|
} |
107
|
|
|
|
|
|
|
|
108
|
354
|
|
|
|
|
1558
|
return $orig->($attr, %opts); |
109
|
84
|
50
|
|
|
|
3650
|
} if $target->can('has'); |
110
|
|
|
|
|
|
|
} |
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
sub _add_metadata { |
113
|
426
|
|
|
426
|
|
1137
|
my ($target, $type, @add) = @_; |
114
|
426
|
|
66
|
|
|
1681
|
my $store = $Meta_Data{$target}{$type} ||= do { |
115
|
156
|
|
|
|
|
325
|
my @data; |
116
|
156
|
50
|
33
|
|
|
618
|
if (Moo::Role->is_role($target) or $target->can("${type}_metadata")) { |
117
|
|
|
|
|
|
|
$target->can('around')->("${type}_metadata", sub { |
118
|
0
|
|
|
0
|
|
0
|
my ($orig, $self) = (shift, shift); |
119
|
0
|
|
|
|
|
0
|
($self->$orig(@_), @data); |
120
|
0
|
|
|
|
|
0
|
}); |
121
|
|
|
|
|
|
|
} else { |
122
|
156
|
|
|
|
|
7956
|
require Sub::Util; |
123
|
156
|
|
|
161
|
|
1475
|
my $method = Sub::Util::set_subname "${target}::${type}_metadata" => sub { @data }; |
|
161
|
|
|
161
|
|
642
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
|
161
|
|
|
|
124
|
6
|
|
|
6
|
|
53
|
no strict 'refs'; |
|
6
|
|
|
|
|
15
|
|
|
6
|
|
|
|
|
1405
|
|
125
|
156
|
|
|
|
|
413
|
*{"${target}::${type}_metadata"} = $method; |
|
156
|
|
|
|
|
748
|
|
126
|
|
|
|
|
|
|
} |
127
|
156
|
|
|
|
|
594
|
\@data; |
128
|
|
|
|
|
|
|
}; |
129
|
|
|
|
|
|
|
|
130
|
426
|
|
|
|
|
970
|
push @$store, @add; |
131
|
426
|
|
|
|
|
1291
|
return; |
132
|
|
|
|
|
|
|
} |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
1; |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
=head1 NAME |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
CatalystX::RequestModel - Inflate Models from a Request Content Body or from URL Query Parameters |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
=head1 SYNOPSIS |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
An example Catalyst Request Model: |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
package Example::Model::RegistrationRequest; |
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
use Moose; |
147
|
|
|
|
|
|
|
use CatalystX::RequestModel; |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
extends 'Catalyst::Model'; |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
namespace 'person'; |
152
|
|
|
|
|
|
|
content_type 'application/x-www-form-urlencoded'; |
153
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
has username => (is=>'ro', property=>1); |
155
|
|
|
|
|
|
|
has first_name => (is=>'ro', property=>1); |
156
|
|
|
|
|
|
|
has last_name => (is=>'ro', property=>1); |
157
|
|
|
|
|
|
|
has password => (is=>'ro', property=>1); |
158
|
|
|
|
|
|
|
has password_confirmation => (is=>'ro', property=>1); |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable(); |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
Using it in a controller: |
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
package Example::Controller::Register; |
165
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
use Moose; |
167
|
|
|
|
|
|
|
use MooseX::MethodAttributes; |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
extends 'Catalyst::Controller'; |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
sub root :Chained(/root) PathPart('register') CaptureArgs(0) { } |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
sub update :POST Chained('root') PathPart('') Args(0) Does(RequestModel) BodyModel(RegistrationRequest) { |
174
|
|
|
|
|
|
|
my ($self, $c, $request_model) = @_; |
175
|
|
|
|
|
|
|
## Do something with the $request_model (instance of 'Example::Model::RegistrationRequest'). |
176
|
|
|
|
|
|
|
} |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
sub list :GET Chained('root') PathPart('') Args(0) Does(RequestModel) QueryModel(PagingModel) { |
179
|
|
|
|
|
|
|
my ($self, $c, $paging_model) = @_; |
180
|
|
|
|
|
|
|
} |
181
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable; |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
Now if the incoming POST /update looks like this: |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
.-------------------------------------+--------------------------------------. |
188
|
|
|
|
|
|
|
| Parameter | Value | |
189
|
|
|
|
|
|
|
+-------------------------------------+--------------------------------------+ |
190
|
|
|
|
|
|
|
| person.username | jjn | |
191
|
|
|
|
|
|
|
| person.first_name [multiple] | 2, John | |
192
|
|
|
|
|
|
|
| person.last_name | Napiorkowski | |
193
|
|
|
|
|
|
|
| person.password | abc123 | |
194
|
|
|
|
|
|
|
| person.password_confirmation | abc123 | |
195
|
|
|
|
|
|
|
'-------------------------------------+--------------------------------------' |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
The object instance C<$request_model> would look like: |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
say $request_model->username; # jjn |
200
|
|
|
|
|
|
|
say $request_model->first_name; # John |
201
|
|
|
|
|
|
|
say $request_model->last_name; # Napiorkowski |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
And if the incoming is GET /list looks like |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
[debug] Query Parameters are: |
206
|
|
|
|
|
|
|
.-------------------------------------+--------------------------------------. |
207
|
|
|
|
|
|
|
| Parameter | Value | |
208
|
|
|
|
|
|
|
+-------------------------------------+--------------------------------------+ |
209
|
|
|
|
|
|
|
| page | 2 | |
210
|
|
|
|
|
|
|
| status | active | |
211
|
|
|
|
|
|
|
'-------------------------------------+--------------------------------------' |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
The object instance C<$paging_model> would look like: |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
say $paging_model->page; # 2 |
216
|
|
|
|
|
|
|
say $paging_model->status; # 'active' |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
And C<$request_model> has additional helper public methods to query attributes marked as request |
220
|
|
|
|
|
|
|
fields (via the C<property> attribute field) which you can read about below. |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
See L<CatalystX::RequestModel::ContentBodyParser::JSON> for an example of using this with JSON |
223
|
|
|
|
|
|
|
request content. |
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
=head1 DESCRIPTION |
226
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
Dealing with incoming POSTed (or PUTed/ PATCHed, etc) request content bodies is one of the most common |
228
|
|
|
|
|
|
|
code issues we have to deal with. L<Catalyst> has generic capacities for handling common incoming |
229
|
|
|
|
|
|
|
content types such as form URL encoded (common with HTML forms) and JSON as well as the ability to |
230
|
|
|
|
|
|
|
add in parsing for other types of contents (see L<Catalyst#DATA-HANDLERS>). However these parsers |
231
|
|
|
|
|
|
|
only check that a given body content is well formed and not that it's valid for your given problem |
232
|
|
|
|
|
|
|
domain. Additionally I find that we spend a lot of code lines in controllers that are doing nothing |
233
|
|
|
|
|
|
|
but munging and trying to wack incoming parameters into a form that can be actually used. |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
I've seen this approach of mapping incoming content bodies to models put to good use in frameworks |
236
|
|
|
|
|
|
|
in other languages. Mapping to a model gives you a clear place to do any data reformating you |
237
|
|
|
|
|
|
|
need as well as the type of pre validation work we often perform in a controller. Think of it as a |
238
|
|
|
|
|
|
|
type of command class pattern subtype. It promotes looser binding between your controller and your |
239
|
|
|
|
|
|
|
applications models, and it makes for neater, smaller controllers as well as separating out the |
240
|
|
|
|
|
|
|
types of work we do into smaller, more comprehendible classes. Lastly we encapsulate some of the |
241
|
|
|
|
|
|
|
more common types of issues into configuration (for example dealing with how HTML form POSTed |
242
|
|
|
|
|
|
|
parameters can cause you issues when they are sometimes in array form) as well as improve security |
243
|
|
|
|
|
|
|
by having an explict interface to the model. |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
Also once we have a model that defines an expected request, we should be able to build upon the meta data |
246
|
|
|
|
|
|
|
it exposed to do things like auto generate Open API / JSON Schema definition files (TBD but possible). |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
Basically you convert an unknown hash of values into a well defined object. This should reduce typo |
249
|
|
|
|
|
|
|
induced errors at the very least. |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
The main downside here is the time you need to inflate the additional classes as well as some documentation |
252
|
|
|
|
|
|
|
efforts needed to help new programmers understand this approach. |
253
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
If you hate this idea but still like the thought of having more structure in mapping your incoming |
255
|
|
|
|
|
|
|
random parameters you might want to check out L<Catalyst::TraitFor::Request::StructuredParameters>. |
256
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
B<NOTE> This is work in progress / late beta code. What I mean by that is that I will try to maintain |
258
|
|
|
|
|
|
|
the public API of this code (as described in the documentation) and only change it if absolutely |
259
|
|
|
|
|
|
|
needed to move the code forward. However the non public code is subject to change at any time. |
260
|
|
|
|
|
|
|
So if you are subclassing this and overriding non public methods you need to check carefully at each |
261
|
|
|
|
|
|
|
new release, but if you are just using the code as described you just need to review the changelog |
262
|
|
|
|
|
|
|
for any deprecation / breaking changes notices. |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
=head2 Declaring a model to accept request content bodies |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
To create a L<Catalyst> model that is ready to accept incoming content body data mapped to its attributes |
267
|
|
|
|
|
|
|
you just need to use L<CatalystX::RequestModel>: |
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
package Example::Model::RegistrationRequest; |
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
use Moose; |
272
|
|
|
|
|
|
|
use CatalystX::RequestModel; # <=== The important bit |
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
extends 'Catalyst::Model'; |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
namespace 'person'; # <=== Optional but useful when you have nested form data |
277
|
|
|
|
|
|
|
content_type 'application/x-www-form-urlencoded'; <=== Required so that we know which content parser to use |
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
has username => (is=>'ro', property=>1); |
280
|
|
|
|
|
|
|
has first_name => (is=>'ro', property=>1); |
281
|
|
|
|
|
|
|
has last_name => (is=>'ro', property=>1); |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable(); |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
When you include "use CatalystX::RequestModel" we apply the role L<CatalystX::RequestModel::DoesRequestModel> |
286
|
|
|
|
|
|
|
to you model, which gives you some useful methods as well as the ability to store the meta data needed |
287
|
|
|
|
|
|
|
to properly mapped parsed content bodies to your model. You also get two imported subroutines and a |
288
|
|
|
|
|
|
|
new field on your attribute declarations: |
289
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
C<namespace>: This is an optional imported subroutine which allows you to declare the namespace under which |
291
|
|
|
|
|
|
|
we expect to find the attribute mappings. This can be useful if your fields are not top level in your |
292
|
|
|
|
|
|
|
request content body (as in the example given above). This is optional and if you leave it off we just |
293
|
|
|
|
|
|
|
assume all fields are in the top level of the parsed data hash that you content parser builds based on whatever |
294
|
|
|
|
|
|
|
is in the content body. |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
C<content_type>: This is the request content type which this model is designed to handle. For now you can |
297
|
|
|
|
|
|
|
only declare one content type per model (if your endpoint can handle more than one content type you'll need |
298
|
|
|
|
|
|
|
for now to define a request model for each one; I'm open to changing this to allow one than one content type |
299
|
|
|
|
|
|
|
per request model, but I need to see your use cases for this before I paint myself into a corner codewise). |
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
C<property>: This is a new field allowed on your attribute declarations. Setting its value to C<1> (as in |
302
|
|
|
|
|
|
|
the example above) just means to use all the default settings for the declared content_type but you can declare |
303
|
|
|
|
|
|
|
this as a hashref instead if you have special handling needs. For example: |
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
has notes => (is=>'ro', property=>+{ expand=>'JSON' }); |
306
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
Here's the current list of property settings and what they do. You can also request the test cases for more |
308
|
|
|
|
|
|
|
examples: |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
=over 4 |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
=item name |
313
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
The name of the field in the request body we are mapping to the request model. The default is to just use |
315
|
|
|
|
|
|
|
the name of the attribute. |
316
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
=item omit_empty |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
Defaults to true. If there's no matching field in the request body we leave the request model attribute |
320
|
|
|
|
|
|
|
empty (we don't stick an undef in there). If for some reason you don't want that, setting this to false |
321
|
|
|
|
|
|
|
will put an undef into a scalar fields, and an empty array into an indexed one. If has no effect on |
322
|
|
|
|
|
|
|
attributes that map to a submodel since I have no idea what that should be (your use cases welcomed). |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
=item flatten |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
If the value associated with a field is an array, flatten it to a single value. The default is based on |
327
|
|
|
|
|
|
|
the body content parser. Its really a hack to deal with HTML form POST and Query parameters since the |
328
|
|
|
|
|
|
|
way those formats work you can't be sure if a value is flat or an array. This isn't a problem with |
329
|
|
|
|
|
|
|
JSON encoded request bodies. You'll need to check the docs for the Content Body Parser you are using to |
330
|
|
|
|
|
|
|
see what this does. |
331
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
=item always_array |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
Similar to C<flatten> but opposite, it forces a value into an array even if there's just one value. Again |
335
|
|
|
|
|
|
|
mostly useful to deal with ideosyncracies of HTML form post. |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
B<NOTE>: The attribute property settings C<flatten> and C<always_array> are currently exclusive (only one of |
338
|
|
|
|
|
|
|
the two will apply if you supply both. The C<always_array> property always takes precedence. At some point |
339
|
|
|
|
|
|
|
in the future supplying both might generate an exception so its best not to do that. I'm only leaving it |
340
|
|
|
|
|
|
|
allowed for now since I'm not sure there's a use case for both. |
341
|
|
|
|
|
|
|
|
342
|
|
|
|
|
|
|
=item boolean |
343
|
|
|
|
|
|
|
|
344
|
|
|
|
|
|
|
Defaults to false. If true will convert value to the common Perl convention 0 is false, 1 is true. The way |
345
|
|
|
|
|
|
|
this is converted is partly dependent on your content body parser. |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
=item expand |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
Example the value into a data structure by parsing it. Right now there's only one value this will take, |
350
|
|
|
|
|
|
|
which is C<JSON> and will then parse the value into a structure using a JSON parser. Again this is mostly |
351
|
|
|
|
|
|
|
useful for HTML form posting and coping with some limitations you have in classic HTML form input types. |
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
=back |
354
|
|
|
|
|
|
|
|
355
|
|
|
|
|
|
|
=head2 Setting a required attribute |
356
|
|
|
|
|
|
|
|
357
|
|
|
|
|
|
|
Generally it's best to not mark attributes which map to request properties as required and to handled anything |
358
|
|
|
|
|
|
|
like thia via your validation layer so that you can provide more useful feedback to your application users. |
359
|
|
|
|
|
|
|
If you do need to mark something required in order for your request model to be valid, please note that we |
360
|
|
|
|
|
|
|
capture the exception created by Moo/se and throw L<CatalystX::RequestModel::Utils::BadRequest>. If you are |
361
|
|
|
|
|
|
|
using L<CatalystX::Errors> this will get rendered as a HTTP 400 Bad Request; otherwise you just get the |
362
|
|
|
|
|
|
|
generic L<Catalyst> HTTP 500 Server Error or as you might have written in your custom error handling code. |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
=head2 Nested and Indexed attributes |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
Very often you will have incoming request data that is complex (or is trying to be, as in the case with |
367
|
|
|
|
|
|
|
HTML form post where you use a serialization format to flatten a deep structure into a flat list) In that |
368
|
|
|
|
|
|
|
case your body parser will attempt to deserialize that into a deep structure. In the case when you have |
369
|
|
|
|
|
|
|
a nested structure you can indicate that via mapping an attribute to a sub Catalyst model. For example: |
370
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
package Example::Model::AccountRequest; |
372
|
|
|
|
|
|
|
|
373
|
|
|
|
|
|
|
use Moose; |
374
|
|
|
|
|
|
|
use CatalystX::RequestModel; |
375
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
extends 'Catalyst::Model'; |
377
|
|
|
|
|
|
|
namespace 'person'; |
378
|
|
|
|
|
|
|
content_type 'application/x-www-form-urlencoded'; |
379
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
has username => (is=>'ro', required=>1, property=>{ always_array=>1 }); |
381
|
|
|
|
|
|
|
has first_name => (is=>'ro', property=>1); |
382
|
|
|
|
|
|
|
has last_name => (is=>'ro', property=>1); |
383
|
|
|
|
|
|
|
has profile => (is=>'ro', property=>+{ model=>'AccountRequest::Profile' }); |
384
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable(); |
386
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
package Example::Model::AccountRequest::Profile; |
388
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
use Moose; |
390
|
|
|
|
|
|
|
use CatalystX::RequestModel; |
391
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
extends 'Catalyst::Model'; |
393
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
has id => (is=>'ro', property=>1); |
395
|
|
|
|
|
|
|
has address => (is=>'ro', property=>1); |
396
|
|
|
|
|
|
|
has city => (is=>'ro', property=>1); |
397
|
|
|
|
|
|
|
has state_id => (is=>'ro', property=>1); |
398
|
|
|
|
|
|
|
has zip => (is=>'ro', property=>1); |
399
|
|
|
|
|
|
|
has phone_number => (is=>'ro', property=>1); |
400
|
|
|
|
|
|
|
has birthday => (is=>'ro', property=>1); |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable(); |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
If you had incoming body parameters like this (using the Form Content Body Parser): |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
.-------------------------------------+--------------------------------------. |
407
|
|
|
|
|
|
|
| Parameter | Value | |
408
|
|
|
|
|
|
|
+-------------------------------------+--------------------------------------+ |
409
|
|
|
|
|
|
|
| person.username | jjn | |
410
|
|
|
|
|
|
|
| person.first_name | John | |
411
|
|
|
|
|
|
|
| person.last_name | Napiorkowski | |
412
|
|
|
|
|
|
|
| person.profile.id | 1 | |
413
|
|
|
|
|
|
|
| person.profile.address | 15604 Harry Lind Road | |
414
|
|
|
|
|
|
|
| person.profile.city | Elgin | |
415
|
|
|
|
|
|
|
| person.profile.state_id | 2 | |
416
|
|
|
|
|
|
|
| person.profile.zip | 78621 | |
417
|
|
|
|
|
|
|
| person.profile.phone_number | 16467081837 | |
418
|
|
|
|
|
|
|
| person.profile.birthday | 2000-01-01 | |
419
|
|
|
|
|
|
|
'-------------------------------------+--------------------------------------' |
420
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
It would parse and inflate a request model like |
422
|
|
|
|
|
|
|
|
423
|
|
|
|
|
|
|
my $request_model = $c->model('AccountRequest'); |
424
|
|
|
|
|
|
|
|
425
|
|
|
|
|
|
|
$request_model->username; # jjn |
426
|
|
|
|
|
|
|
$request_model->first_name; # John |
427
|
|
|
|
|
|
|
$request_model->last_name; # Napiorkowski |
428
|
|
|
|
|
|
|
$request_model->profile->address; # 15604 Harry Lind Road |
429
|
|
|
|
|
|
|
$request_model->profile->city; # Elgin |
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
...and so on. |
432
|
|
|
|
|
|
|
|
433
|
|
|
|
|
|
|
If your nested models are directly under the main request model's namespace (as in the |
434
|
|
|
|
|
|
|
above example) you can shorten the value of the C<model> option to include only the |
435
|
|
|
|
|
|
|
affix. For example the following: |
436
|
|
|
|
|
|
|
|
437
|
|
|
|
|
|
|
has profile => (is=>'ro', property=>+{ model=>'AccountRequest::Profile' }); |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
Could be shortened to: |
440
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
has profile => (is=>'ro', property=>+{ model=>'::Profile' }); |
442
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
In the case when your deep structure also is an array/list you can mark that so via the |
444
|
|
|
|
|
|
|
C<indexed> option of the property field as in the following example: |
445
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
package Example::Model::AccountRequest; |
447
|
|
|
|
|
|
|
|
448
|
|
|
|
|
|
|
use Moose; |
449
|
|
|
|
|
|
|
use CatalystX::RequestModel; |
450
|
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
extends 'Catalyst::Model'; |
452
|
|
|
|
|
|
|
namespace 'person'; |
453
|
|
|
|
|
|
|
content_type 'application/x-www-form-urlencoded'; |
454
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
has username => (is=>'ro', required=>1, property=>{always_array=>1}); |
456
|
|
|
|
|
|
|
has first_name => (is=>'ro', property=>1); |
457
|
|
|
|
|
|
|
has last_name => (is=>'ro', property=>1); |
458
|
|
|
|
|
|
|
has credit_cards => (is=>'ro', property=>+{ indexed=>1, model=>'AccountRequest::CreditCard' }); |
459
|
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable(); |
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
package Example::Model::AccountRequest::CreditCard; |
463
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
use Moose; |
465
|
|
|
|
|
|
|
use CatalystX::RequestModel; |
466
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
extends 'Catalyst::Model'; |
468
|
|
|
|
|
|
|
|
469
|
|
|
|
|
|
|
has id => (is=>'ro', property=>1); |
470
|
|
|
|
|
|
|
has card_number => (is=>'ro', property=>1); |
471
|
|
|
|
|
|
|
has expiration => (is=>'ro', property=>1); |
472
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
Now if your incoming request looks like this it will be parsed into a deep structure by the |
474
|
|
|
|
|
|
|
correct body parser and mapped to the request object: |
475
|
|
|
|
|
|
|
|
476
|
|
|
|
|
|
|
.-------------------------------------+--------------------------------------. |
477
|
|
|
|
|
|
|
| Parameter | Value | |
478
|
|
|
|
|
|
|
+-------------------------------------+--------------------------------------+ |
479
|
|
|
|
|
|
|
| person.username | jjn | |
480
|
|
|
|
|
|
|
| person.first_name | John | |
481
|
|
|
|
|
|
|
| person.last_name | Napiorkowski | |
482
|
|
|
|
|
|
|
| person.credit_cards[0].card_number | 123123123123123 | |
483
|
|
|
|
|
|
|
| person.credit_cards[0].expiration | 3000-01-01 | |
484
|
|
|
|
|
|
|
| person.credit_cards[0].id | 1 | |
485
|
|
|
|
|
|
|
| person.credit_cards[1].card_number | 4444445555556666 | |
486
|
|
|
|
|
|
|
| person.credit_cards[1].expiration | 4000-01-01 | |
487
|
|
|
|
|
|
|
| person.credit_cards[1].id | 2 |
488
|
|
|
|
|
|
|
'-------------------------------------+--------------------------------------' |
489
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
It would parse and inflate a request model like |
491
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
my $request_model = $c->model('AccountRequest'); |
493
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
$request_model->username; # jjn |
495
|
|
|
|
|
|
|
$request_model->first_name; # John |
496
|
|
|
|
|
|
|
$request_model->last_name; # Napiorkowski |
497
|
|
|
|
|
|
|
$request_model->credit_cards->[0]->card_number; # 123123123123123 |
498
|
|
|
|
|
|
|
$request_model->credit_cards->[0]->expiration; # 3000-01-01 |
499
|
|
|
|
|
|
|
$request_model->credit_cards->[1]->card_number; # 4444445555556666 |
500
|
|
|
|
|
|
|
$request_model->credit_cards->[1]->expiration; # 4000-01-01 |
501
|
|
|
|
|
|
|
|
502
|
|
|
|
|
|
|
Please note the difference between a request property that is marked as C<indexed> versus |
503
|
|
|
|
|
|
|
C<always_array>. An C<indexed> property is required to have an array value while C<always_array> |
504
|
|
|
|
|
|
|
merely coerces a scalar to an array if the value isn't already an array. You cannot use |
505
|
|
|
|
|
|
|
C<indexed> and C<always_array> in the same request property. |
506
|
|
|
|
|
|
|
|
507
|
|
|
|
|
|
|
B<NOTE> You can use the C<indexed> attribute property with simple scalar values as well as |
508
|
|
|
|
|
|
|
deep structured objects. See test cases for more. |
509
|
|
|
|
|
|
|
|
510
|
|
|
|
|
|
|
Please see L<CatalystX::RequestModel::ContentBodyParser::JSON> for an example JSON request body |
511
|
|
|
|
|
|
|
with nesting. JSON is actually easier since we don't need a parsing convention to turn the |
512
|
|
|
|
|
|
|
flat list you get with HTML Form post into a deep structure, nor deal with some of form posting's |
513
|
|
|
|
|
|
|
idiocracies. |
514
|
|
|
|
|
|
|
|
515
|
|
|
|
|
|
|
=head2 Endpoints with more than one request model |
516
|
|
|
|
|
|
|
|
517
|
|
|
|
|
|
|
If an endpoint can handle more than one type of incoming content type you can define that |
518
|
|
|
|
|
|
|
via the subroutine attribute and the code will pick the right one or throw an exception if none match |
519
|
|
|
|
|
|
|
(See L</EXCEPTIONS> for more). |
520
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
sub update :POST Chained('root') PathPart('') Args(0) |
522
|
|
|
|
|
|
|
Does(RequestModel) |
523
|
|
|
|
|
|
|
RequestModel(RegistrationRequestForm) |
524
|
|
|
|
|
|
|
RequestModel(RegistrationRequesJSON) |
525
|
|
|
|
|
|
|
{ |
526
|
|
|
|
|
|
|
my ($self, $c, $request_model) = @_; |
527
|
|
|
|
|
|
|
## Do something with the $request_model |
528
|
|
|
|
|
|
|
} |
529
|
|
|
|
|
|
|
|
530
|
|
|
|
|
|
|
Also see L<Catalyst::ActionRole::RequestModel>. |
531
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
=head1 QUERY PARAMETERS |
533
|
|
|
|
|
|
|
|
534
|
|
|
|
|
|
|
See L<CatalystX::QueryModel>. |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
=head2 Requests with mixed query and body models |
537
|
|
|
|
|
|
|
|
538
|
|
|
|
|
|
|
You might have a request that has both query parameters (via the URL) as well as a content body request. |
539
|
|
|
|
|
|
|
In that case you make the content body request in the same way as you normally do and then add a second |
540
|
|
|
|
|
|
|
request model that specifies the query parameters. For example you might have a form post with mixed |
541
|
|
|
|
|
|
|
query and body parameters. You create your models as normal: |
542
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
package Example::Model::InfoQuery; |
544
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
use Moose; |
546
|
|
|
|
|
|
|
use CatalystX::QueryModel; |
547
|
|
|
|
|
|
|
|
548
|
|
|
|
|
|
|
extends 'Catalyst::Model'; |
549
|
|
|
|
|
|
|
|
550
|
|
|
|
|
|
|
has page => (is=>'ro', required=>1, property=>1); |
551
|
|
|
|
|
|
|
has offset => (is=>'ro', property=>1); |
552
|
|
|
|
|
|
|
has search => (is=>'ro', property=>1); |
553
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable(); |
555
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
package Example::Model::LoginRequest; |
557
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
use Moose; |
559
|
|
|
|
|
|
|
use CatalystX::RequestModel; |
560
|
|
|
|
|
|
|
|
561
|
|
|
|
|
|
|
extends 'Catalyst::Model'; |
562
|
|
|
|
|
|
|
content_type 'application/x-www-form-urlencoded'; |
563
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
has username => (is=>'ro', required=>1, property=>1); |
565
|
|
|
|
|
|
|
has password => (is=>'ro', property=>1); |
566
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable(); |
568
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
And in your action you list the request models: |
570
|
|
|
|
|
|
|
|
571
|
|
|
|
|
|
|
sub postinfo :Chained(/) Args(0) Does(RequestModel) RequestModel(LoginRequest) QueryModel(InfoQuery) { |
572
|
|
|
|
|
|
|
my ($self, $c, $login_request, $info_query) = @_; |
573
|
|
|
|
|
|
|
} |
574
|
|
|
|
|
|
|
|
575
|
|
|
|
|
|
|
Now if you get a request like this: |
576
|
|
|
|
|
|
|
|
577
|
|
|
|
|
|
|
[debug] Query Parameters are: |
578
|
|
|
|
|
|
|
.-------------------------------------+--------------------------------------. |
579
|
|
|
|
|
|
|
| Parameter | Value | |
580
|
|
|
|
|
|
|
+-------------------------------------+--------------------------------------+ |
581
|
|
|
|
|
|
|
| offset | 100 | |
582
|
|
|
|
|
|
|
| page | 10 | |
583
|
|
|
|
|
|
|
| search | nope | |
584
|
|
|
|
|
|
|
'-------------------------------------+--------------------------------------' |
585
|
|
|
|
|
|
|
[debug] Body Parameters are: |
586
|
|
|
|
|
|
|
.-------------------------------------+--------------------------------------. |
587
|
|
|
|
|
|
|
| Parameter | Value | |
588
|
|
|
|
|
|
|
+-------------------------------------+--------------------------------------+ |
589
|
|
|
|
|
|
|
| password | abc123 | |
590
|
|
|
|
|
|
|
| username | jjn | |
591
|
|
|
|
|
|
|
'-------------------------------------+--------------------------------------' |
592
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
You'll get two models like this: |
594
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
print $login_request->username; # "jjn" |
596
|
|
|
|
|
|
|
print $login_request->password; # "abc123" |
597
|
|
|
|
|
|
|
|
598
|
|
|
|
|
|
|
print $info_query->offset; # 100 |
599
|
|
|
|
|
|
|
print $info_query->page; # 10 |
600
|
|
|
|
|
|
|
print $info_query->search; # "nope" |
601
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
You can also do nesting and indexing with query params (See L<CatalystX::QueryModel::QueryParser> and |
603
|
|
|
|
|
|
|
L<CatalystX::QueryModel::DoesQueryModel>) |
604
|
|
|
|
|
|
|
|
605
|
|
|
|
|
|
|
B<IMPORTANT NOTE>: Due to a limitation in how Catalyst finds subroutine attributes on an action we cannot |
606
|
|
|
|
|
|
|
determine the order of declaration of dissimilar attributes (such as BodyModel and QueryModel). As a |
607
|
|
|
|
|
|
|
result when you have both attibutes on an action we will process and add to the arguments list first the |
608
|
|
|
|
|
|
|
Body Models and then the Query models, even if you list the Query model first in the method |
609
|
|
|
|
|
|
|
declaration. |
610
|
|
|
|
|
|
|
|
611
|
|
|
|
|
|
|
=head1 CONTENT BODY PARSERS |
612
|
|
|
|
|
|
|
|
613
|
|
|
|
|
|
|
This distribution comes bundled with the following content body parsers for handling common needs. If |
614
|
|
|
|
|
|
|
you need to create you own you should subclass L<CatalystX::RequestModel::ContentBodyParser> and place |
615
|
|
|
|
|
|
|
the class in the C<CatalystX::RequestModel::ContentBodyParser> namespace. |
616
|
|
|
|
|
|
|
|
617
|
|
|
|
|
|
|
=head2 Form URL Encoded |
618
|
|
|
|
|
|
|
|
619
|
|
|
|
|
|
|
When a model declares its content_type to be 'application/x-www-form-urlencoded' we use |
620
|
|
|
|
|
|
|
L<CatalystX::RequestModel::ContentBodyParser::FormURLEncoded> to parse it. Please see the documention |
621
|
|
|
|
|
|
|
for more regarding how we parse the flat list of posted body content into a deep structure. |
622
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
This handles both POST HTML form content as well as query parameters. |
624
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
=head2 Multi Part Uploads |
626
|
|
|
|
|
|
|
|
627
|
|
|
|
|
|
|
This handles content types of 'multipart/form-data'. Uploads are mapped to attributes with a value that |
628
|
|
|
|
|
|
|
is an instance of L<Catalyst::Request::Upload>. |
629
|
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
=head2 JSON |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
When a model declares its content_type to be 'application/json' we use |
633
|
|
|
|
|
|
|
L<CatalystX::RequestModel::ContentBodyParser::JSON> to parse it. |
634
|
|
|
|
|
|
|
|
635
|
|
|
|
|
|
|
=head1 METHODS |
636
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
Please see L<CatalystX::RequestModel::DoesRequestModel> for the public API details. |
638
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
=head1 EXCEPTIONS |
640
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
This class can throw the following exceptions. Please note all exceptions are compatible with |
642
|
|
|
|
|
|
|
L<CatalystX::Errors> to make it easy and consistent to convert errors to actual error responses. |
643
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
=head2 Bad Request |
645
|
|
|
|
|
|
|
|
646
|
|
|
|
|
|
|
If your request generates an exception when trying to instantiate your model (basically when calling ->new |
647
|
|
|
|
|
|
|
on it) we capture that error, log the error and throw a L<CatalystX::RequestModel::Utils::BadRequest> |
648
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
=head2 Invalid Request Content Type |
650
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
If the incoming content body doesn't have a content type header that matches one of the available |
652
|
|
|
|
|
|
|
content body parsers then we throw an L<CatalystX::RequestModel::Utils::InvalidContentType>. This |
653
|
|
|
|
|
|
|
will get interpretated as an HTTP 415 status client error if you are using L<CatalystX::Errors>. |
654
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
=head1 AUTHOR |
656
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
John Napiorkowski <jjnapiork@cpan.org> |
658
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
=head1 COPYRIGHT |
660
|
|
|
|
|
|
|
|
661
|
|
|
|
|
|
|
2023 |
662
|
|
|
|
|
|
|
|
663
|
|
|
|
|
|
|
=head1 LICENSE |
664
|
|
|
|
|
|
|
|
665
|
|
|
|
|
|
|
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. |
666
|
|
|
|
|
|
|
|
667
|
|
|
|
|
|
|
=cut |
668
|
|
|
|
|
|
|
|