Typed Route Parameters in Dancer2

Dancer2 version 0.300000 introduced typed route parameters, allowing you to use Type::Tiny type constraints with your named route parameters.

Quick Intro

The default type library is the one shipped with Dancer2: Dancer2::Core::Types. This extends Types::Standard with a small number of extra types, allowing simple type constraints like the following:

get '/user/:id[Int]' => sub {
    # matches /user/34 but not /user/jamesdean

    my $user_id = route_parameters->get('id');
};
 
get '/user/:username[Str]' => sub {
    # matches /user/jamesdean but not /user/34 since that is caught
    # by previous route

    my $username = route_parameters->get('username');
};

You can even use type constraints to add a regexp check:

get '/book/:date[StrMatch[qr{\d\d\d\d-\d\d-\d\d}]]' => sub {
    # matches /book/2014-02-04

    my $date = route_parameters->get('date');
};

Constraints can be combined in the normal Type::Tiny way, such as:

get '/some/:thing[Int|[StrMatch[qr{\d\d\d\d-\d\d-\d\d}]]' => sub {
    # matches Int like /some/234 and dates like /some/2020-12-08

    my $thing = route_parameters->get('thing');
};

Using Your Own Type Library

To access the full power of typed route parameters you will probably want to create your own custom types. Assuming your main app class is MyApp, then you might want to create MyApp::Types to hold your type library. For example:

package MyApp::Types;

# import Type::Library and declare our exported types
use Type::Library -base, -declare => qw(
   IsoDate
   ItemAction
   Slug
   Username
);

# import sugar from Type::Utils
use Type::Utils -all;

# here we import all of the type libraries whose types we want to include
# in our library
BEGIN {
    extends qw(
      Dancer2::Core::Types
      Types::Common::Numeric
      Types::Common::String
    );
}

# An ISO date.
# simplified example, you'd probably want something with better validation
declare IsoDate, as NonEmptySimpleStr,
    where { $_ =~ /^(\d+)-(\d{2})-(\d{2})$/ };

# Enums are a nice way to constrain to a set of values
enum ItemAction => [qw/ comsume drop equip repair unequip /];

# Valid slug: lowercase alphanumeric with hyphens
declare Slug, as NonEmptySimpleStr,
    where { $_ =~ /^[a-z0-9-]*$/ };

# If usernames are length-constrained then is better than Str
declare Username, as NonEmptySimpleStr,
    where { length($_) > 6 && length ($_) < 50 };

1;

You then need to add the following line to your config.yml so that Dancer2 knows which type library to use for typed parameters:

type_library: MyApp::Types

Now you're ready to use your type checks in your route definitions:

package MyApp;

use Dancer2;

get '/user/:id[PositiveInt]' => sub {
    # PositiveInt imported from Types::Common::Numeric gives us a better
    # check than simple Int

    my $user_id = route_parameters->get('id');
};

get '/user/:username[Username]' => sub {
    my $username = route_parameters->get('username');
};

get '/book/:date[IsoDate]' => sub {
    my $date = route_parameters->get('date');
};

get '/item/:action[ItemAction]/:item[Slug|PositiveInt]' => sub {
    # action constrained by enum
    # item by its symbolic slug, or its integer ID

    my $action = route_parameters->get('action');
    my $item   = route_parameters->get('item');
};

true;

Using Other Type Libraries

You can always import other type libraries into your own library, as per the example in the previous example, but if you just want to use a type once you might not want to do that. In this case you can simply include the type library in the type definition of the route parameter:

get '/user/:username[My::Type::Library::Username]' => sub {
    my $username = route_parameters->get('username');
};

Need Typed Query or Body Parameters?

For now core Dancer2 doesn't support this, but if you need it, then have a look at SawyerX's excellent Dancer2::Plugin::ParamTypes.

Author

This article has been written by Peter Mottram (SysPete) for the Twelve Days of Dancer 2020.

Copyright

No copyright retained. Enjoy, and keep Dancing!