Skip to content

Commit

Permalink
simple implementation for placeholder types
Browse files Browse the repository at this point in the history
  • Loading branch information
kraih committed Dec 25, 2017
1 parent 3e03b78 commit c39011c
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 22 deletions.
23 changes: 23 additions & 0 deletions lib/Mojolicious/Guides/Routing.pod
Expand Up @@ -527,6 +527,29 @@ non-capturing groups C<(?:...)> are fine though.
This way you get easily readable routes and the raw power of regular
expressions.

=head2 Placeholder types

And if you have multiple routes using restrictive placeholders you can also turn
them into placeholder types.

# A type with alternatives
$r->add_type(futurama_name => ['bender', 'leela']);

# /fry -> undef
# /bender -> {controller => 'foo', action => 'bar', name => 'bender'}
# /leela -> {controller => 'foo', action => 'bar', name => 'leela'}
$r->get('/(name:futurama_name)')->to('foo#bar');

Placeholder types work just like restrictive placeholders, they are just
reusable with the C<(placeholder:type)> notation.

# A type adjusting the regular expression
$r->add_type(num => qr/\d+/);

# /23 -> {controller => 'foo', action => 'bar', number => 23}
# /test -> undef
$r->get('/(number:num)')->to('foo#bar');

=head2 Introspection

The command L<Mojolicious::Command::routes> can be used from the command line
Expand Down
18 changes: 17 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 @@ -273,6 +274,13 @@ Namespaces to load controllers from.
Contains all available shortcuts.
=head2 types
my $types = $r->types;
$r = $r->types({int => qr/[0-9]+/});
Placeholder types.
=head1 METHODS
L<Mojolicious::Routes> inherits all methods from L<Mojolicious::Routes::Route>
Expand Down Expand Up @@ -301,6 +309,14 @@ Register a shortcut.
...
});
=head2 add_type
$r = $r->add_type(foo => sub {...});
Register a placeholder type.
$r->add_type(int => qr/[0-9]+/);
=head2 continue
$r->continue(Mojolicious::Controller->new);
Expand Down
68 changes: 47 additions & 21 deletions lib/Mojolicious/Routes/Pattern.pm
@@ -1,8 +1,10 @@
package Mojolicious::Routes::Pattern;
use Mojo::Base -base;

has [qw(constraints defaults)] => sub { {} };
has placeholder_start => ':';
use Carp 'croak';

has [qw(constraints defaults types)] => sub { {} };
has [qw(placeholder_start type_start)] => ':';
has [qw(placeholders tree)] => sub { [] };
has quote_end => ')';
has quote_start => '(';
Expand Down Expand Up @@ -57,23 +59,23 @@ sub render {
my $str = '';
for my $token (reverse @{$self->tree}) {
my ($op, $value) = @$token;
my $fragment = '';
my $part = '';

# Text
if ($op eq 'text') { ($fragment, $optional) = ($value, 0) }
if ($op eq 'text') { ($part, $optional) = ($value, 0) }

# Slash
elsif ($op eq 'slash') { $fragment = '/' unless $optional }
elsif ($op eq 'slash') { $part = '/' unless $optional }

# Placeholder
else {
my $default = $self->defaults->{$value};
$fragment = $values->{$value} // $default // '';
if (!defined $default || ($default ne $fragment)) { $optional = 0 }
elsif ($optional) { $fragment = '' }
$part = $values->{$value} // $default // '';
if (!defined $default || ($default ne $part)) { $optional = 0 }
elsif ($optional) { $part = '' }
}

$str = $fragment . $str;
$str = $part . $str;
}

# Format can be optional
Expand All @@ -86,15 +88,17 @@ sub _compile {
my $placeholders = $self->placeholders;
my $constraints = $self->constraints;
my $defaults = $self->defaults;
my $start = $self->type_start;
my $types = $self->types;

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

# Text
if ($op eq 'text') { ($fragment, $optional) = (quotemeta $value, 0) }
if ($op eq 'text') { ($part, $optional) = (quotemeta $value, 0) }

# Slash
elsif ($op eq 'slash') {
Expand All @@ -105,17 +109,23 @@ sub _compile {

# Placeholder
else {
if ($value =~ /^(.+)\Q$start\E(.+)$/) {
croak qq{Unknown placeholder type: $value} unless my $r = $types->{$2};
($value, $part) = ($1, _compile_req($r));
}
else {
$part = $type ? $type eq 'relaxed' ? '([^/]+)' : '(.+)' : '([^/.]+)';
}
unshift @$placeholders, $value;
$fragment = $type ? $type eq 'relaxed' ? '([^/]+)' : '(.+)' : '([^/.]+)';

# Custom regex
if (my $c = $constraints->{$value}) { $fragment = _compile_req($c) }
if (my $c = $constraints->{$value}) { $part = _compile_req($c) }

# Optional placeholder
exists $defaults->{$value} ? ($fragment .= '?') : ($optional = 0);
exists $defaults->{$value} ? ($part .= '?') : ($optional = 0);
}

$block = $fragment . $block;
$block = $part . $block;
}

# Not rooted with a slash
Expand Down Expand Up @@ -157,30 +167,32 @@ sub _tokenize {
my $relaxed = $self->relaxed_start;
my $wildcard = $self->wildcard_start;

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

# Quoted
if ($char eq $quote_start) { push @tree, ['placeholder', ''] if ++$spec }
elsif ($char eq $quote_end) { $spec = 0 }
elsif ($char eq $quote_end) { $spec = $more = 0 }

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

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

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

# Placeholder
elsif ($spec) { $tree[-1][1] .= $char }
elsif ($spec && ++$more) { $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 @@ -287,6 +299,20 @@ 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 type_start
my $start = $pattern->type_start;
$pattern = $pattern->type_start('|');
Character indicating the start of a placeholder type, defaults to C<:>.
=head2 types
my $types = $pattern->types;
$pattern = $pattern->types({int => qr/[0-9]+/});
Placeholder types.
=head2 unparsed
my $unparsed = $pattern->unparsed;
Expand Down
1 change: 1 addition & 0 deletions lib/Mojolicious/Routes/Route.pm
Expand Up @@ -27,6 +27,7 @@ sub add_child {
my ($self, $route) = @_;
Scalar::Util::weaken $route->remove->parent($self)->{parent};
push @{$self->children}, $route;
$route->pattern->types($self->root->types);
return $self;
}

Expand Down

0 comments on commit c39011c

Please sign in to comment.