The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Kelp::Routes - Routing for a Kelp app

SYNOPSIS

    use Kelp::Routes;
    my $r = Kelp::Routes->new( base => 'MyApp' );
    $r->add( '/home', 'home' );

DESCRIPTION

The router provides the connection between the HTTP requests and the web application code. It tells the application "If you see a request coming to *this* URI, send it to *that* subroutine for processing". For example, if a request comes to /home, then send it to sub home in the current namespace. The process of capturing URIs and sending them to their corresponding code is called routing.

This router was specifically crafted as part of the Kelp web framework. It is, however, possible to use it on its own, if needed.

It provides a simple, yet sophisticated routing utilizing Perl 5.10's regular expressions, which makes it fast, robust and reliable.

The routing process can roughly be broken down into three steps:

Adding routes

First you create a router object:

    my $r = Kelp::Routes->new();

Then you add your application's routes and their descriptions:

    $r->add( '/path' => 'Module::function' );
    ...
Matching

Once you have your routes added, you can match with the "match" subroutine.

    my $patterns_aref = $r->match( $path, $method );

The Kelp framework already does matching for you, so you may never have to do your own matching. The above example is provided only for reference.

The order of patterns in $patterns_aref is the order in which the framework will be executing the routes. Bridges are always before regular routes, and shorter routes come first within a given type (bridge or no-bridge). If route patterns are exactly the same, the ones defined earlier will also be executed earlier.

Routes will continue going through that execution chain until one of the bridges return a false value, one of the non-bridges return a defined value, or one of the routes renders something explicitly using methods in Kelp::Response. It is generally not recommended to have more than one non-bridge route matching a pattern as it may be harder to debug which one gets to actually render a response.

Building URLs from routes

You can name each of your routes, and use that name later to build a URL:

    $r->add( '/begin' => { to => 'function', name => 'home' } );
    my $url = $r->url('home');    # /begin

This can be used in views and other places where you need the full URL of a route.

PLACEHOLDERS

Often routes may get more complicated. They may contain variable parts. For example this one /user/1000 is expected to do something with user ID 1000. So, in this case we need to capture a route that begins with /user/ and then has something else after it.

Naturally, when it comes to capturing routes, the first instinct of the Perl programmer is to use regular expressions, like this:

    qr{/user/(\d+)} -> "sub home"

This module will let you do that, however regular expressions can get very complicated, and it won't be long before you lose track of what does what.

This is why a good router (this one included) allows for named placeholders. These are words prefixed with special symbols, which denote a variable piece in the URI. To use the above example:

    "/user/:id" -> "sub home"

It looks a little cleaner.

Placeholders are variables you place in the route path. They are identified by a prefix character and their names must abide to the rules of a regular Perl variable. If necessary, curly braces can be used to separate placeholders from the rest of the path.

There are three types of place holders:

Explicit

These placeholders begin with a column (:) and must have a value in order for the route to match. All characters are matched, except for the forward slash.

    $r->add( '/user/:id' => 'Module::sub' );
    # /user/a       -> match (id = 'a')
    # /user/123     -> match (id = 123)
    # /user/        -> no match
    # /user         -> no match
    # /user/10/foo  -> no match

    $r->add( '/page/:page/line/:line' => 'Module::sub' );
    # /page/1/line/2        -> match (page = 1, line = 2)
    # /page/bar/line/foo    -> match (page = 'bar', line = 'foo')
    # /page/line/4          -> no match
    # /page/5               -> no match

    $r->add( '/{:a}ing/{:b}ing' => 'Module::sub' );
    # /walking/singing      -> match (a = 'walk', b = 'sing')
    # /cooking/ing          -> no match
    # /ing/ing              -> no match

Optional

Optional placeholders begin with a question mark ? and denote an optional value. You may also specify a default value for the optional placeholder via the "defaults" option. Again, like the explicit placeholders, the optional ones capture all characters, except the forward slash.

    $r->add( '/data/?id' => 'Module::sub' );
    # /bar/foo          -> match ( id = 'foo' )
    # /bar/             -> match ( id = undef )
    # /bar              -> match ( id = undef )

    $r->add( '/:a/?b/:c' => 'Module::sub' );
    # /bar/foo/baz      -> match ( a = 'bar', b = 'foo', c = 'baz' )
    # /bar/foo          -> match ( a = 'bar', b = undef, c = 'foo' )
    # /bar              -> no match
    # /bar/foo/baz/moo  -> no match

