NAME

Dispatch::Fu - Compute a static key and dispatch to the corresponding Perl handler

SYNOPSIS

use strict;
use warnings;
use Dispatch::Fu;

my $input = [qw/1 2 3 4 5/];

my $result = dispatch {
    my $values = shift;

    return scalar(@$values) > 5
      ? q{bucket5}
      : sprintf q{bucket%d}, scalar @$values;
}
$input,
  on bucket0 => sub { return q{bucket 0} },
  on bucket1 => sub { return q{bucket 1} },
  on bucket2 => sub { return q{bucket 2} },
  on bucket3 => sub { return q{bucket 3} },
  on bucket4 => sub { return q{bucket 4} },
  on bucket5 => sub { return q{bucket 5} };

print "$result\n";    # bucket 5

Dispatch::Fu exports dispatch, on, cases, xdefault, and xshift_and_deref by default. The same names are also available through @EXPORT_OK.

DESCRIPTION

Dispatch::Fu provides a small, idiomatic layer around Perl's familiar hash-based dispatch-table pattern. Instead of requiring the input value to already be a suitable hash key, a dispatch block computes a static key from whatever input and application rules are appropriate. That key selects a handler registered with on.

This is useful when the decision depends on ranges, several values, request metadata, normalization, or other logic that would otherwise grow into a long if/elsif chain. The classification logic remains ordinary Perl; the resulting action remains an ordinary hash-style dispatch.

For example, a traditional dispatch table works naturally when $action is already one of a fixed set of keys:

my $handlers = {
    start => sub { ... },
    stop  => sub { ... },
};

die "Unsupported action\n"
  if not defined $action or not exists $handlers->{$action};

my $result = $handlers->{$action}->();

Dispatch::Fu adds a classification stage when the key must first be derived:

my $result = dispatch {
    my $value = shift;

    return q{small}  if $value < 10;
    return q{medium} if $value < 100;
    return q{large};
}
$value,
  on small  => sub { ... },
  on medium => sub { ... },
  on large  => sub { ... };

The value passed to dispatch is passed unchanged to both the classification block and the selected handler.

API

dispatch BLOCK, INPUT, CASES

my $result = dispatch {
    my $input = shift;
    return q{some_case};
}
$input,
  on some_case  => sub { ... },
  on other_case => sub { ... };

dispatch coerces BLOCK to a subroutine reference. The block receives the single INPUT scalar and must return the name of a registered case.

INPUT can be any scalar value, including a reference. A reference is often convenient when the classification decision needs several values:

my $input = {
    method => q{POST},
    role   => q{admin},
};

my $result = dispatch {
    my $input = shift;

    return q{admin_post}
      if $input->{method} eq q{POST} and $input->{role} eq q{admin};

    return q{other};
}
$input,
  on admin_post => sub { ... },
  on other      => sub { ... };

The selected handler receives the original INPUT scalar unchanged. dispatch returns whatever the handler returns and preserves the caller's context. A handler may therefore return a scalar, a reference, a list, or any other normal Perl return value.

my @values = dispatch {
    return q{numbers};
}
undef,
  on numbers => sub { return qw/1 2 3 4 5/ };

If the classification block returns a key that is not registered to a CODE reference, dispatch throws an exception with croak.

Each call to dispatch starts with a fresh internal dispatch table. Cases from a previous successful or failed dispatch are never carried into the next call.

on KEY => CODEREF

on start => sub { ... }

on contributes a static case name and handler to the current dispatch call. The handler must be a CODE reference.

The on expressions are part of the argument list to dispatch, so they must be separated with commas. Accidentally ending one with a semicolon moves on into void or scalar context. Dispatch::Fu detects that common mistake and emits a warning with carp.

my $result = dispatch {
    return q{start};
}
$input,
  on start => sub { ... },
  on stop  => sub { ... };

cases

my @cases = cases;

cases returns the currently registered case names as a sorted list.

During the dispatch classification block, the list contains all cases registered by on plus the built-in default case. This makes cases useful for introspection while deciding which key to return:

my $result = dispatch {
    my $candidate = shift;
    my %supported = map { $_ => 1 } cases;

    return $supported{$candidate} ? $candidate : q{default};
}
$action,
  on default => sub { ... },
  on start   => sub { ... },
  on stop    => sub { ... };

The internal table is reset before the selected handler is invoked. Therefore, outside the classification block, including from inside a selected handler, cases reflects only the built-in default case. This reset is deliberate: it keeps one dispatch operation isolated from the next, even when an earlier operation fails.

xdefault CASE, [DEFAULT]

my $key = xdefault $candidate;
my $key = xdefault $candidate, q{not_found};

xdefault is a shortcut for the common case where the candidate value itself should be used as the dispatch key when it exactly matches one of the currently registered cases.

