File Coverage

blib/lib/Dancer/Route.pm
Criterion Covered Total %
statement 187 190 98.4
branch 78 88 88.6
condition 14 20 70.0
subroutine 30 31 96.7
pod 1 11 9.0
total 310 340 91.1


line stmt bran cond sub pod time code
1             package Dancer::Route;
2             our $AUTHORITY = 'cpan:SUKRIA';
3             # ABSTRACT: Class to represent a route in Dancer
4             $Dancer::Route::VERSION = '1.3520';
5 170     170   1168 use strict;
  170         356  
  170         4973  
6 170     170   892 use warnings;
  170         402  
  170         3914  
7 170     170   2201 use Carp;
  170         361  
  170         9684  
8 170     170   1084 use base 'Dancer::Object';
  170         456  
  170         17934  
9              
10 170     170   3058 use Dancer::App;
  170         393  
  170         5363  
11 170     170   1408 use Dancer::Logger;
  170         411  
  170         4758  
12 170     170   1016 use Dancer::Config 'setting';
  170         471  
  170         10387  
13 170     170   90823 use Dancer::Request;
  170         733  
  170         6309  
14 170     170   1319 use Dancer::Response;
  170         429  
  170         4789  
15 170     170   1030 use Dancer::Exception qw(:all);
  170         454  
  170         24498  
16 170     170   1430 use Dancer::Factory::Hook;
  170         558  
  170         81983  
