Reducing boilerplate and managing exports in Dancer2

Dancer makes it easy to start small and grow fast. Today we will be looking at a techique to help manage that growth.

A typical mature Dancer2 app will span multiple packages and each package will make use of Dancer's plugins and other modules available on CPAN.

use Scalar::Util qw< blessed >;
use List::Util   qw< first all none sum >;
use Dancer2 appname => 'MyApp';
use Dancer2::Plugin::DBIC qw< schema resultset >;
use Dancer2::Plugin::Redis;

After the first couple of files, the copy-pasting becomes tedious. If you later change your mind about what to include, you leave yourself open to errors when you try to call a function you never imported into the package namespace.

What Dancer2 exports

As a web framework, Dancer2 provides you with many keywords you can use to write your web code. Usually, this presents no problem, but in some cases Dancer2 might give you a function with the same name as a function in another module you wish to use. For example, Dancer2 provides a function any, so you can write endpoints that work on multiple HTTP verbs, while List::Util provides any, a function which returns a true value if at least one item in a list matches a condition.

If you want to use List::Util/any in a package where Dancer2 has been used, you will find you cannot import both at once. Instead, you can qualify the any from List::Util:

any '/page/:id' => sub {
  my $id = param('id');
  send_error("Blocked!", 401)
      if List::Util::any { $_ eq $id } @blocked_resources;
  template 'page', { id => $id };
};

If you're using the any from List::Util more frequently than Dancer's any, then you might decide you prefer to qualify the latter and import the former.

However, you cannot selectively import functions from Dancer2, and you cannot use the qualified form Dancer2::any, because there isn't actually an any function in the Dancer2 package.

The reason for this is that Dancer2's import function (which is called when you use it) does something clever, so that you can have multiple separate Dancer2 apps running in the same perl instance.

use Dancer2 appname => 'MyApp';

NB: See http://advent.perldancer.org/2014/10 for more on appname and running multiple apps.

At this point, rather than exporting the function in the usual way, Dancer2 creates a new function in your package namespace, which calls a method of the same name on a Dancer2::Core::App object which it has created for you (NB: most of the methods are actually in the Dancer2::Role::DSL class).

So is there any way to control which parts of Dancer's DSL to import?

The answer is yes - but you need to write another package of your own to act as a façade.

Write your own DSL

The solution is to write a package which uses Dancer, and which provides the syntax you want to all your other packages.

A simple package would look like:

package MyApp::DSL;
use Dancer2 appname => MyApp;
use Exporter;
1;

(NB: DSL stands for "Domain Specific Language", and in this case is just a small collection of functions. Dancer is a DSL for writing Plack-based web apps).

This would provide access to the Dancer DSL as follows:

use MyApp::DSL;
MyApp::DSL::get('/' => sub { ... });

Having to prefix everything with MyApp::DSL:: isn't very nice, though.

Modules like Exporter provide a way to explicitly request that symbols be exported, for instance:

package MyApp::DSL;
use Dancer2 appname => 'MyApp';
use Exporter qw< import >;

our @EXPORT_OK = qw< get post put any del >;
1;

Now you can write:

use MyApp::DSL qw< get post put del >;
get '/' => sub { ... };

Or maybe in another route, you just want to retrieve values from your config? That's ok, too:

use MyApp::DSL qw< config >;
my @plugins = keys %{ config('plugins') };

By listing functions in @EXPORT_OK, we have allowed them to be exported when they are specified in the use statement.

Note that a limitation of this approach is that you need at least one package per app. If you're only running one app then this won't be an issue.

Group your exports with tags

Chances are you don't want to name each and every function you need in your use statement in every. Luckily, Exporter provides more options, too. Populating @EXPORT will cause those functions to always be exported. You can also use %EXPORT_TAGS to export groups of symbols.

Don't forget, it's not just Dancer2's functions that you can put in your DSL package. If you have other functions you use frequently and want to make available across your whole app (such as Scalar::Util/blessed), you can add them too.

This also gives you the opportunity to think about separation of concerns: for instance, you may decide that your route handling packages need access to all of Dancer2's functions, whereas the packages which handle your business logic and flow control don't need the route handling functions, while your schema packages have yet another set of functions.

One strategy therefore is to create a different tag for each group of concerns, e.g. :controller, :model, :route, etc. Alternatively, the tags could represent where the functions come from, e.g. :util, :config, :storage, :routing, etc.

So now you know enough to be able to replace

use Scalar::Util qw< blessed >;
use List::Util qw< first all none sum >;
use Dancer2 appname => 'MyApp';
use Dancer2::Plugin::DBIC qw< schema resultset >;
use Dancer2::Plugin::Redis;

with an expression like:

use MyApp::DSL qw< :util :storage :config >;

or:

use MyApp::DSL qw< :model >;

Author

This article was written by Daniel Perrett <perrettdl@googlemail.com> for the Perl Dancer Advent Calendar 2016.

Copyright

© 2016 Daniel Perrett; you are free to reuse code without restriction and text under the Creative Commons Attribution-ShareAlike License 4.0.