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   213953 use Carp;
  37         100  
  37         2128  
4              
5 37     37   1116 use Kelp::Base;
  37         72  
  37         248  
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 4392 my $class = shift;
22 214         777 my $self = $class->SUPER::new(@_);
23 214         592 $self->{_tokens} = [];
24 214         642 $self->regex; # Compile the regex
25 214         781 return $self;
26             }
27              
28             sub _rep_regex {
29 96     96   418 my ( $self, $char, $switch, $token ) = @_;
30              
31 96         163 push @{$self->{_tokens}}, $token;
  96         230  
32              
33 96         299 my ( $a, $b, $r ) = ( "(?<$token>", ')', undef );
34 96         184 for ($switch) {
35 96 100 100     280 if ( $_ eq ':' || $_ eq '?' ) {
36 92   100     239 $r = $a . ( $self->check->{$token} // '[^\/]+' ) . $b
37             }
38 96 100       318 if ( $_ eq '*' ) {
39 4         17 $r = $a . '.+' . $b
40             }
41             }
42              
43 96 100 100     386 $char = $char . '?' if $char eq '/' && $switch eq '?';
44 96 100       202 $r .= '?' if $switch eq '?';
45              
46 96         422 return $char . $r;
47             }
48              
49             sub _build_regex {
50 214     214   346 my $self = shift;
51 214 100       565 return $self->pattern if ref $self->pattern eq 'Regexp';
52              
53 210         429 my $PAT = '(.?)([:*?])(\w+)';
54 210         439 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         736 $pattern =~ s/[{}]/\0/g;
61 210         1885 $pattern =~ s{$PAT}{$self->_rep_regex($1, $2, $3)}eg;
  96         268  
62 210         534 $pattern =~ s/\0//g;
63 210 100       691 $pattern .= '/?' unless $pattern =~ m{/$};
64 210 100       539 $pattern .= '$' unless $self->bridge;
65              
66 210         4300 return qr{^$pattern};
67             }
68              
69             sub _rep_build {
70 65     65   251 my ( $self, $switch, $token, %args ) = @_;
71 65   100     202 my $rep = $args{$token} // $self->defaults->{$token} // '';
      100        
72 65 100 100     223 if ($switch ne '?' && !$rep) {
73 12         62 return '{?' . $token . '}';
74             }
75 53         117 my $check = $self->check->{$token};
76 53 100 100     167 if ( $check && $args{$token} !~ $check ) {
77 2         12 return '{!' . $token . '}';
78             }
79 51         200 return $rep;
80             }
81              
82             sub build {
83 37     37 1 150 my ( $self, %args ) = @_;
84              
85 37         95 my $pattern = $self->pattern;
86 37 50       91 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         69 my $PAT = '([:*?])(\w+)';
92 37         361 $pattern =~ s/{?$PAT}?/$self->_rep_build($1, $2, %args)/eg;
  65         207  
93 37 100       151 if ($pattern =~ /{([!?])(\w+)}/) {
94 13 100       1318 carp $1 eq '!'
95             ? "Field $2 doesn't match checks"
96             : "Default value for field $2 is missing";
97 13         694 return;
98             }
99 24         131 return $pattern;
100             }
101              
102             sub match {
103 1206     1206 1 11038 my ( $self, $path, $method ) = @_;
104 1206 100 100     2336 return 0 if ( $self->method && $self->method ne ( $method // '' ) );
      100        
105 1132 100       2365 return 0 unless my @matched = $path =~ $self->regex;
106              
107 282 100       1003 @matched = () unless $#+; # were there any captures? see perlvar @+
108              
109             # Initialize the named parameters hash and its default values
110 282     8   1859 my %named = map { $_ => $+{$_} } keys %+;
  187         1262  
  8         14149  
  8         3143  
  8         1870  
111 282         569 for ( keys %{ $self->defaults } ) {
  282         759  
112 4 100       15 $named{$_} = $self->defaults->{$_} unless exists $named{$_};
113             }
114 282         1012 $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       448 if ( my @tokens = @{ $self->{_tokens} } ) {
  282         884  
119 116         212 $self->param( [ map { $named{$_} } @tokens ] );
  197         571  
120             }
121             else {
122 166 100       546 $self->param( [ map { $_ eq '' ? undef : $_ } @matched] );
  13         51  
123             }
124              
125 282         1304 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