17              
18             Dancer::Route->attributes(
19             qw(
20             app
21             method
22             pattern
23             prefix
24             code
25             prev
26             regexp
27             next
28             options
29             match_data
30             )
31             );
32              
33             Dancer::Factory::Hook->instance->install_hooks(
34             qw/on_route_exception/
35             );
36              
37             # supported options and aliases
38             my @_supported_options = Dancer::Request->get_attributes();
39             my %_options_aliases = (agent => 'user_agent');
40              
41             sub init {
42 1444     1444 1 2586 my ($self) = @_;
43 1444         2861 $self->{'_compiled_regexp'} = undef;
44              
45 1444 50       3265 raise core_route => "cannot create Dancer::Route without a pattern"
46             unless defined $self->pattern;
47              
48             # If the route is a Regexp, store it directly
49 1444 100       3164 $self->regexp($self->pattern)
50             if ref($self->pattern) eq 'Regexp';
51              
52 1444         3866 $self->check_options();
53 1443         3350 $self->app(Dancer::App->current);
54 1443 100       3355 $self->prefix(Dancer::App->current->prefix) if not $self->prefix;
55 1443 100       3133 $self->_init_prefix() if $self->prefix;
56 1443         3645 $self->_build_regexp();
57 1443 100       3408 $self->set_previous($self->prev) if $self->prev;
58              
59 1443         3067 return $self;
60             }
61              
62             sub set_previous {
63 1178     1178 0 2080 my ($self, $prev) = @_;
64 1178         2901 $self->prev($prev);
65 1178         2248 $self->prev->{'next'} = $self;
66 1178         2170 return $prev;
67             }
68              
69             sub save_match_data {
70 649     649 0 1421 my ($self, $request, $match_data) = @_;
71 649         2019 $self->match_data($match_data);
72 649         2286 $request->_set_route_params($match_data);
73              
74 649         2769 return $match_data;
75             }
76              
77             # Does the route match the request
78             sub match {
79 1725     1725 0 2941 my ($self, $request) = @_;
80              
81 1725         3449 my $method = lc($request->method);
82 1725         3886 my $path = $request->path_info;
83              
84             Dancer::Logger::core(
85             sprintf "Trying to match '%s %s' against /%s/ (generated from '%s')",
86 1725         3656 $request->method, $path, $self->{_compiled_regexp}, $self->pattern
87             );
88              
89 1725         20635 my @values = $path =~ $self->{_compiled_regexp};
90              
91 1725 100       5935 return unless @values;
92              
93             Dancer::Logger::core(
94             " --> got " .
95 649 100       1311 map { defined $_ ? $_ : 'undef' } @values
  707         3879  
96             );
97              
98              
99             # if some named captures found, return captures
100             # no warnings is for perl < 5.10
101 649 100       1371 if (my %captures =
102 170     170   1446 do { no warnings; %+ }
  170     170   572  
  170         8768  
  170         82783  
  170         74329  
  170         328674  
  649         4469  
103             )
104             {
105 2 50       17 Dancer::Logger::core(
106             " --> captures are: " . join(", ", keys(%captures)))
107             if keys %captures;
108 2         17 return $self->save_match_data($request, {captures => \%captures});
109             }
110              
111              
112             # save the route pattern that matched
113             # TODO : as soon as we have proper Dancer::Internal, we should remove
114             # that, it's just a quick hack for plugins to access the matching
115             # pattern.
116             # NOTE: YOU SHOULD NOT USE THAT, OR IF YOU DO, YOU MUST KNOW
117             # IT WILL MOVE VERY SOON
118 647         2057 $request->{_route_pattern} = $self->pattern;
119              
120             # regex comments are how we know if we captured a token,
121             # splat or a megasplat
122             my @token_or_splat
123 647         2980 = $self->{_compiled_regexp} =~ /\(\?#([token|(?:mega)?splat]+)\)/g;
124 647 100       1661 if (@token_or_splat) {
125             # named tokens
126 249 50       425 my @tokens = @{$self->{_params} || []};
  249         861  
127 249 100       1176 Dancer::Logger::core(" --> named tokens are: @tokens") if @tokens;
128              
129 249         546 my %params;
130             my @splat;
131 249         924 for ( my $i = 0; $i < @values; $i++ ) {
132             # Is this value from a token?
133 301 100       658 if ( $token_or_splat[$i] eq 'token' ) {
134 274         660 $params{ shift @tokens } = $values[$i];
135 274         728 next;
136             }
137              
138             # megasplat values are split on '/'
139 27 100       62 if ($token_or_splat[$i] eq 'megasplat') {
140 9   50     42 $values[$i] = [ split '/', $values[$i] || '' ];
141             }
142 27         83 push @splat, $values[$i];
143             }
144 249 100       1173 return $self->save_match_data( $request, {
145             %params,
146             ( @splat ? ( splat => \@splat ) : () ),
147             });
148             }
149              
150 398 100       1029 if ($self->{_should_capture}) {
151 17         80 return $self->save_match_data($request, {splat => \@values});
152             }
153              
154 381         1196 return $self->save_match_data($request, {});
155             }
156              
157             sub has_options {
158 534     534 0 1039 my ($self) = @_;
159 534 100       820 return keys(%{$self->options}) ? 1 : 0;
  534         1227  
160             }
161              
162             sub check_options {
163 1444     1444 0 2464 my ($self) = @_;
164 1444 100       3139 return 1 unless defined $self->options;
165              
166 1439         2325 for my $opt (keys %{$self->options}) {
  1439         2691  
167             raise core_route => "Not a valid option for route matching: `$opt'"
168 240         684 if not( (grep {/^$opt$/} @{$_supported_options[0]})
  12         33  
169 12 100 100     22 || (grep {/^$opt$/} keys(%_options_aliases)));
  9         86  
170             }
171 1438         2404 return 1;
172             }
173              
174             sub validate_options {
175 26     26 0 49 my ($self, $request) = @_;
176              
177 26         36 while (my ($option, $value) = each %{$self->options}) {
  38         77  
178             $option = $_options_aliases{$option}
179 26 50       76 if exists $_options_aliases{$option};
180 26 100 100     74 return 0 if (not $request->$option) || ($request->$option !~ $value);
181             }
182 12         44 return 1;
183             }
184              
185             sub run {
186 559     559 0 1268 my ($self, $request) = @_;
187              
188             my $content = try {
189 559     559   16720 $self->execute();
190             } continuation {
191 120     120   280 my ($continuation) = @_;
192             # route related continuation
193 120 100       426 $continuation->isa('Dancer::Continuation::Route')
194             or $continuation->rethrow();
195             # If the continuation carries some content, get it
196 111         396 my $content = $continuation->return_value();
197 111 100       619 defined $content or return; # to avoid returning undef;
198 18         124 return $content;
199             } catch {
200 28     28   67 my ($exception) = @_;
201 28         133 Dancer::Factory::Hook->execute_hooks('on_route_exception', $exception);
202 28         282 die $exception;
203 559         4158 };
204 522         22217 my $response = Dancer::SharedData->response;
205              
206 522 100 66     2312 if ( $response && $response->is_forwarded ) {
207             my $new_req =
208 15         59 Dancer::Request->forward($request, $response->{forward});
209 15         61 my $marshalled = Dancer::Handler->handle_request($new_req);
210              
211             return Dancer::Response->new(
212             encoded => 1,
213             status => $marshalled->[0],
214             headers => $marshalled->[1],
215             # if the forward failed with 404, marshalled->[2] is not an array, but a GLOB
216 15 50       55 content => ref($marshalled->[2]) eq "ARRAY" ? @{ $marshalled->[2] } : $marshalled->[2]
  15         59  
217             );
218             }
219              
220 507 100 66     1912 if ($response && $response->has_passed) {
221 78         242 $response->pass(0);
222              
223             # find the next matching route and run it
224 78         193 while ($self = $self->next) {
225 139 100       296 return $self->run($request) if $self->match($request);
226             }
227              
228 1         4 Dancer::Logger::core('Last matching route passed!');
229 1         9 return Dancer::Renderer->render_error(404);
230             }
231              
232             # coerce undef content to empty string to
233             # prevent warnings
234 429 100       1079 $content = (defined $content) ? $content : '';
235              
236 429 100 66     1579 my $ct =
237             ( defined $response && defined $response->content_type )
238             ? $response->content_type()
239             : setting('content_type');
240              
241 429 50       2576 my $st = defined $response ? $response->status : 200;
242              
243 429         842 my $headers = [];
244 429 50       1197 push @$headers, @{ $response->headers_to_array } if defined $response;
  429         1166  
245              
246             # content type may have already be set earlier
247             # (eg: with send_error)
248             push(@$headers, 'Content-Type' => $ct)
249 429 100       1530 unless grep {/Content-Type/} @$headers;
  200         617  
250              
251 429 100       1269 return $content if ref($content) eq 'Dancer::Response';
252 410         1484 return Dancer::Response->new(
253             status => $st,
254             headers => $headers,
255             content => $content,
256             );
257             }
258              
259             sub execute {
260 559     559 0 1148 my ($self) = @_;
261              
262 559 100       1647 if (Dancer::Config::setting('warnings')) {
263 9         13 my $warning;
264 9         14 my $content = do {
265 9   33 3   67 local $SIG{__WARN__} = sub { $warning ||= $_[0] };
  3         86  
266 9         35 $self->code->();
267             };
268 3 50       11 if ($warning) {
269 3         47 die "Warning caught during route execution: $warning";
270             }
271 0         0 return $content;
272             }
273             else {
274 550         1695 return $self->code->();
275             }
276             }
277              
278             sub _init_prefix {
279 83     83   135 my ($self) = @_;
280 83         159 my $prefix = $self->prefix;
281              
282 83 100       181 if ($self->is_regexp) {
    100          
283 2         7 my $regexp = $self->regexp;
284 2 50       22 if ($regexp !~ /^$prefix/) {
285 2         24 $self->regexp(qr{${prefix}${regexp}});
286             }
287             }
288             elsif ($self->pattern eq '/') {
289              
290             # if pattern is '/', we should match:
291             # - /prefix/
292             # - /prefix
293             # this is done by creating a regex for this case
294 8         23 my $qpattern = quotemeta( $self->pattern );
295 8         23 my $qprefix = quotemeta( $self->prefix );
296 8         272 my $regex = qr/^$qprefix(?:$qpattern)?$/;
297 8         38 $self->{regexp} = $regex;
298 8         21 $self->{pattern} = $regex;
299             }
300             else {
301 73         162 $self->{pattern} = $prefix . $self->pattern;
302             }
303              
304 83         153 return $prefix;
305             }
306              
307             sub equals {
308 0     0 0 0 my ($self, $route) = @_;
309 0         0 return $self->regexp eq $route->regexp;
310             }
311              
312             sub is_regexp {
313 1526     1526 0 2406 my ($self) = @_;
314 1526         3076 return defined $self->regexp;
315             }
316              
317             sub _build_regexp {
318 1443     1443   2428 my ($self) = @_;
319              
320 1443 100       2814 if ($self->is_regexp) {
321 20         52 $self->{_compiled_regexp} = $self->regexp;
322 20         583 $self->{_compiled_regexp} = qr/^$self->{_compiled_regexp}$/;
323 20         63 $self->{_should_capture} = 1;
324             }
325             else {
326 1423         3573 $self->_build_regexp_from_string($self->pattern);
327             }
328              
329 1443         2415 return $self->{_compiled_regexp};
330             }
331              
332             sub _build_regexp_from_string {
333 1423     1423   2645 my ($self, $pattern) = @_;
334 1423         2080 my $capture = 0;
335 1423         1859 my @params;
336              
337             # look for route with params (/hello/:foo)
338 1423 100       4089 if ($pattern =~ /:/) {
339 185         1087 @params = $pattern =~ /:([^\/\.\?]+)/g;
340 185 50       571 if (@params) {
341 185         802 $pattern =~ s!(:[^\/\.\?]+)!(?#token)([^/]+)!g;
342 185         373 $capture = 1;
343             }
344             }
345              
346             # parse megasplat
347             # we use {0,} instead of '*' not to fall in the splat rule
348             # same logic for [^\n] instead of '.'
349 1423 100       3292 $capture = 1 if $pattern =~ s!\Q**\E!(?#megasplat)([^\n]+)!g;
350              
351             # parse wildcards
352 1423 100       3093 $capture = 1 if $pattern =~ s!\*!(?#splat)([^/]+)!g;
353              
354             # escape dots
355 1423 100       3125 $pattern =~ s/\./\\\./g if $pattern =~ /\./;
356              
357             # escape slashes
358 1423         4625 $pattern =~ s/\//\\\//g;
359              
360 1423         3859 $self->{_compiled_regexp} = "^${pattern}\$";
361 1423         4114 $self->{_params} = \@params;
362 1423         2581 $self->{_should_capture} = $capture;
363              
364 1423         2724 return $self->{_compiled_regexp};
365             }
366              
367             1;
368              
369             __END__