Optional default values may be specified via the defaults option.

    $r->add(
        '/user/?name' => {
            to       => 'Module::sub',
            defaults => { name => 'hank' }
        }
    );

    # /user             -> match ( name = 'hank' )
    # /user/            -> match ( name = 'hank' )
    # /user/jane        -> match ( name = 'jane' )
    # /user/jane/cho    -> no match

Wildcards

The wildcard placeholders expect a value and capture all characters, including the forward slash.

    $r->add( '/:a/*b/:c'  => 'Module::sub' );
    # /bar/foo/baz/bat  -> match ( a = 'bar', b = 'foo/baz', c = 'bat' )
    # /bar/bat          -> no match

Slurpy

Slurpy placeholders will take as much as they can or nothing. It's a mix of a wildcard and optional placeholder.

    $r->add( '/path/>rest'  => 'Module::sub' );
    # /path            -> match ( rest = undef )
    # /path/foo        -> match ( rest = '/foo' )
    # /path/foo/bar    -> match ( rest = '/foo/bar' )

Just like optional parameters, they may have defaults.

Using curly braces

Curly braces may be used to separate the placeholders from the rest of the path:

    $r->add( '/{:a}ing/{:b}ing' => 'Module::sub' );
    # /looking/seeing       -> match ( a = 'look', b = 'see' )
    # /ing/ing              -> no match

    $r->add( '/:a/{?b}ing' => 'Module::sub' );
    # /bar/hopping          -> match ( a = 'bar', b = 'hopp' )
    # /bar/ing              -> match ( a = 'bar' )
    # /bar                  -> no match

    $r->add( '/:a/{*b}ing/:c' => 'Module::sub' );
    # /bar/hop/ping/foo     -> match ( a = 'bar', b = 'hop/p', c = 'foo' )
    # /bar/ing/foo          -> no match

BRIDGES

The "match" subroutine will stop and return the route that best matches the specified path. If that route is marked as a bridge, then "match" will continue looking for another match, and will eventually return an array of one or more routes. Bridges can be used for authentication or other route preprocessing.

    $r->add( '/users/*', { to => 'Users::auth', bridge => 1 } );
    $r->add( '/users/:action' => 'Users::dispatch' );

The above example will require /users/profile to go through two subroutines: Users::auth and Users::dispatch:

    my $arr = $r->match('/users/view');
    # $arr is an array of two routes now, the bridge and the last one matched

Just like regular routes, bridges can render a response, but it must be done manually by calling $self->res->render() or other methods from Kelp::Response. When a render happens in a bridge, its return value will be discarded and no other routes in chain will be run as if a false value was returned. For example, this property can be used to render a login page in place instead of a 403 response, or just simply redirect to one.

TREES

A quick way to add bridges is to use the "tree" option. It allows you to define all routes under a bridge. Example:

    $r->add(
        '/users/*' => {
            to   => 'users#auth',
            name => 'users',
            tree => [
                '/profile' => {
                    name => 'profile',
                    to   => 'users#profile'
                },
                '/settings' => {
                    name => 'settings',
                    to   => 'users#settings',
                    tree => [
                        '/email' => { name => 'email', to => 'users#email' },
                        '/login' => { name => 'login', to => 'users#login' }
                    ]
                }
            ]
        }
    );

The above call to add causes the following to occur under the hood:

  • The paths of all routes inside the tree are joined to the path of their parent, so the following five new routes are created:

        /users                  -> MyApp::Users::auth
        /users/profile          -> MyApp::Users::profile
        /users/settings         -> MyApp::Users::settings
        /users/settings/email   -> MyApp::Users::email
        /users/settings/login   -> MyApp::Users::login
  • The names of the routes are joined via _ with the name of their parent:

        /users                  -> 'users'
        /users/profile          -> 'users_profile'
        /users/settings         -> 'users_settings'
        /users/settings/email   -> 'users_settings_email'
        /users/settings/login   -> 'users_settings_login'
  • The /users and /users/settings routes are automatically marked as bridges, because they contain a tree.

LOCATIONS

