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 is a lightweight routes implementation. L aims to be easy to use in any web framework. =head1 FEATURES =head2 C $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 $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 $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 $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 $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 # 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 = 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 $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 is important. $routes->add_route('/articles'); # is different from $routes->add_route('/articles/'); If you don't want this behaviour pass C to the constructor: my $routes = Routes::Tiny->new(strict_trailing_slash => 0); =head2 C Routes::Tiny is case sensitive by default (since 0.20). It means that $routes->add_route('/admin/'); will NOT match both C and C. If you don't want this behaviour pass C to the constructor: my $routes = Routes::Tiny->new(strict_case => 0); =head1 METHODS =head2 C my $routes = Routes::Tiny->new; =head2 C $routes->add_route('/:service/:action'); Add a new route. =head2 C $routes->mount('/admin/', $subroutes) Includes one Routes::Tiny instance into another with given prefix. =head2 C $routes->match('/hello/world'); Match against a path. =head2 C $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. =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