File Coverage

blib/lib/Dancer2/Core/Route.pm
Criterion Covered Total %
statement 129 130 99.2
branch 69 76 90.7
condition 7 8 87.5
subroutine 17 17 100.0
pod 2 4 50.0
total 224 235 95.3


line stmt bran cond sub pod time code
1             # ABSTRACT: Dancer2's route handler
2             $Dancer2::Core::Route::VERSION = '0.400000';
3             use Moo;
4 144     144   147288 use Dancer2::Core::Types;
  144         12135  
  144         994  
5 144     144   49621 use Module::Runtime 'use_module';
  144         354  
  144         934  
6 144     144   1002757 use Carp 'croak';
  144         3634  
  144         1132  
7 144     144   7051 use List::Util 'first';
  144         299  
  144         7991  
8 144     144   830 use Scalar::Util 'blessed';
  144         331  
  144         8271  
9 144     144   830 use Ref::Util qw< is_regexpref >;
  144         278  
  144         6066  
10 144     144   1770 use Type::Registry;
  144         1624  
  144         5769  
11 144     144   66839  
  144         831552  
  144         1480  
12             our ( $REQUEST, $RESPONSE, $RESPONDER, $WRITER, $ERROR_HANDLER );
13              
14             has method => (
15             is => 'ro',
16             isa => Dancer2Method,
17             required => 1,
18             );
19              
20             has code => (
21             is => 'ro',
22             required => 1,
23             isa => CodeRef,
24             );
25              
26             has regexp => (
27             is => 'ro',
28             required => 1,
29             );
30              
31             has spec_route => ( is => 'ro' );
32              
33             has prefix => (
34             is => 'ro',
35             isa => Maybe [Dancer2Prefix],
36             predicate => 1,
37             );
38              
39             has options => (
40             is => 'ro',
41             isa => HashRef,
42             trigger => \&_check_options,
43             predicate => 1,
44             );
45              
46             my ( $self, $options ) = @_;
47             return 1 unless defined $options;
48 587     587   72187  
49 587 50       1736 my @supported_options = (
50             qw/content_type agent user_agent content_length
51 587         1845 path_info/
52             );
53             for my $opt ( keys %{$options} ) {
54             croak "Not a valid option for route matching: `$opt'"
55 587         926 if not( grep {/^$opt$/} @supported_options );
  587         1770  
56             }
57 3 50       5 return 1;
  15         60  
58             }
59 587         10521  
60             # private attributes
61              
62             has _should_capture => (
63             is => 'ro',
64             isa => Bool,
65             );
66              
67             has _match_data => (
68             is => 'rw',
69             isa => HashRef,
70             );
71              
72             has _params => (
73             is => 'ro',
74             isa => ArrayRef,
75             default => sub { [] },
76             );
77              
78             has _typed_params => (
79             is => 'ro',
80             isa => ArrayRef,
81             default => sub { [] },
82             );
83              
84             my ( $self, $request ) = @_;
85              
86             if ( $self->has_options ) {
87 1224     1224 1 24153 return unless $self->validate_options($request);
88             }
89 1224 100       5524  
90 1088 100       2450 my @values = $request->path =~ $self->regexp;
91              
92             return unless @values;
93 1221         3794  
94             # if some named captures are found, return captures
95 1221 100       24450 # - Note no @values implies no named captures
96             if (my %captures = %+ ) {
97             return $self->_match_data( { captures => \%captures } );
98             }
99 144 100   144   134088  
  144         55989  
  144         172164  
  585         5240  
