File Coverage

blib/lib/Catalyst/ActionRole/RequestModel.pm
Criterion Covered Total %
statement 121 121 100.0
branch 55 78 70.5
condition 33 49 67.3
subroutine 17 17 100.0
pod n/a
total 226 265 85.2


line stmt bran cond sub pod time code
1             package Catalyst::ActionRole::RequestModel;
2              
3 6     6   875039 use Moose::Role;
  6         18  
  6         62  
4 6     6   35839 use Catalyst::Utils;
  6         23  
  6         185  
5 6     6   49 use CatalystX::RequestModel::Utils::InvalidContentType;
  6         23  
  6         203  
6 6     6   2771 use String::CamelCase;
  6         3717  
  6         322  
7 6     6   65 use Carp;
  6         23  
  6         12893  
8              
9             requires 'attributes', 'execute';
10              
11             our $DEFAULT_BODY_POSTFIX = 'Body';
12             our $DEFAULT_BODY_PREFIX_NAMESPACE = '';
13             our $DEFAULT_QUERY_POSTFIX = 'Query';
14             our $DEFAULT_QUERY_PREFIX_NAMESPACE = '';
15              
16             around 'execute', sub {
17             my ($orig, $self, $controller, $ctx, @args) = @_;
18             my @req_models = $self->_get_request_model($controller, $ctx);
19             push @args, @req_models if @req_models;
20              
21             return $self->$orig($controller, $ctx, @args);
22             };
23              
24             sub _default_body_postfix {
25 2     2   6 my ($self, $controller, $ctx) = @_;
26 2 50       9 return $controller->config->{body_model_postfix} if exists $controller->config->{body_model_postfix};
27 2 50       177 return $controller->default_body_postfix($self, $ctx) if $controller->can('default_body_postfix');
28 2         10 return $DEFAULT_BODY_POSTFIX;
29             }
30              
31             sub _default_body_prefix_namespace {
32 4     4   11 my ($self, $controller, $ctx) = @_;
33 4 50       19 return $controller->config->{body_model_prefix_namespace} if exists $controller->config->{body_model_prefix_namespace};
34 4 50       331 return $controller->default_body_prefix_namespace($self, $ctx) if $controller->can('default_body_prefix_namespace');
35 4         15 return $DEFAULT_BODY_PREFIX_NAMESPACE;
36             }
37              
38             sub _default_body_model {
39 1     1   6 my ($self, $controller, $ctx) = @_;
40 1         5 return $self->_default_body_model_for_action($controller, $ctx, $self);
41             }
42              
43             sub _default_body_model_for_action {
44 2     2   6 my ($self, $controller, $ctx, $action) = @_;
45 2 50       20 return $controller->default_body_model($self, $ctx) if $controller->can('default_body_model');
46              
47 2         9 my $prefix = $self->_default_body_prefix_namespace($controller, $ctx);
48 2 50 33     7 $prefix .= '::' if length($prefix) && $prefix !~m/::$/;
49              
50 2         77 my $action_namepart = String::CamelCase::camelize($action->reverse);
51 2         87 $action_namepart =~s/\//::/g;
52            
53 2         7 my $postfix = $self->_default_body_postfix($controller, $ctx);
54 2         6 my $model_component_name = "${prefix}${action_namepart}${postfix}";
55              
56 2 50       7 $ctx->log->debug("Initializing RequestModel: $model_component_name") if $ctx->debug;
57 2         15 return $model_component_name;
58             }
59              
60              
61             sub _process_body_model {
62 17     17   95 my ($self, $controller, $ctx, $model) = @_;
63 17 100       105 return $model unless $model =~m/^~/;
64              
65 2         11 $model =~s/^~(::)?//;
66              
67 2         8 my $prefix = $self->_default_body_prefix_namespace($controller, $ctx);
68 2 50 33     13 $prefix .= '::' if length($prefix) && $prefix !~m/::$/;
69              
70 2         16 my $namepart = String::CamelCase::camelize($controller->action_namespace);
71 2         239 $namepart =~s/\//::/g;
72              
73 2 50       11 my $model_component_name = length("${prefix}${namepart}") ? "${prefix}${namepart}::${model}" : $model;
74              
75 2 50       9 $ctx->log->debug("Initializing Body Model: $model_component_name") if $ctx->debug;
76 2         15 return $model_component_name;
77             }
78              
79             sub _default_query_postfix {
80 2     2   7 my ($self, $controller, $ctx) = @_;
81 2 50       7 return $controller->config->{query_model_postfix} if exists $controller->config->{query_model_postfix};
82 2 50       174 return $controller->default_query_postfix($self, $ctx) if $controller->can('default_query_postfix');
83 2         20 return $DEFAULT_QUERY_POSTFIX;
84             }
85              
86             sub _default_query_prefix_namespace {
87 4     4   9 my ($self, $controller, $ctx) = @_;
88 4 50       21 return $controller->config->{query_model_prefix_namespace} if exists $controller->config->{query_model_prefix_namespace};
89 4 50       351 return $controller->default_query_prefix_namespace($self, $ctx) if $controller->can('default_query_prefix_namespace');
90 4         18 return $DEFAULT_QUERY_PREFIX_NAMESPACE;
91             }
92              
93             sub _default_query_model {
94 1     1   2 my ($self, $controller, $ctx) = @_;
95 1         5 return $self->_default_query_model_for_action($controller, $ctx, $self);
96             }
97              
98             sub _default_query_model_for_action {
99 2     2   9 my ($self, $controller, $ctx, $action) = @_;
100 2 50       19 return $controller->default_query_model($self, $ctx) if $controller->can('default_query_model');
101              
102 2         10 my $prefix = $self->_default_query_prefix_namespace($controller, $ctx);
103 2 50 33     16 $prefix .= '::' if length($prefix) && $prefix !~m/::$/;
104              
105 2         62 my $action_namepart = String::CamelCase::camelize($action->reverse);
106 2         69 $action_namepart =~s/\//::/g;
107            
108 2         11 my $postfix = $self->_default_query_postfix($controller, $ctx);
109 2         10 my $model_component_name = "${prefix}${action_namepart}${postfix}";
110              
111 2 50       11 $ctx->log->debug("Initializing Query Model: $model_component_name") if $ctx->debug;
112 2         12 return $model_component_name;
113             }
114              
115              
116             sub _process_query_model {
117 8     8   26 my ($self, $controller, $ctx, $model) = @_;
118 8 100       51 return $model unless $model =~m/^~/;
119              
120 2         12 $model =~s/^~(::)?//;
121              
122 2         11 my $prefix = $self->_default_query_prefix_namespace($controller, $ctx);
123 2 50 33     12 $prefix .= '::' if length($prefix) && $prefix !~m/::$/;
124              
125 2         14 my $namepart = String::CamelCase::camelize($controller->action_namespace);
126 2         247 $namepart =~s/\//::/g;
127              
128 2 50       16 my $model_component_name = length("${prefix}${namepart}") ? "${prefix}${namepart}::${model}" : $model;
129              
130 2 50       9 $ctx->log->debug("Initializing Query Model: $model_component_name") if $ctx->debug;
131 2         15 return $model_component_name;
132             }
133              
134             sub _get_request_model {
135 23     23   79 my ($self, $controller, $ctx) = @_;
136             return unless exists $self->attributes->{RequestModel} ||
137             exists $self->attributes->{QueryModel} ||
138             exists $self->attributes->{BodyModel} ||
139             exists $self->attributes->{QueryModelFor} ||
140 23 50 100     595 exists $self->attributes->{BodyModelFor};
      100        
      100        
      66        
141              
142 16         81 my @models = map { $_=~s/^\s+|\s+$//g; $_ } # Allow RequestModel( Model ) and RequestModel (Model, Model2)
  16         57  
143 17   100     334 map {split ',', $_||'' } # Allow RequestModel(Model1,Model2)
144 23 100       548 @{$self->attributes->{RequestModel} || []},
145 23 100       1173 @{$self->attributes->{BodyModel} || []};
  23         1264  
146              
147 23         194 my $request_content_type = $ctx->req->content_type;
148              
149 23 100 100     2932 if(exists($self->attributes->{RequestModel}) || exists($self->attributes->{BodyModel})) {
150 14 100       412 @models = $self->_default_body_model($controller, $ctx) unless @models;
151             @models = map {
152 14         42 $self->_process_body_model($controller, $ctx, $_);
  17         61  
153             } @models;
154             }
155              
156 23 100       369 if(my ($action_name) = @{$self->attributes->{BodyModelFor}||[]}) {
  23 100       587  
157 1   33     24 my $action = $controller->action_for($action_name) || croak "There is no action for '$action_name'";
158 1         240 my $model = $self->_default_body_model_for_action($controller, $ctx, $action);
159 1         5 @models = ($model);
160             }
161              
162             # Allow GET to hijack form encoded
163 23 100 66     395 $request_content_type = "application/x-www-form-urlencoded"
164             if (($ctx->req->method eq 'GET') && !$request_content_type);
165              
166             my (@matching_models) = grep {
167 15 100       106 (lc($_->content_type) eq lc($request_content_type)) || ($_->get_content_in eq 'query')
168             } map {
169 23         1498 $self->_build_request_model_instance($controller, $ctx, $_)
  17         79  
170             } @models;
171              
172 21 100 100     614 if(exists($self->attributes->{RequestModel}) ||exists($self->attributes->{BodyModelFor}) || exists($self->attributes->{BodyModel})) {
      100        
173 13         580 my ($content_type, @params) = $ctx->req->content_type; # handle "multipart/form-data; boundary=xYzZY"
174 13 50       1241 return CatalystX::RequestModel::Utils::InvalidContentType->throw(ct=>$content_type) unless @matching_models;
175             }
176              
177             ## Query
178 7         34 my @qmodels = map { $_=~s/^\s+|\s+$//g; $_ } # Allow RequestModel( Model ) and RequestModel (Model, Model2)
  7         27  
179 8   100     121 map {split ',', $_||'' } # Allow RequestModel(Model1,Model2)
180 21 100       542 @{$self->attributes->{QueryModel} || []};
  21         511  
181              
182 21 100       732 if(exists($self->attributes->{QueryModel})) {
183 8 100       80 @qmodels = $self->_default_query_model($controller, $ctx) unless @qmodels;
184             @qmodels = map {
185 8         21 $self->_process_query_model($controller, $ctx, $_);
  8         40  
186             } @qmodels;
187             }
188              
189 21 100       139 if(my ($action_name) = @{$self->attributes->{QueryModelFor}||[]}) {
  21 100       509  
190 1   33     23 my $action = $controller->action_for($action_name) || croak "There is no action for '$action_name'";
191 1         252 my $qmodel = $self->_default_query_model_for_action($controller, $ctx, $action);
192 1         4 @qmodels = ($qmodel);
193             }
194              
195             # Loop over all the found models. Create each one and then filter by request
196             # content type if that is defined. This allows you to have different query paremters
197             # based on the incoming content type.
198              
199             push @matching_models, grep {
200 9 50       41 $_->has_content_type ? (lc($_->content_type) eq lc($request_content_type)) : 1;
201             } map {
202 21         309 $self->_build_request_model_instance($controller, $ctx, $_)
  9         30  
203             } @qmodels;
204              
205 21         81 return @matching_models;
206             }
207              
208             sub _build_request_model_instance {
209 26     26   89 my ($self, $controller, $ctx, $request_model_class) = @_;
210 26   33     126 my $request_model_instance = $ctx->model($request_model_class)
211             || croak "Request Model '$request_model_class' doesn't exist";
212 24         119 return $request_model_instance;
213             }
214              
215             1;
216              
217             =head1 NAME
218              
219             Catalyst::ActionRole::RequestModel - Inflate a Request Model
220              
221             =head1 SYNOPSIS
222              
223             package Example::Controller::Account;
224              
225             use Moose;
226             use MooseX::MethodAttributes;
227              
228             extends 'Catalyst::Controller';
229              
230             sub root :Chained(/root) PathPart('account') CaptureArgs(0) { }
231              
232             sub update :POST Chained('root') PathPart('') Args(0) Does(RequestModel) BodyModel(AccountRequest) {
233             my ($self, $c, $request_model) = @_;
234             ## Do something with the $request_model
235             }
236              
237             sub list :GET Chained('root') PathPart('') Args(0) Does(RequestModel) QueryModel(PagingModel) {
238             my ($self, $c, $paging_model) = @_;
239             }
240              
241             __PACKAGE__->meta->make_immutable;
242              
243             =head1 DESCRIPTION
244              
245             Moves creating the request model into the action class execute phase. The following two actions are essentially
246             the same in effect:
247              
248             sub update :POST Chained('root') PathPart('') Args(0) Does(RequestModel) BodyModel(AccountRequest) {
249             my ($self, $c, $request_model) = @_;
250             ## Do something with the $request_model
251             }
252              
253             sub update :POST Chained('root') PathPart('') Args(0) {
254             my ($self, $c) = @_;
255             my $request_model = $c->model('AccountRequest');
256             ## Do something with the $request_model
257             }
258              
259             The main reason for moving this into the action attributes line is the thought that it allows us
260             to declare the request model as meta data on the action and in the future we will be able to
261             introspect that meta data at application setup time to do things like generate an Open API specification.
262             Also, if you have several request models for the endpoint you can declare all of them on the
263             attributes line and we will match the incoming request to the best request model, or throw an exception
264             if none match. So if you have more than one this saves you writing that boilerplate code to chose and
265             to handled the no match conditions.
266              
267             You might also just find the code neater and more clean reading. Downside is for people unfamiliar with
268             this system it might increase learning curve time.
269              
270             =head1 ATTRITBUTE VALUE DEFAULTS
271              
272             Although you may prefer to be explicit in defining the request model name, we infer default values for
273             both B<BodyModeL> and B<QueryModel> based on the action name and the controller namespace. For example,
274              
275             package Example::Controller::Account;
276              
277             use Moose;
278             use MooseX::MethodAttributes;
279              
280             extends 'Catalyst::Controller';
281              
282             sub root :Chained(/root) PathPart('account') CaptureArgs(0) { }
283              
284             sub update :POST Chained('root') PathPart('') Args(0) Does(RequestModel) BodyModel() {
285             my ($self, $c, $request_model) = @_;
286             ## Do something with the $request_model
287             }
288              
289             sub list :GET Chained('root') PathPart('') Args(0) Does(RequestModel) QueryModel() {
290             my ($self, $c, $paging_model) = @_;
291             }
292              
293             For the body model associated with the C<update> action, we will look for a model named
294             C<Example::Model::Account:UpdateBody> and for the query model associated with the C<list> action
295             we will look for a model named C<Example::Model::Account:ListQuery>. You can change the default
296             'postfix' for both types of models by defining the following methods in your controller class:
297              
298             sub default_body_postfix { return 'Body' }
299             sub default_query_postfix { return 'Query' }
300              
301             Or via the controller configuration:
302              
303             __PACKAGE__->config(
304             default_body_postfix => 'Body',
305             default_query_postfix => 'Query',
306             );
307              
308             You can also prepend a namespace affix to either the body or query model name by defining the following
309             methods in your controller class:
310              
311             sub default_body_prefix_namespace { return 'MyApp::Model' }
312             sub default_query_prefix_namespace { return 'MyApp::Model' }
313              
314             Or via the controller configuration:
315              
316             __PACKAGE__->config(
317             default_body_prefix_namespace => 'MyApp::Model',
318             default_query_prefix_namespace => 'MyApp::Model',
319             );
320              
321             By default both namespace prefixes are empty, while the postfixes are 'Body' and 'Query' respectively.
322             This I think sets a reasonable pattern that you can reuse to help make your code more consistent while
323             allowing overrides for special cases.
324              
325             Alternatively you can use the action namespace of the current controller as a namespace prefix for
326             the model name. For example, if you have the following controller:
327              
328             package Example::Controller::Account;
329              
330             use Moose;
331             use MooseX::MethodAttributes;
332              
333             extends 'Catalyst::Controller';
334              
335             sub root :Chained(/root) PathPart('account') CaptureArgs(0) { }
336              
337             ## You can use either ~ or ~:: to indicate 'under the current namespace'.
338              
339             sub update :POST Chained('root') PathPart('') Args(0) Does(RequestModel) BodyModel(~::RequestBody) {
340             my ($self, $c, $request_model) = @_;
341             ## Do something with the $request_model
342             }
343              
344             sub list :GET Chained('root') PathPart('') Args(0) Does(RequestModel) QueryModel(~RequestQuery) {
345             my ($self, $c, $paging_model) = @_;
346             }
347              
348             __PACKAGE__->meta->make_immutable;
349              
350             Then we will look for a model named C<Example::Model::Account::RequestBody>
351             and C<Example::Model::Account:RequestQuery> in your application namespace. This approach also can
352             set a query and body namespace prefix but not the postfix.
353              
354             =head1 METHOD ATTRIBUTES
355              
356             This action role defines the following method attributes
357              
358             =head2 RequestModel
359              
360             Deprecated; for now this is an alias for BodyModel. Use BodyModel instead and please convert your code to use.
361              
362             =head2 BodyModel
363              
364             Should be the name of a L<Catalyst::Model> subclass that does <CatalystX::RequestModel::DoesRequestModel>. You may
365             supply more than one value to handle different request content types (the code will match the incoming
366             content type to an available request model and throw an L<CatalystX::RequestModel::Utils::InvalidContentType>
367             exception if none of the available models match.
368              
369             Example of an action with more than one request model, which will be matched based on request content type.
370              
371             sub update :POST Chained('root') PathPart('') Args(0) Does(RequestModel) BodyModel(AccountRequestForm) RequestModel(AccountRequestJSON) {
372             my ($self, $c, $request_model) = @_;
373             ## Do something with the $request_model
374             }
375              
376             Also, if more than one model matches, you'll get an instance of each matching model.
377              
378             You can also leave the C<BodyModel> value empty; if you do so it use a default model based on the action private name.
379             For example if the private name is C</posts/user_comments> we will look for a model package name C<MyApp::Model::Posts::UserCommentsBody>.
380             Please see L</ATTRITBUTE VALUE DEFAULTS> for more on configurating and controlling how this works.
381              
382              
383             =head2 QueryModel
384              
385             Should be the name of a L<Catalyst::Model> subclass that does L<CatalystX::QueryModel::DoesQueryModel>. You may
386             supply more than one value to handle different request content types (the code will match the incoming
387             content type to an available query model and throw an L<CatalystX::RequestModel::Utils::InvalidContentType>
388             exception if none of the available models match.
389              
390             sub root :Chained(/root) PathPart('users') CaptureArgs(0) { }
391              
392             sub list :GET Chained('root') PathPart('') Args(0) Does(RequestModel) QueryModel(PagingModel) {
393             my ($self, $c, $paging_model) = @_;
394             }
395              
396             B<NOTE>: In the situation where you have QueryModel and BodyModel for the same action, the request models
397             will be added first to the action argument list, followed by the query models, no matter what order they appear
398             in the action method declaration. This is due to a limitation in how Catalyst collects the subroutine attributes
399             (we can't know the order of dissimilar attributes since this information is stored in a hash, not an array, and
400             L<Catalyst> allows a controller to inherit attributes from a base class, or from a role or even from configutation).
401             However the order of QueryModels and RequestModels independently are preserved.
402              
403             You can also leave the C<QueryModel> value empty; if you do so it use a default model based on the action private name.
404             For example if the private name is C</posts/user_comments> we will look for a model package name C<MyApp::Model::Posts::UserCommentsQuery>.
405             Please see L</ATTRITBUTE VALUE DEFAULTS> for more on configurating and controlling how this works.
406              
407             =head2 BodyModelFor
408              
409             =head2 QueryModelFor
410              
411             Use the default models for a different action in the same controller. Useful for example if you have a lot of
412             basic CRUD style controllers where the create and update actions need the same parameters.
413              
414             =head1 AUTHOR
415              
416             See L<CatalystX::RequestModel>.
417            
418             =head1 COPYRIGHT
419            
420             See L<CatalystX::RequestModel>.
421              
422             =head1 LICENSE
423            
424             See L<CatalystX::RequestModel>.
425            
426             =cut