Perl500503Syntax-OrDie

 view release on metacpan or  search on metacpan

t/corpus-stack/PSGI-Handy/lib/PSGI/Handy/Router.pm  view on Meta::CPAN

# match - look up a route
#   my $r = $router->match($method, $path);
# Returns:
#   - on success           : { handler => CODE, params => HASH }
#   - path matched, method not (405) : { allowed => [ method, ... ] }
#   - no match at all (404) : undef
# First registered matching route wins.
# --------------------------------------------------------------------
sub match {
    my ($self, $method, $path) = @_;
    defined $method or croak "match: method is required";
    defined $path   or croak "match: path is required";
    $method = uc($method);

    my @allowed;
    my $route;
    for $route (@{ $self->{routes} }) {
        my @caps = ($path =~ $route->{regex});
        next unless @caps;                 # this pattern did not match the path

        if ($route->{method} eq $method) {
            my %params;
            my $names = $route->{names};
            my $i;
            for ($i = 0; $i < scalar(@$names); $i++) {
                $params{ $names->[$i] } = $caps[$i];
            }
            return { handler => $route->{handler}, params => { %params } };
        }
        push @allowed, $route->{method};   # remember for a possible 405
    }

    if (@allowed) {
        my %seen;
        my @uniq = grep { !$seen{$_}++ } @allowed;
        return { allowed => \@uniq };
    }
    return undef;
}

# --------------------------------------------------------------------
# routes - return the internal route list (array reference).
# Mainly for introspection and testing.
# --------------------------------------------------------------------
sub routes {
    my $self = shift;
    return $self->{routes};
}

# --------------------------------------------------------------------
# _compile - turn a path pattern into (qr//, \@param_names)
#
# A pattern is split on '/' into segments. Each segment becomes:
#   ':name'  -> ([^/]+)   and records the parameter name 'name'
#   '*' (only as the LAST segment) -> (.*)   recorded as 'splat'
#   anything else -> quotemeta (literal, dots are NOT wildcards)
# The whole thing is anchored with \A ... \z so matching is exact.
# --------------------------------------------------------------------
sub _compile {
    my ($pattern) = @_;
    my @segs = split m{/}, $pattern, -1;   # -1 keeps trailing empty fields
    my @names;
    my @parts;
    my $last = $#segs;
    my $i;
    for ($i = 0; $i <= $last; $i++) {
        my $seg = $segs[$i];
        if ($seg eq '*' && $i == $last) {
            push @parts, '(.*)';
            push @names, 'splat';
        }
        elsif ($seg =~ /\A:([A-Za-z_]\w*)\z/) {
            push @parts, '([^/]+)';
            push @names, $1;
        }
        else {
            push @parts, quotemeta($seg);
        }
    }
    my $source = '\\A' . join('/', @parts) . '\\z';
    my $regex  = qr{$source};
    return ($regex, \@names);
}

1;

__END__

=head1 NAME

PSGI::Handy::Router - tiny PSGI route dispatcher for Perl 5.5.3 and later

=head1 VERSION

Version 0.01

=head1 SYNOPSIS

    use PSGI::Handy::Router;

    my $router = PSGI::Handy::Router->new;

    $router->add('GET',  '/',            \&home);
    $router->add('GET',  '/users/:id',   \&show_user);
    $router->add('POST', '/users',       \&create_user);
    $router->add('GET',  '/files/*',     \&serve_file);

    my $r = $router->match('GET', '/users/42');
    if ($r && $r->{handler}) {
        # $r->{params} = { id => 42 }
        my $response = $r->{handler}->($env, $r->{params});
    }
    elsif ($r && $r->{allowed}) {
        # path exists but method not allowed -> HTTP 405
        # Allow: join(', ', @{ $r->{allowed} })
    }
    else {
        # no route matched -> HTTP 404
    }

=head1 TABLE OF CONTENTS



( run in 2.377 seconds using v1.01-cache-2.11-cpan-71847e10f99 )