package Mojolicious::Routes::Pattern; use Mojo::Base -base; use Carp qw(croak); has [qw(constraints defaults types)] => sub { {} }; has [qw(placeholder_start type_start)] => ':'; has [qw(placeholders tree)] => sub { [] }; has quote_end => '>'; has quote_start => '<'; has [qw(regex unparsed)]; has relaxed_start => '#'; has wildcard_start => '*'; sub match { my ($self, $path, $detect) = @_; my $captures = $self->match_partial(\$path, $detect); return !$path || $path eq '/' ? $captures : undef; } sub match_partial { my ($self, $pathref, $detect) = @_; # Compile on demand $self->_compile($detect) unless $self->{regex}; return undef unless my @captures = $$pathref =~ $self->regex; $$pathref = ${^POSTMATCH}; @captures = () if $#+ == 0; my $captures = {%{$self->defaults}}; for my $placeholder (@{$self->placeholders}, 'format') { last unless @captures; my $capture = shift @captures; $captures->{$placeholder} = $capture if defined $capture; } return $captures; } sub new { @_ > 1 ? shift->SUPER::new->parse(@_) : shift->SUPER::new } sub parse { my $self = shift; my $pattern = @_ % 2 ? (shift // '/') : '/'; $pattern =~ s!^/*|/+!/!g; return $self->constraints({@_}) if $pattern eq '/'; $pattern =~ s!/$!!; return $self->constraints({@_})->_tokenize($pattern); } sub render { my ($self, $values, $endpoint) = @_; # Placeholders can only be optional without a format my $optional = !(my $format = $values->{format}); my $str = ''; for my $token (reverse @{$self->tree}) { my ($op, $value) = @$token; my $part = ''; # Text if ($op eq 'text') { ($part, $optional) = ($value, 0) } # Slash elsif ($op eq 'slash') { $part = '/' unless $optional } # Placeholder else { my $name = $value->[0]; my $default = $self->defaults->{$name}; $part = $values->{$name} // $default // ''; if (!defined $default || ($default ne $part)) { $optional = 0 } elsif ($optional) { $part = '' } } $str = $part . $str; } # Format can be optional return $endpoint && $format ? "$str.$format" : $str; } sub _compile { my ($self, $detect) = @_; my $constraints = $self->constraints; my $defaults = $self->defaults; my $types = $self->types; my $block = my $regex = ''; my $optional = 1; for my $token (reverse @{$self->tree}) { my ($op, $value, $type) = @$token; my $part = ''; # Text if ($op eq 'text') { ($part, $optional) = (quotemeta $value, 0) } # Slash elsif ($op eq 'slash') { $regex = ($optional ? "(?:/$block)?" : "/$block") . $regex; ($block, $optional) = ('', 1); next; } # Placeholder else { if ($value->[1]) { $part = _compile_req($types->{$value->[1]} // '?!') } else { $part = $type ? $type eq 'relaxed' ? '([^/]+)' : '(.+)' : '([^/.]+)' } # Custom regex if (my $c = $constraints->{$value->[0]}) { $part = _compile_req($c) } # Optional placeholder exists $defaults->{$value->[0]} ? ($part .= '?') : ($optional = 0); } $block = $part . $block; } # Not rooted with a slash $regex = $block . $regex if $block; # Format $regex .= _compile_format($constraints->{format}, exists $defaults->{format}) if $detect; $self->regex(qr/^$regex/ps); } sub _compile_format { my ($format, $has_default) = @_; # No regex return '' unless $format; # Default regex return '/?(?:\.([^/]+))?$' if $format eq '1'; # Compile custom regex my $regex = '\.' . _compile_req($format); return $has_default ? "/?(?:$regex)?\$" : "/?$regex\$"; } sub _compile_req { my $req = shift; return "($req)" if ref $req ne 'ARRAY'; return '(' . join('|', map {quotemeta} reverse sort @$req) . ')'; } sub _tokenize { my ($self, $pattern) = @_; my $placeholders = $self->placeholders; my $type_start = $self->type_start; my $quote_end = $self->quote_end; my $quote_start = $self->quote_start; my $start = $self->placeholder_start; my $relaxed = $self->relaxed_start; my $wildcard = $self->wildcard_start; my (@tree, $spec, $more); for my $char (split //, $pattern) { # Quoted if ($char eq $quote_start) { push @tree, ['placeholder', ''] if ++$spec } elsif ($char eq $quote_end) { $spec = $more = 0 } # Placeholder elsif (!$more && $char eq $start) { push @tree, ['placeholder', ''] unless $spec++ } # Relaxed or wildcard (upgrade when quoted) elsif (!$more && ($char eq $relaxed || $char eq $wildcard)) { push @tree, ['placeholder', ''] unless $spec++; $tree[-1][2] = $char eq $relaxed ? 'relaxed' : 'wildcard'; } # Slash elsif ($char eq '/') { push @tree, ['slash']; $spec = $more = 0; } # Placeholder elsif ($spec && ++$more) { $tree[-1][1] .= $char } # Text (optimize slash+text and *+text+slash+text) elsif ($tree[-1][0] eq 'text') { $tree[-1][-1] .= $char } elsif (!$tree[-2] && $tree[-1][0] eq 'slash') { @tree = (['text', "/$char"]) } elsif ($tree[-2] && $tree[-2][0] eq 'text' && $tree[-1][0] eq 'slash') { pop @tree && ($tree[-1][-1] .= "/$char") } else { push @tree, ['text', $char] } } # Placeholder types for my $token (reverse @tree) { next unless $token->[0] eq 'placeholder'; $token->[1] = $token->[1] =~ /^(.+)\Q$type_start\E(.+)$/ ? [$1, $2] : [$token->[1]]; unshift @$placeholders, $token->[1][0]; } return $self->unparsed($pattern)->tree(\@tree); } 1; =encoding utf8 =head1 NAME Mojolicious::Routes::Pattern - Route pattern =head1 SYNOPSIS use Mojolicious::Routes::Pattern; # Create pattern my $pattern = Mojolicious::Routes::Pattern->new('/test/:name'); # Match routes my $captures = $pattern->match('/test/sebastian'); say $captures->{name}; =head1 DESCRIPTION L is the core of L. =head1 ATTRIBUTES L implements the following attributes. =head2 constraints my $constraints = $pattern->constraints; $pattern = $pattern->constraints({foo => qr/\w+/}); Regular expression constraints. =head2 defaults my $defaults = $pattern->defaults; $pattern = $pattern->defaults({foo => 'bar'}); Default parameters. =head2 placeholder_start my $start = $pattern->placeholder_start; $pattern = $pattern->placeholder_start(':'); Character indicating a placeholder, defaults to C<:>. =head2 placeholders my $placeholders = $pattern->placeholders; $pattern = $pattern->placeholders(['foo', 'bar']); Placeholder names. =head2 quote_end my $end = $pattern->quote_end; $pattern = $pattern->quote_end('}'); Character indicating the end of a quoted placeholder, defaults to C>. =head2 quote_start my $start = $pattern->quote_start; $pattern = $pattern->quote_start('{'); Character indicating the start of a quoted placeholder, defaults to C>. =head2 regex my $regex = $pattern->regex; $pattern = $pattern->regex($regex); Pattern in compiled regular expression form. =head2 relaxed_start my $start = $pattern->relaxed_start; $pattern = $pattern->relaxed_start('*'); Character indicating a relaxed placeholder, defaults to C<#>. =head2 tree my $tree = $pattern->tree; $pattern = $pattern->tree([['text', '/foo']]); Pattern in parsed form. Note that this structure should only be used very carefully since it is very dynamic. =head2 type_start my $start = $pattern->type_start; $pattern = $pattern->type_start('|'); Character indicating the start of a placeholder type, defaults to C<:>. =head2 types my $types = $pattern->types; $pattern = $pattern->types({int => qr/[0-9]+/}); Placeholder types. =head2 unparsed my $unparsed = $pattern->unparsed; $pattern = $pattern->unparsed('/:foo/:bar'); Raw unparsed pattern. =head2 wildcard_start my $start = $pattern->wildcard_start; $pattern = $pattern->wildcard_start('*'); Character indicating the start of a wildcard placeholder, defaults to C<*>. =head1 METHODS L inherits all methods from L and implements the following new ones. =head2 match my $captures = $pattern->match('/foo/bar'); my $captures = $pattern->match('/foo/bar', 1); Match pattern against entire path, format detection is disabled by default. =head2 match_partial my $captures = $pattern->match_partial(\$path); my $captures = $pattern->match_partial(\$path, 1); Match pattern against path and remove matching parts, format detection is disabled by default. =head2 new my $pattern = Mojolicious::Routes::Pattern->new; my $pattern = Mojolicious::Routes::Pattern->new('/users/:id'); my $pattern = Mojolicious::Routes::Pattern->new('/user/:id', id => qr/\d+/); my $pattern = Mojolicious::Routes::Pattern->new(format => ['json', 'yaml']); Construct a new L object and L pattern if necessary. =head2 parse $pattern = $pattern->parse('/user/:id'); $pattern = $pattern->parse('/user/:id', id=> qr/\d+/); $pattern = $pattern->parse(format => ['json', 'yaml']); Parse pattern. =head2 render my $path = $pattern->render({id => 24}); my $path = $pattern->render({id => 24}, 1); Render pattern into a path with parameters, format rendering is disabled by default. =head1 SEE ALSO L, L, L. =cut