File Coverage

blib/lib/Kelp/Routes/Pattern.pm
Criterion Covered Total %
statement 105 105 100.0
branch 45 48 93.7
condition 26 28 92.8
subroutine 14 14 100.0
pod 3 4 75.0
total 193 199 96.9


line stmt bran cond sub pod time code
1             package Kelp::Routes::Pattern;
2              
3 32     32   167028 use Carp;
  32         72  
  32         2733  
4              
5 32     32   763 use Kelp::Base;
  32         66  
  32         200  
6              
7             our @CARP_NOT = qw(Kelp::Routes);
8              
9             attr pattern => sub { die "pattern is required" };
10             attr via => undef;
11             attr method => sub { $_[0]->via };
12             attr has_name => undef;
13             attr name => sub { $_[0]->pattern };
14             attr check => sub { {} };
15             attr defaults => sub { {} };
16             attr bridge => 0;
17             attr order => 0;
18             attr regex => sub { $_[0]->_build_regex };
19             attr named => sub { {} };
20             attr param => sub { [] };
21             attr to => undef;
22             attr dest => undef;
23             attr psgi_middleware => undef;
24              
25             # helpers for matching different types of wildcards
26             sub __noslash
27             {
28 111     111   199 1 == grep { $_[0] eq $_ } ':', '?';
  222         551  
29             }
30              
31             sub __matchall
32             {
33 28     28   53 1 == grep { $_[0] eq $_ } '*', '>';
  56         137  
34             }
35              
36             sub __optional
37             {
38 213     213   415 1 == grep { $_[0] eq $_ } '?', '>';
  426         1097  
39             }
40              
41             sub new
42             {
43 283     283 0 498524 my $class = shift;
44 283         1104 my $self = $class->SUPER::new(@_);
45 283   66     1746 $self->has_name(defined $self->{name} && length $self->{name}); # remember if pattern was named
46              
47 283         953 $self->_fix_pattern;
48 283         790 $self->regex; # Compile the regex
49 283         1078 return $self;
50             }
51              
52             sub _fix_pattern
53             {
54 283     283   584 my ($self) = @_;
55 283         820 my $pattern = $self->pattern;
56 283 100       736 return if ref $pattern; # only fix non-regex patterns
57              
58             # operations performed
59 278         2054 $pattern =~ s{/+}{/}g;
60              
61 278         868 $self->pattern($pattern);
62             }
63              
64             sub _rep_regex
65             {
66 119     119   682 my ($self, $char, $switch, $token) = @_;
67 119         298 my $qchar = quotemeta $char;
68 119         174 my $re;
69              
70             my $optional = sub {
71 119 100   119   236 return unless __optional($switch);
72 22 100       66 $re = "(?:$re)" if $char eq '/';
73 22         36 $re .= '?';
74 119         521 };
75              
76             # no token - only valid for the wildcard * and slurpy >
77 119 100       315 if (!defined $token) {
78              
79             # do nothing
80 8 50       41 return $char . $switch
81             unless __matchall($switch);
82              
83 8         17 $re = $qchar . '(.+)';
84             }
85             else {
86 111         199 push @{$self->{_tokens}}, $token;
  111         263  
87              
88 111         271 my ($prefix, $suffix) = ("(?<$token>", ')');
89 111 100       234 if (__noslash($switch)) {
    50          
90 100   100     335 $re = $qchar . $prefix . ($self->check->{$token} // '[^\/]+') . $suffix;
91             }
92             elsif (__matchall($switch)) {
93 11   100     56 $re = $qchar . $prefix . ($self->check->{$token} // '.+') . $suffix;
94             }
95             }
96              
97 119         295 $optional->();
98 119         169 push @{$self->{_rep_regex_parts}}, $re;
  119         284  
99 119         1400 return '{}';
100             }
101              
102             sub _build_regex
103             {
104 283     283   463 my $self = shift;
105 283         824 $self->{_tokens} = [];
106              
107 283 100       683 return $self->pattern if ref $self->pattern eq 'Regexp';
108              
109 278         1200 my $placeholder_pattern = qr{
110             ( [^\0]? ) # preceding char, may change behavior of some placeholders
111             ( [:*?>] ) # placeholder sigil
112             ( \w+ )? # placeholder label
113             }x;
114 278         697 my $pattern = $self->pattern;
115              
116             # Curly braces and brackets are only used for separation.
117             # We replace all of them with \0, then convert the pattern
118             # into a regular expression. This way if the regular expression
119             # contains curlies, they won't be removed.
120 278         804 $pattern =~ s/[{}]/\0/g;
121              
122 278         721 $self->{_rep_regex_parts} = [];
123 278         2800 $pattern =~ s{$placeholder_pattern}{$self->_rep_regex($1, $2, $3)}eg;
  119         360  
124              
125             # Now remove all curlies remembered as \0 - We will use curlies again for
126             # special behavior in a moment
127 278         728 $pattern =~ s/\0//g;
128              
129             # remember if the pattern has a trailing slash before we quote it
130 278         776 my $trailing_slash = $pattern =~ m{/$};
131              
132             # _rep_regex reused curies for {} placeholders, so we want to split the
133             # string by that (and include them in the result by capturing the
134             # separator)
135 278         1036 my @parts = split /(\Q{}\E)/, $pattern, -1;
136              
137             # If we have a placeholder, replace it with next part. If not, quote it to
138             # avoid misusing regex in patterns.
139 278         677 foreach my $part (@parts) {
140 515 100       1062 if ($part eq '{}') {
141 119         192 $part = shift @{$self->{_rep_regex_parts}};
  119         265  
142             }
143             else {
144 396         910 $part = quotemeta $part;
145             }
146             }
147              
148 278         797 $pattern = join '', @parts;
149 278 100       764 if ($self->bridge) {
150              
151             # bridge must be followed by a slash or end of string, so that:
152             # - /test matches
153             # - /test/ matches
154             # - /test/something matches
155             # - /testsomething does not match
156             # if the bridge is already followed by a trailing slash, it's not a
157             # concern
158 20 100       69 $pattern .= '(?:/|$)' unless $trailing_slash;
159             }
160             else {
161              
162             # regular pattern must end immediately
163 258 100       729 $pattern .= quotemeta('/') . '?' unless $trailing_slash;
164 258         426 $pattern .= '$';
165             }
166              
167 278         7849 return qr{^$pattern};
168             }
169              
170             sub _rep_build
171             {
172 94     94   337 my ($self, $switch, $token, %args) = @_;
173              
174 94 100       174 if (!defined $token) {
175 9 50       12 return $switch unless __matchall($switch);
176 9         12 $token = $switch;
177             }
178              
179 94   100     228 my $rep = $args{$token} // $self->defaults->{$token} // '';
      100        
180 94 100 100     188 if (!__optional($switch) && !length $rep) {
181 16         75 return '{?' . $token . '}';
182             }
183              
184 78         165 my $check = $self->check->{$token};
185 78 100 100     219 if ($check && $args{$token} !~ $check) {
186 2         11 return '{!' . $token . '}';
187             }
188              
189 76         293 return $rep;
190             }
191              
192             sub build
193             {
194 54     54 1 648 my ($self, %args) = @_;
195              
196 54         134 my $pattern = $self->pattern;
197 54 100       151 croak "Can't build a regular expression route"
198             if ref $pattern eq 'Regexp';
199              
200 53         212 my $placeholder_pattern = qr{
201             \{? # may be embraced in curlies
202             ( [:*?>] ) # placeholder sigil
203             ( \w+ )? # placeholder label
204             \}?
205             }x;
206              
207 53         464 $pattern =~ s/$placeholder_pattern/$self->_rep_build($1, $2, %args)/eg;
  94         209  
208 53 100       229 if ($pattern =~ /{([!?])(\w+|[*>])}/) {
209 17 100       233 croak "Can't build '$pattern', " . (
210             $1 eq '!'
211             ? "field $2 doesn't match checks"
212             : "Default value for field $2 is missing"
213             );
214             }
215              
216 36         209 return $pattern;
217             }
218              
219             sub match
220             {
221 1359     1359 1 32988 my ($self, $path, $method) = @_;
222 1359 100 100     3128 return 0 if ($self->method && $self->method ne ($method // ''));
      100        
223 1285 100       2717 return 0 unless my @matched = $path =~ $self->regex;
224 393         1166 my $has_matches = $#+; # see perlvar @+
225              
226             # Initialize the named parameters hash and its default values
227 393         670 my %named = (%{$self->defaults}, %+);
  393         1149  
228              
229 393 100       1260 if (@{$self->{_tokens}}) {
  393 100       1502  
230              
231             # values of the named placeholders in the order they appear in the
232             # regex.
233 142         250 @matched = map { $named{$_} } @{$self->{_tokens}};
  223         727  
  142         2183  
234             }
235             elsif ($has_matches) {
236 17 100 50     43 @matched = map { length($_ // '') ? $_ : undef } @matched;
  22         109  
237             }
238             else {
239 234         533 @matched = ();
240             }
241              
242 393         1379 $self->named(\%named);
243 393         1249 $self->param(\@matched);
244              
245 393         1718 return 1;
246             }
247              
248             sub compare
249             {
250 42     42 1 84 my ($self, $other) = @_;
251              
252 42   100     99 return $other->bridge <=> $self->bridge
253             || $self->order <=> $other->order
254             || $self->pattern cmp $other->pattern;
255             }
256              
257             1;
258              
259             __END__
260              
261             =head1 NAME
262              
263             Kelp::Routes::Pattern - Route patterns for Kelp routes
264              
265             =head1 SYNOPSIS
266              
267             my $p = Kelp::Routes::Pattern->new( pattern => '/:name/:place' );
268             if ( $p->match('/james/london') ) {
269             %named = %{ $p->named }; # ( name => 'james', place => 'london' )
270             @param = @{ $p->param }; # ( 'james', 'london' )
271             }
272              
273             =head1 DESCRIPTION
274              
275             This module is needed by L<Kelp::Routes>. It provides matching for
276             individual route patterns, returning the named placeholders in a hash and an
277             array.
278              
279             =head1 ATTRIBUTES
280              
281             =head2 pattern
282              
283             The pattern to match against. Each pattern is a string, which may contain named
284             placeholders. For more information on the types and use of placeholders, look at
285             L<Kelp::Routes/PLACEHOLDERS>.
286              
287             my $p = Kelp::Routes::Patters->new( pattern => '/:id/*other' );
288             ...
289             $p->match('/4/something-else'); # True
290              
291             =head2 method
292              
293             Specifies an HTTP method to be matched by the route.
294              
295             my $p = Kelp::Routes::Patters->new(
296             pattern => '/:id/*other',
297             method => 'PUT'
298             );
299              
300             $p->match('/4/something-else', 'GET'); # False. Only PUT allowed.
301              
302             =head2 name
303              
304             You are encouraged to give each route a name, so you can look it up later when
305             you build a URL for it.
306              
307             my $p = Kelp::Routes::Patters->new(
308             pattern => '/:id/*other',
309             name => 'other_id'
310             );
311             ...
312              
313             say $p->build( 'other_id', id => '100', other => 'something-else' );
314             # Prints '/100/something-else'
315              
316             If no name is provided for the route, the C<pattern> is used.
317              
318             =head2 has_name
319              
320             A boolean signifying whether this route was originally given a specific name.
321             It will be false if the name was taken from C<pattern>.
322              
323             =head2 check
324              
325             A hashref with placeholder names as keys and regular expressions as values. It
326             is used to match the values of the placeholders against the provided regular
327             expressions.
328              
329             my $p = Kelp::Routes::Patters->new(
330             pattern => '/:id/*other',
331             check => { id => qr/\d+/ } # id may only be a didgit
332             );
333              
334             $p->match('/4/other'); # True
335             $p->match('/q/other'); # False
336              
337             Note: Do not add C<^> at the beginning or C<$> at the end of the regular
338             expressions, because they are merged into a bigger regex.
339              
340             =head2 defaults
341              
342             A hashref with placeholder defaults. This only applies to optional placeholders,
343             or those prefixed with a question mark. If a default value is provided for any
344             of them, it will be used in case the placeholder value is missing.
345              
346             my $p = Kelp::Routes::Patters->new(
347             pattern => '/:id/?other',
348             defaults => { other => 'info' }
349             );
350              
351             $p->match('/100');
352             # $p->named will contain { id => 100, other => 'info' }
353              
354             $p->match('/100/delete');
355             # $p->named will contain { id => 100, other => 'delete' }
356              
357             =head2 bridge
358              
359             A True/False value. Specifies if the route is a bridge. For more information
360             about bridges, please see L<Kelp::Routes/BRIDGES>
361              
362             =head2 order
363              
364             A numeric order of this route. Default order is C<0>, so if you want some
365             routes to take priority, you can use C<-1>. Lower is earlier.
366              
367             =head2 regex
368              
369             We recommend that you stick to using patterns, because they are simpler and
370             easier to read, but if you need to match a really complicated route, then
371             you can use a regular expression.
372              
373             my $p = Kelp::Routes::Patters->new( regex => qr{^(\d+)/(\d+)$} );
374             $p->match('/100/200'); # True. $p->param will be [ 100, 200 ]
375              
376             After matching, the L</param> array will be initialized with the values of the
377             captures in the order they appear in the regex.
378             If you used a regex with named captures, then a hashref L</named> will also be
379             initialized with the names and values of the named placeholders. In other words,
380             this hash will be a permanent copy of the C<%+> built-in hash.
381              
382             my $p = Kelp::Routes::Patters->new( regex => qr{^(?<id>\d+)/(?<line>\d+)$} );
383             $p->match('/100/200'); # True.
384             # $p->param will be [ 100, 200 ]
385             # $p->named will be { id => 100, line => 200 }
386              
387             If C<regex> is not explicitly given a value it will be built from the
388             C<pattern>.
389              
390             =head2 named
391              
392             A hashref which will be initialized by the L</match> function. After matching,
393             it will contain placeholder names and values for the matched route.
394              
395             =head2 param
396              
397             An arrayref, which will be initialized by the L</match> function. After matching,
398             it will contain all placeholder values in the order they were specified in the
399             pattern.
400              
401             =head2 to
402              
403             Specifies the route destination. See examples in L<Kelp::Routes>.
404              
405             =head2 dest
406              
407             The loaded destination. An array reference with two values, a controller name
408             (or undef if not a controller) and the code reference to the method. It will be
409             automatically generated by the router based on the contents of L</to>.
410              
411             =head2 psgi_middleware
412              
413             Extra middleware for Kelp, for this route only. It must be a code reference,
414             and the middleware must wrap L<Kelp/NEXT_APP>.
415              
416             =head1 METHODS
417              
418             =head2 match
419              
420             C<match( $path, $method )>
421              
422             Matches an already initialized route against a path and http method. If the match
423             was successful, this sub will return a true value and the L</named> and L</param>
424             attributes will be initialized with the names and values of the matched placeholders.
425              
426             =head2 build
427              
428             C<build( %args )>
429              
430             Builds a URL from a pattern.
431              
432             my $p = Kelp::Routes::Patters->new( pattern => '/:id/:line/:row' );
433             $p->build( id => 100, line => 5, row => 8 ); # Returns '/100/5/8'
434              
435             If the pattern contains an unnamed wildcard C<*> or slurpy C<< > >>, then it
436             should be built like this:
437              
438             my $p = Kelp::Routes::Patters->new( pattern => '/hello/*/>' );
439             $p->build( '*' => 'kelp', '>' => 'world' ); # Returns '/hello/kelp/world'
440              
441             If the pattern contains more than one unnamed items, then you should
442             probably give them some names.
443              
444             =head2 compare
445              
446             C<$compare( $other )>
447              
448             Compares two routes. Used for sorting matched routes in a router.
449              
450             =head1 ACKNOWLEDGEMENTS
451              
452             This module was inspired by L<Routes::Tiny>.
453              
454             The concept of bridges was borrowed from L<Mojolicious>
455              
456             =cut
457