File Coverage

blib/lib/Catalyst/ActionRole/QueryParameter.pm
Criterion Covered Total %
statement 61 76 80.2
branch 46 72 63.8
condition 16 27 59.2
subroutine 6 6 100.0
pod n/a
total 129 181 71.2


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