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             package Dancer2::Core::Route;
2             # ABSTRACT: Dancer2's route handler
3             $Dancer2::Core::Route::VERSION = '0.400001';
4 146     146   165661 use Moo;
  146         13872  
  146         1097  
5 146     146   56038 use Dancer2::Core::Types;
  146         385  
  146         1074  
6 146     146   1631993 use Module::Runtime 'use_module';
  146         3712  
  146         1251  
7 146     146   7972 use Carp 'croak';
  146         406  
  146         9340  
8 146     146   1032 use List::Util 'first';
  146         354  
  146         9422  
9 146     146   1034 use Scalar::Util 'blessed';
  146         356  
  146         7084  
10 146     146   1896 use Ref::Util qw< is_regexpref >;
  146         1507  
  146         6775  
11 146     146   71156 use Type::Registry;
  146         1197386  
  146         1252  
12              
13             our ( $REQUEST, $RESPONSE, $RESPONDER, $WRITER, $ERROR_HANDLER );
14              
15             has method => (
16             is => 'ro',
17             isa => Dancer2Method,
18             required => 1,
19             );
20              
21             has code => (
22             is => 'ro',
23             required => 1,
24             isa => CodeRef,
25             );
26              
27             has regexp => (
28             is => 'ro',
29             required => 1,
30             );
31              
32             has spec_route => ( is => 'ro' );
33              
34             has prefix => (
35             is => 'ro',
36             isa => Maybe [Dancer2Prefix],
37             predicate => 1,
38             );
39              
40             has options => (
41             is => 'ro',
42             isa => HashRef,
43             trigger => \&_check_options,
44             predicate => 1,
45             );
46              
47             sub _check_options {
48 593     593   77599 my ( $self, $options ) = @_;
49 593 50       1865 return 1 unless defined $options;
50              
51 593         1906 my @supported_options = (
52             qw/content_type agent user_agent content_length
53             path_info/
54             );
55 593         1010 for my $opt ( keys %{$options} ) {
  593         1857  
56             croak "Not a valid option for route matching: `$opt'"
57 3 50       7 if not( grep {/^$opt$/} @supported_options );
  15         85  
58             }
59 593         12052 return 1;
60             }
61              
62             # private attributes
63              
64             has _should_capture => (
65             is => 'ro',
66             isa => Bool,
67             );
68              
69             has _match_data => (
70             is => 'rw',
71             isa => HashRef,
72             );
73              
74             has _params => (
75             is => 'ro',
76             isa => ArrayRef,
77             default => sub { [] },
78             );
79              
80             has _typed_params => (
81             is => 'ro',
82             isa => ArrayRef,
83             default => sub { [] },
84             );
85              
86             sub match {
87 1228     1228 1 23451 my ( $self, $request ) = @_;
88              
89 1228 100       4115 if ( $self->has_options ) {
90 1092 100       2780 return unless $self->validate_options($request);
91             }
92              
93 1225         3925 my @values = $request->path =~ $self->regexp;
94              
95 1225 100       25560 return unless @values;
96              
97             # if some named captures are found, return captures
98             # - Note no @values implies no named captures
99 146 100   146   140479 if (my %captures = %+ ) {
  146         63210  
  146         197173  
  588         4910  
100 2         48 return $self->_match_data( { captures => \%captures } );
101             }
102              
103             # regex comments are how we know if we captured a token,
104             # splat or a megasplat
105 586         2626 my @token_or_splat =
106             $self->regexp =~ /\(\?#((?:typed_)?token|(?:mega)?splat)\)/g;
107              
108 586 100       1687 if (@token_or_splat) {
109             # our named tokens
110 86         177 my @tokens = @{ $self->_params };
  86         379  
111 86         188 my @typed_tokens = @{ $self->_typed_params };
  86         336  
112              
113 86         202 my %params;
114             my @splat;
115 86         340 for ( my $i = 0; $i < @values; $i++ ) {
116             # Is this value from a token?
117 107 100       326 if ( defined $token_or_splat[$i] ) {
118 106 100       345 if ( $token_or_splat[$i] eq 'typed_token' ) {
119 13         19 my ( $token, $type ) = @{ shift @typed_tokens };
  13         38  
120              
121 13 100       34 if (defined $values[$i]) {
122             # undef value mean that token was marked as optional so
123             # we only do type check on defined value
124             return
125 11 100       82 unless $type->check($values[$i]);
126             }
127 10         1480 $params{$token} = $values[$i];
128 10         37 next;
129             }
130 93 100       311 if ( $token_or_splat[$i] eq 'token' ) {
131 29         93 $params{ shift @tokens } = $values[$i];
132 29         107 next;
133             }
134              
135             # megasplat values are split on '/'
136 64 100       218 if ($token_or_splat[$i] eq 'megasplat') {
137 25 100       145 $values[$i] = [
138             defined $values[$i] ? split( m{/} , $values[$i], -1 ) : ()
139             ];
140             }
141             }
142 65         257 push @splat, $values[$i];
143             }
144 83         2053 return $self->_match_data( {
145             %params,
146             (splat => \@splat)x!! @splat,
147             });
148             }
149              
150 500 100       1987 if ( $self->_should_capture ) {
151 3         72 return $self->_match_data( { splat => \@values } );
152             }
153              
154 497         10124 return $self->_match_data( {} );
155             }
156              
157             sub execute {
158 561     561 1 1816 my ( $self, $app, @args ) = @_;
159 561         1629 local $REQUEST = $app->request;
160 561         9099 local $RESPONSE = $app->response;
161              
162 561         6251 my $content = $self->code->( $app, @args );
163              
164             # users may set content in the response. If the response has
165             # content, and the returned value from the route code is not
166             # an object (well, reference) we ignore the returned value
167             # and use the existing content in the response instead.
168 450 100 66     53881 $RESPONSE->has_content && !ref $content
169             and return $app->_prep_response( $RESPONSE );
170              
171 449 100       2684 my $type = blessed($content)
172             or return $app->_prep_response( $RESPONSE, $content );
173              
174             # Plack::Response: proper ArrayRef-style response
175 1 50       3 $type eq 'Plack::Response'
176             and $RESPONSE = Dancer2::Core::Response->new_from_plack($RESPONSE);
177              
178             # CodeRef: raw PSGI response
179             # do we want to allow it and forward it back?
180             # do we want to upgrade it to an asynchronous response?
181 1 50       4 $type eq 'CODE'
182             and die "We do not support returning code references from routes.\n";
183              
184             # Dancer2::Core::Response, Dancer2::Core::Response::Delayed:
185             # proper responses
186 1 50       4 $type eq 'Dancer2::Core::Response'
187             and return $RESPONSE;
188              
189 1 50       7 $type eq 'Dancer2::Core::Response::Delayed'
190             and return $content;
191              
192             # we can't handle arrayref or hashref
193             # because those might be serialized back
194 0         0 die "Unrecognized response type from route: $type.\n";
195             }
196              
197             # private subs
198              
199             sub BUILDARGS {
200 664     664 0 675634 my ( $class, %args ) = @_;
201              
202 664         2020 my $prefix = $args{prefix};
203 664         1314 my $regexp = $args{regexp};
204              
205 664         1520 my $type_library = delete $args{type_library};
206 664 100       2062 if ( $type_library) {
207 3 50       6 eval { use_module($type_library); 1 }
  3         20  
  3         58106  
208             or croak "type_library $type_library cannot be loaded";
209             }
210 664   100     3568 $type_library ||= 'Dancer2::Core::Types';
211              
212             # init prefix
213 664 100       2698 if ( $prefix ) {
    100          
214             $args{regexp} =
215 32 100       156 is_regexpref($regexp) ? qr{^\Q${prefix}\E${regexp}$} :
216             $prefix . $regexp;
217             }
218             elsif ( !is_regexpref($regexp) ) {
219             # No prefix, so ensure regexp begins with a '/'
220 625 100       2394 index( $regexp, '/', 0 ) == 0 or $args{regexp} = "/$regexp";
221             }
222              
223             # init regexp
224 664         1340 $regexp = $args{regexp}; # updated value
225 664         1377 $args{spec_route} = $regexp;
226              
227 664 100       1666 if ( is_regexpref($regexp)) {
228 8         20 $args{_should_capture} = 1;
229             }
230             else {
231             @args{qw/ regexp _params _typed_params _should_capture/} =
232 656         1077 @{ _build_regexp_from_string($regexp, $type_library) };
  656         1730  
233             }
234              
235 663         18086 return \%args;
236             }
237              
238             sub _build_regexp_from_string {
239 656     656   1534 my ($string, $type_library) = @_;
240              
241 656         1107 my $capture = 0;
242 656         1237 my ( @params, @typed_params );
243              
244 656         3212 my $type_registry = Type::Registry->new;
245 656         6205 $type_registry->add_types($type_library);
246              
247             # look for route with tokens [aka params] (/hello/:foo)
248 656 100       7309492 if ( $string =~ /:/ ) {
249 53         407 my @found = $string =~ m|:([^/.\?]+)|g;
250 53         165 foreach my $token ( @found ) {
251 59 100       254 if ( $token =~ s/\[(.+)\]$// ) {
252              
253             # typed token
254 15         66 my $type = $type_registry->lookup($1);
255 14         6400 push @typed_params, [ $token, $type ];
256             }
257             else {
258 44         144 push @params, $token;
259             }
260             }
261 52 100       189 if (@typed_params) {
262 13         92 $string =~ s!(:[^/.\?]+\[[^/.\?]+\])!(?#typed_token)([^/]+)!g;
263 13         30 $capture = 1;
264             }
265 52 100       156 if (@params) {
266 44     44   236 first { $_ eq 'splat' } @params
267 40 100       324 and warn q{Named placeholder 'splat' is deprecated};
268              
269 44     44   184 first { $_ eq 'captures' } @params
270 40 100       244 and warn q{Named placeholder 'captures' is deprecated};
271              
272 40         265 $string =~ s!(:[^\/\.\?]+)!(?#token)([^/]+)!g;
273 40         103 $capture = 1;
274             }
275             }
276              
277             # parse megasplat
278             # we use {0,} instead of '*' not to fall in the splat rule
279             # same logic for [^\n] instead of '.'
280 655 100       2584 $capture = 1 if $string =~ s!\Q**\E!(?#megasplat)([^\n]+)!g;
281              
282             # parse wildcards
283 655 100       2021 $capture = 1 if $string =~ s!\*!(?#splat)([^/]+)!g;
284              
285             # escape dots
286 655 100       1932 $string =~ s/\./\\\./g if $string =~ /\./;
287              
288             # escape slashes
289 655         2638 $string =~ s/\//\\\//g;
290              
291 655         8944 return [ "^$string\$", \@params, \@typed_params, $capture ];
292             }
293              
294             sub validate_options {
295 1092     1092 0 2090 my ( $self, $request ) = @_;
296              
297 1092         3553 for my $option ( keys %{ $self->options } ) {
  1092         3426  
298             return 0
299             if (
300             ( not $request->$option )
301 6 100 100     72 || ( $request->$option !~ $self->options->{ $option } )
302             )
303             }
304 1089         2860 return 1;
305             }
306              
307             1;
308              
309             __END__
310              
311             =pod
312              
313             =encoding UTF-8
314              
315             =head1 NAME
316              
317             Dancer2::Core::Route - Dancer2's route handler
318              
319             =head1 VERSION
320              
321             version 0.400001
322              
323             =head1 ATTRIBUTES
324              
325             =head2 method
326              
327             The HTTP method of the route (lowercase). Required.
328              
329             =head2 code
330              
331             The code reference to execute when the route is ran. Required.
332              
333             =head2 regexp
334              
335             The regular expression that defines the path of the route.
336             Required. Coerce from Dancer2's route I<patterns>.
337              
338             =head2 prefix
339              
340             The prefix to prepend to the C<regexp>. Optional.
341              
342             =head2 options
343              
344             A HashRef of conditions on which the matching will depend. Optional.
345              
346             =head1 METHODS
347              
348             =head2 match
349              
350             Try to match the route with a given L<Dancer2::Core::Request> object.
351             Returns the hash of matching data if success (captures and values of the route
352             against the path of the request) or C<undef> if not.
353              
354             my $match = $route->match( $request );
355              
356             =head2 execute
357              
358             Runs the coderef of the route.
359              
360             =head1 AUTHOR
361              
362             Dancer Core Developers
363              
364             =head1 COPYRIGHT AND LICENSE
365              
366             This software is copyright (c) 2023 by Alexis Sukrieh.
367              
368             This is free software; you can redistribute it and/or modify it under
369             the same terms as the Perl 5 programming language system itself.
370              
371             =cut