File Coverage

blib/lib/Catalyst/ActionRole/QueryParameter.pm
Criterion Covered Total %
statement 53 66 80.3
branch 41 66 62.1
condition 14 24 58.3
subroutine 6 6 100.0
pod n/a
total 114 162 70.3


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