Instead of using trees, you can alternatively use locations returned by the "add" method, which will work exactly the same. The object returned from add will be a facade implementing a localized version of add:

    # /users
    my $users = $r->add( '/users' => {
        to   => 'users#auth',
        name => 'users',
    } );

    # /users/profile, /users becomes a bridge
    my $profile = $users->add( '/profile' => {
        name => 'profile',
        to   => 'users#profile'
    } );

    # /users/settings, has its own tree so it's a bridge
    my $settings = $users->add( '/settings' => {
        name => 'settings',
        to   => 'users#settings',
        tree => [
            '/email' => { name => 'email', to => 'users#email' },
            '/login' => { name => 'login', to => 'users#login' }
        ],
    } );

PLACK APPS

Kelp makes it easy to nest Plack/PSGI applications inside your Kelp app. All you have to do is provide a Plack application runner in to and set psgi to a true value.

    use Plack::App::File;

    $r->add( '/static/>path' => {
        to => Plack::App::File->new(root => "/path/to/static")->to_app,
        psgi => 1,
    });

You must provide a proper placeholder at the end if you want your app to occpy all the subpaths under the base path. A slurpy placeholder like >path works best and mimics Plack::App::URLMap's behavior. It is an error to only provide a placeholder in the middle of the pattern. Kelp will take the last placeholder and assume it comes after the base route. If it doesn't, the paths set for the nested app will be wrong.

Note that a route cannot have psgi and bridge (or tree) simultaneously.

ATTRIBUTES

base

Sets the base class for the routes destinations.

    my $r = Kelp::Routes->new( base => 'MyApp' );

This will prepend MyApp:: to all route destinations.

    $r->add( '/home' => 'home' );          # /home -> MyApp::home
    $r->add( '/user' => 'user#home' );     # /user -> MyApp::User::home
    $r->add( '/view' => 'User::view' );    # /view -> MyApp::User::view

A Kelp application will automatically set this value to the name of the main class. If you need to use a route located in another package, you must prefix it with a plus sign:

    # Problem:

    $r->add( '/outside' => 'Outside::Module::route' );
    # /outside -> MyApp::Outside::Module::route
    # (most likely not what you want)

    # Solution:

    $r->add( '/outside' => '+Outside::Module::route' );
    # /outside -> Outside::Module::route

rebless

Switch used to set whether the router should rebless the app into the controller classes (subclasses of "base"). Boolean value, false by default.

pattern_obj

A full class name of an object used for each pattern, Kelp::Routes::Pattern by default. Works the same as its counterpart "request_obj" in Kelp.

fatal

A boolean. If set to true, errors in route definitions will crash the application instead of just raising a warning. False by default.

cache

Routes will be cached in memory, so repeating requests will be dispatched much faster. The default cache entries never expire, so it will continue to grow as long as the process lives. It also stores full Kelp::Routes::Pattern objects, which is fast and light when stored in Perl but makes it cumbersome when they are serialized.

The cache attribute can optionally be initialized with an instance of a caching module with interface similar to CHI and Cache. This allows for giving them expiration time and possibly sharing them between processes, but extra care must be taken to properly serialize them. Patterns are sure to contain hardly serializable code references and are way heavier when serialized. The cache should probably be configured to have an in-memory L1 cache which will map a serialized route identifier (stored in the main cache) to a pattern object registered in the router. The module interface should at the very least provide the following methods:

get($key)

retrieve a key from the cache

set($key, $value, $expiration)

set a key in the cache

clear()

clear all cache

The caching module should be initialized in the config file:

    # config.pl
    {
        modules_init => {
            Routes => {
                cache => Cache::Memory->new(
                    namespace       => 'MyApp',
                    default_expires => '3600 sec'
                );
            }
        }
    }

SUBROUTINES

add

Adds a new route definition to the routes array.

    $r->add( $path, $destination );

$path can be a path string, e.g. '/user/view' or an ARRAY containing a method and a path, e.g. [ PUT => '/item' ].

Returns an object on which you can call add again. If you do, the original route will become a bridge. It will work as if you included the extra routes in the route's tree.

The route destination is very flexible. It can be one of these three things:

  • A string name of a subroutine, for example "Users::item". Using a # sign to replace :: is also allowed, in which case the name will get converted. "users#item" becomes "Users::item".

        $r->add( '/home' => 'user#home' );
  • A code reference.

        $r->add( '/system' => sub { return \%ENV } );
  • A hashref with options.

        # GET /item/100 -> MyApp::Items::view
        $r->add(
            '/item/:id', {
                to     => 'items#view',
                method => 'GET'
            }
        );

    See "Destination Options" for details.

