File Coverage

blib/lib/Dancer2/Core/Route.pm
Criterion Covered Total %
statement 126 127 99.2
branch 69 76 90.7
condition 7 8 87.5
subroutine 16 16 100.0
pod 2 4 50.0
total 220 231 95.2


line stmt bran cond sub pod time code
1             package Dancer2::Core::Route;
2             # ABSTRACT: Dancer2's route handler
3             $Dancer2::Core::Route::VERSION = '2.0.1';
4 159     159   588253 use Moo;
  159         26136  
  159         5708  
5 159     159   83425 use Dancer2::Core::Types;
  159         1679  
  159         2252  
6 159     159   2073889 use Module::Runtime 'use_module';
  159         6968  
  159         1701  
7 159     159   10404 use Carp 'croak';
  159         394  
  159         13764  
8 159     159   1366 use List::Util 'first';
  159         384  
  159         12601  
9 159     159   1112 use Scalar::Util 'blessed';
  159         409  
  159         9711  
10 159     159   2945 use Ref::Util qw< is_regexpref >;
  159         2798  
  159         8628  
11 159     159   109636 use Type::Registry;
  159         1694575  
  159         2227  
12              
13             our ( $REQUEST, $RESPONSE, $RESPONDER, $WRITER, $ERROR_HANDLER );
14              
15             has name => (
16             is => 'ro',
17             isa => Str,
18             predicate => 'has_name',
19             );
20              
21             has method => (
22             is => 'ro',
23             isa => Dancer2Method,
24             required => 1,
25             );
26              
27             has code => (
28             is => 'ro',
29             required => 1,
30             isa => CodeRef,
31             );
32              
33             has regexp => (
34             is => 'ro',
35             required => 1,
36             );
37              
38             has spec_route => ( is => 'ro' );
39              
40             has prefix => (
41             is => 'ro',
42             isa => Maybe [Dancer2Prefix],
43             predicate => 1,
44             );
45              
46             has options => (
47             is => 'ro',
48             isa => HashRef,
49             trigger => \&_check_options,
50             predicate => 1,
51             );
52              
53             sub _check_options {
54 8     8   1112 my ( $self, $options ) = @_;
55 8 50       27 return 1 unless defined $options;
56              
57 8         29 my @supported_options = (
58             qw/content_type agent user_agent content_length
59             path_info/
60             );
61 8         15 for my $opt ( keys %{$options} ) {
  8         28  
62             croak "Not a valid option for route matching: `$opt'"
63 7 50       14 if not( grep {/^$opt$/} @supported_options );
  35         178  
64             }
65 8         162 return 1;
66             }
67              
68             # private attributes
69              
70             has _should_capture => (
71             is => 'ro',
72             isa => Bool,
73             );
74              
75             has _match_data => (
76             is => 'rw',
77             isa => HashRef,
78             );
79              
80             has _params => (
81             is => 'ro',
82             isa => ArrayRef,
83             default => sub { [] },
84             );
85              
86             has _typed_params => (
87             is => 'ro',
88             isa => ArrayRef,
89             default => sub { [] },
90             );
91              
92             sub match {
93 1419     1419 1 30211 my ( $self, $request ) = @_;
94              
95 1419 100       5898 if ( $self->has_options ) {
96 13 100       50 return unless $self->validate_options($request);
97             }
98              
99 1410         5432 my @values = $request->path =~ $self->regexp;
100              
101 1410 100       37176 return unless @values;
102              
103             # if some named captures are found, return captures
104             # - Note no @values implies no named captures
105 623 100       6037 if (my %captures = %+ ) {
106 2         72 return $self->_match_data( { captures => \%captures } );
107             }
108              
109             # regex comments are how we know if we captured a token,
110             # splat or a megasplat
111 621         3511 my @token_or_splat =
112             $self->regexp =~ /\(\?#((?:typed_)?token|(?:mega)?splat)\)/g;
113              
114 621 100       2902 if (@token_or_splat) {
115             # our named tokens
116 86         3877 my @tokens = @{ $self->_params };
  86         440  
117 86         209 my @typed_tokens = @{ $self->_typed_params };
  86         487  
118              
119 86         201 my %params;
120             my @splat;
121 86         416 for ( my $i = 0; $i < @values; $i++ ) {
122             # Is this value from a token?
123 107 100       348 if ( defined $token_or_splat[$i] ) {
124 106 100       401 if ( $token_or_splat[$i] eq 'typed_token' ) {
125 13         31 my ( $token, $type ) = @{ shift @typed_tokens };
  13         39  
126              
127 13 100       43 if (defined $values[$i]) {
128             # undef value mean that token was marked as optional so
129             # we only do type check on defined value
130             return
131 11 100       92 unless $type->check($values[$i]);
132             }
133 10         1993 $params{$token} = $values[$i];
134 10         59 next;
135             }
136 93 100       307 if ( $token_or_splat[$i] eq 'token' ) {
137 29         125 $params{ shift @tokens } = $values[$i];
138 29         174 next;
139             }
140              
141             # megasplat values are split on '/'
142 64 100       219 if ($token_or_splat[$i] eq 'megasplat') {
143 25 100       179 $values[$i] = [
144             defined $values[$i] ? split( m{/} , $values[$i], -1 ) : ()
145             ];
146             }
147             }
148 65         273 push @splat, $values[$i];
149             }
150 83         2715 return $self->_match_data( {
151             %params,
152             (splat => \@splat)x!! @splat,
153             });
154             }
155              
156 535 100       2781 if ( $self->_should_capture ) {
157 5         145 return $self->_match_data( { splat => \@values } );
158             }
159              
160 530         18340 return $self->_match_data( {} );
161             }
162              
163             sub execute {
164 596     596 1 2295 my ( $self, $app, @args ) = @_;
165 596         2632 local $REQUEST = $app->request;
166 596         17734 local $RESPONSE = $app->response;
167              
168 596         8608 my $content = $self->code->( $app, @args );
169              
170             # users may set content in the response. If the response has
171             # content, and the returned value from the route code is not
172             # an object (well, reference) we ignore the returned value
173             # and use the existing content in the response instead.
174 482 100 66     84249 $RESPONSE->has_content && !ref $content
175             and return $app->_prep_response( $RESPONSE );
176              
177 481 100       3786 my $type = blessed($content)
178             or return $app->_prep_response( $RESPONSE, $content );
179              
180             # Plack::Response: proper ArrayRef-style response
181 1 50       4 $type eq 'Plack::Response'
182             and $RESPONSE = Dancer2::Core::Response->new_from_plack($RESPONSE);
183              
184             # CodeRef: raw PSGI response
185             # do we want to allow it and forward it back?
186             # do we want to upgrade it to an asynchronous response?
187 1 50       34 $type eq 'CODE'
188             and die "We do not support returning code references from routes.\n";
189              
190             # Dancer2::Core::Response, Dancer2::Core::Response::Delayed:
191             # proper responses
192 1 50       5 $type eq 'Dancer2::Core::Response'
193             and return $RESPONSE;
194              
195 1 50       11 $type eq 'Dancer2::Core::Response::Delayed'
196             and return $content;
197              
198             # we can't handle arrayref or hashref
199             # because those might be serialized back
200 0         0 die "Unrecognized response type from route: $type.\n";
201             }
202              
203             # private subs
204              
205             sub BUILDARGS {
206 727     727 0 2292490 my ( $class, %args ) = @_;
207              
208 727         2404 my $prefix = $args{prefix};
209 727         2008 my $regexp = $args{regexp};
210              
211 727         2218 my $type_library = delete $args{type_library};
212 727 100       3008 if ( $type_library) {
213 3 50       7 eval { use_module($type_library); 1 }
  3         21  
  3         65903  
214             or croak "type_library $type_library cannot be loaded";
215             }
216 727   100     5035 $type_library ||= 'Dancer2::Core::Types';
217              
218             # init prefix
219 727 100       3867 if ( $prefix ) {
    100          
220             $args{regexp} =
221 34 100       265 is_regexpref($regexp) ? qr{^\Q${prefix}\E${regexp}$} :
222             $prefix . $regexp;
223             }
224             elsif ( !is_regexpref($regexp) ) {
225             # No prefix, so ensure regexp begins with a '/'
226 680 100       3051 index( $regexp, '/', 0 ) == 0 or $args{regexp} = "/$regexp";
227             }
228              
229             # init regexp
230 727         3273 $regexp = $args{regexp}; # updated value
231 727         2031 $args{spec_route} = $regexp;
232              
233 727 100       2147 if ( is_regexpref($regexp)) {
234 14         44 $args{_should_capture} = 1;
235             }
236             else {
237             @args{qw/ regexp _params _typed_params _should_capture/} =
238 713         1663 @{ _build_regexp_from_string($regexp, $type_library) };
  713         2431  
239             }
240              
241 724         31810 return \%args;
242             }
243              
244             sub _build_regexp_from_string {
245 713     713   2161 my ($string, $type_library) = @_;
246              
247 713         1404 my $capture = 0;
248 713         1988 my ( @params, @typed_params );
249              
250 713         5071 my $type_registry = Type::Registry->new;
251 713         8551 $type_registry->add_types($type_library);
252              
253             # look for route with tokens [aka params] (/hello/:foo)
254 713 100       9491656 if ( $string =~ /:/ ) {
255 60         479 my @found = $string =~ m|:([^/.\?]+)|g;
256 60         205 foreach my $token ( @found ) {
257 68 100       381 if ( $token =~ s/\[(.+)\]$// ) {
258              
259             # typed token
260 17         118 my $type = $type_registry->lookup($1);
261 16         9908 push @typed_params, [ $token, $type ];
262             }
263             else {
264 51         188 push @params, $token;
265             }
266             }
267 59 100       215 if (@typed_params) {
268 15         100 $string =~ s!(:[^/.\?]+\[[^/.\?]+\])!(?#typed_token)([^/]+)!g;
269 15         31 $capture = 1;
270             }
271 59 100       232 if (@params) {
272 51     51   442 first { $_ eq 'splat' } @params
273 47 100       748 and croak q{Named placeholder 'splat' is deprecated};
274              
275 50     50   376 first { $_ eq 'captures' } @params
276 46 100       324 and croak q{Named placeholder 'captures' is deprecated};
277              
278 45         355 $string =~ s!(:[^\/\.\?]+)!(?#token)([^/]+)!g;
279 45         142 $capture = 1;
280             }
281             }
282              
283             # parse megasplat
284             # we use {0,} instead of '*' not to fall in the splat rule
285             # same logic for [^\n] instead of '.'
286 710 100       3652 $capture = 1 if $string =~ s!\Q**\E!(?#megasplat)([^\n]+)!g;
287              
288             # parse wildcards
289 710 100       3049 $capture = 1 if $string =~ s!\*!(?#splat)([^/]+)!g;
290              
291             # escape dots
292 710 100       2914 $string =~ s/\./\\\./g if $string =~ /\./;
293              
294             # escape slashes
295 710         3687 $string =~ s/\//\\\//g;
296              
297 710         15508 return [ "^$string\$", \@params, \@typed_params, $capture ];
298             }
299              
300             sub validate_options {
301 13     13 0 319 my ( $self, $request ) = @_;
302              
303 13         28 for my $option ( keys %{ $self->options } ) {
  13         62  
304             return 0
305             if (
306             ( not $request->$option )
307 14 100 100     89 || ( $request->$option !~ $self->options->{ $option } )
308             )
309             }
310 4         464 return 1;
311             }
312              
313             1;
314              
315             __END__