Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
add support for typed placeholders
  • Loading branch information
kraih committed Feb 18, 2016
1 parent fa61c14 commit f4c55e5
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 90 deletions.
4 changes: 4 additions & 0 deletions Changes
@@ -1,5 +1,9 @@

6.47 2016-02-18
- Added support for typed placeholders.
- Added types attribute to Mojolicious::Routes and
Mojolicious::Routes::Pattern.
- Added add_type method to Mojolicious::Routes.
- Fixed datetime_field helper to use the correct type attribute value.

6.46 2016-02-13
Expand Down
45 changes: 23 additions & 22 deletions lib/Mojolicious/Guides/Routing.pod
Expand Up @@ -118,36 +118,37 @@ parentheses.

/i♥mojolicious -> /(one)♥(two) -> {one => 'i', two => 'mojolicious'}

=head2 Relaxed placeholders
=head2 Typed placeholders

Relaxed placeholders are just like standard placeholders, but use a hash prefix
and match all characters except C</>, similar to the regular expression
C<([^/]+)>.
You can also assign your placeholders a type, such as
L<Mojolicious::Routes::Pattern/"relaxed">, which allows them to match all
characters except C</>, similar to the regular expression C<([^/]+)>.

/hello -> /#name/hello -> undef
/sebastian/23/hello -> /#name/hello -> undef
/sebastian.23/hello -> /#name/hello -> {name => 'sebastian.23'}
/sebastian/hello -> /#name/hello -> {name => 'sebastian'}
/sebastian23/hello -> /#name/hello -> {name => 'sebastian23'}
/sebastian 23/hello -> /#name/hello -> {name => 'sebastian 23'}
/hello -> /(name:relaxed)/hello -> undef
/sebastian/23/hello -> /(name:relaxed)/hello -> undef
/sebastian.23/hello -> /(name:relaxed)/hello -> {name => 'sebastian.23'}
/sebastian/hello -> /(name:relaxed)/hello -> {name => 'sebastian'}
/sebastian23/hello -> /(name:relaxed)/hello -> {name => 'sebastian23'}
/sebastian 23/hello -> /(name:relaxed)/hello -> {name => 'sebastian 23'}

They can be especially useful for manually matching file names with extensions,
This can be especially useful for manually matching file names with extensions,
rather than using L<format detection|/"Formats">.

/music/song.mp3 -> /music/#filename -> {filename => 'song.mp3'}

=head2 Wildcard placeholders

Wildcard placeholders are just like the two types of placeholders above, but
use an asterisk prefix and match absolutely everything, including C</> and
The type L<Mojolicious::Routes::Pattern/"wildcard"> is very similar, but it
allows your placeholders to match absolutely everything, including C</> and
C<.>, similar to the regular expression C<(.+)>.