100 2         40 # regex comments are how we know if we captured a token,
101             # splat or a megasplat
102             my @token_or_splat =
103             $self->regexp =~ /\(\?#((?:typed_)?token|(?:mega)?splat)\)/g;
104              
105 583         2535 if (@token_or_splat) {
106             # our named tokens
107             my @tokens = @{ $self->_params };
108 583 100       1757 my @typed_tokens = @{ $self->_typed_params };
109              
110 86         170 my %params;
  86         375  
111 86         173 my @splat;
  86         316  
112             for ( my $i = 0; $i < @values; $i++ ) {
113 86         225 # Is this value from a token?
114             if ( defined $token_or_splat[$i] ) {
115 86         339 if ( $token_or_splat[$i] eq 'typed_token' ) {
116             my ( $token, $type ) = @{ shift @typed_tokens };
117 107 100       331  
118 106 100       317 if (defined $values[$i]) {
119 13         18 # undef value mean that token was marked as optional so
  13         29  
120             # we only do type check on defined value
121 13 100       31 return
122             unless $type->check($values[$i]);
123             }
124             $params{$token} = $values[$i];
125 11 100       81 next;
126             }
127 10         1255 if ( $token_or_splat[$i] eq 'token' ) {
128 10         32 $params{ shift @tokens } = $values[$i];
129             next;
130 93 100       286 }
131 29         104  
132 29         110 # megasplat values are split on '/'
133             if ($token_or_splat[$i] eq 'megasplat') {
134             $values[$i] = [
135             defined $values[$i] ? split( m{/} , $values[$i], -1 ) : ()
136 64 100       208 ];
137 25 100       143 }
138             }
139             push @splat, $values[$i];
140             }
141             return $self->_match_data( {
142 65         258 %params,
143             (splat => \@splat)x!! @splat,
144 83         1946 });
145             }
146              
147             if ( $self->_should_capture ) {
148             return $self->_match_data( { splat => \@values } );
149             }
150 497 100       1855  
151 3         59 return $self->_match_data( {} );
152             }
153              
154 494         9058 my ( $self, $app, @args ) = @_;
155             local $REQUEST = $app->request;
156             local $RESPONSE = $app->response;
157              
158 558     558 1 1694 my $content = $self->code->( $app, @args );
159 558         3486  
160 558         7864 # users may set content in the response. If the response has
161             # content, and the returned value from the route code is not
162 558         6272 # an object (well, reference) we ignore the returned value
163             # and use the existing content in the response instead.
164             $RESPONSE->has_content && !ref $content
165             and return $app->_prep_response( $RESPONSE );
166              
167             my $type = blessed($content)
168 448 100 66     47381 or return $app->_prep_response( $RESPONSE, $content );
169              
170             # Plack::Response: proper ArrayRef-style response
171 447 100       2861 $type eq 'Plack::Response'
172             and $RESPONSE = Dancer2::Core::Response->new_from_plack($RESPONSE);
173              
174             # CodeRef: raw PSGI response
175 1 50       4 # do we want to allow it and forward it back?
176             # do we want to upgrade it to an asynchronous response?
177             $type eq 'CODE'
178             and die "We do not support returning code references from routes.\n";
179              
180             # Dancer2::Core::Response, Dancer2::Core::Response::Delayed:
181 1 50       2 # proper responses
182             $type eq 'Dancer2::Core::Response'
183             and return $RESPONSE;
184              
185             $type eq 'Dancer2::Core::Response::Delayed'
186 1 50       3 and return $content;
187              
188             # we can't handle arrayref or hashref
189 1 50       5 # because those might be serialized back
190             die "Unrecognized response type from route: $type.\n";
191             }
192              
193             # private subs
194 0         0  
195             my ( $class, %args ) = @_;
196              
197             my $prefix = $args{prefix};
198             my $regexp = $args{regexp};
199              
200 658     658 0 610681 my $type_library = delete $args{type_library};
201             if ( $type_library) {
202 658         1552 eval { use_module($type_library); 1 }
203 658         1264 or croak "type_library $type_library cannot be loaded";
204             }
205 658         1381 $type_library ||= 'Dancer2::Core::Types';
206 658 100       1887  
207 3 50       6 # init prefix
  3         15  
  3         50432  
208             if ( $prefix ) {
209             $args{regexp} =
210 658   100     3429 is_regexpref($regexp) ? qr{^\Q${prefix}\E${regexp}$} :
211             $prefix . $regexp;
212             }
213 658 100       2496 elsif ( !is_regexpref($regexp) ) {
    100          
214             # No prefix, so ensure regexp begins with a '/'
215 32 100       159 index( $regexp, '/', 0 ) == 0 or $args{regexp} = "/$regexp";
216             }
217              
218             # init regexp
219             $regexp = $args{regexp}; # updated value
220 619 100       2358 $args{spec_route} = $regexp;
221              
222             if ( is_regexpref($regexp)) {
223             $args{_should_capture} = 1;
224 658         1230 }
225 658         1279 else {
226             @args{qw/ regexp _params _typed_params _should_capture/} =
227 658 100       1617 @{ _build_regexp_from_string($regexp, $type_library) };
228 8         16 }
229              
230             return \%args;
231             }
232 650         972  
  650         1641  
