Dynamic routing in Dancer is dynamic
Dancer provides with a number of ways to do dynamic routing, A.K.A., chained actions. Let's take a look at situations that lead to such a requirement and how to achieve it.
The use-case
Let's say you were building a message board that supported any number of boards - each with a custom URL. For example, if this were something for a Beethoven fan site you might have a board for discussing each genre of work composed by him, with URLs like:
- http://www.beethovenfans.com/string-quartets/
- http://www.beethovenfans.com/symphonies/
- http://www.beethovenfans.com/piano-sonatas/
Visiting the top-level board url would show you a list of all posts under that board. You could start a discussion by simply using /new-post under the board name. So to start a discussion under string-quartets, you'd go to:
http://www.beethovenfans.com/string-quartets/new-post
Similarly, you could view or permalink to a specific post as:
http://www.beethovenfans.com/string-quartets/opus-18-no-6
Where opus-18-no-6 is the unique identifier of the post - possibly generated from the title of the post.
Similarly, to upvote a post on the board:
http://www.beethovenfans.com/string-quartets/opus-18-no-6/upvote
Each time someone accesses one of these URLs, you first need to check if the discussion board exists. To put it in code:
get '/:board' => sub { my $board = param('board'); # validation code for $board ... }; get '/:board/new-post' => sub { my $board = param('board'); # validation code for $board }; get '/:board/opus-18-no-6/upvote' => sub { my $board = param('board'); # validation code for $board };
Each of these handlers have to start by validating the existence of a discussion board before they do anything else. It makes sense to have the discussion board validation logic in one place.
Setup method
One way to do this is by defining a special setup method which you call
when the application starts up. This method can use the prefix
keyword to set up routes for each board.
sub get_boards { # in a real life application this would come from a # database. for the sake of simplicity, we simply return # a hard-coded list return qw(string-quartets symphonies piano-sonatas piano-trios); } sub setup_routes { my @boards = get_boards; foreach my $board (@boards) { prefix "/$board" => sub { # any variables here will be accessible to the handlers # defined below my $welcome_message = "Welcome to the group for discussing: $board"; get '/' => sub { # show a list of all the posts under the board $board # for demo's sake simply return the welcome message return $welcome_message; }; get '/:post_id' => sub { # do the work here or delegate to a specialised class my $post_id = param('post_id'); # check if the ID is valid and do something with it # for demo's sake show variables you have access to: return "I was called with board: $board, post id: $post_id" . " and have access to the welcome_message variable: $welcome_message"; }; }; } }
Then in your app.pl:
#!/usr/bin/env perl use FindBin; use lib "$FindBin::Bin/../lib"; use beethoven; beethoven->setup_routes; beethoven->to_app;
Calling setup_routes essentially materialises the routes beforehand based on
what get_boards
returns. If the user enters a board that doesn't exist, it
will automatically get picked up by Dancer's default handling of missing
routes and a 404 error will be returned to the user.
This approach works well if the number of boards is small. Say in hundreds, or may be in thousands, but not in hundreds of thousands. After a certain point the Dancer process will grow too big in memory. That might or might not be a problem depending on your infrastructure.
Also if boards are not fixed and are created all the time then this approach will not work very well as it would required an app restart to pick up all the new boards (or for that matter changes to the URL slugs for the existing boards, or deletions).
The hook
There is another way that allows us to dynamically check for a board rather
than materialising all our routes up front. The basic idea is to combine
prefix
with a before
handler. Let's put together some
code to illustrate this idea:
hook before => sub { my $board = param('board'); if ($board && board_is_valid($board)) { var board => $board; } }; prefix '/:board' => sub { get '/' => sub { # show a list of all the posts under the board $board # for demo's sake simply return the welcome message my $board = var('board') or pass; my $welcome_message = "Welcome to the group for discussing: $board"; return $welcome_message; }; get '/:post_id' => sub { my $board = var('board') or pass; # do the work here my $post_id = param('post_id'); # check if the ID is valid and do something with it # for demo's sake show variables you have access to: return "I was called with board: $board, post id: $post_id"; }; }; sub board_is_valid { my $board = shift; return if not defined $board; # in a real life application we would check this against a table in # a database or through a backend service. for the sake of # simplicity, let's just check against a hardcoded list my %valid_boards = map { $_ => 1 } qw(string-quartets symphonies piano-sonatas piano-trios); return defined $valid_boards{$board}; }
This time, our before
handler will catch the request first. It sets up
the board
variable using var
that is automatically passed around
to each handler. The handler first checks if the board
variable exists
and if it doesn't, it simply defers the handling to the Dancer's default
route handling. Since no other routes to handle the :board
exist, this
results in a 404.
While it alleviates the need to build all possible routes
up front, the one downside to this approach is having to check for the
presence of board
variable in each handler. On the positive side, you can
dynamically add boards without restarting the application for them to be
available.
Even more ways
Additional approaches for the dynamic route handling problem exist. Most notably, another approach was recently documented by Yanick Champoux uses megasplat to produce chained actions.
Minimum version
This article requires Dancer2 version 0.159000.
(hopefully released soon enough.)
Conclusion
Dancer has many methods of producing dynamic routes (or "chained actions") and each of them has its benefits and drawbacks.
Author
This article has been written by Deepak Gulati for the Perl Dancer Advent Calendar 2014.
Copyright
Creative Commons Deepak Gulati 2014.