add files
|
1 |
package Mojolicious::Routes::Pattern; |
2 |
use Mojo::Base -base; |
|
3 | ||
4 |
has [qw(constraints defaults)] => sub { {} }; |
|
5 |
has [qw(format_regex pattern regex)]; |
|
6 |
has placeholder_start => ':'; |
|
7 |
has [qw(placeholders tree)] => sub { [] }; |
|
8 |
has quote_end => ')'; |
|
9 |
has quote_start => '('; |
|
10 |
has relaxed_start => '#'; |
|
11 |
has wildcard_start => '*'; |
|
12 | ||
13 |
sub new { shift->SUPER::new->parse(@_) } |
|
14 | ||
15 |
sub match { |
|
16 |
my ($self, $path, $detect) = @_; |
|
17 |
my $captures = $self->match_partial(\$path, $detect); |
|
18 |
return !$path || $path eq '/' ? $captures : undef; |
|
19 |
} |
|
20 | ||
21 |
sub match_partial { |
|
22 |
my ($self, $pathref, $detect) = @_; |
|
23 | ||
24 |
# Compile on demand |
|
25 |
my $regex = $self->regex || $self->_compile; |
|
26 |
my $format |
|
27 |
= $detect ? ($self->format_regex || $self->_compile_format) : undef; |
|
28 | ||
29 |
# Match |
|
30 |
return undef unless my @captures = $$pathref =~ $regex; |
|
31 |
$$pathref = ${^POSTMATCH}; |
|
32 | ||
33 |
# Merge captures |
|
34 |
my $captures = {%{$self->defaults}}; |
|
35 |
for my $placeholder (@{$self->placeholders}) { |
|
36 |
last unless @captures; |
|
37 |
my $capture = shift @captures; |
|
38 |
$captures->{$placeholder} = $capture if defined $capture; |
|
39 |
} |
|
40 | ||
41 |
# Format |
|
42 |
my $constraint = $self->constraints->{format}; |
|
43 |
return $captures if !$detect || defined $constraint && !$constraint; |
|
44 |
if ($$pathref =~ s!^/?$format!!) { $captures->{format} = $1 } |
|
45 |
elsif ($constraint) { return undef unless $captures->{format} } |
|
46 | ||
47 |
return $captures; |
|
48 |
} |
|
49 | ||
50 |
sub parse { |
|
51 |
my $self = shift; |
|
52 | ||
53 |
# Make sure we have a viable pattern |
|
54 |
my $pattern = @_ % 2 ? (shift || '/') : '/'; |
|
55 |
$pattern = "/$pattern" unless $pattern =~ m!^/!; |
|
56 |
$self->constraints({@_}); |
|
57 | ||
58 |
return $pattern eq '/' ? $self : $self->pattern($pattern)->_tokenize; |
|
59 |
} |
|
60 | ||
61 |
sub render { |
|
62 |
my ($self, $values, $render) = @_; |
|
63 | ||
64 |
# Merge values with defaults |
|
65 |
my $format = ($values ||= {})->{format}; |
|
66 |
$values = {%{$self->defaults}, %$values}; |
|
67 | ||
68 |
# Placeholders can only be optional without a format |
|
69 |
my $optional = !$format; |
|
70 | ||
71 |
my $str = ''; |
|
72 |
for my $token (reverse @{$self->tree}) { |
|
73 |
my $op = $token->[0]; |
|
74 |
my $rendered = ''; |
|
75 | ||
76 |
# Slash |
|
77 |
if ($op eq 'slash') { $rendered = '/' unless $optional } |
|
78 | ||
79 |
# Text |
|
80 |
elsif ($op eq 'text') { |
|
81 |
$rendered = $token->[1]; |
|
82 |
$optional = 0; |
|
83 |
} |
|
84 | ||
85 |
# Placeholder, relaxed or wildcard |
|
86 |
elsif ($op eq 'placeholder' || $op eq 'relaxed' || $op eq 'wildcard') { |
|
87 |
my $name = $token->[1]; |
|
88 |
$rendered = $values->{$name} // ''; |
|
89 |
my $default = $self->defaults->{$name}; |
|
90 |
if (!defined $default || ($default ne $rendered)) { $optional = 0 } |
|
91 |
elsif ($optional) { $rendered = '' } |
|
92 |
} |
|
93 | ||
94 |
$str = "$rendered$str"; |
|
95 |
} |
|
96 | ||
97 |
# Format is optional |
|
98 |
$str ||= '/'; |
|
99 |
return $render && $format ? "$str.$format" : $str; |
|
100 |
} |
|
101 | ||
102 |
sub _compile { |
|
103 |
my $self = shift; |
|
104 | ||
105 |
my $block = my $regex = ''; |
|
106 |
my $optional = 1; |
|
107 |
my $constraints = $self->constraints; |
|
108 |
my $defaults = $self->defaults; |
|
109 |
for my $token (reverse @{$self->tree}) { |
|
110 |
my $op = $token->[0]; |
|
111 |
my $compiled = ''; |
|
112 | ||
113 |
# Slash |
|
114 |
if ($op eq 'slash') { |
|
115 |
$regex = ($optional ? "(?:/$block)?" : "/$block") . $regex; |
|
116 |
$block = ''; |
|
117 |
next; |
|
118 |
} |
|
119 | ||
120 |
# Text |
|
121 |
elsif ($op eq 'text') { |
|
122 |
$compiled = quotemeta $token->[1]; |
|
123 |
$optional = 0; |
|
124 |
} |
|
125 | ||
126 |
# Placeholder |
|
127 |
elsif ($op eq 'placeholder' || $op eq 'relaxed' || $op eq 'wildcard') { |
|
128 |
my $name = $token->[1]; |
|
129 |
unshift @{$self->placeholders}, $name; |
|
130 | ||
131 |
# Placeholder |
|
132 |
if ($op eq 'placeholder') { $compiled = '([^\/\.]+)' } |
|
133 | ||
134 |
# Relaxed |
|
135 |
elsif ($op eq 'relaxed') { $compiled = '([^\/]+)' } |
|
136 | ||
137 |
# Wildcard |
|
138 |
elsif ($op eq 'wildcard') { $compiled = '(.+)' } |
|
139 | ||
140 |
# Custom regex |
|
141 |
my $constraint = $constraints->{$name}; |
|
142 |
$compiled = _compile_req($constraint) if $constraint; |
|
143 | ||
144 |
# Optional placeholder |
|
145 |
$optional = 0 unless exists $defaults->{$name}; |
|
146 |
$compiled .= '?' if $optional; |
|
147 |
} |
|
148 | ||
149 |
$block = "$compiled$block"; |
|
150 |
} |
|
151 | ||
152 |
# Not rooted with a slash |
|
153 |
$regex = "$block$regex" if $block; |
|
154 | ||
155 |
return $self->regex(qr/^$regex/ps)->regex; |
|
156 |
} |
|
157 | ||
158 |
sub _compile_format { |
|
159 |
my $self = shift; |
|
160 | ||
161 |
# Default regex |
|
162 |
my $c = $self->constraints; |
|
163 |
return $self->format_regex(qr!\.([^/]+)$!)->format_regex |
|
164 |
unless defined $c->{format}; |
|
165 | ||
166 |
# No regex |
|
167 |
return undef unless $c->{format}; |
|
168 | ||
169 |
# Compile custom regex |
|
170 |
my $regex = _compile_req($c->{format}); |
|
171 |
return $self->format_regex(qr!\.$regex$!)->format_regex; |
|
172 |
} |
|
173 | ||
174 |
sub _compile_req { |
|
175 |
my $req = shift; |
|
176 |
return "($req)" if ref $req ne 'ARRAY'; |
|
177 |
return '(' . join('|', map {quotemeta} reverse sort @$req) . ')'; |
|
178 |
} |
|
179 | ||
180 |
sub _tokenize { |
|
181 |
my $self = shift; |
|
182 | ||
183 |
my $quote_end = $self->quote_end; |
|
184 |
my $quote_start = $self->quote_start; |
|
185 |
my $placeholder = $self->placeholder_start; |
|
186 |
my $relaxed = $self->relaxed_start; |
|
187 |
my $wildcard = $self->wildcard_start; |
|
188 | ||
189 |
my $pattern = $self->pattern; |
|
190 |
my $state = 'text'; |
|
191 |
my (@tree, $quoted); |
|
192 |
for my $char (split '', $pattern) { |
|
193 |
my $inside = !!grep { $_ eq $state } qw(placeholder relaxed wildcard); |
|
194 | ||
195 |
# Quote start |
|
196 |
if ($char eq $quote_start) { |
|
197 |
$quoted = 1; |
|
198 |
push @tree, ['placeholder', '']; |
|
199 |
$state = 'placeholder'; |
|
200 |
} |
|
201 | ||
202 |
# Placeholder start |
|
203 |
elsif ($char eq $placeholder) { |
|
204 |
push @tree, ['placeholder', ''] if $state ne 'placeholder'; |
|
205 |
$state = 'placeholder'; |
|
206 |
} |
|
207 | ||
208 |
# Relaxed or wildcard start (upgrade when quoted) |
|
209 |
elsif ($char eq $relaxed || $char eq $wildcard) { |
|
210 |
push @tree, ['placeholder', ''] unless $quoted; |
|
211 |
$tree[-1][0] = $state = $char eq $relaxed ? 'relaxed' : 'wildcard'; |
|
212 |
} |
|
213 | ||
214 |
# Quote end |
|
215 |
elsif ($char eq $quote_end) { |
|
216 |
$quoted = 0; |
|
217 |
$state = 'text'; |
|
218 |
} |
|
219 | ||
220 |
# Slash |
|
221 |
elsif ($char eq '/') { |
|
222 |
push @tree, ['slash']; |
|
223 |
$state = 'text'; |
|
224 |
} |
|
225 | ||
226 |
# Placeholder, relaxed or wildcard |
|
227 |
elsif ($inside && $char =~ /\w/) { $tree[-1][-1] .= $char } |
|
228 | ||
229 |
# Text |
|
230 |
else { |
|
231 |
push @tree, ['text', $char] and next unless $tree[-1][0] eq 'text'; |
|
232 |
$tree[-1][-1] .= $char; |
|
233 |
$state = 'text'; |
|
234 |
} |
|
235 |
} |
|
236 | ||
237 |
return $self->tree(\@tree); |
|
238 |
} |
|
239 | ||
240 |
1; |
|
241 | ||
242 |
=encoding utf8 |
|
243 | ||
244 |
=head1 NAME |
|
245 | ||
246 |
Mojolicious::Routes::Pattern - Routes pattern engine |
|
247 | ||
248 |
=head1 SYNOPSIS |
|
249 | ||
250 |
use Mojolicious::Routes::Pattern; |
|
251 | ||
252 |
# Create pattern |
|
253 |
my $pattern = Mojolicious::Routes::Pattern->new('/test/:name'); |
|
254 | ||
255 |
# Match routes |
|
256 |
my $captures = $pattern->match('/test/sebastian'); |
|
257 |
say $captures->{name}; |
|
258 | ||
259 |
=head1 DESCRIPTION |
|
260 | ||
261 |
L<Mojolicious::Routes::Pattern> is the core of L<Mojolicious::Routes>. |
|
262 | ||
263 |
=head1 ATTRIBUTES |
|
264 | ||
265 |
L<Mojolicious::Routes::Pattern> implements the following attributes. |
|
266 | ||
267 |
=head2 constraints |
|
268 | ||
269 |
my $constraints = $pattern->constraints; |
|
270 |
$pattern = $pattern->constraints({foo => qr/\w+/}); |
|
271 | ||
272 |
Regular expression constraints. |
|
273 | ||
274 |
=head2 defaults |
|
275 | ||
276 |
my $defaults = $pattern->defaults; |
|
277 |
$pattern = $pattern->defaults({foo => 'bar'}); |
|
278 | ||
279 |
Default parameters. |
|
280 | ||
281 |
=head2 format_regex |
|
282 | ||
283 |
my $regex = $pattern->format_regex; |
|
284 |
$pattern = $pattern->format_regex($regex); |
|
285 | ||
286 |
Compiled regular expression for format matching. |
|
287 | ||
288 |
=head2 pattern |
|
289 | ||
290 |
my $pattern = $pattern->pattern; |
|
291 |
$pattern = $pattern->pattern('/(foo)/(bar)'); |
|
292 | ||
293 |
Raw unparsed pattern. |
|
294 | ||
295 |
=head2 placeholder_start |
|
296 | ||
297 |
my $start = $pattern->placeholder_start; |
|
298 |
$pattern = $pattern->placeholder_start(':'); |
|
299 | ||
300 |
Character indicating a placeholder, defaults to C<:>. |
|
301 | ||
302 |
=head2 placeholders |
|
303 | ||
304 |
my $placeholders = $pattern->placeholders; |
|
305 |
$pattern = $pattern->placeholders(['foo', 'bar']); |
|
306 | ||
307 |
Placeholder names. |
|
308 | ||
309 |
=head2 quote_end |
|
310 | ||
311 |
my $end = $pattern->quote_end; |
|
312 |
$pattern = $pattern->quote_end(']'); |
|
313 | ||
314 |
Character indicating the end of a quoted placeholder, defaults to C<)>. |
|
315 | ||
316 |
=head2 quote_start |
|
317 | ||
318 |
my $start = $pattern->quote_start; |
|
319 |
$pattern = $pattern->quote_start('['); |
|
320 | ||
321 |
Character indicating the start of a quoted placeholder, defaults to C<(>. |
|
322 | ||
323 |
=head2 regex |
|
324 | ||
325 |
my $regex = $pattern->regex; |
|
326 |
$pattern = $pattern->regex($regex); |
|
327 | ||
328 |
Pattern in compiled regular expression form. |
|
329 | ||
330 |
=head2 relaxed_start |
|
331 | ||
332 |
my $start = $pattern->relaxed_start; |
|
333 |
$pattern = $pattern->relaxed_start('*'); |
|
334 | ||
335 |
Character indicating a relaxed placeholder, defaults to C<#>. |
|
336 | ||
337 |
=head2 tree |
|
338 | ||
339 |
my $tree = $pattern->tree; |
|
340 |
$pattern = $pattern->tree([['slash'], ['text', 'foo']]); |
|
341 | ||
342 |
Pattern in parsed form. Note that this structure should only be used very |
|
343 |
carefully since it is very dynamic. |
|
344 | ||
345 |
=head2 wildcard_start |
|
346 | ||
347 |
my $start = $pattern->wildcard_start; |
|
348 |
$pattern = $pattern->wildcard_start('*'); |
|
349 | ||
350 |
Character indicating the start of a wildcard placeholder, defaults to C<*>. |
|
351 | ||
352 |
=head1 METHODS |
|
353 | ||
354 |
L<Mojolicious::Routes::Pattern> inherits all methods from L<Mojo::Base> and |
|
355 |
implements the following new ones. |
|
356 | ||
357 |
=head2 new |
|
358 | ||
359 |
my $pattern = Mojolicious::Routes::Pattern->new('/:action'); |
|
360 |
my $pattern |
|
361 |
= Mojolicious::Routes::Pattern->new('/:action', action => qr/\w+/); |
|
362 |
my $pattern = Mojolicious::Routes::Pattern->new(format => 0); |
|
363 | ||
364 |
Construct a new L<Mojolicious::Routes::Pattern> object and L</"parse"> pattern |
|
365 |
if necessary. |
|
366 | ||
367 |
=head2 match |
|
368 | ||
369 |
my $captures = $pattern->match('/foo/bar'); |
|
370 |
my $captures = $pattern->match('/foo/bar', 1); |
|
371 | ||
372 |
Match pattern against entire path, format detection is disabled by default. |
|
373 | ||
374 |
=head2 match_partial |
|
375 | ||
376 |
my $captures = $pattern->match_partial(\$path); |
|
377 |
my $captures = $pattern->match_partial(\$path, 1); |
|
378 | ||
379 |
Match pattern against path and remove matching parts, format detection is |
|
380 |
disabled by default. |
|
381 | ||
382 |
=head2 parse |
|
383 | ||
384 |
$pattern = $pattern->parse('/:action'); |
|
385 |
$pattern = $pattern->parse('/:action', action => qr/\w+/); |
|
386 |
$pattern = $pattern->parse(format => 0); |
|
387 | ||
388 |
Parse pattern. |
|
389 | ||
390 |
=head2 render |
|
391 | ||
392 |
my $path = $pattern->render({action => 'foo'}); |
|
393 |
my $path = $pattern->render({action => 'foo'}, 1); |
|
394 | ||
395 |
Render pattern into a path with parameters, format rendering is disabled by |
|
396 |
default. |
|
397 | ||
398 |
=head1 SEE ALSO |
|
399 | ||
400 |
L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>. |
|
401 | ||
402 |
=cut |