File Coverage

blib/lib/Mojolicious/Routes/Pattern.pm
Criterion Covered Total %
statement 109 109 100.0
branch 88 90 97.7
condition 35 39 89.7
subroutine 11 11 100.0
pod 5 5 100.0
total 248 254 97.6


line stmt bran cond sub pod time code
1             package Mojolicious::Routes::Pattern;
2 52     52   1009 use Mojo::Base -base;
  52         145  
  52         409  
3              
4 52     52   385 use Carp qw(croak);
  52         138  
  52         112906  
5              
6             has [qw(constraints defaults types)] => sub { {} };
7             has [qw(placeholder_start type_start)] => ':';
8             has [qw(placeholders tree)] => sub { [] };
9             has quote_end => '>';
10             has quote_start => '<';
11             has [qw(regex unparsed)];
12             has relaxed_start => '#';
13             has wildcard_start => '*';
14              
15             sub match {
16 174     174 1 633 my ($self, $path, $detect) = @_;
17 174         409 my $captures = $self->match_partial(\$path, $detect);
18 174 100 100     1086 return !$path || $path eq '/' ? $captures : undef;
19             }
20              
21             sub match_partial {
22 17809     17809 1 32595 my ($self, $pathref, $detect) = @_;
23              
24             # Compile on demand
25 17809 100       40223 $self->_compile($detect) unless $self->{regex};
26              
27 17809 100       34867 return undef unless my @captures = $$pathref =~ $self->regex;
28 2661         7997 $$pathref = ${^POSTMATCH};
29 2661 100       7454 @captures = () if $#+ == 0;
30 2661         4715 my $captures = {%{$self->defaults}};
  2661         6498  
31 2661         4744 for my $placeholder (@{$self->placeholders}, 'format') {
  2661         5847  
32 2929 100       7267 last unless @captures;
33 371         806 my $capture = shift @captures;
34 371 100       1320 $captures->{$placeholder} = $capture if defined $capture;
35             }
36              
37 2661         8421 return $captures;
38             }
39              
40 1121 100   1121 1 7411 sub new { @_ > 1 ? shift->SUPER::new->parse(@_) : shift->SUPER::new }
41              
42             sub parse {
43 1034     1034 1 1979 my $self = shift;
44              
45 1034 100 100     3430 my $pattern = @_ % 2 ? (shift // '/') : '/';
46 1034         6824 $pattern =~ s!^/*|/+!/!g;
47 1034 100       3000 return $self->constraints({@_}) if $pattern eq '/';
48              
49 933         1818 $pattern =~ s!/$!!;
50 933         2733 return $self->constraints({@_})->_tokenize($pattern);
51             }
52              
53             sub render {
54 720     720 1 15715 my ($self, $values, $endpoint) = @_;
55              
56             # Placeholders can only be optional without a format
57 720         1306 my $optional = !(my $format = $values->{format});
58              
59 720         1127 my $str = '';
60 720         1012 for my $token (reverse @{$self->tree}) {
  720         1498  
61 787         1805 my ($op, $value) = @$token;
62 787         1126 my $part = '';
63              
64             # Text
65 787 100       1698 if ($op eq 'text') { ($part, $optional) = ($value, 0) }
  381 100       709  
66              
67             # Slash
68 216 100       456 elsif ($op eq 'slash') { $part = '/' unless $optional }
69              
70             # Placeholder
71             else {
72 190         366 my $name = $value->[0];
73 190         424 my $default = $self->defaults->{$name};
74 190   100     1043 $part = $values->{$name} // $default // '';
      100        
75 190 100 100     646 if (!defined $default || ($default ne $part)) { $optional = 0 }
  154 100       261  
76 22         54 elsif ($optional) { $part = '' }
77             }
78              
79 787         2010 $str = $part . $str;
80             }
81              
82             # Format can be optional
83 720 100 100     3946 return $endpoint && $format ? "$str.$format" : $str;
84             }
85              
86             sub _compile {
87 692     692   1480 my ($self, $detect) = @_;
88              
89 692         1883 my $constraints = $self->constraints;
90 692         1675 my $defaults = $self->defaults;
91 692         1710 my $types = $self->types;
92              
93 692         1415 my $block = my $regex = '';
94 692         1130 my $optional = 1;
95 692         1023 for my $token (reverse @{$self->tree}) {
  692         1636  
96 778         2325 my ($op, $value, $type) = @$token;
97 778         1228 my $part = '';
98              
99             # Text
100 778 100       1758 if ($op eq 'text') { ($part, $optional) = (quotemeta $value, 0) }
  553 100       1257  
101              
102             # Slash
103             elsif ($op eq 'slash') {
104 119 100       351 $regex = ($optional ? "(?:/$block)?" : "/$block") . $regex;
105 119         246 ($block, $optional) = ('', 1);
106 119         258 next;
107             }
108              
109             # Placeholder
110             else {
111 106 100 100     265 if ($value->[1]) { $part = _compile_req($types->{$value->[1]} // '?!') }
  7         45  
112 99 100       253 else { $part = $type ? $type eq 'relaxed' ? '([^/]+)' : '(.+)' : '([^/.]+)' }
    100          
113              
114             # Custom regex
115 106 100       349 if (my $c = $constraints->{$value->[0]}) { $part = _compile_req($c) }
  14         39  
116              
117             # Optional placeholder
118 106 100       304 exists $defaults->{$value->[0]} ? ($part .= '?') : ($optional = 0);
119             }
120              
121 659         1861 $block = $part . $block;
122             }
123              
124             # Not rooted with a slash
125 692 100       2281 $regex = $block . $regex if $block;
126              
127             # Format
128 692 100       2954 $regex .= _compile_format($constraints->{format}, exists $defaults->{format}) if $detect;
129              
130 692         11767 $self->regex(qr/^$regex/ps);
131             }
132              
133             sub _compile_format {
134 548     548   1653 my ($format, $has_default) = @_;
135              
136             # No regex
137 548 100       1564 return '' unless $format;
138              
139             # Default regex
140 45 100       178 return '/?(?:\.([^/]+))?$' if $format eq '1';
141              
142             # Compile custom regex
143 41         131 my $regex = '\.' . _compile_req($format);
144 41 100       190 return $has_default ? "/?(?:$regex)?\$" : "/?$regex\$";
145             }
146              
147             sub _compile_req {
148 62     62   131 my $req = shift;
149 62 100       222 return "($req)" if ref $req ne 'ARRAY';
150 47         188 return '(' . join('|', map {quotemeta} reverse sort @$req) . ')';
  75         334  
151             }
152              
153             sub _tokenize {
154 933     933   2092 my ($self, $pattern) = @_;
155              
156 933         2114 my $placeholders = $self->placeholders;
157 933         2245 my $type_start = $self->type_start;
158 933         2270 my $quote_end = $self->quote_end;
159 933         2137 my $quote_start = $self->quote_start;
160 933         2201 my $start = $self->placeholder_start;
161 933         2170 my $relaxed = $self->relaxed_start;
162 933         2115 my $wildcard = $self->wildcard_start;
163              
164 933         1866 my (@tree, $spec, $more);
165 933         4394 for my $char (split //, $pattern) {
166              
167             # Quoted
168 10283 50 100     63664 if ($char eq $quote_start) { push @tree, ['placeholder', ''] if ++$spec }
  30 100 100     103  
    100 66        
    100 66        
    100 66        
    100 66        
    100 100        
    100          
    100          
    100          
169 30         70 elsif ($char eq $quote_end) { $spec = $more = 0 }
170              
171             # Placeholder
172 83 100       349 elsif (!$more && $char eq $start) { push @tree, ['placeholder', ''] unless $spec++ }
173              
174             # Relaxed or wildcard (upgrade when quoted)
175             elsif (!$more && ($char eq $relaxed || $char eq $wildcard)) {
176 46 100       175 push @tree, ['placeholder', ''] unless $spec++;
177 46 100       158 $tree[-1][2] = $char eq $relaxed ? 'relaxed' : 'wildcard';
178             }
179              
180             # Slash
181             elsif ($char eq '/') {
182 1323         3314 push @tree, ['slash'];
183 1323         2537 $spec = $more = 0;
184             }
185              
186             # Placeholder
187 786         1383 elsif ($spec && ++$more) { $tree[-1][1] .= $char }
188              
189             # Text (optimize slash+text and *+text+slash+text)
190 6799         11160 elsif ($tree[-1][0] eq 'text') { $tree[-1][-1] .= $char }
191 875         3458 elsif (!$tree[-2] && $tree[-1][0] eq 'slash') { @tree = (['text', "/$char"]) }
192 281 50       894 elsif ($tree[-2] && $tree[-2][0] eq 'text' && $tree[-1][0] eq 'slash') { pop @tree && ($tree[-1][-1] .= "/$char") }
193 30         107 else { push @tree, ['text', $char] }
194             }
195              
196             # Placeholder types
197 933         2393 for my $token (reverse @tree) {
198 1219 100       2929 next unless $token->[0] eq 'placeholder';
199 147 100       1149 $token->[1] = $token->[1] =~ /^(.+)\Q$type_start\E(.+)$/ ? [$1, $2] : [$token->[1]];
200 147         475 unshift @$placeholders, $token->[1][0];
201             }
202              
203 933         2726 return $self->unparsed($pattern)->tree(\@tree);
204             }
205              
206             1;
207              
208             =encoding utf8
209              
210             =head1 NAME
211              
212             Mojolicious::Routes::Pattern - Route pattern
213              
214             =head1 SYNOPSIS
215              
216             use Mojolicious::Routes::Pattern;
217              
218             # Create pattern
219             my $pattern = Mojolicious::Routes::Pattern->new('/test/:name');
220              
221             # Match routes
222             my $captures = $pattern->match('/test/sebastian');
223             say $captures->{name};
224              
225             =head1 DESCRIPTION
226              
227             L is the core of L.
228              
229             =head1 ATTRIBUTES
230              
231             L implements the following attributes.
232              
233             =head2 constraints
234              
235             my $constraints = $pattern->constraints;
236             $pattern = $pattern->constraints({foo => qr/\w+/});
237              
238             Regular expression constraints.
239              
240             =head2 defaults
241              
242             my $defaults = $pattern->defaults;
243             $pattern = $pattern->defaults({foo => 'bar'});
244              
245             Default parameters.
246              
247             =head2 placeholder_start
248              
249             my $start = $pattern->placeholder_start;
250             $pattern = $pattern->placeholder_start(':');
251              
252             Character indicating a placeholder, defaults to C<:>.
253              
254             =head2 placeholders
255              
256             my $placeholders = $pattern->placeholders;
257             $pattern = $pattern->placeholders(['foo', 'bar']);
258              
259             Placeholder names.
260              
261             =head2 quote_end
262              
263             my $end = $pattern->quote_end;
264             $pattern = $pattern->quote_end('}');
265              
266             Character indicating the end of a quoted placeholder, defaults to C>.
267              
268             =head2 quote_start
269              
270             my $start = $pattern->quote_start;
271             $pattern = $pattern->quote_start('{');
272              
273             Character indicating the start of a quoted placeholder, defaults to C>.
274              
275             =head2 regex
276              
277             my $regex = $pattern->regex;
278             $pattern = $pattern->regex($regex);
279              
280             Pattern in compiled regular expression form.
281              
282             =head2 relaxed_start
283              
284             my $start = $pattern->relaxed_start;
285             $pattern = $pattern->relaxed_start('*');
286              
287             Character indicating a relaxed placeholder, defaults to C<#>.
288              
289             =head2 tree
290              
291             my $tree = $pattern->tree;
292             $pattern = $pattern->tree([['text', '/foo']]);
293              
294             Pattern in parsed form. Note that this structure should only be used very carefully since it is very dynamic.
295              
296             =head2 type_start
297              
298             my $start = $pattern->type_start;
299             $pattern = $pattern->type_start('|');
300              
301             Character indicating the start of a placeholder type, defaults to C<:>.
302              
303             =head2 types
304              
305             my $types = $pattern->types;
306             $pattern = $pattern->types({int => qr/[0-9]+/});
307              
308             Placeholder types.
309              
310             =head2 unparsed
311              
312             my $unparsed = $pattern->unparsed;
313             $pattern = $pattern->unparsed('/:foo/:bar');
314              
315             Raw unparsed pattern.
316              
317             =head2 wildcard_start
318              
319             my $start = $pattern->wildcard_start;
320             $pattern = $pattern->wildcard_start('*');
321              
322             Character indicating the start of a wildcard placeholder, defaults to C<*>.
323              
324             =head1 METHODS
325              
326             L inherits all methods from L and implements the following new ones.
327              
328             =head2 match
329              
330             my $captures = $pattern->match('/foo/bar');
331             my $captures = $pattern->match('/foo/bar', 1);
332              
333             Match pattern against entire path, format detection is disabled by default.
334              
335             =head2 match_partial
336              
337             my $captures = $pattern->match_partial(\$path);
338             my $captures = $pattern->match_partial(\$path, 1);
339              
340             Match pattern against path and remove matching parts, format detection is disabled by default.
341              
342             =head2 new
343              
344             my $pattern = Mojolicious::Routes::Pattern->new;
345             my $pattern = Mojolicious::Routes::Pattern->new('/users/:id');
346             my $pattern = Mojolicious::Routes::Pattern->new('/user/:id', id => qr/\d+/);
347             my $pattern = Mojolicious::Routes::Pattern->new(format => ['json', 'yaml']);
348              
349             Construct a new L object and L pattern if necessary.
350              
351             =head2 parse
352              
353             $pattern = $pattern->parse('/user/:id');
354             $pattern = $pattern->parse('/user/:id', id=> qr/\d+/);
355             $pattern = $pattern->parse(format => ['json', 'yaml']);
356              
357             Parse pattern.
358              
359             =head2 render
360              
361             my $path = $pattern->render({id => 24});
362             my $path = $pattern->render({id => 24}, 1);
363              
364             Render pattern into a path with parameters, format rendering is disabled by default.
365              
366             =head1 SEE ALSO
367              
368             L, L, L.
369              
370             =cut