package Mojolicious::Routes::Route; use Mojo::Base -base; use Carp qw(croak); use Mojo::DynamicMethods -dispatch; use Mojo::Util; use Mojolicious::Routes::Pattern; has [qw(inline partial)]; has 'children' => sub { [] }; has parent => undef, weak => 1; has pattern => sub { Mojolicious::Routes::Pattern->new }; # Reserved stash values my %RESERVED = map { $_ => 1 } ( qw(action app cb controller data extends format handler inline json layout namespace path status template text), qw(variant) ); sub BUILD_DYNAMIC { my ($class, $method, $dyn_methods) = @_; return sub { my $self = shift; my $dynamic = $dyn_methods->{$self->root}{$method}; return $self->$dynamic(@_) if $dynamic; my $package = ref($self); croak qq{Can't locate object method "$method" via package "$package"}; }; } sub add_child { my ($self, $route) = @_; push @{$self->children}, $route->remove->parent($self); $route->pattern->types($self->root->types); return $self; } sub any { shift->_generate_route(ref $_[0] eq 'ARRAY' ? shift : [], @_) } sub delete { shift->_generate_route(DELETE => @_) } sub find { shift->_index->{shift()} } sub get { shift->_generate_route(GET => @_) } sub has_custom_name { !!shift->{custom} } sub has_websocket { my $self = shift; return $self->{has_websocket} if exists $self->{has_websocket}; return $self->{has_websocket} = grep { $_->is_websocket } @{$self->_chain}; } sub is_endpoint { $_[0]->inline ? undef : !@{$_[0]->children} } sub is_reserved { !!$RESERVED{$_[1]} } sub is_websocket { !!shift->{websocket} } sub methods { my $self = shift; return $self->{methods} unless @_; my $methods = [map uc($_), @{ref $_[0] ? $_[0] : [@_]}]; $self->{methods} = $methods if @$methods; return $self; } sub name { my $self = shift; return $self->{name} unless @_; @$self{qw(name custom)} = (shift, 1); return $self; } sub options { shift->_generate_route(OPTIONS => @_) } sub parse { my $self = shift; $self->{name} = $self->pattern->parse(@_)->unparsed // ''; $self->{name} =~ s/\W+//g; return $self; } sub patch { shift->_generate_route(PATCH => @_) } sub post { shift->_generate_route(POST => @_) } sub put { shift->_generate_route(PUT => @_) } sub remove { my $self = shift; return $self unless my $parent = $self->parent; @{$parent->children} = grep { $_ ne $self } @{$parent->children}; return $self->parent(undef); } sub render { my ($self, $values) = @_; my $path = join '', map { $_->pattern->render($values, !@{$_->children} && !$_->partial) } @{$self->_chain}; return $path || '/'; } sub root { shift->_chain->[0] } sub requires { my $self = shift; # Routes with conditions can't be cached return $self->{requires} unless @_; my $conditions = ref $_[0] eq 'ARRAY' ? $_[0] : [@_]; return $self unless @$conditions; $self->{requires} = $conditions; $self->root->cache->max_keys(0); return $self; } sub suggested_method { my $self = shift; my %via; for my $route (@{$self->_chain}) { next unless my @via = @{$route->methods // []}; %via = map { $_ => 1 } keys %via ? grep { $via{$_} } @via : @via; } return 'POST' if $via{POST} && !$via{GET}; return $via{GET} ? 'GET' : (sort keys %via)[0] || 'GET'; } sub to { my $self = shift; my $pattern = $self->pattern; return $pattern->defaults unless @_; my ($shortcut, %defaults) = Mojo::Util::_options(@_); if ($shortcut) { # Application if (ref $shortcut || $shortcut =~ /^[\w:]+$/) { $defaults{app} = $shortcut } # Controller and action elsif ($shortcut =~ /^([\w\-:]+)?\#(\w+)?$/) { $defaults{controller} = $1 if defined $1; $defaults{action} = $2 if defined $2; } } @{$pattern->defaults}{keys %defaults} = values %defaults; return $self; } sub to_string { join '', map { $_->pattern->unparsed // '' } @{shift->_chain}; } sub under { shift->_generate_route(under => @_) } sub websocket { my $route = shift->get(@_); $route->{websocket} = 1; return $route; } sub _chain { my @chain = (my $parent = shift); unshift @chain, $parent while $parent = $parent->parent; return \@chain; } sub _generate_route { my ($self, $methods, @args) = @_; my (@conditions, @constraints, %defaults, $name, $pattern); while (defined(my $arg = shift @args)) { # First scalar is the pattern if (!ref $arg && !$pattern) { $pattern = $arg } # Scalar elsif (!ref $arg && @args) { push @conditions, $arg, shift @args } # Last scalar is the route name elsif (!ref $arg) { $name = $arg } # Callback elsif (ref $arg eq 'CODE') { $defaults{cb} = $arg } # Constraints elsif (ref $arg eq 'ARRAY') { push @constraints, @$arg } # Defaults elsif (ref $arg eq 'HASH') { %defaults = (%defaults, %$arg) } } my $route = $self->_route($pattern, @constraints)->requires(\@conditions)->to(\%defaults); $methods eq 'under' ? $route->inline(1) : $route->methods($methods); return defined $name ? $route->name($name) : $route; } sub _index { my $self = shift; my (%auto, %custom); my @children = (@{$self->children}); while (my $child = shift @children) { if ($child->has_custom_name) { $custom{$child->name} ||= $child } else { $auto{$child->name} ||= $child } push @children, @{$child->children}; } return {%auto, %custom}; } sub _route { my $self = shift; my $route = $self->add_child(__PACKAGE__->new->parse(@_))->children->[-1]; my $new_pattern = $route->pattern; croak qq{Route pattern "@{[$new_pattern->unparsed]}" contains a reserved stash value} if grep { $self->is_reserved($_) } @{$new_pattern->placeholders}; my $old_pattern = $self->pattern; my $constraints = $old_pattern->constraints; $new_pattern->constraints->{format} //= $constraints->{format} if exists $constraints->{format}; my $defaults = $old_pattern->defaults; $new_pattern->defaults->{format} //= $defaults->{format} if exists $defaults->{format}; return $route; } 1; =encoding utf8 =head1 NAME Mojolicious::Routes::Route - Route =head1 SYNOPSIS use Mojolicious::Routes::Route; my $r = Mojolicious::Routes::Route->new; =head1 DESCRIPTION L is the route container used by L. =head1 ATTRIBUTES L implements the following attributes. =head2 children my $children = $r->children; $r = $r->children([Mojolicious::Routes::Route->new]); The children of this route, used for nesting routes. =head2 inline my $bool = $r->inline; $r = $r->inline($bool); Allow L semantics for this route. =head2 parent my $parent = $r->parent; $r = $r->parent(Mojolicious::Routes::Route->new); The parent of this route, usually a L object. Note that this attribute is weakened. =head2 partial my $bool = $r->partial; $r = $r->partial($bool); Route has no specific end, remaining characters will be captured in C. =head2 pattern my $pattern = $r->pattern; $r = $r->pattern(Mojolicious::Routes::Pattern->new); Pattern for this route, defaults to a L object. =head1 METHODS L inherits all methods from L and implements the following new ones. =head2 add_child $r = $r->add_child(Mojolicious::Routes::Route->new); Add a child to this route, it will be automatically removed from its current parent if necessary. # Reattach route $r->add_child($r->find('foo')); =head2 any my $route = $r->any; my $route = $r->any('/:foo'); my $route = $r->any('/:foo' => sub ($c) {...}); my $route = $r->any('/:foo' => sub ($c) {...} => 'name'); my $route = $r->any('/:foo' => {foo => 'bar'} => sub ($c) {...}); my $route = $r->any('/:foo' => [foo => qr/\w+/] => sub ($c) {...}); my $route = $r->any('/:foo' => (agent => qr/Firefox/) => sub ($c) {...}); my $route = $r->any(['GET', 'POST'] => '/:foo' => sub ($c) {...}); my $route = $r->any(['GET', 'POST'] => '/:foo' => [foo => qr/\w+/]); Generate L object matching any of the listed HTTP request methods or all. # Route with pattern and destination $r->any('/user')->to('user#whatever'); All arguments are optional, but some have to appear in a certain order, like the two supported array reference values, which contain the HTTP methods to match and restrictive placeholders. # Route with HTTP methods, pattern, restrictive placeholders and destination $r->any(['DELETE', 'PUT'] => '/:foo' => [foo => qr/\w+/])->to('foo#bar'); There are also two supported string values, containing the route pattern and the route name, defaulting to the pattern C and a name based on the pattern. # Route with pattern, name and destination $r->any('/:foo' => 'foo_route')->to('foo#bar'); An arbitrary number of key/value pairs in between the route pattern and name can be used to specify route conditions. # Route with pattern, condition and destination $r->any('/' => (agent => qr/Firefox/))->to('foo#bar'); A hash reference is used to specify optional placeholders and default values for the stash. # Route with pattern, optional placeholder and destination $r->any('/:foo' => {foo => 'bar'})->to('foo#bar'); And a code reference can be used to specify a C value to be merged into the default values for the stash. # Route with pattern and a closure as destination $r->any('/:foo' => sub ($c) { $c->render(text => 'Hello World!'); }); See L and L for more information. =head2 delete my $route = $r->delete; my $route = $r->delete('/:foo'); my $route = $r->delete('/:foo' => sub ($c) {...}); my $route = $r->delete('/:foo' => sub ($c) {...} => 'name'); my $route = $r->delete('/:foo' => {foo => 'bar'} => sub ($c) {...}); my $route = $r->delete('/:foo' => [foo => qr/\w+/] => sub ($c) {...}); my $route = $r->delete('/:foo' => (agent => qr/Firefox/) => sub ($c) {...}); Generate L object matching only C requests, takes the same arguments as L (except for the HTTP methods to match, which are implied). See L and L for more information. # Route with destination $r->delete('/user')->to('user#remove'); =head2 find my $route = $r->find('foo'); Find child route by name, custom names have precedence over automatically generated ones. # Change default parameters of a named route $r->find('show_user')->to(foo => 'bar'); =head2 get my $route = $r->get; my $route = $r->get('/:foo'); my $route = $r->get('/:foo' => sub ($c) {...}); my $route = $r->get('/:foo' => sub ($c) {...} => 'name'); my $route = $r->get('/:foo' => {foo => 'bar'} => sub ($c) {...}); my $route = $r->get('/:foo' => [foo => qr/\w+/] => sub ($c) {...}); my $route = $r->get('/:foo' => (agent => qr/Firefox/) => sub ($c) {...}); Generate L object matching only C requests, takes the same arguments as L (except for the HTTP methods to match, which are implied). See L and L for more information. # Route with destination $r->get('/user')->to('user#show'); =head2 has_custom_name my $bool = $r->has_custom_name; Check if this route has a custom name. =head2 has_websocket my $bool = $r->has_websocket; Check if this route has a WebSocket ancestor and cache the result for future checks. =head2 is_endpoint my $bool = $r->is_endpoint; Check if this route qualifies as an endpoint. =head2 is_reserved my $bool = $r->is_reserved('controller'); Check if string is a reserved stash value. =head2 is_websocket my $bool = $r->is_websocket; Check if this route is a WebSocket. =head2 methods my $methods = $r->methods; $r = $r->methods('GET'); $r = $r->methods('GET', 'POST'); $r = $r->methods(['GET', 'POST']); Restrict HTTP methods this route is allowed to handle, defaults to no restrictions. # Route with two methods and destination $r->any('/foo')->methods('GET', 'POST')->to('foo#bar'); =head2 name my $name = $r->name; $r = $r->name('foo'); The name of this route, defaults to an automatically generated name based on the route pattern. Note that the name C is reserved for referring to the current route. # Route with destination and custom name $r->get('/user')->to('user#show')->name('show_user'); =head2 options my $route = $r->options; my $route = $r->options('/:foo'); my $route = $r->options('/:foo' => sub ($c) {...}); my $route = $r->options('/:foo' => sub ($c) {...} => 'name'); my $route = $r->options('/:foo' => {foo => 'bar'} => sub ($c) {...}); my $route = $r->options('/:foo' => [foo => qr/\w+/] => sub ($c) {...}); my $route = $r->options('/:foo' => (agent => qr/Firefox/) => sub ($c) {...}); Generate L object matching only C requests, takes the same arguments as L (except for the HTTP methods to match, which are implied). See L and L for more information. # Route with destination $r->options('/user')->to('user#overview'); =head2 parse $r = $r->parse('/user/:id'); $r = $r->parse('/user/:id', id => qr/\d+/); $r = $r->parse(format => ['json', 'yaml']); Parse pattern. =head2 patch my $route = $r->patch; my $route = $r->patch('/:foo'); my $route = $r->patch('/:foo' => sub ($c) {...}); my $route = $r->patch('/:foo' => sub ($c) {...} => 'name'); my $route = $r->patch('/:foo' => {foo => 'bar'} => sub ($c) {...}); my $route = $r->patch('/:foo' => [foo => qr/\w+/] => sub ($c) {...}); my $route = $r->patch('/:foo' => (agent => qr/Firefox/) => sub ($c) {...}); Generate L object matching only C requests, takes the same arguments as L (except for the HTTP methods to match, which are implied). See L and L for more information. # Route with destination $r->patch('/user')->to('user#update'); =head2 post my $route = $r->post; my $route = $r->post('/:foo'); my $route = $r->post('/:foo' => sub ($c) {...}); my $route = $r->post('/:foo' => sub ($c) {...} => 'name'); my $route = $r->post('/:foo' => {foo => 'bar'} => sub ($c) {...}); my $route = $r->post('/:foo' => [foo => qr/\w+/] => sub ($c) {...}); my $route = $r->post('/:foo' => (agent => qr/Firefox/) => sub ($c) {...}); Generate L object matching only C requests, takes the same arguments as L (except for the HTTP methods to match, which are implied). See L and L for more information. # Route with destination $r->post('/user')->to('user#create'); =head2 put my $route = $r->put; my $route = $r->put('/:foo'); my $route = $r->put('/:foo' => sub ($c) {...}); my $route = $r->put('/:foo' => sub ($c) {...} => 'name'); my $route = $r->put('/:foo' => {foo => 'bar'} => sub ($c) {...}); my $route = $r->put('/:foo' => [foo => qr/\w+/] => sub ($c) {...}); my $route = $r->put('/:foo' => (agent => qr/Firefox/) => sub ($c) {...}); Generate L object matching only C requests, takes the same arguments as L (except for the HTTP methods to match, which are implied). See L and L for more information. # Route with destination $r->put('/user')->to('user#replace'); =head2 remove $r = $r->remove; Remove route from parent. # Remove route completely $r->find('foo')->remove; # Reattach route to new parent $r->any('/foo')->add_child($r->find('bar')->remove); =head2 render my $path = $r->render({foo => 'bar'}); Render route with parameters into a path. =head2 root my $root = $r->root; The L object this route is a descendant of. =head2 requires my $requires = $r->requires; $r = $r->requires(foo => 1); $r = $r->requires(foo => 1, bar => {baz => 'yada'}); $r = $r->requires([foo => 1, bar => {baz => 'yada'}]); Activate conditions for this route. Note that this automatically disables the routing cache, since conditions are too complex for caching. # Route with condition and destination $r->get('/foo')->requires(host => qr/mojolicious\.org/)->to('foo#bar'); =head2 suggested_method my $method = $r->suggested_method; Suggested HTTP method for reaching this route, C and C are preferred. =head2 to my $defaults = $r->to; $r = $r->to(action => 'foo'); $r = $r->to({action => 'foo'}); $r = $r->to('controller#action'); $r = $r->to('controller#action', foo => 'bar'); $r = $r->to('controller#action', {foo => 'bar'}); $r = $r->to(Mojolicious->new); $r = $r->to(Mojolicious->new, foo => 'bar'); $r = $r->to(Mojolicious->new, {foo => 'bar'}); $r = $r->to('MyApp'); $r = $r->to('MyApp', foo => 'bar'); $r = $r->to('MyApp', {foo => 'bar'}); Set default parameters for this route. =head2 to_string my $str = $r->to_string; Stringify the whole route. =head2 under my $route = $r->under(sub ($c) {...}); my $route = $r->under('/:foo' => sub ($c) {...}); my $route = $r->under('/:foo' => {foo => 'bar'}); my $route = $r->under('/:foo' => [foo => qr/\w+/]); my $route = $r->under('/:foo' => (agent => qr/Firefox/)); my $route = $r->under([format => ['json', 'yaml']]); Generate L object for a nested route with its own intermediate destination, takes the same arguments as L (except for the HTTP methods to match, which are not available). See L and L for more information. # Longer version $r->any('/:foo' => sub ($c) {...})->inline(1); # Intermediate destination and prefix shared between two routes my $auth = $r->under('/user')->to('user#auth'); $auth->get('/show')->to('#show'); $auth->post('/create')->to('#create'); =head2 websocket my $route = $r->websocket; my $route = $r->websocket('/:foo'); my $route = $r->websocket('/:foo' => sub ($c) {...}); my $route = $r->websocket('/:foo' => sub ($c) {...} => 'name'); my $route = $r->websocket('/:foo' => {foo => 'bar'} => sub ($c) {...}); my $route = $r->websocket('/:foo' => [foo => qr/\w+/] => sub ($c) {...}); my $route = $r->websocket('/:foo' => (agent => qr/Firefox/) => sub ($c) {...}); Generate L object matching only WebSocket handshakes, takes the same arguments as L (except for the HTTP methods to match, which are implied). See L and L for more information. # Route with destination $r->websocket('/echo')->to('example#echo'); =head1 SHORTCUTS In addition to the L and L above you can also call shortcuts provided by L on L objects. # Add a "firefox" shortcut $r->root->add_shortcut(firefox => sub ($r, $path) { $r->get($path, agent => qr/Firefox/); }); # Use "firefox" shortcut to generate routes $r->firefox('/welcome')->to('firefox#welcome'); $r->firefox('/bye')->to('firefox#bye'); =head1 SEE ALSO L, L, L. =cut