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.3514_04'; # TRIAL
5             $Dancer::Route::VERSION = '1.351404';
6 171     171   1430 use strict;
  171         269  
  171         4440  
7 171     171   737 use warnings;
  171         273  
  171         3205  
8 171     171   790 use Carp;
  171         294  
  171         8281  
9 171     171   1016 use base 'Dancer::Object';
  171         355  
  171         16049  
10              
11 171     171   2492 use Dancer::App;
  171         308  
  171         4128  
12 171     171   893 use Dancer::Logger;
  171         336  
  171         3979  
13 171     171   802 use Dancer::Config 'setting';
  171         315  
  171         8691  
14 171     171   74000 use Dancer::Request;
  171         534  
  171         5062  
15 171     171   1243 use Dancer::Response;
  171         335  
  171         3485  
16 171     171   778 use Dancer::Exception qw(:all);
  171         327  
  171         21001  
17 171     171   1078 use Dancer::Factory::Hook;
  171         345  
  171         64677  
18              
19             Dancer::Route->attributes(
20             qw(
21             app
22             method
23             pattern
24             prefix
25             code
26             prev
27             regexp
28             next
29             options
30             match_data
31             )
32             );
33              
34             Dancer::Factory::Hook->instance->install_hooks(
35             qw/on_route_exception/
36             );
37              
38             # supported options and aliases
39             my @_supported_options = Dancer::Request->get_attributes();
40             my %_options_aliases = (agent => 'user_agent');
41              
42             sub init {
43 1448     1448 1 2042 my ($self) = @_;
44 1448         2236 $self->{'_compiled_regexp'} = undef;
45              
46 1448 50       2709 raise core_route => "cannot create Dancer::Route without a pattern"
47             unless defined $self->pattern;
48              
49             # If the route is a Regexp, store it directly
50 1448 100       2724 $self->regexp($self->pattern)
51             if ref($self->pattern) eq 'Regexp';
52              
53 1448         3008 $self->check_options();
54 1447         2731 $self->app(Dancer::App->current);
55 1447 100       2713 $self->prefix(Dancer::App->current->prefix) if not $self->prefix;
56 1447 100       2543 $self->_init_prefix() if $self->prefix;
57 1447         2940 $self->_build_regexp();
58 1447 100       2808 $self->set_previous($self->prev) if $self->prev;
59              
60 1447         2402 return $self;
61             }
62              
63             sub set_previous {
64 1180     1180 0 1648 my ($self, $prev) = @_;
65 1180         2348 $self->prev($prev);
66 1180         1881 $self->prev->{'next'} = $self;
67 1180         1876 return $prev;
68             }
69              
70             sub save_match_data {
71 652     652 0 1228 my ($self, $request, $match_data) = @_;
72 652         1817 $self->match_data($match_data);
73 652         2029 $request->_set_route_params($match_data);
74              
75 652         2267 return $match_data;
76             }
77              
78             # Does the route match the request
79             sub match {
80 1731     1731 0 2576 my ($self, $request) = @_;
81              
82 1731         3066 my $method = lc($request->method);
83 1731         3340 my $path = $request->path_info;
84              
85             Dancer::Logger::core(
86             sprintf "Trying to match '%s %s' against /%s/ (generated from '%s')",
87 1731         3241 $request->method, $path, $self->{_compiled_regexp}, $self->pattern
88             );
89              
90 1731         17858 my @values = $path =~ $self->{_compiled_regexp};
91              
92 1731 100       5048 return unless @values;
93              
94             Dancer::Logger::core(
95             " --> got " .
96 652 100       1227 map { defined $_ ? $_ : 'undef' } @values
  710         3221  
97             );
98              
99              
100             # if some named captures found, return captures
101             # no warnings is for perl < 5.10
102 652 100       1175 if (my %captures =
103 171     171   1209 do { no warnings; %+ }
  171     171   445  
  171         7654  
  171         67682  
  171         59165  
  171         261135  
  652         4141  
104             )
105             {
106 2 50       20 Dancer::Logger::core(
107             " --> captures are: " . join(", ", keys(%captures)))
108             if keys %captures;
109 2         14 return $self->save_match_data($request, {captures => \%captures});
110             }
111              
112              
113             # save the route pattern that matched
114             # TODO : as soon as we have proper Dancer::Internal, we should remove
115             # that, it's just a quick hack for plugins to access the matching
116             # pattern.
117             # NOTE: YOU SHOULD NOT USE THAT, OR IF YOU DO, YOU MUST KNOW
118             # IT WILL MOVE VERY SOON
119 650         1954 $request->{_route_pattern} = $self->pattern;
120              
121             # regex comments are how we know if we captured a token,
122             # splat or a megasplat
123             my @token_or_splat
124 650         2490 = $self->{_compiled_regexp} =~ /\(\?#([token|(?:mega)?splat]+)\)/g;
125 650 100       1489 if (@token_or_splat) {
126             # named tokens
127 249 50       312 my @tokens = @{$self->{_params} || []};
  249         735  
128 249 100       1025 Dancer::Logger::core(" --> named tokens are: @tokens") if @tokens;
129              
130 249         469 my %params;
131             my @splat;
132 249         658 for ( my $i = 0; $i < @values; $i++ ) {
133             # Is this value from a token?
134 301 100       598 if ( $token_or_splat[$i] eq 'token' ) {
135 274         570 $params{ shift @tokens } = $values[$i];
136 274         596 next;
137             }
138              
139             # megasplat values are split on '/'
140 27 100       55 if ($token_or_splat[$i] eq 'megasplat') {
141 9   50     49 $values[$i] = [ split '/', $values[$i] || '' ];
142             }
143 27         74 push @splat, $values[$i];
144             }
145 249 100       1093 return $self->save_match_data( $request, {
146             %params,
147             ( @splat ? ( splat => \@splat ) : () ),
148             });
149             }
150              
151 401 100       968 if ($self->{_should_capture}) {
152 17         54 return $self->save_match_data($request, {splat => \@values});
153             }
154              
155 384         1078 return $self->save_match_data($request, {});
156             }
157              
158             sub has_options {
159 537     537 0 961 my ($self) = @_;
160 537 100       691 return keys(%{$self->options}) ? 1 : 0;
  537         1289  
161             }
162              
163             sub check_options {
164 1448     1448 0 2271 my ($self) = @_;
165 1448 100       2616 return 1 unless defined $self->options;
166              
167 1443         1829 for my $opt (keys %{$self->options}) {
  1443         2296  
168             raise core_route => "Not a valid option for route matching: `$opt'"
169 240         538 if not( (grep {/^$opt$/} @{$_supported_options[0]})
  12         26  
170 12 100 100     18 || (grep {/^$opt$/} keys(%_options_aliases)));
  9         72  
171             }
172 1442         1973 return 1;
173             }
174              
175             sub validate_options {
176 26     26 0 48 my ($self, $request) = @_;
177              
178 26         36 while (my ($option, $value) = each %{$self->options}) {
  38         67  
179             $option = $_options_aliases{$option}
180 26 50       87 if exists $_options_aliases{$option};
181 26 100 100     81 return 0 if (not $request->$option) || ($request->$option !~ $value);
182             }
183 12         54 return 1;
184             }
185              
186             sub run {
187 562     562 0 1175 my ($self, $request) = @_;
188              
189             my $content = try {
190 562     562   13808 $self->execute();
191             } continuation {
192 120     120   249 my ($continuation) = @_;
193             # route related continuation
194 120 100       413 $continuation->isa('Dancer::Continuation::Route')
195             or $continuation->rethrow();
196             # If the continuation carries some content, get it
197 111         421 my $content = $continuation->return_value();
198 111 100       501 defined $content or return; # to avoid returning undef;
199 18         94 return $content;
200             } catch {
201 28     28   69 my ($exception) = @_;
202 28         104 Dancer::Factory::Hook->execute_hooks('on_route_exception', $exception);
203 28         268 die $exception;
204 562         3647 };
205 525         18668 my $response = Dancer::SharedData->response;
206              
207 525 100 66     1996 if ( $response && $response->is_forwarded ) {
208             my $new_req =
209 15         42 Dancer::Request->forward($request, $response->{forward});
210 15         66 my $marshalled = Dancer::Handler->handle_request($new_req);
211              
212             return Dancer::Response->new(
213             encoded => 1,
214             status => $marshalled->[0],
215             headers => $marshalled->[1],
216             # if the forward failed with 404, marshalled->[2] is not an array, but a GLOB
217 15 50       39 content => ref($marshalled->[2]) eq "ARRAY" ? @{ $marshalled->[2] } : $marshalled->[2]
  15         38  
218             );
219             }
220              
221 510 100 66     1768 if ($response && $response->has_passed) {
222 78         198 $response->pass(0);
223              
224             # find the next matching route and run it
225 78         221 while ($self = $self->next) {
226 139 100       269 return $self->run($request) if $self->match($request);
227             }
228              
229 1         4 Dancer::Logger::core('Last matching route passed!');
230 1         5 return Dancer::Renderer->render_error(404);
231             }
232              
233             # coerce undef content to empty string to
234             # prevent warnings
235 432 100       1035 $content = (defined $content) ? $content : '';
236              
237 432 100 66     1488 my $ct =
238             ( defined $response && defined $response->content_type )
239             ? $response->content_type()
240             : setting('content_type');
241              
242 432 50       2269 my $st = defined $response ? $response->status : 200;
243              
244 432         836 my $headers = [];
245 432 50       1178 push @$headers, @{ $response->headers_to_array } if defined $response;
  432         1123  
246              
247             # content type may have already be set earlier
248             # (eg: with send_error)
249             push(@$headers, 'Content-Type' => $ct)
250 432 100       1464 unless grep {/Content-Type/} @$headers;
  200         507  
251              
252 432 100       1173 return $content if ref($content) eq 'Dancer::Response';
253 413         1393 return Dancer::Response->new(
254             status => $st,
255             headers => $headers,
256             content => $content,
257             );
258             }
259              
260             sub execute {
261 562     562 0 1032 my ($self) = @_;
262              
263 562 100       1392 if (Dancer::Config::setting('warnings')) {
264 9         12 my $warning;
265 9         10 my $content = do {
266 9   33 3   56 local $SIG{__WARN__} = sub { $warning ||= $_[0] };
  3         64  
267 9         25 $self->code->();
268             };
269 3 50       8 if ($warning) {
270 3         22 die "Warning caught during route execution: $warning";
271             }
272 0         0 return $content;
273             }
274             else {
275 553         1511 return $self->code->();
276             }
277             }
278              
279             sub _init_prefix {
280 83     83   122 my ($self) = @_;
281 83         129 my $prefix = $self->prefix;
282              
283 83 100       134 if ($self->is_regexp) {
    100          
284 2         5 my $regexp = $self->regexp;
285 2 50       15 if ($regexp !~ /^$prefix/) {
286 2         18 $self->regexp(qr{${prefix}${regexp}});
287             }
288             }
289             elsif ($self->pattern eq '/') {
290              
291             # if pattern is '/', we should match:
292             # - /prefix/
293             # - /prefix
294             # this is done by creating a regex for this case
295 8         18 my $qpattern = quotemeta( $self->pattern );
296 8         18 my $qprefix = quotemeta( $self->prefix );
297 8         88 my $regex = qr/^$qprefix(?:$qpattern)?$/;
298 8         26 $self->{regexp} = $regex;
299 8         19 $self->{pattern} = $regex;
300             }
301             else {
302 73         138 $self->{pattern} = $prefix . $self->pattern;
303             }
304              
305 83         126 return $prefix;
306             }
307              
308             sub equals {
309 0     0 0 0 my ($self, $route) = @_;
310 0         0 return $self->regexp eq $route->regexp;
311             }
312              
313             sub is_regexp {
314 1530     1530 0 2185 my ($self) = @_;
315 1530         2887 return defined $self->regexp;
316             }
317              
318             sub _build_regexp {
319 1447     1447   1946 my ($self) = @_;
320              
321 1447 100       2436 if ($self->is_regexp) {
322 20         37 $self->{_compiled_regexp} = $self->regexp;
323 20         244 $self->{_compiled_regexp} = qr/^$self->{_compiled_regexp}$/;
324 20         44 $self->{_should_capture} = 1;
325             }
326             else {
327 1427         2757 $self->_build_regexp_from_string($self->pattern);
328             }
329              
330 1447         1954 return $self->{_compiled_regexp};
331             }
332              
333             sub _build_regexp_from_string {
334 1427     1427   2373 my ($self, $pattern) = @_;
335 1427         1698 my $capture = 0;
336 1427         1543 my @params;
337              
338             # look for route with params (/hello/:foo)
339 1427 100       3431 if ($pattern =~ /:/) {
340 185         909 @params = $pattern =~ /:([^\/\.\?]+)/g;
341 185 50       467 if (@params) {
342 185         683 $pattern =~ s!(:[^\/\.\?]+)!(?#token)([^/]+)!g;
343 185         330 $capture = 1;
344             }
345             }
346              
347             # parse megasplat
348             # we use {0,} instead of '*' not to fall in the splat rule
349             # same logic for [^\n] instead of '.'
350 1427 100       2724 $capture = 1 if $pattern =~ s!\Q**\E!(?#megasplat)([^\n]+)!g;
351              
352             # parse wildcards
353 1427 100       2430 $capture = 1 if $pattern =~ s!\*!(?#splat)([^/]+)!g;
354              
355             # escape dots
356 1427 100       3061 $pattern =~ s/\./\\\./g if $pattern =~ /\./;
357              
358             # escape slashes
359 1427         3715 $pattern =~ s/\//\\\//g;
360              
361 1427         3361 $self->{_compiled_regexp} = "^${pattern}\$";
362 1427         3439 $self->{_params} = \@params;
363 1427         2064 $self->{_should_capture} = $capture;
364              
365 1427         2260 return $self->{_compiled_regexp};
366             }
367              
368             1;
369              
370             __END__