233             my ($string, $type_library) = @_;
234              
235 657         18357 my $capture = 0;
236             my ( @params, @typed_params );
237              
238             my $type_registry = Type::Registry->new;
239 650     650   1464 $type_registry->add_types($type_library);
240              
241 650         1165 # look for route with tokens [aka params] (/hello/:foo)
242 650         2228 if ( $string =~ /:/ ) {
243             my @found = $string =~ m|:([^/.\?]+)|g;
244 650         3066 foreach my $token ( @found ) {
245 650         5761 if ( $token =~ s/\[(.+)\]$// ) {
246              
247             # typed token
248 650 100       4210805 my $type = $type_registry->lookup($1);
249 53         444 push @typed_params, [ $token, $type ];
250 53         148 }
251 59 100       261 else {
252             push @params, $token;
253             }
254 15         53 }
255 14         5317 if (@typed_params) {
256             $string =~ s!(:[^/.\?]+\[[^/.\?]+\])!(?#typed_token)([^/]+)!g;
257             $capture = 1;
258 44         149 }
259             if (@params) {
260             first { $_ eq 'splat' } @params
261 52 100       158 and warn q{Named placeholder 'splat' is deprecated};
262 13         64  
263 13         21 first { $_ eq 'captures' } @params
264             and warn q{Named placeholder 'captures' is deprecated};
265 52 100       167  
266 44     44   258 $string =~ s!(:[^\/\.\?]+)!(?#token)([^/]+)!g;
267 40 100       390 $capture = 1;
268             }
269 44     44   241 }
270 40 100       305  
271             # parse megasplat
272 40         263 # we use {0,} instead of '*' not to fall in the splat rule
273 40         96 # same logic for [^\n] instead of '.'
274             $capture = 1 if $string =~ s!\Q**\E!(?#megasplat)([^\n]+)!g;
275              
276             # parse wildcards
277             $capture = 1 if $string =~ s!\*!(?#splat)([^/]+)!g;
278              
279             # escape dots
280 649 100       2697 $string =~ s/\./\\\./g if $string =~ /\./;
281              
282             # escape slashes
283 649 100       1945 $string =~ s/\//\\\//g;
284              
285             return [ "^$string\$", \@params, \@typed_params, $capture ];
286 649 100       2000 }
287              
288             my ( $self, $request ) = @_;
289 649         2561  
290             for my $option ( keys %{ $self->options } ) {
291 649         8946 return 0
292             if (
293             ( not $request->$option )
294             || ( $request->$option !~ $self->options->{ $option } )
295 1088     1088 0 1833 )
296             }
297 1088         1447 return 1;
  1088         3087  
298             }
299              
300             1;
301 6 100 100     49  
302              
303             =pod
304 1085         2540  
305             =encoding UTF-8
306              
307             =head1 NAME
308              
309             Dancer2::Core::Route - Dancer2's route handler
310              
311             =head1 VERSION
312              
313             version 0.400000
314              
315             =head1 ATTRIBUTES
316              
317             =head2 method
318              
319             The HTTP method of the route (lowercase). Required.
320              
321             =head2 code
322              
323             The code reference to execute when the route is ran. Required.
324              
325             =head2 regexp
326              
327             The regular expression that defines the path of the route.
328             Required. Coerce from Dancer2's route I<patterns>.
329              
330             =head2 prefix
331              
332             The prefix to prepend to the C<regexp>. Optional.
333              
334             =head2 options
335              
336             A HashRef of conditions on which the matching will depend. Optional.
337              
338             =head1 METHODS
339              
340             =head2 match
341              
342             Try to match the route with a given L<Dancer2::Core::Request> object.
343             Returns the hash of matching data if success (captures and values of the route
344             against the path of the request) or C<undef> if not.
345              
346             my $match = $route->match( $request );
347              
348             =head2 execute
349              
350             Runs the coderef of the route.
351              
352             =head1 AUTHOR
353              
354             Dancer Core Developers
355              
356             =head1 COPYRIGHT AND LICENSE
357              
358             This software is copyright (c) 2022 by Alexis Sukrieh.
359              
360             This is free software; you can redistribute it and/or modify it under
361             the same terms as the Perl 5 programming language system itself.
362              
363             =cut