File Coverage

blib/lib/Catalyst/ActionRole/RequestModel.pm
Criterion Covered Total %
statement 125 125 100.0
branch 56 80 70.0
condition 33 49 67.3
subroutine 17 17 100.0
pod n/a
total 231 271 85.2


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