File Coverage

blib/lib/Dancer2/Plugin/ParamTypes.pm
Criterion Covered Total %
statement 99 100 99.0
branch 33 42 78.5
condition 9 12 75.0
subroutine 15 15 100.0
pod 3 4 75.0
total 159 173 91.9


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::ParamTypes;
2             # ABSTRACT: Parameter type checking plugin for Dancer2
3             $Dancer2::Plugin::ParamTypes::VERSION = '0.004';
4 7     7   4494178 use strict;
  7         59  
  7         169  
5 7     7   29 use warnings;
  7         12  
  7         202  
6             use constant {
7 7         558 'ARGS_NUM_BASIC' => 3,
8             'ARGS_NUM_OPTIONAL' => 4,
9             'HTTP_BAD_REQUEST' => 400,
10 7     7   29 };
  7         12  
11              
12 7     7   38 use Carp ();
  7         11  
  7         126  
13 7     7   2622 use Dancer2::Plugin;
  7         66035  
  7         44  
14 7     7   18807 use Scalar::Util ();
  7         16  
  7         128  
15 7     7   29 use Ref::Util qw< is_ref is_arrayref >;
  7         14  
  7         7117  
16              
17             plugin_keywords(qw);
18              
19             has 'type_checks' => (
20             'is' => 'ro',
21             'default' => sub { +{} },
22             );
23              
24             has 'type_actions' => (
25             'is' => 'ro',
26             'builder' => '_build_type_actions',
27             );
28              
29             sub _build_type_actions {
30 7     7   14402 my $self = shift;
31 7         28 Scalar::Util::weaken( my $plugin = $self );
32             return {
33             'error' => sub {
34 7     7   16 my ( $self, $details ) = @_;
35 7         15 my ( $type, $name ) = @{$details}{qw};
  7         19  
36              
37 7         26 $plugin->dsl->send_error(
38             "Parameter $name must be $type",
39             HTTP_BAD_REQUEST(),
40             );
41             },
42              
43             'missing' => sub {
44 6     6   17 my ( $self, $details ) = @_;
45 6         15 my ( $name, $type ) = @{$details}{qw};
  6         17  
46              
47 6         29 $self->dsl->send_error(
48             "Missing parameter: $name ($type)",
49             HTTP_BAD_REQUEST(),
50             );
51             },
52 7         146 };
53             }
54              
55             sub register_type_check {
56 7     7 1 510 my ( $self, $name, $cb ) = @_;
57 7         40 $self->type_checks->{$name} = $cb;
58 7         20 return;
59             }
60              
61             sub register_type_action {
62 1     1 1 11 my ( $self, $name, $cb ) = @_;
63 1         4 $self->type_actions->{$name} = $cb;
64 1         3 return;
65             }
66              
67             sub with_types {
68 8     8 1 176 my ( $self, $full_type_details, $cb ) = @_;
69 8         17 my %params_to_check;
70              
71 8 50       30 is_arrayref($full_type_details)
72             or Carp::croak('Input for with_types must be arrayref');
73              
74             ## no critic qw(ControlStructures::ProhibitCStyleForLoops)
75 8         44 for ( my $idx = 0; $idx <= $#{$full_type_details}; $idx++ ) {
  17         108  
76 9         21 my $item = $full_type_details->[$idx];
77 9 50       36 my ( $is_optional, $type_details )
    100          
78             = is_arrayref($item) ? ( 0, $item )
79             : $item eq 'optional' ? ( 1, $full_type_details->[ ++$idx ] )
80             : Carp::croak("Unsupported type option: $item");
81              
82 9         14 my ( $sources, $name, $type, $action ) = @{$type_details};
  9         22  
83              
84 9         32 @{$type_details} == ARGS_NUM_BASIC() ||
85 9 50 66     18 @{$type_details} == ARGS_NUM_OPTIONAL()
  1         4  
86             or Carp::croak("Incorrect number of elements for type ($name)");
87              
88             # default action
89 9 100 66     33 defined $action && length $action
90             or $action = 'error';
91              
92 9 100       24 if ( is_ref($sources) ) {
93 1 50       4 is_arrayref($sources)
94 0         0 or Carp::croak("Source cannot be of @{[ ref $sources ]}");
95              
96 1 50       2 @{$sources} > 0
  1         3  
97             or Carp::croak('You must provide at least one source');
98             } else {
99 8         14 $sources = [$sources];
100             }
101              
102 9         27 foreach my $src ( @{$sources} ) {
  9         29  
103 10 50 100     74 $src eq 'route' || $src eq 'query' || $src eq 'body'
      66        
104             or Carp::croak("Type $name provided from unknown source '$src'");
105             }
106              
107 9 50       41 defined $self->type_checks->{$type}
108             or Carp::croak("Type $name provided unknown type '$type'");
109              
110 9 50       35 defined $self->type_actions->{$action}
111             or
112             Carp::croak("Type $name provided unknown action '$action'");
113              
114 9 50       30 defined $self->type_actions->{'missing'}
115             or Carp::croak('You need to provide a "missing" action');
116              
117 9         13 my $src = join ':', sort @{$sources};
  9         30  
118 9         58 $params_to_check{$src}{$name} = {
119             'optional' => $is_optional,
120             'source' => $src,
121             'name' => $name,
122             'type' => $type,
123             'action' => $action,
124             };
125             }
126              
127             # Couldn't prove yet that this is required, but it makes sense to me
128 8         34 Scalar::Util::weaken( my $plugin = $self );
129              
130             return sub {
131 25     25   928607 my @route_args = @_;
132              
133             # Hash::MultiValue has "each" method which we could use to
134             # traverse it in the opposite direction (for each parameter sent
135             # we find the appropriate value and check it), but that could
136             # possibly introduce an attack vector of sending a lot of
137             # parameters to force longer loops. For now, the loop is based
138             # on how many parameters to added to be checked, which is a known
139             # set. (GET has a max limit, PUT/POST...?) -- SX
140              
141             # Only check if anything was supplied
142 25         101 foreach my $source ( keys %params_to_check ) {
143 25         38 foreach my $name ( keys %{ $params_to_check{$source} } ) {
  25         67  
144 28         96 my @sources = split /:/xms, $source;
145 28         58 my $details = $params_to_check{$source}{$name};
146              
147 28 100       74 if ( @sources == 1 ) {
148             $plugin->run_check($details)
149             or
150 22 100       98 $self->type_actions->{'missing'}->( $self, $details );
151             } else {
152 6         7 my $found;
153 6         13 foreach my $single_source (@sources) {
154 10         20 $details->{'source'} = $single_source;
155 10 100       25 if ( $plugin->run_check($details) ) {
156 2         4 $found++;
157 2         4 last;
158             }
159             }
160              
161             $found
162             or
163 4 100       21 $self->type_actions->{'missing'}->( $self, $details );
164             }
165             }
166             }
167              
168 11         33 $cb->(@route_args);
169 8         65 };
170             }
171              
172             sub run_check {
173 32     32 0 68 my ( $self, $details ) = @_;
174              
175             my ( $source, $name, $type, $action, $optional )
176 32         48 = @{$details}{qw};
  32         105  
177              
178 32         96 my $app = $self->app;
179 32         73 my $request = $app->request;
180              
181 32 100       189 my $params
    100          
182             = $source eq 'route' ? $request->route_parameters
183             : $source eq 'query' ? $request->query_parameters
184             : $request->body_parameters;
185              
186             # No parameter value, is this okay or not?
187 32 100       2369 if ( !exists $params->{$name} ) {
188             # It's okay, ignore
189 11 100       30 $optional
190             and return 1;
191              
192             # Not okay, missing when it's required!
193 10         67 return;
194             }
195              
196 21         92 my @param_values = $params->get_all($name);
197 21         317 my $check_cb = $self->type_checks->{$type};
198              
199 21         47 foreach my $param_value (@param_values) {
200 23 100       85 if ( ! $check_cb->($param_value) ) {
201             my $action_cb
202 8         60 = $self->type_actions->{$action};
203              
204 8         27 return $action_cb->( $self, $details );
205             }
206             }
207              
208 13         99 return 1;
209             }
210              
211             1;
212              
213             __END__