package Routes::Tiny;
use strict;
use warnings;
require Carp;
require Scalar::Util;
use Routes::Tiny::Pattern;
our $VERSION = 0.21;
sub new {
my $class = shift;
my (%params) = @_;
my $self = {};
bless $self, $class;
$self->{strict_trailing_slash} = $params{strict_trailing_slash};
$self->{strict_case} = $params{strict_case};
$self->{default_method} = $params{default_method};
$self->{parent_pattern} = undef;
$self->{patterns} = [];
$self->{names} = {};
$self->{strict_trailing_slash} = 1
unless defined $self->{strict_trailing_slash};
$self->{strict_case} = 1
unless defined $self->{strict_case};
return $self;
}
sub add_route {
my $self = shift;
my $pattern = $self->_build_pattern(@_);
push @{$self->{patterns}}, $pattern;
$self->_register_pattern_name($pattern) if $pattern->{name};
return $pattern;
}
sub mount {
my $self = shift;
my ($pattern, $routes, @args) = @_;
$pattern = $self->add_route($pattern, subroutes => $routes, @args);
$routes->{parent_pattern} = $pattern;
$self->_register_pattern_name($_) for values %{ $routes->{names} };
Scalar::Util::weaken($routes->{parent_pattern});
return $pattern;
}
sub match {
my $self = shift;
my ($path, @args) = @_;
foreach my $pattern (@{$self->{patterns}}) {
if (my $m = $pattern->match($path, @args)) {
return $m;
}
}
return;
}
sub build_path {
my $self = shift;
my ($name, @args) = @_;
my $pattern = $self->{names}->{$name};
return $pattern->build_path(@args) if $pattern;
Carp::croak("Unknown name '$name' used to build a path");
}
sub _register_pattern_name {
my $self = shift;
my ($pattern) = @_;
my $name = $pattern->name;
if (exists $self->{names}->{ $name }) {
Carp::carp("pattern name '$name' already used");
}
else {
$self->{names}->{ $name } = $pattern;
my $parent_routes = $self->{parent_pattern} && $self->{parent_pattern}->{routes};
if ($parent_routes) {
$parent_routes->_register_pattern_name(@_);
}
}
}
sub _build_pattern {
my $self = shift;
if (@_ % 2) {
unshift(@_, 'pattern');
} else {
my $method = shift;
my $pattern = shift;
if ($method =~ /^(GET|HEAD|POST|PUT|DELETE|TRACE|OPTIONS|CONNECT|PATCH)$/i) {
unshift(@_, pattern => $pattern);
unshift(@_, method => $method);
} else {
Carp::croak("Unknown pattern http method '$_[0]'");
}
}
return Routes::Tiny::Pattern->new(
strict_trailing_slash => $self->{strict_trailing_slash},
strict_case => $self->{strict_case},
default_method => $self->{default_method},
routes => $self,
@_
)
}
1;
__END__
=head1 NAME
Routes::Tiny - Routes
=head1 SYNOPSIS
my $routes = Routes::Tiny->new;
# Constraints
$routes->add_route('/articles/:id', constraints => {id => qr/\d+/});
# Optional placeholders
$routes->add_route('/archive/:year/(:month)?');
# Defaults
$routes->add_route('/articles/:id',
defaults => {controller => 'bar', action => 'foo'});
# Grouping (matches 'hello-bar')
$routes->add_route('/(:foo)-bar');
# Globbing (matches 'photos/foo/bar/baz')
$routes->add_route('/photos/*other');
# Path building
$routes->add_route('/:foo/:bar', name => 'default');
$routes->build_path('default', foo => 'hello', bar => 'world');
# Matching
my $match = $routes->match('/hello/world');
my $captures_hashref = $match->captures;
# Matching with method
$routes->add_route('/hello/world', method => 'GET');
my $match = $routes->match('/hello/world', method => 'GET');
# Subroutes
my $subroutes = Routes::Tiny->new;
$subroutes->add_route('/article/:id');
$routes->mount('/admin/', $subroutes);
=head1 DESCRIPTION
L<Routes::Tiny> is a lightweight routes implementation.
L<Routes::Tiny> aims to be easy to use in any web framework.
=head1 FEATURES
=head2 C<Constraints>
$routes->add_route('/articles/:id', constraints => {id => qr/\d+/});
$match = $routes->match('/articles/1'); # Routes::Tiny::Match object
$match = $routes->match('/article/foo'); # undef
It is possible to specify a constraint that a placeholder must match using a
normal Perl regular expression.
Constraints can be passed as array references:
$routes->add_route('/articles/:action',
constraints => {action => [qw/add update/]});
$match = $routes->match('/articles/add'); # Routes::Tiny::Match object
$match = $routes->match('/articles/delete'); # undef
=head2 C<Optional placeholders>
$routes->add_route('/admin/:service(/:action)?', defaults => {action => 'list'});
my $match = $routes->match('/admin/foo');
# $m->captures is {service => 'foo', action => 'list'}
It is possible to specify an optional placeholder with a default value.
=head2 C<Grouping>
$routes->add_route('/(:foo)-bar');
$match = $routes->match('/hello-bar');
# $match->captures is {foo => 'hello'}
It is possible to create a placeholder that doesn't occupy all the space between
slashes.
=head2 C<Globbing>
$routes->add_route('/photos/*other');
$routes->add_route('/books/*section/:title');
$routes->add_route('/*a/foo/*b');
$match = $routes->match('photos/foo/bar/baz');
# $match->captures is {other => 'foo/bar/baz'}
$match = $routes->match('books/some/section/last-words-a-memoir');
# $match->captures is {section => 'some/section', title => 'last-words-a-memoir'}
$match = $routes->match('zoo/woo/foo/bar/baz');
# $match->captures is {a => 'zoo/woo', b => 'bar/baz'}
It is possible to specify a globbing placeholder.
=head2 C<Passing arguments AS IS>
$routes->add_route('/', arguments => {one => 'two'});
$match = $routes->match('/');
# $match->arguments is {one => 'two'}
It is possible to pass arguments to the match object AS IS.
=head2 C<Matching with methods>
# Exact HTTP method definition
$routes->add_route('/articles', method => 'GET', defaults => {action => 'list'});
# Sweeter method definition
# METHOD => PATTERN should go as first parameters to add_route()
$routes->add_route(PUT => '/articles', defaults => {action => 'create'});
$match = $routes->match('/articles', method => 'GET');
# $m->captures is {action => 'list'}
$match = $routes->match('/articles', method => 'PUT');
# $m->captures is {action => 'create'}
=head2 C<Subroutes>
$subroutes = Routes::Tiny->new;
$subroutes->add_route('/articles/:id', name => 'admin-article');
$routes->mount('/admin/', $subroutes);
$match = $routes->match('/admin/articles/3/');
# $match->captures is {id => 3}
It is possible to capture params in mount routes
$subroutes = Routes::Tiny->new;
$subroutes->add_route('/comments/:page/', name => 'comments');
$routes->mount('/:type/:id/', $subroutes);
$match = $routes->match('/articles/3/comments/5/');
# $match->captures is {page => 5}
# $match->parent->captures is {type => 'articles', id => 3}
Parent routes mounts names of children routes, so it's possible to buil path
$path = $routes->build_path('admin-article', id => 123);
# $path is '/admin/articles/123'
$path = $routes->build_path('comments', type => 'articles', id => 123, page => 5);
# $path is '/articles/123/comments/5/'
=head2 C<Path building>
$routes->add_route('/articles/:id', name => 'article');
$path = $routes->build_path('article', id => 123);
# $path is '/articles/123'
It is possible to reconstruct a path from route's name and parameters.
=head1 WARNINGS
=head2 C<Trailing slash issue>
Trailing slash is important.
$routes->add_route('/articles');
# is different from
$routes->add_route('/articles/');
If you don't want this behaviour pass C<strict_trailing_slash> to the constructor:
my $routes = Routes::Tiny->new(strict_trailing_slash => 0);
=head2 C<Case sensitivity>
Routes::Tiny is case sensitive by default (since 0.20).
It means that
$routes->add_route('/admin/');
will NOT match both C</admin/> and C</ADMIN/>.
If you don't want this behaviour pass C<strict_case> to the constructor:
my $routes = Routes::Tiny->new(strict_case => 0);
=head1 METHODS
=head2 C<new>
my $routes = Routes::Tiny->new;
=head2 C<add_route>
$routes->add_route('/:service/:action');
Add a new route.
=head2 C<mount>
$routes->mount('/admin/', $subroutes)
Includes one Routes::Tiny instance into another with given prefix.
=head2 C<match>
$routes->match('/hello/world');
Match against a path.
=head2 C<build_path>
$pattern->build_path('name', {foo => 'bar'});
Build path from a given name and params.
=head1 DEVELOPMENT
=head2 Repository
http://github.com/vti/routes-tiny
=head1 CREDITS
Sergey Zasenko (und3f)
Roman Galeev (jamhed)
Dmitry Smal (mialinx)
Dinar (ziontab)
Jonathan R. Warden
Alexander Batyrshin
Konstantin Cherednichenko
=head1 AUTHOR
Viacheslav Tykhanovskyi, C<vti@cpan.org>.
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2011-2017, Viacheslav Tykhanovskyi
This program is free software, you can redistribute it and/or modify it under
the terms of the Artistic License version 2.0.
=cut