Destination Options

There are a number of options you can add to modify the behavior of the route, if you specify a hashref for a destination:

to

Sets the destination for the route. It should be a subroutine name or CODE reference.

    $r->add( '/home' => { to => 'users#home' } ); # /home -> MyApp::Users::home
    $r->add( '/sys' => { to => sub { ... } });    # /sys -> execute code
    $r->add( '/item' => { to => 'Items::handle' } ) ;   # /item -> MyApp::Items::handle
    $r->add( '/item' => { to => 'items#handle' } );    # Same as above

method

Specifies an HTTP method to be considered by "match" when matching a route.

    # POST /item -> MyApp::Items::add
    $r->add(
        '/item' => {
            method => 'POST',
            to     => 'items#add'
        }
    );

A shortcut for the above is this:

    $r->add( [ POST => '/item' ] => 'items#add' );

name

Give the route a name, and you can always use it to build a URL later via the "url" subroutine.

    $r->add(
        '/item/:id/:name' => {
            to   => 'items#view',
            name => 'item'
        }
    );

    # Later
    $r->url( 'item', id => 8, name => 'foo' );    # /item/8/foo

check

A hashref of checks to perform on the captures. It should contain capture names and stringified regular expressions. Do not use ^ and $ to denote beginning and ending of the matched expression, because it will get embedded in a bigger Regexp.

    $r->add(
        '/item/:id/:name' => {
            to    => 'items#view',
            check => {
                id   => '\d+',          # id must be a digit
                name => 'open|close'    # name can be 'open' or 'close'
            }
          }
    );

defaults

Set default values for optional placeholders.

    $r->add(
        '/pages/?id' => {
            to       => 'pages#view',
            defaults => { id => 2 }
        }
    );

    # /pages    -> match ( id = 2 )
    # /pages/   -> match ( id = 2 )
    # /pages/4  -> match ( id = 4 )

bridge

If set to 1 this route will be treated as a bridge. Please see "BRIDGES" for more information.

tree

Creates a tree of sub-routes. See "TREES" for more information and examples.

url

    my $url = $r->url($path, @arguments);

Builds an url from path and arguments. If the request is named a name can be specified instead.

match

Returns an array of Kelp::Routes::Pattern objects that match the path and HTTP method provided. Each object will contain a hash with the named placeholders in "named" in Kelp::Routes::Pattern, and an array with their values in the order they were specified in the pattern in "param" in Kelp::Routes::Pattern.

    $r->add( '/:id/:name', "route" );
    for my $pattern ( @{ $r->match('/15/alex') } ) {
        $pattern->named;    # { id => 15, name => 'alex' }
        $pattern->param;    # [ 15, 'alex' ]
    }

Routes that used regular expressions instead of patterns will only initialize the param array with the regex captures, unless those patterns are using named captures in which case the named hash will also be initialized.

dispatch

    my $result = $r->dispatch($kelp, $route_pattern);

Dispatches an instance of Kelp::Routes::Pattern by running the route destination specified in "dest" in Kelp::Routes::Pattern. If dest is not set, it will be computed using "load_destination" with unformatted "to" in Kelp::Routes::Pattern.

The $kelp instance may be shallow-cloned and reblessed into another class if it is a subclass of "base" and "rebless" is configured. Modifications made to top-level attributes of $kelp object will be gone after the action is complete.

build_pattern

Override this method to do change the creation of the pattern. Same role as "build_request" in Kelp.

format_to

Override this method to change the formatting process of "to" in Kelp::Routes::Pattern. See code for details.

load_destination

Override this method to change the loading process of "dest" in Kelp::Routes::Pattern. See code for details.

wrap_psgi

Override this method to change the way a Plack/PSGI application is extracted from a destination. See code for details.

EXTENDING

This is the default router class for each new Kelp application, but it doesn't have to be. You can create your own subclass that better suits your needs. It's generally enough to override the "dispatch", "format_to" or "load_destination" methods.

ACKNOWLEDGEMENTS

This module was inspired by Routes::Tiny.