File Coverage

blib/lib/Catalyst/ActionRole/QueryParameter.pm
Criterion Covered Total %
statement 62 77 80.5
branch 48 74 64.8
condition 16 27 59.2
subroutine 6 6 100.0
pod n/a
total 132 184 71.7


line stmt bran cond sub pod time code
1             package Catalyst::ActionRole::QueryParameter;
2              
3 3     3   2393001 use Moose::Role;
  3         412178  
  3         23  
4 3     3   14918 use Scalar::Util ();
  3         6  
  3         3113  
5             requires 'attributes', 'match', 'match_captures';
6              
7             our $VERSION = '0.09';
8              
9             sub _resolve_query_attrs {
10 32 100   32   59 @{shift->attributes->{QueryParam} || []};
  32         730  
11             }
12              
13             has query_constraints => (
14             is=>'ro',
15             required=>1,
16             isa=>'HashRef',
17             lazy=>1,
18             builder=>'_prepare_query_constraints');
19              
20             sub _prepare_query_constraints {
21 32     32   65 my ($self) = @_;
22              
23 32         61 my @constraints;
24             my $compare = sub {
25 24     24   55 my ($op, $cond) = @_;
26              
27 24 100 66     164 if(defined $cond && length $cond && !defined $op) {
      100        
28 1 50       12 die "You must use a newer version of Catalyst (5.90090+) if you want to use Type Constraint '$cond'"
29             unless $self->can('resolve_type_constraint');
30 1         7 my ($tc) = $self->resolve_type_constraint($cond);
31 1 50       176 die "We think $cond is a type constraint, but its not" unless $tc;
32 1         13 return sub { $tc->check(shift) };
  1         8  
33             }
34              
35 23 100       58 if(defined $op) {
36 10 50       50 die "No such op of $op" unless $op =~m/^(==|eq|!=|<=|>=|>|=~|<|gt|ge|lt|le)$/i;
37             # we have an $op, make sure there's a comparator
38 10 50       29 die "You can't have an operator without a target condition" unless defined($cond);
39             } else {
40             # No op mean the field just need to exist with a defined value
41 13         51 return sub { defined(shift) };
  33         80  
42             }
43              
44 10 100 100     60 return sub { my $v = shift; return defined($v) ? (Scalar::Util::looks_like_number($v) && ($v == $cond)) : 0 } if $op eq '==';
  12 100       27  
  12         90  
45 8 0 0     24 return sub { my $v = shift; return defined($v) ? (Scalar::Util::looks_like_number($v) && ($v != $cond)) : 0 } if $op eq '!=';
  0 50       0  
  0         0  
46 8 0 0     23 return sub { my $v = shift; return defined($v) ? (Scalar::Util::looks_like_number($v) && ($v <= $cond)) : 0 } if $op eq '<=';
  0 50       0  
  0         0  
47 8 100 100     46 return sub { my $v = shift; return defined($v) ? (Scalar::Util::looks_like_number($v) && ($v >= $cond)) : 0 } if $op eq '>=';
  12 100       29  
  12         85  
48 6 100 100     31 return sub { my $v = shift; return defined($v) ? (Scalar::Util::looks_like_number($v) && ($v > $cond)) : 0 } if $op eq '>';
  14 100       33  
  14         143  
49 2 0 0     8 return sub { my $v = shift; return defined($v) ? (Scalar::Util::looks_like_number($v) && ($v < $cond)) : 0 } if $op eq '<';
  0 50       0  
  0         0  
50 2 50       10 return sub { my $v = shift; return defined($v) ? ($v =~ $cond) : 0 } if $op eq '=~';
  3 100       6  
  3         52  
51 1 0       13 return sub { my $v = shift; return defined($v) ? ($v ge $cond) : 0 } if $op eq 'ge';
  0 50       0  
  0         0  
52 1 0       5 return sub { my $v = shift; return defined($v) ? ($v lt $cond) : 0 } if $op eq 'lt';
  0 50       0  
  0         0  
53 1 0       3 return sub { my $v = shift; return defined($v) ? ($v le $cond) : 0 } if $op eq 'le';
  0 50       0  
  0         0  
54 1 100       8 return sub { my $v = shift; return defined($v) ? ($v eq $cond) : 0 } if $op eq 'eq';
  5 50       12  
  5         21  
55              
56 0         0 die "your op '$op' is not allowed!";
57 32         214 };
58              
59 32 100       107 if(my @attrs = $self->_resolve_query_attrs) {
60             my %matched = map {
61 21 50       274 my ($not, $attr_param, $op, $cond) =
  24 100       193  
62             ref($_) eq 'ARRAY' ?
63             ($_[0] eq '!' ? (@$_) :(0, @$_)) :
64             ($_=~m/^([\?\!]?)([^\:]+)\:?(==|eq|!=|<=|>=|>|=~|<|gt|ge|lt|le)?(.*)$/);
65              
66 24         88 my $evaluator = $compare->($op, $cond);
67              
68 24         55 my $default = undef;
69 24 100       81 if($attr_param =~m/=/) {
70 2         21 ($attr_param, $default) = split('=', $attr_param);
71 2 100       8 $not = '?' unless $default; # allow "arg=" to mean "?arg"
72             }
73              
74 24 50 66     87 if($default and ($not eq '?')) {
75 0         0 die "Can't combine a default with an optional for action ${\$self->name}";
  0         0  
76             }
77              
78             $attr_param => [ $not, $attr_param, $op, $cond, sub {
79 80     80   195 my ($value, $ctx) = @_;
80 80 100       228 if(!defined($value)) {
81 24         42 $value = $default;
82 24         68 $ctx->req->query_parameters->{$attr_param} = $value;
83             }
84              
85 80         1319 my $state = $evaluator->($value);
86 80 100       417 return ($not eq '!') ? not($state) : $state;
87 24         191 }];
88             } @attrs;
89 21         765 return \%matched;
90             } else {
91 11         599 return +{};
92             }
93             }
94              
95             around $_, sub {
96             my ($orig, $self, $ctx, @more) = @_;
97              
98             foreach my $constrained (keys %{$self->query_constraints}) {
99             my ($not, $attr_param, $op, $cond, $evaluator) = @{$self->query_constraints->{$constrained}};
100              
101             my $req_value = exists($ctx->req->query_parameters->{$constrained}) ?
102             $ctx->req->query_parameters->{$constrained} : (($not eq '?') ? next : undef );
103              
104             my $is_success = $evaluator->($req_value, $ctx) ||0;
105              
106             if($ctx->debug) {
107             my $display_req_value = defined($req_value) ? $req_value : 'undefined';
108             $ctx->log->debug(
109             sprintf "QueryParam value for action $self, param '$constrained' with value '$display_req_value' compared as: %s %s %s '%s'",
110             (($not eq '!') ? 'not' : 'is'), $attr_param, ($op ? $op:''), ($cond ? $cond:''),
111             );
112             $ctx->log->debug("QueryParam for $self on key $constrained value $display_req_value has success of $is_success");
113             }
114              
115             #If we fail once, game over;
116             return 0 unless $is_success;
117            
118             }
119             return $self->$orig($ctx, @more);
120             #If we get this far, its all good
121             } for qw(match match_captures);
122              
123             1;
124              
125             =head1 NAME
126              
127             Catalyst::ActionRole::QueryParameter - Dispatch rules using query parameters
128              
129             =head1 SYNOPSIS
130              
131             package MyApp::Controller::Foo;
132              
133             use Moose;
134             use MooseX::MethodAttributes;
135              
136             extends 'Catalyst::Controller:';
137              
138             ## Add the ActionRole to all the Controller's actions. You can also
139             ## selectively add the ActionRole with the :Does action attribute or in
140             ## controller configuration. See Catalyst::Controller::ActionRole for
141             ## more information.
142              
143             __PACKAGE__->config(
144             action_roles => ['QueryParameter'],
145             );
146              
147             ## Match an incoming request matching "http://myhost/path?page=1"
148             sub paged_results : Path('foo') QueryParam('page') { ... }
149              
150             ## Match an incoming request matching "http://myhost/path"
151             sub no_paging : Path('foo') QueryParam('!page') { ... }
152              
153             ## Match a request using a type constraint
154              
155             use Types::Standard 'Int';
156             sub an_int :Path('foo') QueryParam('page:Int') { ... }
157              
158             ## Match optionally (if the parameters exists it MUST pass the constraint
159             ## BUT it is allowed to not exist
160              
161             use Types::Standard 'Int';
162             sub an_int :Path('foo') QueryParam('?page:Int') { ... }
163              
164             ## Match with a default value if the query parameter does not exist'
165              
166             sub with_path :Path('foo') QueryParam('?page=1') { ... }
167              
168              
169             =head1 DESCRIPTION
170              
171             Let's you require conditions on request query parameters (as you would access
172             via C<< $ctx->request->query_parameters >>) as part of your dispatch matching.
173             This ActionRole is not intended to be used for general HTML form and parameter
174             processing or validation, for that purpose there are many other options (such
175             as L<HTML::FormHandler>, L<Data::Manager> or L<HTML::FormFu>.) What it can be
176             useful for is when you want to delegate work to various Actions inside your
177             Controller based on what the incoming query parameters say.
178              
179             Generally speaking, it is not great development practice to abuse query
180             parameters this way. However I find there is a limited and controlled subset
181             of use cases where this feature is valuable. As a result, the features of this
182             ActionRole are also limited to simple defined or undefined checking, and basic
183             Perl relational operators.
184              
185             You can specify multiple C<QueryParam>s per Action. If you do have more than
186             one we will try to match Actions that match ALL the given C<QueryParam>
187             attributes.
188              
189             There's a functioning L<Catalyst> example application in the test directory for
190             your review as well.
191              
192             =head1 QUERY PARAMETER CONDITION MATCHING
193              
194             The value of the C<QueryParam> attribute allows for condition matching based
195             on query parameter definedness and via Perl relational operators. For example,
196             you can match for a particular value or if a given value is greater than another.
197             This can be useful when you want to perform a different Action when (for
198             example) your user is on page 10 of a search, which might indicate they are not
199             finding what they want and could use some additional help. I also sometimes
200             find that I want special handling of the first page of a search result.
201              
202             Although you can handle this with conditional logic inside your Action, I find
203             the ability to declare what I want from an Action to be one of the more valuable
204             aspects of L<Catalyst>.
205              
206             Here are some example C<QueryParam> attributes and the queries they match:
207              
208             QueryParam('page') ## 'page' must exist
209             QueryParam('page=1') ## 'page' defaults to 1
210             QueryParam('!page') ## 'page' must NOT exist
211             QueryParam('?page') ## 'page' may optionally exist
212             QueryParam('page=') ## Same as previous
213             QueryParam('page:==1') ## 'page' must equal numeric one
214             QueryParam('page:>1') ## 'page' must be great than one
215             QueryParam('!page:>1') ## 'page' must NOT be great than one
216             QueryParam(page:Int) ## 'page' matches an Int constraint (see below)
217             QueryParam('?page:Int') ## 'page' may optionally exist, but if it does must be an Int
218             QueryParam('page=:Int') ## Same as previou
219              
220              
221             Since as I mentioned, it is generally not awesome web development practice to
222             make excessive use of query parameters for mapping your action logic, I have
223             limited the condition matching to basic Perl operators. The general pattern
224             is as follows:
225              
226             ([!?]?)($parameter):?($condition?)
227              
228             Which can be roughly translated as "A $parameter should match the $condition
229             but we can tack a "!" to the front of the expression to reverse the match. If
230             you don't specify a $condition, the default condition is definedness."
231              
232             Please note your $parameter my define a simple default value using the '='
233             operator. This means your actual query parameter may not have a '=' in it.
234             Patches to fix welcomed (it would probably be easy to provide some sort of escaping
235             indicator). Default may be combined with conditions, but you can't combine a
236             defualt AND an optional '?' indicator (will cause an error).
237              
238             A C<$condition> is basically a Perl relational operator followed by a value.
239             Relation Operators we current support: C<< ==,eq,>,<,!=,<=,>=,gt,ge,lt,le >>.
240             In addition, we support the regular expression match operator C<=~>. For
241             documentation on Perl Relational Operators see: C<perldoc perlop>. For
242             documentation on Perl Regular Expressions see C<perldoc perlre>.
243              
244             A C<$condition> may also be a L<Moose::Types> or similar type constraint. See
245             below for more.
246              
247             B<NOTE> For numeric comparisions we first check that the value 'looks_like_number'
248             via L<Scalar::Util> before doing the comparison. If it doesn't look like a
249             number that is automatic fail.
250              
251             B<NOTE> The ? optional indicator is probably most useful when combined with a condition
252             or/and a default.
253              
254             =head1 USING TYPE CONSTRAINTS
255              
256             To provide more flexibility and reuse in your parameter constraints, you may
257             use types constraints as your constraint condition if you are using a recent
258             build of L<Catalyst> (at least version 5.90090 or greater). This allows you to
259             use an imported type constraint, such as you might get from L<MooseX::Types>
260             or from L<Type::Tiny> or L<Types::Standard>. For example:
261              
262             package MyApp::Controller::Root;
263              
264             use base 'Catalyst::Controller';
265             use Types::Standard 'Int';
266              
267             sub root :Chained(/) PathPart('') CaptureArgs(0) { }
268              
269             sub int :Chained(root) Args(0) QueryParam(page:Int) {
270             my ($self, $c) = @_;
271             $c->res->body('order');
272             }
273              
274             MyApp::Controller::Root->config(
275             action_roles => ['QueryParameter'],
276             );
277              
278             This would require a URL with a 'page' query that is an Integer, for example,
279             "https://localhost/int/100".
280              
281             This feature uses the type constraint resolution features built into the
282             new versions of L<Catalyst> so it behaves the same way.
283              
284             =head1 USING CATALYST CONFIGURATION INSTEAD OF ATTRIBUTES
285              
286             You may prefer to set your Query Parameter requirements via the L<Catalyst>
287             general application configuration, rather than in subroutine attributes. Doing
288             so allows you to use different settings in different environments and it also
289             allows you to use more extended values. Here's an example comparing both
290             approaches
291              
292             ## subroutine attribute approach
293             sub first_page : Path('foo') QueryParam('page:==1') { ... }
294              
295             ## configuration approach
296             __PACKAGE__->config(
297             action => {
298             first_page => { Path => 'foo', QueryParam => 'page:==1'},
299             },
300             );
301              
302             Since the configuration approach allows richer use of Perl, you can replace the
303             string version of the QueryParam value with the following:
304              
305             ## configuration approach, richer Perl data structure
306             __PACKAGE__->config(
307             action => {
308             first_page => { Path => 'foo', QueryParam => [['page','==','1']] },
309             no_page_query => { Path => 'foo', QueryParam => [['!','page']] },
310             },
311             );
312              
313             If you are using the configuration approach, this second option is preferred.
314             Please note that since each attribute or configuration key can have an array
315             of values, if you use the 'rich Perl data structure' approach in your
316             configuration you will need to place the arrayref inside an arrayref as in the
317             example above (that is not a typo!)
318              
319             =head1 NOTE REGARDING CATALYST DISPATCH RESOLUTION
320              
321             This document has been superceded by a new core documentation document. Please
322             see L<Catalyst::RouteMatching>.
323              
324             =head1 LIMITATIONS
325              
326             Currently this only works for 'single' query parameters. For example:
327              
328             ?foo=1&bar=2
329              
330             Not:
331              
332             ?foo=1&foo=2
333              
334             Patches welcomed!
335              
336             =head1 AUTHOR
337              
338             John Napiorkowski L<email:jjnapiork@cpan.org>
339              
340             =head1 SEE ALSO
341              
342             L<Catalyst>, L<Catalyst::Controller::ActionRole>, L<Moose>.
343              
344             =head1 COPYRIGHT & LICENSE
345              
346             Copyright 2015, John Napiorkowski L<email:jjnapiork@cpan.org>
347              
348             This library is free software; you can redistribute it and/or modify it under
349             the same terms as Perl itself.
350              
351             =cut