File Coverage

blib/lib/CatalystX/RequestModel.pm
Criterion Covered Total %
statement 81 95 85.2
branch 16 24 66.6
condition 6 11 54.5
subroutine 113 133 84.9
pod 0 7 0.0
total 216 270 80.0


line stmt bran cond sub pod time code
1             package CatalystX::RequestModel;
2              
3             our $VERSION = '0.017';
4              
5 6     6   13080704 use Class::Method::Modifiers;
  6         10378  
  6         413  
6 6     6   56 use Scalar::Util;
  6         23  
  6         217  
7 6     6   2725 use Moo::_Utils;
  6         16815  
  6         324  
8 6     6   63 use Module::Pluggable::Object;
  6         16  
  6         175  
9 6     6   43 use Module::Runtime ();
  6         12  
  6         102  
10 6     6   2885 use CatalystX::RequestModel::Utils::InvalidContentType;
  6         2246  
  6         2853  
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 302 sub default_roles { return @DEFAULT_ROLES }
21 168     168 0 585 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 65 my $ct = shift;
28 33   33     181 return $ContentBodyParsers{$ct} || CatalystX::RequestModel::Utils::InvalidContentType->throw(ct=>$ct);
29             }
30              
31             sub load_content_body_parsers {
32 84     84 0 187 my $class = shift;
33 84         839 my @packages = Module::Pluggable::Object->new(
34             search_path => "${class}::ContentBodyParser"
35             )->plugins;
36              
37             %ContentBodyParsers = map {
38 252         3470 $_->content_type => $_;
39             } map {
40 84         279114 Module::Runtime::use_module $_;
  252         5604  
41             } @packages;
42             }
43              
44             sub import {
45 84     84   828602 my $class = shift;
46 84         289 my $target = caller;
47              
48 84         1682 $class->load_content_body_parsers;
49              
50 84 50       568 unless (Moo::Role->is_role($target)) {
51 84         4545 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   59 no strict 'refs';
  6         20  
  6         4239  
57 0         0 *{"${target}::request_metadata"} = $method;
  0         0  
58             }
59 0         0 &$orig;
60 84         745 });
61             }
62              
63 84         4747 foreach my $default_role ($class->default_roles) {
64 84 50       326 next if Role::Tiny::does_role($target, $default_role);
65 84         1591 Moo::Role->apply_roles_to_package($target, $default_role);
66 84         56390 foreach my $export ($class->default_exports) {
67 420         13582 Moo::_Utils::_install_tracked($target, "__${export}_for_exporter", \&{"${target}::${export}"});
  420         1649  
68             }
69             }
70              
71             my %cb = map {
72 84         3210 $_ => $target->can("__${_}_for_exporter");
  420         1776  
73             } $class->default_exports;
74              
75 84         373 foreach my $exported_method (keys %cb) {
76             my $sub = sub {
77 582 100   582   162533 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         565 return $cb{$exported_method}->(@_);
79             } else {
80 426         1759 return $cb{$exported_method}->($target, @_);
81             }
82 420         12586 };
83 420         962 Moo::_Utils::_install_tracked($target, $exported_method, $sub);
84             }
85              
86             Class::Method::Modifiers::install_modifier $target, 'around', 'has', sub {
87 354     354   404904 my $orig = shift;
88 354         1580 my ($attr, %opts) = @_;
89              
90 354         611 my $predicate;
91 354 100       1093 unless($opts{required}) {
92 312 50       842 if(exists $opts{predicate}) {
93 0         0 $predicate = $opts{predicate};
94             } else {
95 312         832 $predicate = "__cx_req_model_has_${attr}";
96 312         675 $opts{predicate} = $predicate;
97             }
98             }
99              
100 354 50       1032 if(my $info = delete $opts{property}) {
101 354 100 100     1833 $info = +{ name=>$attr } unless (ref($info)||'') eq 'HASH';
102 354 100       982 $info->{attr_predicate} = $predicate if defined($predicate);
103 354 100       907 $info->{omit_empty} = 1 unless exists($info->{omit_empty});
104 354         533 my $method = \&{"${target}::property"};
  354         1608  
105 354         972 $method->($attr, $info, \%opts);
106             }
107              
108 354         1474 return $orig->($attr, %opts);
109 84 50       3691 } if $target->can('has');
110             }
111              
112             sub _add_metadata {
113 426     426   1062 my ($target, $type, @add) = @_;
114 426   66     1521 my $store = $Meta_Data{$target}{$type} ||= do {
115 156         285 my @data;
116 156 50 33     569 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         7477 require Sub::Util;
123 156     161   1421 my $method = Sub::Util::set_subname "${target}::${type}_metadata" => sub { @data };
  161     161   632  
        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   49 no strict 'refs';
  6         28  
  6         1504  
125 156         441 *{"${target}::${type}_metadata"} = $method;
  156         732  
126             }
127 156         573 \@data;
128             };
129              
130 426         979 push @$store, @add;
131 426         1247 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