Skip to content

Commit

Permalink
remove typed placeholders again
Browse files Browse the repository at this point in the history
  • Loading branch information
kraih committed Feb 18, 2016
1 parent 6984366 commit 9a825fd
Show file tree
Hide file tree
Showing 9 changed files with 58 additions and 162 deletions.
4 changes: 0 additions & 4 deletions Changes
@@ -1,9 +1,5 @@

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
68 changes: 24 additions & 44 deletions lib/Mojolicious/Guides/Routing.pod
Expand Up @@ -118,43 +118,36 @@ parentheses.

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

=head2 Typed placeholders
=head2 Relaxed placeholders

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

/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'}
/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'}

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

/music/song.mp3 -> /music/(filename:relaxed) -> {filename => 'song.mp3'}
/music/song.mp3 -> /music/#filename -> {filename => 'song.mp3'}

The type L<Mojolicious::Routes::Pattern/"wildcard"> is very similar, but makes
your placeholders match absolutely everything, including C</> and C<.>, similar
to the regular expression C<(.+)>.
=head2 Wildcard placeholders

/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'}
Wildcard placeholders are just like the two types of placeholders above, but use
an asterisk prefix and match absolutely everything, including C</> and C<.>,
similar to the regular expression C<(.+)>.

The C<relaxed> and C<wildcard> pleaceholder types are so common, they also have
a shortcut form.

