use Carp;
attr pattern => sub { die "pattern is required" };
attr via => undef;
attr name => sub { $_[0]->pattern };
attr check => sub { {} };
attr defaults => sub { {} };
attr bridge => 0;
attr regex => sub { $_[0]->_build_regex };
attr named => sub { {} };
attr param => sub { [] };
attr to => undef;
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
$self->{_tokens} = [];
$self->regex; # Compile the regex
return $self;
}
sub _rep_regex {
my ( $self, $char, $switch, $token ) = @_;
push @{$self->{_tokens}}, $token;
my ( $a, $b, $r ) = ( "(?<$token>", ')', undef );
for ($switch) {
if ( $_ eq ':' || $_ eq '?' ) {
$r = $a . ( $self->check->{$token} // '[^\/]+' ) . $b
}
if ( $_ eq '*' ) {
$r = $a . '.+' . $b
}
}
$char = $char . '?' if $char eq '/' && $switch eq '?';
$r .= '?' if $switch eq '?';
return $char . $r;
}
sub _build_regex {
my $self = shift;
return $self->pattern if ref $self->pattern eq 'Regexp';
my $PAT = '(.?)([:*?])(\w+)';
my $pattern = $self->pattern;
$pattern =~ s{$PAT}{$self->_rep_regex($1, $2, $3)}eg;
$pattern =~ s/[{}]//g;
$pattern .= '/?' unless $pattern =~ m{/$};
$pattern .= '$' unless $self->bridge;
return qr{^$pattern};
}
sub _rep_build {
my ( $self, $switch, $token, %args ) = @_;
my $rep = $args{$token} // $self->defaults->{$token} // '';
if ($switch ne '?' && !$rep) {
return '{?' . $token . '}';
}
my $check = $self->check->{$token};
if ( $check && $args{$token} !~ $check ) {
return '{!' . $token . '}';
}
return $rep;
}
sub build {
my ( $self, %args ) = @_;
my $pattern = $self->pattern;
if ( ref $pattern eq 'Regexp' ) {
carp "Can't build a path for regular expressions";
return;
}
my $PAT = '([:*?])(\w+)';
$pattern =~ s/{?$PAT}?/$self->_rep_build($1, $2, %args)/eg;
if ($pattern =~ /{([!?])(\w+)}/) {
carp $1 eq '!'
? "Field $2 doesn't match checks"
: "Default value for field $2 is missing";
return undef;
}
return $pattern;
}
sub match {
my ( $self, $path, $method ) = @_;
return 0 if ( $self->via && $self->via ne ( $method // '' ) );
return 0 unless my @matched = $path =~ $self->regex;
# Initialize the named parameters hash and its default values
my %named = map { $_ => $+{$_} } keys %+;
for ( keys %{ $self->defaults } ) {
$named{$_} = $self->defaults->{$_} unless exists $named{$_};
}
$self->named( \%named );
# Initialize the param array, containing the values of the
# named placeholders in the order they appear in the regex.
if ( my @tokens = @{ $self->{_tokens} } ) {
$self->param( [ map { $named{$_} } @tokens ] );
}
else {
$self->param( \@matched );
}
return 1;
}
1;
__END__
=head1 NAME
Kelp::Routes::Pattern - Route patterns for Kelp routes
=head1 SYNOPSIS
my $p = Kelp::Routes::Pattern->new( pattern => '/:name/:place' );
if ( $p->match('/james/london') ) {
%named = %{ $p->named }; # ( name => 'james', place => 'london' )
@param = @{ $p->param }; # ( 'james', 'london' )
}
=head1 DESCRIPTION
This module is needed by L<Kelp::Routes>. It provides matching for
individual route patterns, returning the named placeholders in a hash and an
array.
=head1 ATTRIBUTES
=head2 pattern
The pattern to match against. Each pattern is a string, which may contain named
placeholders. For more information on the types and use of placeholders, look at
L<Kelp::Routes/PLACEHOLDERS>.
my $p = Kelp::Routes::Patters->new( pattern => '/:id/*other' );
...
$p->match('/4/something-else'); # True
=head2 via
Specifies an HTTP method to be matched by the route.
my $p = Kelp::Routes::Patters->new(
pattern => '/:id/*other',
via => 'PUT'
);
$p->match('/4/something-else', 'GET'); # False. Only PUT allowed.
=head2 name
You are encouraged to give each route a name, so you can look it up later when
you build a URL for it.
my $p = Kelp::Routes::Patters->new(
pattern => '/:id/*other',
name => 'other_id'
);
...
say $p->build( 'other_id', id => '100', other => 'something-else' );
# Prints '/100/something-else'
If no name is provided for the route, the C<pattern> is used.
=head2 check
A hashref with placeholder names as keys and regular expressions as values. It
is used to match the values of the placeholders against the provided regular
expressions.
my $p = Kelp::Routes::Patters->new(
pattern => '/:id/*other',
check => { id => qr/\d+/ } # id may only be a didgit
);
$p->match('/4/other'); # True
$p->match('/q/other'); # False
Note: Do not add C<^> at the beginning or C<$> at the end of the regular
expressions, because they are merged into a bigger regex.
=head2 defaults
A hashref with placeholder defaults. This only applies to optional placeholders,
or those prefixed with a question mark. If a default value is provided for any
of them, it will be used in case the placeholder value is missing.
my $p = Kelp::Routes::Patters->new(
pattern => '/:id/?other',
defaults => { other => 'info' }
);
$p->match('/100');
# $p->named will contain { id => 100, other => 'info' }
$p->match('/100/delete');
# $p->named will contain { id => 100, other => 'delete' }
=head2 bridge
A True/False value. Specifies if the route is a bridge. For more information
about bridges, please see L<Kelp::Routes/BRIDGES>
=head2 regex
We recommend that you stick to using patterns, because they are simpler and
easier to read, but if you need to match a really complicated route, then
you can use a regular expression.
my $p = Kelp::Routes::Patters->new( regex => qr{^(\d+)/(\d+)$} );
$p->match('/100/200'); # True. $p->param will be [ 100, 200 ]
After matching, the L</param> array will be initialized with the values of the
captures in the order they appear in the regex.
If you used a regex with named captures, then a hashref L</named> will also be
initialized with the names and values of the named placeholders. In other words,
this hash will be a permanent copy of the C<%+> built-in hash.
my $p = Kelp::Routes::Patters->new( regex => qr{^(?<id>\d+)/(?<line>\d+)$} );
$p->match('/100/200'); # True.
# $p->param will be [ 100, 200 ]
# $p->named will be { id => 100, line => 200 }
If C<regex> is not explicitly given a value it will be built from the
C<pattern>.
=head2 named
A hashref which will be initialized by the L</match> function. After matching,
it will contain placeholder names and values for the matched route.
=head2 param
An arrayref, which will be initialized by the L</match> function. After matching,
it will contain all placeholder values in the order they were specified in the
pattern.
=head2 to
Specifies the route destination. See examples in L<Kelp::Routes>.
=head1 METHODS
=head2 match
C<match( $path, $method )>
Matches an already initialized route against a path and http method. If the match
was successful, this sub will return a true value and the L</named> and L</param>
attributes will be initialized with the names and values of the matched placeholders.
=head2 build
C<build( %args )>
Builds a URL from a pattern.
my $p = Kelp::Routes::Patters->new( pattern => '/:id/:line/:row' );
$p->build( id => 100, line => 5, row => 8 ); # Returns '/100/5/8'
=head1 ACKNOWLEDGEMENTS
This module was inspired by L<Routes::Tiny>.
The concept of bridges was borrowed from L<Mojolicious>
=cut