Ron Savage

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 become MyApp::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 and foo_GET you'll get foo_post and foo_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 and prefix 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 the table 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.

This module uses Hash::FieldHash, which has an XS component!

Yep.

My policy is that stand-alone modules should use a light-weight object manager (my choice is Hash::FieldHash), whereas apps can - and probably should - use Moose.

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

http://www.perlmonks.org/?node_id=380424.

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.

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



Hosting generously
sponsored by Bytemark