File Coverage

blib/lib/Kelp/Routes/Pattern.pm
Criterion Covered Total %
statement 72 74 97.3
branch 35 36 97.2
condition 24 24 100.0
subroutine 9 9 100.0
pod 2 3 66.6
total 142 146 97.2


line stmt bran cond sub pod time code
1             package Kelp::Routes::Pattern;
2              
3 37     37   209218 use Carp;
  37         150  
  37         2164  
4              
5 37     37   1047 use Kelp::Base;
  37         79  
  37         229  
6              
7             attr pattern => sub { die "pattern is required" };
8             attr via => undef;
9             attr method => sub { $_[0]->via };
10             attr name => sub { $_[0]->pattern };
11             attr check => sub { {} };
12             attr defaults => sub { {} };
13             attr bridge => 0;
14             attr regex => sub { $_[0]->_build_regex };
15             attr named => sub { {} };
16             attr param => sub { [] };
17             attr to => undef;
18              
19              
20             sub new {
21 214     214 0 4092 my $class = shift;
22 214         754 my $self = $class->SUPER::new(@_);
23 214         555 $self->{_tokens} = [];
24 214         614 $self->regex; # Compile the regex
25 214         794 return $self;
26             }
27              
28             sub _rep_regex {
29 96     96   395 my ( $self, $char, $switch, $token ) = @_;
30              
31 96         202 push @{$self->{_tokens}}, $token;
  96         223  
32              
33 96         256 my ( $a, $b, $r ) = ( "(?<$token>", ')', undef );
34 96         188 for ($switch) {
35 96 100 100     268 if ( $_ eq ':' || $_ eq '?' ) {
36 92   100     275 $r = $a . ( $self->check->{$token} // '[^\/]+' ) . $b
37             }
38 96 100       309 if ( $_ eq '*' ) {
39 4         12 $r = $a . '.+' . $b
40             }
41             }
42              
43 96 100 100     354 $char = $char . '?' if $char eq '/' && $switch eq '?';
44 96 100       210 $r .= '?' if $switch eq '?';
45              
46 96         429 return $char . $r;
47             }
48              
49             sub _build_regex {
50 214     214   349 my $self = shift;
51 214 100       546 return $self->pattern if ref $self->pattern eq 'Regexp';
52              
53 210         442 my $PAT = '(.?)([:*?])(\w+)';
54 210         436 my $pattern = $self->pattern;
55              
56             # Curly braces and brackets are only used for separation.
57             # We replace all of them with \0, then convert the pattern
58             # into a regular expression. This way if the regular expression
59             # contains curlies, they won't be removed.
60 210         713 $pattern =~ s/[{}]/\0/g;
61 210         1935 $pattern =~ s{$PAT}{$self->_rep_regex($1, $2, $3)}eg;
  96         280  
62 210         549 $pattern =~ s/\0//g;
63 210 100       688 $pattern .= '/?' unless $pattern =~ m{/$};
64 210 100       580 $pattern .= '$' unless $self->bridge;
65              
66 210         4435 return qr{^$pattern};
67             }
68              
69             sub _rep_build {
70 65     65   249 my ( $self, $switch, $token, %args ) = @_;
71 65   100     214 my $rep = $args{$token} // $self->defaults->{$token} // '';
      100        
72 65 100 100     219 if ($switch ne '?' && !$rep) {
73 12         66 return '{?' . $token . '}';
74             }
75 53         115 my $check = $self->check->{$token};
76 53 100 100     166 if ( $check && $args{$token} !~ $check ) {
77 2         13 return '{!' . $token . '}';
78             }
79 51         205 return $rep;
80             }
81              
82             sub build {
83 37     37 1 147 my ( $self, %args ) = @_;
84              
85 37         90 my $pattern = $self->pattern;
86 37 50       95 if ( ref $pattern eq 'Regexp' ) {
87 0         0 carp "Can't build a path for regular expressions";
88 0         0 return;
89             }
90              
91 37         73 my $PAT = '([:*?])(\w+)';
92 37         334 $pattern =~ s/{?$PAT}?/$self->_rep_build($1, $2, %args)/eg;
  65         185  
93 37 100       155 if ($pattern =~ /{([!?])(\w+)}/) {
94 13 100       1300 carp $1 eq '!'
95             ? "Field $2 doesn't match checks"
96             : "Default value for field $2 is missing";
97 13         626 return;
98             }
99 24         136 return $pattern;
100             }
101              
102             sub match {
103 1206     1206 1 10778 my ( $self, $path, $method ) = @_;
104 1206 100 100     2310 return 0 if ( $self->method && $self->method ne ( $method // '' ) );
      100        
105 1132 100       2283 return 0 unless my @matched = $path =~ $self->regex;
106              
107 282 100       1007 @matched = () unless $#+; # were there any captures? see perlvar @+
108              
109             # Initialize the named parameters hash and its default values
110 282     8   1800 my %named = map { $_ => $+{$_} } keys %+;
  187         1241  
  8         13482  
  8         2951  
  8         1855  
111 282         536 for ( keys %{ $self->defaults } ) {
  282         774  
112 4 100       32 $named{$_} = $self->defaults->{$_} unless exists $named{$_};
113             }
114 282         955 $self->named( \%named );
115              
116             # Initialize the param array, containing the values of the
117             # named placeholders in the order they appear in the regex.
118 282 100       433 if ( my @tokens = @{ $self->{_tokens} } ) {
  282         879  
119 116         204 $self->param( [ map { $named{$_} } @tokens ] );
  197         567  
120             }
121             else {
122 166 100       528 $self->param( [ map { $_ eq '' ? undef : $_ } @matched] );
  13         47  
123             }
124              
125 282         1282 return 1;
126             }
127              
128             1;
129              
130             __END__
131              
132             =head1 NAME
133              
134             Kelp::Routes::Pattern - Route patterns for Kelp routes
135              
136             =head1 SYNOPSIS
137              
138             my $p = Kelp::Routes::Pattern->new( pattern => '/:name/:place' );
139             if ( $p->match('/james/london') ) {
140             %named = %{ $p->named }; # ( name => 'james', place => 'london' )
141             @param = @{ $p->param }; # ( 'james', 'london' )
142             }
143              
144             =head1 DESCRIPTION
145              
146             This module is needed by L<Kelp::Routes>. It provides matching for
147             individual route patterns, returning the named placeholders in a hash and an
148             array.
149              
150             =head1 ATTRIBUTES
151              
152             =head2 pattern
153              
154             The pattern to match against. Each pattern is a string, which may contain named
155             placeholders. For more information on the types and use of placeholders, look at
156             L<Kelp::Routes/PLACEHOLDERS>.
157              
158             my $p = Kelp::Routes::Patters->new( pattern => '/:id/*other' );
159             ...
160             $p->match('/4/something-else'); # True
161              
162             =head2 method
163              
164             Specifies an HTTP method to be matched by the route.
165              
166             my $p = Kelp::Routes::Patters->new(
167             pattern => '/:id/*other',
168             method => 'PUT'
169             );
170              
171             $p->match('/4/something-else', 'GET'); # False. Only PUT allowed.
172              
173             =head2 name
174              
175             You are encouraged to give each route a name, so you can look it up later when
176             you build a URL for it.
177              
178             my $p = Kelp::Routes::Patters->new(
179             pattern => '/:id/*other',
180             name => 'other_id'
181             );
182             ...
183              
184             say $p->build( 'other_id', id => '100', other => 'something-else' );
185             # Prints '/100/something-else'
186              
187             If no name is provided for the route, the C<pattern> is used.
188              
189             =head2 check
190              
191             A hashref with placeholder names as keys and regular expressions as values. It
192             is used to match the values of the placeholders against the provided regular
193             expressions.
194              
195             my $p = Kelp::Routes::Patters->new(
196             pattern => '/:id/*other',
197             check => { id => qr/\d+/ } # id may only be a didgit
198             );
199              
200             $p->match('/4/other'); # True
201             $p->match('/q/other'); # False
202              
203             Note: Do not add C<^> at the beginning or C<$> at the end of the regular
204             expressions, because they are merged into a bigger regex.
205              
206             =head2 defaults
207              
208             A hashref with placeholder defaults. This only applies to optional placeholders,
209             or those prefixed with a question mark. If a default value is provided for any
210             of them, it will be used in case the placeholder value is missing.
211              
212             my $p = Kelp::Routes::Patters->new(
213             pattern => '/:id/?other',
214             defaults => { other => 'info' }
215             );
216              
217             $p->match('/100');
218             # $p->named will contain { id => 100, other => 'info' }
219              
220             $p->match('/100/delete');
221             # $p->named will contain { id => 100, other => 'delete' }
222              
223             =head2 bridge
224              
225             A True/False value. Specifies if the route is a bridge. For more information
226             about bridges, please see L<Kelp::Routes/BRIDGES>
227              
228             =head2 regex
229              
230             We recommend that you stick to using patterns, because they are simpler and
231             easier to read, but if you need to match a really complicated route, then
232             you can use a regular expression.
233              
234             my $p = Kelp::Routes::Patters->new( regex => qr{^(\d+)/(\d+)$} );
235             $p->match('/100/200'); # True. $p->param will be [ 100, 200 ]
236              
237             After matching, the L</param> array will be initialized with the values of the
238             captures in the order they appear in the regex.
239             If you used a regex with named captures, then a hashref L</named> will also be
240             initialized with the names and values of the named placeholders. In other words,
241             this hash will be a permanent copy of the C<%+> built-in hash.
242              
243             my $p = Kelp::Routes::Patters->new( regex => qr{^(?<id>\d+)/(?<line>\d+)$} );
244             $p->match('/100/200'); # True.
245             # $p->param will be [ 100, 200 ]
246             # $p->named will be { id => 100, line => 200 }
247              
248             If C<regex> is not explicitly given a value it will be built from the
249             C<pattern>.
250              
251             =head2 named
252              
253             A hashref which will be initialized by the L</match> function. After matching,
254             it will contain placeholder names and values for the matched route.
255              
256             =head2 param
257              
258             An arrayref, which will be initialized by the L</match> function. After matching,
259             it will contain all placeholder values in the order they were specified in the
260             pattern.
261              
262             =head2 to
263              
264             Specifies the route destination. See examples in L<Kelp::Routes>.
265              
266             =head1 METHODS
267              
268             =head2 match
269              
270             C<match( $path, $method )>
271              
272             Matches an already initialized route against a path and http method. If the match
273             was successful, this sub will return a true value and the L</named> and L</param>
274             attributes will be initialized with the names and values of the matched placeholders.
275              
276             =head2 build
277             C<build( %args )>
278              
279             Builds a URL from a pattern.
280              
281             my $p = Kelp::Routes::Patters->new( pattern => '/:id/:line/:row' );
282             $p->build( id => 100, line => 5, row => 8 ); # Returns '/100/5/8'
283              
284             =head1 ACKNOWLEDGEMENTS
285              
286             This module was inspired by L<Routes::Tiny>.
287              
288             The concept of bridges was borrowed from L<Mojolicious>
289              
290             =cut