Newer Older
402 lines | 9.742kb
add files
Yuki Kimoto authored on 2014-03-26
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