/hello -> /*name/hello -> undef
/sebastian/23/hello -> /*name/hello -> {name => 'sebastian/23'}
/sebastian.23/hello -> /*name/hello -> {name => 'sebastian.23'}
/sebastian/hello -> /*name/hello -> {name => 'sebastian'}
/sebastian23/hello -> /*name/hello -> {name => 'sebastian23'}
/sebastian 23/hello -> /*name/hello -> {name => 'sebastian 23'}
/hello -> /(name:wildcard)/hello -> undef
/sebastian/23/hello -> /(name:wildcard)/hello -> {name => 'sebastian/23'}
/sebastian.23/hello -> /(name:wildcard)/hello -> {name => 'sebastian.23'}
/sebastian/hello -> /(name:wildcard)/hello -> {name => 'sebastian'}
/sebastian23/hello -> /(name:wildcard)/hello -> {name => 'sebastian23'}
/sebastian 23/hello -> /(name:wildcard)/hello -> {name => 'sebastian 23'}

For a full list of placeholder types see
L<Mojolicious::Routes::Pattern/"TYPES">.

=head1 BASICS

Expand Down Expand Up @@ -695,7 +696,7 @@ requests that did not match in your last route with an optional wildcard
placeholder.

# * /*
$r->any('/*whatever' => {whatever => ''} => sub {
$r->any('/(whatever:wildcard)' => {whatever => ''} => sub {
my $c = shift;
my $whatever = $c->param('whatever');
$c->render(text => "/$whatever did not match.", status => 404);
Expand Down
31 changes: 8 additions & 23 deletions lib/Mojolicious/Guides/Tutorial.pod
Expand Up @@ -328,34 +328,19 @@ L<Mojolicious::Controller/"param">.

app->start;

=head2 Relaxed Placeholders
=head2 Typed placeholders

Relaxed placeholders allow matching of everything until a C</> occurs, similar
to the regular expression C<([^/]+)>.

use Mojolicious::Lite;

# /hello/test
# /hello/test.html
get '/hello/#you' => 'groovy';

app->start;
__DATA__

@@ groovy.html.ep
Your name is <%= $you %>.

=head2 Wildcard placeholders

Wildcard placeholders allow matching absolutely everything, including C</> and
C<.>, similar to the regular expression C<(.+)>.
To change how much of the request path your placeholder should capture, you can
also assign it a type like L<Mojolicious::Routes::Pattern/"wildcard">, which
matches absolutely everything, including C</> and C<.>, similar to the regular
expression C<(.+)>.

use Mojolicious::Lite;

# /hello/test
# /hello/test123
# /hello/test.123/test/123
get '/hello/*you' => 'groovy';
get '/hello/(:you:wildcard)' => 'groovy';

app->start;
__DATA__
Expand Down Expand Up @@ -422,8 +407,8 @@ stash all the time.

=head2 Restrictive placeholders

The easiest way to make placeholders more restrictive are alternatives, you
just make a list of possible values.
A very easy way to make placeholders more restrictive are alternatives, you just
make a list of possible values.

use Mojolicious::Lite;

Expand Down
16 changes: 15 additions & 1 deletion lib/Mojolicious/Routes.pm
Expand Up @@ -10,12 +10,13 @@ use Scalar::Util 'weaken';

has base_classes => sub { [qw(Mojolicious::Controller Mojo)] };
has cache => sub { Mojo::Cache->new };
has [qw(conditions shortcuts)] => sub { {} };
has [qw(conditions shortcuts types)] => sub { {} };
has hidden => sub { [qw(attr has new tap)] };
has namespaces => sub { [] };

sub add_condition { $_[0]->conditions->{$_[1]} = $_[2] and return $_[0] }
sub add_shortcut { $_[0]->shortcuts->{$_[1]} = $_[2] and return $_[0] }
sub add_type { $_[0]->types->{$_[1]} = $_[2] and return $_[0] }

sub continue {
my ($self, $c) = @_;
Expand Down Expand Up @@ -279,6 +280,13 @@ Namespaces to load controllers from.
Contains all available shortcuts.
=head2 types
my $types = $r->types;
$r = $r->types({int => qr/\d+/});
Contains all available placeholder types.
=head1 METHODS
L<Mojolicious::Routes> inherits all methods from L<Mojolicious::Routes::Route>
Expand Down Expand Up @@ -307,6 +315,12 @@ Register a shortcut.
...
});
=head2 add_type
$r = $r->add_type(foo => qr/.../);
Register a placeholder type.
=head2 continue
$r->continue(Mojolicious::Controller->new);
Expand Down
76 changes: 51 additions & 25 deletions lib/Mojolicious/Routes/Pattern.pm
Expand Up @@ -7,7 +7,8 @@ has [qw(placeholders tree)] => sub { [] };
has quote_end => ')';
has quote_start => '(';
has [qw(regex unparsed)];
has relaxed_start => '#';
has relaxed_start => '#';
has types => sub { {int => '\d+', relaxed => '[^/]+', wildcard => '.+'} };
has wildcard_start => '*';

sub match {
Expand Down Expand Up @@ -86,11 +87,12 @@ sub _compile {
my $placeholders = $self->placeholders;
my $constraints = $self->constraints;
my $defaults = $self->defaults;
my $types = $self->types;

my $block = my $regex = '';
my $optional = 1;
for my $token (reverse @{$self->tree}) {
my ($op, $value) = @$token;
my ($op, $value, $type) = @$token;
my $fragment = '';

# Text
Expand All @@ -108,13 +110,8 @@ sub _compile {
unshift @$placeholders, $value;

# Placeholder
if ($op eq 'placeholder') { $fragment = '([^/.]+)' }

# Relaxed
elsif ($op eq 'relaxed') { $fragment = '([^/]+)' }

# Wildcard
else { $fragment = '(.+)' }
$fragment = _compile_req($types->{$type // ''} // '[^/.]+')
if $op eq 'placeholder';

# Custom regex
if (my $c = $constraints->{$value}) { $fragment = _compile_req($c) }
Expand Down Expand Up @@ -161,42 +158,41 @@ sub _tokenize {

my $quote_end = $self->quote_end;
my $quote_start = $self->quote_start;
my $placeholder = $self->placeholder_start;
my $start = $self->placeholder_start;
my $relaxed = $self->relaxed_start;
my $wildcard = $self->wildcard_start;

my (@tree, $inside, $quoted);
my (@tree, $spec, $type);
for my $char (split '', $pattern) {

# Quote start
# Quoted
if ($char eq $quote_start) {
push @tree, ['placeholder', ''];
($inside, $quoted) = (1, 1);
$spec = 1;
}
elsif ($char eq $quote_end) { ($spec, $type) = (0, 0) }

# Type
elsif ($spec && $tree[-1][1] && $char eq ':') { $type = 1 }
elsif ($type) { $tree[-1][2] .= $char }

# Placeholder start
elsif ($char eq $placeholder) {
push @tree, ['placeholder', ''] unless $inside++;
}
elsif ($char eq $start) { push @tree, ['placeholder', ''] unless $spec++ }

# Relaxed or wildcard start (upgrade when quoted)
elsif ($char eq $relaxed || $char eq $wildcard) {
push @tree, ['placeholder', ''] unless $quoted;
$tree[-1][0] = $char eq $relaxed ? 'relaxed' : 'wildcard';
$inside = 1;
push @tree, ['placeholder', ''] unless $spec++;
$tree[-1][2] = $char eq '#' ? 'relaxed' : 'wildcard';
}

# Quote end
elsif ($char eq $quote_end) { ($inside, $quoted) = (0, 0) }

# Slash
elsif ($char eq '/') {
push @tree, ['slash'];
$inside = 0;
$spec = 0;
}

# Placeholder, relaxed or wildcard
elsif ($inside) { $tree[-1][-1] .= $char }
# Placeholder
elsif ($spec) { $tree[-1][1] .= $char }

# Text (optimize slash+text and *+text+slash+text)
elsif ($tree[-1][0] eq 'text') { $tree[-1][-1] .= $char }
Expand Down Expand Up @@ -235,6 +231,29 @@ Mojolicious::Routes::Pattern - Routes pattern engine
L<Mojolicious::Routes::Pattern> is the core of L<Mojolicious::Routes>.
=head2 TYPES
These placeholder types are available by default.
=head2 int
"/(foo:int)"
Match only decimal digit characters, similar to the regular expression C<(\d+)>.
=head2 relaxed
"/(foo:relaxed)"
Match all characters except C</>, similar to the regular expression C<([^/]+)>.
=head2 wildcard
"/(foo:wildcard)"
Match absolutely everything, including C</> and C<.>, similar to the regular
expression C<(.+)>.
=head1 ATTRIBUTES
L<Mojolicious::Routes::Pattern> implements the following attributes.
Expand Down Expand Up @@ -303,6 +322,13 @@ Character indicating a relaxed placeholder, defaults to C<#>.
Pattern in parsed form. Note that this structure should only be used very
carefully since it is very dynamic.
=head2 types
my $types = $pattern->types;
$pattern = $pattern->types({foo => qr/\w+/});
Placeholder types.
=head2 unparsed
my $unparsed = $pattern->unparsed;
Expand Down
23 changes: 8 additions & 15 deletions lib/Mojolicious/Routes/Route.pm
Expand Up @@ -73,8 +73,6 @@ sub name {
return $self;
}

sub new { shift->SUPER::new->parse(@_) }

sub options { shift->_generate_route(OPTIONS => @_) }

sub over {
Expand Down Expand Up @@ -119,10 +117,15 @@ sub render {
sub root { shift->_chain->[0] }

sub route {
my $self = shift;
my $route = $self->add_child(__PACKAGE__->new(@_))->children->[-1];
my $self = shift;

my $route = __PACKAGE__->new;
my $pattern = $route->pattern;
$pattern->types({%{$pattern->types}, %{$self->root->types}});
$self->add_child($route->parse(@_));
my $format = $self->pattern->constraints->{format};
$route->pattern->constraints->{format} //= 0 if defined $format && !$format;
$pattern->constraints->{format} //= 0 if defined $format && !$format;

return $route;
}

Expand Down Expand Up @@ -395,16 +398,6 @@ the current route.
# Route with destination and custom name
$r->get('/user')->to('user#show')->name('show_user');
=head2 new
my $r = Mojolicious::Routes::Route->new;
my $r = Mojolicious::Routes::Route->new('/:action');
my $r = Mojolicious::Routes::Route->new('/:action', action => qr/\w+/);
my $r = Mojolicious::Routes::Route->new(format => 0);
Construct a new L<Mojolicious::Routes::Route> object and L</"parse"> pattern if
necessary.
=head2 options
my $route = $r->options('/:foo');
Expand Down
4 changes: 2 additions & 2 deletions t/mojolicious/lite_app.t
Expand Up @@ -235,7 +235,7 @@ get '/source' => sub {
or $c->res->headers->header('X-Missing' => 1);
};

get '/foo_relaxed/#test' => sub {
get '/foo_relaxed/(test:relaxed)' => sub {
my $c = shift;
$c->render(text => $c->stash('test') . ($c->req->headers->dnt ? 1 : 0));
};
Expand All @@ -245,7 +245,7 @@ get '/foo_wildcard/(*test)' => sub {
$c->render(text => $c->stash('test'));
};

get '/foo_wildcard_too/*test' => sub {
get '/foo_wildcard_too/(test:wildcard)' => sub {
my $c = shift;
$c->render(text => $c->stash('test'));
};
Expand Down

0 comments on commit f4c55e5

Please sign in to comment.