NAME
CGI::Snapp::Dispatch - Dispatch requests to CGI::Snapp-based objects
Synopsis
CGI Scripts
Here is a minimal CGI instance script. Note the call to new()!
#!/usr/bin/env perl
use CGI::Snapp::Dispatch;
CGI::Snapp::Dispatch -> new -> dispatch;
(The use of new() is discussed in detail under "PSGI Scripts", just below.)
But, to override the default dispatch table, you probably want something like this:
MyApp/Dispatch.pm:
package MyApp::Dispatch;
parent 'CGI::Snapp::Dispatch';
sub dispatch_args
{
my($self) = @_;
return
{
prefix => 'MyApp',
table =>
[
'' => {app => 'Initialize', rm => 'start'},
':app/:rm' => {},
'admin/:app/:rm' => {prefix => 'MyApp::Admin'},
],
};
}
And then you can write ... Note the call to new()!
#!/usr/bin/env perl
use MyApp::Dispatch;
MyApp::Dispatch -> new -> dispatch;
PSGI Scripts
Here is a PSGI script in production on my development machine. Note the call to new()!
#!/usr/bin/env perl
#
# Run with:
# starman -l 127.0.0.1:5020 --workers 1 httpd/cgi-bin/local/wines.psgi &
# or, for more debug output:
# plackup -l 127.0.0.1:5020 httpd/cgi-bin/local/wines.psgi &
use strict;
use warnings;
use CGI::Snapp::Dispatch;
use Plack::Builder;
# ---------------------
my($app) = CGI::Snapp::Dispatch -> new -> as_psgi
(
prefix => 'Local::Wines::Controller', # A sub-class of CGI::Snapp.
table =>
[
'' => {app => 'Initialize', rm => 'display'},
':app' => {rm => 'display'},
':app/:rm/:id?' => {},
],
);
builder
{
enable "ContentLength";
enable "Static",
path => qr!^/(assets|favicon|yui)!,
root => '/dev/shm/html'; # /dev/shm/ is Debian's RAM disk.
$app;
};
Warning! The line my($app) = ... contains a call to "new()". This is definitely not the same as if you were using CGI::Application::Dispatch or CGI::Application::Dispatch::PSGI. They look like this:
my($app) = CGI::Application::Dispatch -> as_psgi
The lack of a call to new() there tells you I've implemented something very similar but different. You have been warned...
The point of this difference is that new() returns an object, and passing that into "as_psgi(@args)" as $self allows the latter method to be much more sophisticated than it would otherwise be. Specifically, it can now share a lot of code with "dispatch(@args)".
Lastly, if you want to use regexps to match the path info, see CGI::Snapp::Dispatch::Regexp.
Description
This module provides a way to automatically look at the path info - $ENV{PATH_INFO} - of the incoming HTTP request, and to process that path info like this:
- o Parse off a module name
- o Parse off a run mode
- o Create an instance of that module (i.e. load it)
- o Run that instance
- o Return the output of that run as the result of requsting that path info (i.e. module and run mode combo)
Thus, it will translate a URI like this:
/app/index.cgi/module_name/run_mode
into something that is functionally equivalent to this:
my($app) = Module::Name -> new(...);
$app -> mode_param(sub {return 'run_mode'});
return $app -> run;
Distributions
This module is available as a Unix-style distro (*.tgz).
See http://savage.net.au/Perl-modules/html/installing-a-module.html for help on unpacking and installing distros.
Installation
Install CGI::Snapp::Dispatch as you would for any Perl
module:
Run:
cpanm CGI::Snapp::Dispatch
or run:
sudo cpan CGI::Snapp::Dispatch
or unpack the distro, and then either:
perl Build.PL
./Build
./Build test
sudo ./Build install
or:
perl Makefile.PL
make (or dmake or nmake)
make test
make install
Constructor and Initialization
new()
is called as my($app) = CGI::Snapp::Dispatch -> new(k1 => v1, k2 => v2, ...)
.
It returns a new object of type CGI::Snapp::Dispatch
.
Key-value pairs accepted in the parameter list (see corresponding methods for details [e.g. "return_type([$string])"]):
- o logger => $aLoggerObject
-
Specify a logger compatible with Log::Handler.
Note: This logs method calls etc inside CGI::Snapp::Dispatch.
To log within CGI::Snapp, see "How do I use my own logger object?".
Default: '' (The empty string).
To clarify: The built-in calls to log() all use a log level of 'debug', so if your logger has 'maxlevel' set to anything less than 'debug', nothing nothing will get logged.
'maxlevel' and 'minlevel' are discussed in Log::Handler#LOG-LEVELS and Log::Handler::Levels.
- o return_type => $integer
-
Possible values for $integer:
- o 0 (zero)
-
dispatch() returns the output of the run mode.
This is the default.
- o 1 (one)
-
dispatch() returns the hashref of args built from combining the output of dispatch_args() and the args to dispatch().
The requested module is not loaded and run. See t/args.t.
- o 2 (two)
-
dispatch() returns the hashref of args build from parsing the path info.
The requested module is not loaded and run. See t/args.t.
Default: 0.
Note: return_type is ignored by "as_psgi(@args)".
Methods
as_psgi(@args)
Returns a PSGI-compatible coderef which, when called, runs your sub-class of CGI::Snapp as a PSGI app.
This works because the coderef actually calls "psgi_app($args_to_new)" in CGI::Snapp.
See the next method, "dispatch(@args)", for a discussion of @args, which may be a hash or hashref.
Lastly: as_psgi() does not support the error_document option the way dispatch({table => {error_document => ...} }) does. Rather, it throws errors of type HTTP::Exception. Consider handling these errors with Plack::Middleware::ErrorDocument or similar.
dispatch(@args)
Returns the output generated by calling a CGI::Snapp-based module.
@args is a hash or hashref of options, which includes the all-important 'table' key, to define a dispatch table. See "What is the structure of the dispatch table?" for details.
The unfortunate mismatch between dispatch() taking a hash and dispatch_args() taking a hashref has been copied from CGI::Application::Dispatch. But, to clean things up, CGI::Snapp::Dispatch allows dispatch() to accept a hashref. You are encouraged to always use hashrefs, to avoid confusion.
(Key => value) pairs which may appear in the hashref parameter ($args[0]):
- o args_to_new => $hashref
-
This is a hashref of arguments that are passed into the constructor (
new()
) of the application.If you wish to set parameters in your app which can be retrieved by the $self -> param($key) method, then use:
my($app) = CGI::Snapp::Dispatch -> new; my($output) = $app -> dispatch(args_to_new => {PARAMS => {key1 => 'value1'} });
This means that inside your app, $self -> param('key1') will return 'value1'.
See t/args.t's test_13(), which calls t/lib/CGI/Snapp/App1.pm's rm2().
See also t/lib/CGI/Snapp/Dispatch/SubClass1.pm's dispatch_args() for how to pass in one or more such values via your sub-class.
- o auto_rest => $Boolean
-
If 1, this tells Dispatch that you are using REST by default and that you care about which HTTP method is being used. Dispatch will append the HTTP method name (upper case by default) to the run mode that is determined after finding the appropriate dispatch rule. So a GET request that translates into
MyApp::Module -> foo
will becomeMyApp::Module -> foo_GET
.This can be overridden on a per-rule basis in a derived class's dispatch table. See also the next option.
Default: 0.
See t/args.t test_27().
- o auto_rest_lc => $Boolean
-
If 1, then in combination with auto_rest, this tells Dispatch that you prefer lower cased HTTP method names. So instead of
foo_POST
andfoo_GET
you'll getfoo_post
andfoo_get
.See t/args.t test_28().
- o default
-
Specify a value to use for the path info if one is not available. This could be the case if the default page is selected (e.g.: '/cgi-bin/x.cgi' or perhaps '/cgi-bin/x.cgi/').
- o error_document
-
Note: When using "as_psgi(@args)", error_document makes no sense, and is ignored. In that case, use Plack::Middleware::ErrorDocument or similar.
If this value is not provided, and something goes wrong, then Dispatch will return a '500 Internal Server Error', using an internal HTML page. See t/args.t, test_25().
Otherwise, the value should be one of the following:
- o A customised error string
-
To use this, the string must start with a single double-quote (") character. This character character will be trimmed from final output.
- o A file name
-
To use this, the string must start with a less-than sign (<) character. This character character will be trimmed from final output.
$ENV{DOCUMENT_ROOT}, if not empty, will be prepended to this file name.
The file will be read in and used as the error document.
See t/args.t, test_26().
- o A URL to which the application will be redirected
-
This happens when the error_document does not start with " or <.
Note: In all 3 cases, the string may contain a '%s', which will be replaced with the error number (by sprintf).
Currently CGI::Snapp::Dispatch uses three HTTP errors:
- o 400 Bad Request
-
This is output if the run mode is not specified, or it contains an invalid character.
- o 404 Not Found
-
This is output if the module name is not specified, or if there was no match with the dispatch table, or the module could not be loaded by Class::Load.
- o 500 Internal Server Error
-
This is output if the application dies.
See t/args.t, test_24().
- o prefix
-
This option will set the string to be prepended to the name of the application module before it is loaded and created.
For instance, consider /app/index.cgi/module_name/run_mode.
This would, by default, load and create a module named 'Module::Name'. But let's say that you have all of your application specific modules under the 'My' namespace. If you set this option -
prefix
- to 'My' then it would instead load the 'My::Module::Name' application module instead.The algorithm for converting a path info into a module name is documented in "translate_module_name($name)".
- o table
-
In most cases, simply using Dispatch with the
default
andprefix
is enough to simplify your application and your URLs, but there are many cases where you want more power. Enter the dispatch table (a hashref), specified here as the value of thetable
key.Since this table can be slightly complicated, a whole section exists on its use. Please see the "What is the structure of the dispatch table?" section.
Examples are in the dispatch_args() method of both t/lib/CGI/Snapp/Dispatch/SubClass1.pm and t/lib/CGI/Snapp/Dispatch/SubClass2.pm.
dispatch_args($args)
Returns a hashref of args to be used by "dispatch(@args)".
This hashref is a dispatch table. See "What is the structure of the dispatch table?" for details.
"dispatch(@args)" calls this method, passing in the hash/hashref which was passed in to "dispatch(@args)".
Default output:
{
args_to_new => {},
default => '',
prefix => '',
table =>
[
':app' => {},
':app/:rm' => {},
],
}
This is the perfect method to override when creating a subclass to provide a richer "What is the structure of the dispatch table?".
See CGI::Snapp::Dispatch::SubClass1 and CGI::Snapp::Dispatch::SubClass2, both under t/lib/. These modules are exercised by t/args.t.
new()
See "Constructor and Initialization" for details on the parameters accepted by "new()".
Returns an object of type CGI::Snapp::Dispatch.
translate_module_name($name)
This method is used to control how the module name is translated from the matching section of the path. See "How does CGI::Snapp parse the path info?".
The main reason that this method exists is so that it can be overridden if it doesn't do exactly what you want.
The following transformations are performed on the input:
- o The text is split on '_'s (underscores)
-
Next, each word has its first letter capitalized. The words are then joined back together using '::'.
- o The text is split on '-'s (hyphens)
-
Next, each word has its first letter capitalized. The words are then joined back together without the '-'s.
Examples:
module_name => Module::Name
module-name => ModuleName
admin_top-scores => Admin::TopScores
FAQ
What is 'path info'?
For a CGI script, it is just $ENV{PATH_INFO}. The value of $ENV{PATH_INFO} is normally set by the web server from the path info sent by the HTTP client.
A request to /cgi-bin/x.cgi/path/info will set $ENV{PATH_INFO} to /path/info.
For Apache, whether $ENV{PATH_INFO} is set or not depends on the setting of the AcceptPathInfo directive.
For a PSGI script, it is $$env{PATH_INFO}, within the $env hashref provided by PSGI.
Path info is also discussed in "mode_param([@new_options])" in CGI::Snapp.
Similar comments apply to the request method (GET, PUT etc) which may be used in rules.
For CGI scripts, request method comes from $ENV{HTTP_REQUEST_METHOD} || $ENV{REQUEST_METHOD}, whereas for PSGI scripts it is just $$env{REQUEST_METHOD}.
Is there any sample code?
Yes. See t/args.t and t/lib/*.
Why did you fork CGI::Application::Dispatch?
To be a companion module for CGI::Snapp.
What version of CGI::Application::Dispatch did you fork?
V 3.07.
How does CGI::Snapp::Dispatch differ from CGI::Application::Dispatch?
There is no module called CGI::Snapp::Dispatch::PSGI
This just means the PSGI-specific code is incorporated into CGI::Snapp::Dispatch. See "as_psgi(@args)".
Processing parameters to dispatch() and dispatch_args()
The code which combines parameters to these 2 subs has been written from scratch. Obviously, the intention is that the new code behave in an identical fashion to the corresponding code in CGI::Application::Dispatch.
Also, the re-write allowed me to support a version of "dispatch(@args)" which accepts a hashref, not just a hash. The same flexibility has been added to "as_psgi(@args)".
No special code for Apache, mod_perl or plugins
I suggest that sort of stuff is best put in sub-classes.
Unsupported features
- o dispatch_path()
-
Method dispatch_path() is not provided. For CGI scripts, the code in dispatch() accesses $ENV{PATH_INFO} directly, whereas for PSGI scripts, as_psgi() accesses the PSGI environment hashref $$env{PATH_INFO}.
Enhanced features
"new()" can take extra parameters:
- o return_type
-
Note: return_type is ignored by "as_psgi(@args)".
This module uses Class::Load to try loading your application's module
CGI::Application::Dispatch uses:
eval "require $module";
whereas CGI::Snapp::Dispatch uses 2 methods from Class::Load:
try_load_class $module;
croak 404 if (! is_class_loaded $module);
For CGI scripts, the 404 (and all other error numbers) is handled by sub _http_error(), whereas for PSGI scripts, the code throws errors of type HTTP::Exception.
Reading an error document from a file
CGI::Application::Dispatch always prepends $ENV{DOCUMENT_ROOT} to the file name. Unfortunately, this means that when $ENV{DOCUMENT_ROOT} is not set, File::Spec prepends a '/' to the file name. So, an error_document of '<x.html' becomes '/x.html'.
This module only prepends $ENV{DOCUMENT_ROOT} if it is not empty. Hence, with an empty $ENV{DOCUMENT_ROOT}, an error_document of '<x.html' becomes 'x.html'.
See sub _parse_error_document() and t/args.t test_26().
Handling of exceptions
CGI::Application::Dispatch uses a combination of eval and Try::Tiny, together with Exception::Class. Likewise, CGI::Application::Dispatch::PSGI uses the same combination, although without Exception::Class.
CGI::Snapp::Dispatch just uses Try::Tiny. This applies both to CGI scripts and PSGI scripts. For CGI scripts, errors are handled by sub _http_errror(). For PSGI scripts, the code throws errors of type HTTP::Exception.
How does CGI::Snapp parse the path info?
Firstly, the path info is split on '/' chars. Hence /module_name/mode1 gives us ('', 'module_name', 'mode1').
The value 'module_name' is passed to "translate_module_name($name)". In this case, the result is 'Module::Name'.
You are free to override "translate_module_name($name)" to customize it.
After that, the prefix option's value, if any, is added to the front of 'Module::Name'. See "dispatch_args($args)" for more about prefix.
FInally, 'mode1' becomes the name of the run mode.
Remember from the docs for CGI::Snapp, that this is the name of the run mode, but is not necessarily the name of the method which will be run. The code in your sub-class of CGI::Snapp can map run mode names to method names.
For instance, a statement like:
$self -> run_modes({rm_name_1 => 'rm_method_1', rm_name_2 => 'rm_method_2'});
in (probably) sub setup(), shows how to separate run mode names from method names.
What is the structure of the dispatch table?
Sometimes it's easiest to explain with an example, so here you go:
CGI::Snapp::Dispatch -> new -> dispatch # Note the new()!
(
args_to_new =>
{
PARAMS => {big => 'small'},
},
default => '/app',
prefix => 'MyApp',
table =>
[
'' => {app => 'Blog', rm => 'recent'},
'posts/:category' => {app => 'Blog', rm => 'posts'},
':app/:rm/:id' => {app => 'Blog'},
'date/:year/:month?/:day?' =>
{
app => 'Blog',
rm => 'by_date',
args_to_new => {PARAMS => {small => 'big'} },
},
]
);
Firstly note, that besides passing this structure into "dispatch(@args)", you could sub-class CGI::Snapp::Dispatch and design "dispatch_args($args)" to return exactly the same structure.
OK. The components, all of which are optional, are:
- o args_to_new => $hashref
-
This is how you specify a hashref of parameters to be passed to the constructor (new() ) of your sub-class of CGI::Snapp.
- o default => $string
-
This specifies a default for the path info in the case this code is called with an empty $ENV{PATH_INFO}.
- o prefix => $string
-
This specifies a namespace to prepend to the class name derived by processing the path info.
E.g. If path info was /module_name, then the above would produce 'MyApp::Module::Name'.
- o table => $arrayref
-
This provides a set of rules, which are compared - 1 at a time, in the given order - with the path info, as the code tries to match the incoming path info to a rule you have provided.
The first match wins.
Each element of the array consists of a rule and an argument list.
Rules can be empty (see '' above), or they may be a combination of '/' chars and tokens. A token can be one of:
- o A literal
-
Any token which does not start with a colon (:) is taken to be a literal string and must appear exactly as-is in the path info in order to match. In the rule 'posts/:category', posts is a literal.
- o A variable
-
Any token which begins with a colon (:) is a variable token. These are simply wild-card place holders in the rule that will match anything - in the corresponding position - in the path info that isn't a slash.
These variables can later be referred to in your application (sub-class of CGI::Snapp) by using the $self -> param($name) mechanism. In the rule 'posts/:category', ':category' is a variable token.
If the path info matched this rule, you could retrieve the value of that token from within your application like so: my($category) = $self -> param('category');.
There are some variable tokens which are special. These can be used to further customize the dispatching.
- o :app
-
This is the module name of the application. The value of this token will be sent to "translate_module_name($name)" and then prefixed with the prefix if there is one.
- o :rm
-
This is the run mode of the application. The value of this token will be the actual name of the run mode used. As explained just above ("How does CGI::Snapp parse the path info?"), this is not necessarily the name of the method within the module which will be run.
- o An optional variable
-
Any token which begins with a colon (:) and ends with a question mark (?) is considered optional. If the rest of the path info matches the rest of the rule, then it doesn't matter whether it contains this token or not. It's best to only include optional variable tokens at the end of your rule. In the rule 'date/:year/:month?/:day?', ':month?' and ':day?' are optional-variable tokens.
Just as with variable tokens, optional-variable tokens' values can be retrieved by the application, if they existed in the path info. Try:
if (defined $self -> param('month') ) { ... }
Lastly, $self -> param('month') will return undef if ':month?' does not match anything in the path info.
- o A wildcard
-
The wildcard token '*' allows for partial matches. The token must appear at the end of the rule.
E.g.: 'posts/list/*'. Given this rule, the 'dispatch_url_remainder' param is set to the remainder of the path info matched by the *. The name ('dispatch_url_remainder') of the param can be changed by setting '*' argument in the argument list. This example:
'posts/list/*' => {'*' => 'post_list_filter'}
specifies that $self -> param('post_list_filter') rather than $self -> param('dispatch_url_remainder') is to be used in your app, to retrieve the value which was passed in via the path info.
See t/args.t, test_21() and test_22(), and the corresponding sub rm5() in t/lib/CGI/Snapp/App2.pm.
- o A HTTP method name
-
You can also dispatch based on HTTP method. This is similar to using auto_rest but offers more fine-grained control. You include the (case insensitive) method name at the end of the rule and enclose it in square brackets. Samples:
':app/news[post]' => {rm => 'add_news' }, ':app/news[get]' => {rm => 'news' }, ':app/news[delete]' => {rm => 'delete_news'},
The main reason that we don't use regular expressions for dispatch rules is that regular expressions did not provide for named back references (until recent versions of Perl), in the way variable tokens do.
How do I use my own logger object?
Study the sample code in CGI::Snapp::Demo::Four, which shows how to supply a Config::Plugin::Tiny *.ini file to configure the logger via the wrapper class CGI::Snapp::Demo::Four::Wrapper.
Also, see t/logs.t, t/log.a.pl and t/log.b.pl.
See also "What else do I need to know about logging?" in CGI::Snapp for important info and sample code.
How do I sub-class CGI::Snapp::Dispatch?
You do this the same way you sub-class CGI::Snapp. See this FAQ entry in CGI::Snapp.
Are there any security implications from using this module?
Yes. Since CGI::Snapp::Dispatch will dynamically choose which modules to use as content generators, it may give someone the ability to execute specially crafted modules on your system if those modules can be found in Perl's @INC path. This should only be a problem if you don't use a prefix.
Of course those modules would have to behave like CGI::Snapp based modules, but that still opens up the door more than most want.
By using the prefix option you are only allowing Dispatch to pick modules from a pre-defined namespace.
Why is CGI::PSGI required in Build.PL and Makefile.PL when it's sometimes not needed?
It's a tradeoff. Leaving it out of those files is convenient for users who don't run under a PSGI environment, but it means users who do use PSGI must install CGI::PSGI explicitly. And, worse, it means their code does not run by default, but only runs after manually installing that module.
So, since CGI::PSGI's only requirement is CGI, it's simpler to just always require it.
Troubleshooting
It doesn't work!
Things to consider:
- o Run the *.cgi script from the command line
-
shell> perl httpd/cgi-bin/cgi.snapp.one.cgi
If that doesn't work, you're in b-i-g trouble. Keep reading for suggestions as to what to do next.
- o Did you try using a logger to trace the method calls?
-
Pass a logger to your sub-class of CGI::Snapp like this:
my($logger) = Log::Handler -> new; $logger -> add ( screen => { maxlevel => 'debug', message_layout => '%m', minlevel => 'error', newline => 1, # When running from the command line. } ); CGI::Snapp::Dispatch -> new -> as_psgi({args_to_new => {logger => $logger} }, ...);
In addition, you can trace CGI::Snapp::Dispatch itself with the same (or a different) logger:
CGI::Snapp::Dispatch -> new(logger => $logger) -> as_psgi({args_to_new => {logger => $logger} }, ...);
The entry to each method in CGI::Snapp and CGI::Snapp::Dispatch is logged using this technique, although only when maxlevel is 'debug'. Lower levels for maxlevel do not trigger logging. See the source for details. By 'this technique' I mean there is a statement like this at the entry of each method:
$self -> log(debug => 'Entered x()');
- o Are you confused about combining parameters to dispatch() and dispatch_args()?
-
I suggest you use the request_type option to "new()" to capture output from the parameter merging code before trying to run your module. See t/args.t.
- o Are you confused about patterns in tables which do/don't use ':app' and ':rm'?
-
The golden rule is:
- o If the rule uses 'app', then it is non-capturing
-
This means the matching app name from $ENV{PATH_INFO} is not saved, so you must provide a modue name in the table's rule. E.g.: 'app/:rm' => {app => 'MyModule}, or perhaps use the prefix option to specify the complete module name.
- o If the rule uses ':app', then it is capturing
-
This means the matching app name from $ENV{PATH_INFO} is saved, and it becomes the name of the module. Of course, prefix might come into play here, too.
- o Did you forget the leading < (read from file) in the customised error document file name?
- o Did you forget the leading " (double-quote) in the customised error document string?
- o Did you forget the embedded %s in the customised error document?
-
This triggers the use of sprintf to merge the error number into the string.
- o Are you trying to use this module with an app non based on CGI::Snapp?
-
Remember that CGI::Snapp's new() takes a hash, not a hashref.
- o Did you get the mysterious error 'No such field "priority"'?
-
You did this:
as_psgi(args_to_new => $logger, ...)
instead of this:
as_psgi(args_to_new => {logger => $logger, ...}, ...)
- o The system Perl 'v' perlbrew
-
Are you using perlbrew? If so, recall that your web server will use the first line of your CGI script to find a Perl, and that line probably says something like #!/usr/bin/env perl.
So, perhaps you'd better turn perlbrew off and install CGI::Snapp and this module under the system Perl, before trying again.
- o Generic advice
See Also
CGI::Snapp - A almost back-compat fork of CGI::Application.
As of V 1.01, CGI::Snapp now supports PSGI-style apps.
And see CGI::Snapp::Dispatch::Regexp for another way of matching the path info.
Machine-Readable Change Log
The file Changes was converted into Changelog.ini by Module::Metadata::Changes.
Version Numbers
Version numbers < 1.00 represent development versions. From 1.00 up, they are production versions.
Credits
Please read "CONTRIBUTORS" in CGI::Application::Dispatch, since this module is a fork of the non-Apache components of CGI::Application::Dispatch.
Repository
https://github.com/ronsavage/CGI-Snapp-Dispatch
Support
Email the author, or log a bug on RT:
https://rt.cpan.org/Public/Dist/Display.html?Name=CGI::Snapp::Dispatch.
Author
CGI::Snapp::Dispatch was written by Ron Savage <ron@savage.net.au> in 2012.
Home page: http://savage.net.au/index.html.
Copyright
Australian copyright (c) 2012, Ron Savage.
All Programs of mine are 'OSI Certified Open Source Software';
you can redistribute them and/or modify them under the terms of
The Artistic License, a copy of which is available at:
http://www.opensource.org/licenses/index.html