Matching is literal string equality, not substring or regular-expression matching. False-but-defined keys such as "0" are valid case names.

If CASE is undefined or does not exactly match a registered case, xdefault returns DEFAULT. When DEFAULT is omitted, it returns the string default.

It is particularly convenient as the last expression in a classification block:

my $result = dispatch {
    xdefault shift;
}
$action,
  on default => sub { ... },
  on start   => sub { ... },
  on stop    => sub { ... };

A custom fallback key works the same way:

my $result = dispatch {
    xdefault shift, q{not_found};
}
$action,
  on not_found => sub { ... },
  on start     => sub { ... },
  on stop      => sub { ... };

xshift_and_deref LIST

my ($x, $y, $z) = xshift_and_deref @_;

xshift_and_deref removes common unpacking boilerplate when INPUT is a reference. It examines the first value in LIST, shifts it, and dereferences it according to its reference type.

It supports HASH, ARRAY, and SCALAR references:

my @values = xshift_and_deref \@array;
my %values = xshift_and_deref \%hash;
my $value  = xshift_and_deref \$scalar;

For unsupported reference types or non-reference values, it returns undef in scalar context (or an empty list in list context).

A typical dispatch using an array reference can be written as:

my $result = dispatch {
    my ($left, $right) = xshift_and_deref @_;
    return $left > $right ? q{left} : q{right};
}
[ $left, $right ],
  on left => sub {
      my ($left, $right) = xshift_and_deref @_;
      return $left;
  },
  on right => sub {
      my ($left, $right) = xshift_and_deref @_;
      return $right;
  };

EXAMPLES

Dispatching on several conditions

The classification block can combine as many inputs as needed while still reducing the final action to a small set of static keys:

my $job = {
    priority => 9,
    retries  => 0,
    enabled  => 1,
};

my $result = dispatch {
    my $job = shift;

    return q{disabled} if not $job->{enabled};
    return q{urgent}   if $job->{priority} >= 8 and $job->{retries} < 2;
    return q{normal};
}
$job,
  on disabled => sub { ... },
  on urgent   => sub { ... },
  on normal   => sub { ... };

CGI::Tiny request dispatch

Dispatch::Fu can also provide a compact routing layer for a small CGI application. CGI::Tiny exposes the request method and path directly, while Dispatch::Fu reduces those values to a static handler name. The original CGI::Tiny object is then passed to the selected handler.

#!/usr/bin/env perl
use strict;
use warnings;
use CGI::Tiny;
use Dispatch::Fu;

cgi {
    my $cgi = $_;

    dispatch {
        my $cgi = shift;
        my $method = $cgi->method;
        my $path   = $cgi->path;

        return q{home}
          if $method eq q{GET} and $path eq q{/};

        return q{create_item}
          if $method eq q{POST} and $path eq q{/item};

        return q{not_found};
    }
    $cgi,
      on home => sub {
          my $cgi = shift;
          return $cgi->render(html => q{<h1>Home</h1>});
      },
      on create_item => sub {
          my $cgi = shift;
          my $name = $cgi->param(q{name});
          return $cgi->render(text => qq{created: $name});
      },
      on not_found => sub {
          my $cgi = shift;
          $cgi->set_response_status(404);
          return $cgi->render(text => q{Not Found});
      };
};

This is not intended to replace a full routing framework. It is useful when a small CGI program already has a modest, static set of actions and the route or action key needs to be computed from several pieces of request state.

DEFAULT HANDLER

Every dispatch table contains an internal default handler. If a classification block returns default and the caller has not supplied its own on default => ... handler, the built-in handler warns with carp and prints the currently supported case names.

Applications normally provide their own default handler when default behavior is expected:

my $result = dispatch {
    xdefault shift;
}
$action,
  on default => sub { return q{unsupported} },
  on start   => sub { return q{started} };

DIAGNOSTICS

Dispatch::Fu uses Carp so diagnostics are reported from the caller's perspective.

  • No cases supplied

    Calling dispatch without any on cases throws an exception with croak. This often indicates that a semicolon ended the argument list too early.

  • Unsupported or invalid computed key

    If the classification block returns a key that is not mapped to a CODE handler, dispatch throws an exception with croak.

  • on used in void or scalar context

    on emits a warning with carp. This is commonly caused by using a semicolon where the dispatch argument list required a comma.

STABILITY

Dispatch::Fu is maintained as production code with a deliberately small public API and core-only runtime dependencies. Changes should preserve existing calling conventions and the lightweight nature of the module. The test suite covers normal dispatch, defaults, introspection, diagnostics, return context, reference unpacking, invalid handlers, state isolation, and false-but-valid case keys.

BUGS

Please report bugs and feature ideas through the project issue tracker.

AUTHOR

O. ODLER 558 <oodler@cpan.org>.

LICENSE AND COPYRIGHT

Same terms as Perl itself.