/hello/♥.mojolicious -> /hello/#name -> {name => '♥.mojolicious'}
/hello/♥/mojolicious -> /hello/*name -> {name => '♥/mojolicious'}

For a full list of placeholder types see
L<Mojolicious::Routes::Pattern/"TYPES">.
/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'}

=head1 BASICS

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

# * /*
$r->any('/(whatever:wildcard)' => {whatever => ''} => sub {
$r->any('/*whatever' => {whatever => ''} => sub {
my $c = shift;
my $whatever = $c->param('whatever');
$c->render(text => "/$whatever did not match.", status => 404);
Expand Down Expand Up @@ -812,19 +805,6 @@ For a full list of available hooks see L<Mojolicious/"HOOKS">.

Less commonly used and more powerful features.

=head2 Adding placeholder types

L<Restrictive placeholders|/"Restrictive placeholders"> can get repetitive, use
L<Mojolicious::Routes/"add_type"> to create your own placeholder types instead.

$r->add_type(isbn10 => qr/\d{9}(?:\d|X)/);

# GET /library/0596001738
$r->get('/library/(book:isbn10)')->to('library#info');

Just remember not to use C<^> and C<$> or capturing groups C<(...)> in your
regular expressions.

=head2 Shortcuts

To make route generation more expressive, you can also add your own shortcuts
Expand Down
29 changes: 22 additions & 7 deletions lib/Mojolicious/Guides/Tutorial.pod
Expand Up @@ -320,7 +320,7 @@ L<Mojolicious::Controller/"param">.

# /testsomething/foo
# /test123something/foo
get '/(bar)something/foo' => sub {
get '/(:bar)something/foo' => sub {
my $c = shift;
my $bar = $c->param('bar');
$c->render(text => "Our :bar placeholder matched $bar");
Expand All @@ -331,19 +331,34 @@ L<Mojolicious::Controller/"param">.
To separate them from the surrounding text, you can surround your placeholders
with parentheses, which also makes the colon prefix optional.

=head2 Typed placeholders
=head2 Relaxed Placeholders

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

use Mojolicious::Lite;

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

app->start;
__DATA__
Expand Down
18 changes: 1 addition & 17 deletions lib/Mojolicious/Routes.pm
Expand Up @@ -10,13 +10,12 @@ use Scalar::Util 'weaken';

has base_classes => sub { [qw(Mojolicious::Controller Mojo)] };
has cache => sub { Mojo::Cache->new };
has [qw(conditions shortcuts types)] => sub { {} };
has [qw(conditions shortcuts)] => 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 @@ -280,13 +279,6 @@ 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 @@ -315,14 +307,6 @@ Register a shortcut.
...
});
=head2 add_type
$r = $r->add_type(foo => qr/.../);
Register a placeholder type.
$r->add_type(hex => qr/[a-f0-9]+/);
=head2 continue
$r->continue(Mojolicious::Controller->new);
Expand Down
44 changes: 4 additions & 40 deletions lib/Mojolicious/Routes/Pattern.pm
Expand Up @@ -7,8 +7,7 @@ has [qw(placeholders tree)] => sub { [] };
has quote_end => ')';
has quote_start => '(';
has [qw(regex unparsed)];
has relaxed_start => '#';
has types => sub { {int => '\d+', relaxed => '[^/]+', wildcard => '.+'} };
has relaxed_start => '#';
has wildcard_start => '*';

sub match {
Expand Down Expand Up @@ -87,7 +86,6 @@ 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;
Expand All @@ -110,7 +108,7 @@ sub _compile {
unshift @$placeholders, $value;

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

# Custom regex
Expand Down Expand Up @@ -162,19 +160,15 @@ sub _tokenize {
my $relaxed = $self->relaxed_start;
my $wildcard = $self->wildcard_start;

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

# Quoted
if ($char eq $quote_start) {
push @tree, ['placeholder', ''];
$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 }
elsif ($char eq $quote_end) { $spec = 0 }

# Placeholder start
elsif ($char eq $start) { push @tree, ['placeholder', ''] unless $spec++ }
Expand Down Expand Up @@ -231,29 +225,6 @@ 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 @@ -322,13 +293,6 @@ 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
11 changes: 3 additions & 8 deletions lib/Mojolicious/Routes/Route.pm
Expand Up @@ -117,15 +117,10 @@ sub render {
sub root { shift->_chain->[0] }

sub route {
my $self = shift;

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

$route->pattern->constraints->{format} //= 0 if defined $format && !$format;
return $route;
}

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:relaxed)' => sub {
get '/foo_relaxed/#test' => 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:wildcard)' => sub {
get '/foo_wildcard_too/*test' => sub {
my $c = shift;
$c->render(text => $c->stash('test'));
};
Expand Down
27 changes: 1 addition & 26 deletions t/mojolicious/pattern.t
Expand Up @@ -88,14 +88,6 @@ is $pattern->render({controller => 'zzz', action => 'lala'}), '/zzztest/lala',
'right result';
ok !$pattern->match('/test/lala'), 'no result';

# Integer
$pattern = Mojolicious::Routes::Pattern->new('/test/(num:int)');
is_deeply $pattern->match('/test/23'), {num => 23}, 'right structure';
is $pattern->render({num => 24}), '/test/24', 'right result';
is_deeply $pattern->match('/test/0'), {num => 0}, 'right structure';
is $pattern->render({num => 0}), '/test/0', 'right result';
ok !$pattern->match('/test/lala'), 'no result';

# Relaxed
$pattern = Mojolicious::Routes::Pattern->new('/test/#controller/:action');
is_deeply $pattern->match('/test/foo.bar/baz'),
Expand All @@ -107,31 +99,14 @@ is_deeply $pattern->match('/test/foo.bar'), {groovy => 'foo.bar'},
'right structure';
is $pattern->defaults->{format}, undef, 'no value';
is $pattern->render({groovy => 'foo.bar'}), '/test/foo.bar', 'right result';
$pattern = Mojolicious::Routes::Pattern->new('/test/(groovy:relaxed)');
is_deeply $pattern->match('/test/foo.bar'), {groovy => 'foo.bar'},
'right structure';
is $pattern->defaults->{format}, undef, 'no value';
is $pattern->render({groovy => 'foo.bar'}), '/test/foo.bar', 'right result';
$pattern = Mojolicious::Routes::Pattern->new('/test/:groovy:relaxed');
is_deeply $pattern->match('/test/foo.bar'), {groovy => 'foo.bar'},
'right structure';
is $pattern->defaults->{format}, undef, 'no value';
is $pattern->render({groovy => 'foo.bar'}), '/test/foo.bar', 'right result';

# Wildcard
$pattern = Mojolicious::Routes::Pattern->new('/test/(:controller)/(*action)');
is_deeply $pattern->match('/test/foo/bar.baz/yada'),
{controller => 'foo', action => 'bar.baz/yada'}, 'right structure';
is $pattern->render({controller => 'foo', action => 'bar.baz/yada'}),
'/test/foo/bar.baz/yada', 'right result';
$pattern
= Mojolicious::Routes::Pattern->new('/tset/:controller/(:action:wildcard)');
is_deeply $pattern->match('/tset/foo/bar.baz/yada'),
{controller => 'foo', action => 'bar.baz/yada'}, 'right structure';
is $pattern->render({controller => 'foo', action => 'bar.baz/yada'}),
'/tset/foo/bar.baz/yada', 'right result';
$pattern
= Mojolicious::Routes::Pattern->new('/tset/:controller/:action:wildcard');
$pattern = Mojolicious::Routes::Pattern->new('/tset/:controller/*action');
is_deeply $pattern->match('/tset/foo/bar.baz/yada'),
{controller => 'foo', action => 'bar.baz/yada'}, 'right structure';
is $pattern->render({controller => 'foo', action => 'bar.baz/yada'}),
Expand Down

1 comment on commit 9a825fd

@s1037989
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh! What went wrong?

Please sign in to comment.