File Coverage

blib/lib/Routes/Tiny/Pattern.pm
Criterion Covered Total %
statement 193 194 99.4
branch 108 120 90.0
condition 32 41 78.0
subroutine 11 11 100.0
pod 5 5 100.0
total 349 371 94.0


line stmt bran cond sub pod time code
1             package Routes::Tiny::Pattern;
2              
3 18     18   145 use strict;
  18         45  
  18         488  
4 18     18   90 use warnings;
  18         37  
  18         567  
5              
6             require Scalar::Util;
7 18     18   6438 use Routes::Tiny::Match;
  18         57  
  18         32544  
8              
9             my $TOKEN = '[^\/()]+';
10              
11             sub new {
12 71     71 1 142 my $class = shift;
13 71         310 my (%params) = @_;
14              
15 71         145 my $self = {};
16 71         141 bless $self, $class;
17              
18 71 100       219 if (my $arguments = delete $params{'+arguments'}) {
19 2         9 $self->{arguments_push} = 1;
20 2         6 $params{arguments} = $arguments;
21             }
22              
23 71         191 $self->{name} = $params{name};
24 71         149 $self->{defaults} = $params{defaults};
25 71         138 $self->{arguments} = $params{arguments};
26 71   100     337 $self->{method} = $params{method} || $params{default_method};
27 71         163 $self->{pattern} = $params{pattern};
28 71   100     318 $self->{constraints} = $params{constraints} || {};
29 71         429 $self->{routes} = $params{routes};
30 71         198 $self->{subroutes} = $params{subroutes};
31 71         135 $self->{strict_trailing_slash} = $params{strict_trailing_slash};
32 71         134 $self->{strict_case} = $params{strict_case};
33              
34 71 50       560 Scalar::Util::weaken($self->{routes}) if $self->{routes};
35 71 50       214 $self->{strict_trailing_slash} = 1 unless defined $self->{strict_trailing_slash};
36 71 50       197 $self->{strict_case} = 0 unless defined $self->{strict_case};
37              
38 71 100       273 if (my $methods = $self->{method}) {
39 7 100       34 $methods = [$methods] unless ref $methods eq 'ARRAY';
40 7         18 $methods = [map {uc} @$methods];
  8         30  
41 7         17 $self->{method} = $methods;
42             }
43              
44 71         161 $self->{captures} = [];
45              
46 71         237 $self->_prepare_pattern;
47              
48 70         301 return $self;
49             }
50              
51 92     92 1 472 sub arguments { return shift->{arguments} }
52              
53 144     144 1 493 sub name { return shift->{name} }
54              
55             sub match {
56 171     171 1 277 my $self = shift;
57 171         390 my ($path, %args) = @_;
58              
59 171 100       528 return unless $self->_match_method($args{method});
60              
61 153 100       590 $path = '/' . $path unless substr($path, 0, 1) eq '/';
62              
63 153 100 66     549 if (!$self->{strict_trailing_slash} && $path ne '/' && $path !~ m{/$}) {
      100        
64 5         13 $path .= '/';
65             }
66              
67 153         941 my @captures = ($path =~ $self->{pattern});
68 153 100       581 return unless @captures;
69              
70 92 100       161 my $captures = {%{$self->{defaults} || {}}};
  92         504  
71              
72 92         211 foreach my $capture (@{$self->{captures}}) {
  92         231  
73 89 50       234 last unless @captures;
74              
75 89         205 my $value = shift @captures;
76              
77 89 100 100     322 if (defined($value) || !exists $captures->{$capture}) {
78 87         259 $captures->{$capture} = $value;
79             }
80             }
81              
82 92         177 my $arguments;
83 92 100       238 if ($self->{arguments_push}) {
84 2         6 %$arguments = %{ $self->arguments };
  2         8  
85              
86 2 50       7 foreach my $key (keys %{ $args{arguments} || {} }) {
  2         17  
87 5         15 my $value = $args{arguments}->{$key};
88              
89 5 100       17 if (exists $arguments->{$key}) {
90 2 50       14 $arguments->{$key} = [$arguments->{$key}] unless ref $arguments->{$key} eq 'ARRAY';
91 2 100       7 unshift @{ $arguments->{$key} }, ref $value eq 'ARRAY' ? @$value : $value;
  2         17  
92             }
93             else {
94 3         11 $arguments->{$key} = $value;
95             }
96             }
97             }
98             else {
99             $arguments = {
100 90 100       409 %{ $args{arguments} || {} },
101 90 100       143 %{ $self->arguments || {} }
  90         363  
102             };
103             }
104              
105             my $match = $self->_build_match(
106             name => $self->name,
107             arguments => $arguments,
108             captures => $captures,
109             parent => $args{parent}
110 92         298 );
111              
112 92 100       303 if ($self->{subroutes}) {
113 21         37 my $parent = $match;
114 21         65 my $tail = substr($path, length $&);
115             $match = $self->{subroutes}
116 21         145 ->match($tail, %args, parent => $parent, arguments => $arguments);
117             }
118              
119 92         355 return $match;
120             }
121              
122             sub build_path {
123 38     38 1 78 my $self = shift;
124 38         117 my (%params) = @_;
125              
126 38         71 my @parts;
127              
128 38         76 my $optional_depth = 0;
129 38         95 my $trailing_slash = 0;
130              
131 38         80 foreach my $group_part (@{$self->{parts}}) {
  38         165  
132 83         168 my $path = '';
133              
134 83         175 foreach my $part (@$group_part) {
135 86         202 my $type = $part->{type};
136 86         155 my $name = $part->{name};
137              
138 86 100       296 if ($type eq 'capture') {
    100          
    50          
139 49 100 100     205 if ($part->{level} && exists $params{$name}) {
140 8         17 $optional_depth = $part->{level};
141             }
142              
143 49 100       134 if (!exists $params{$name}) {
144             next
145 10 100 66     91 if $part->{level} && $part->{level} > $optional_depth;
146              
147 1 50 33     10 if ( exists $self->{defaults}
148             && exists $self->{defaults}->{$name})
149             {
150 1         4 $params{$name} = $self->{defaults}->{$name};
151             }
152             else {
153 0         0 Carp::croak("Required param '$part->{name}' was not "
154             . "passed when building a path");
155             }
156             }
157              
158 40         118 my $param = $params{$name};
159              
160 40 100       137 if (defined(my $constraint = $part->{constraint})) {
161 3 100       245 Carp::croak("Param '$name' fails a constraint")
162             unless $param =~ m/^ $constraint $/xms;
163             }
164              
165 38         117 $path .= $param;
166             }
167             elsif ($type eq 'glob') {
168 8 100       19 if (!exists $params{$name}) {
169 3 100 66     20 if ( exists $self->{defaults}
    100          
170             && exists $self->{defaults}->{$name})
171             {
172 1         2 $params{$name} = $self->{defaults}->{$name};
173             }
174             elsif ($part->{optional}) {
175 1         3 next;
176             }
177             else {
178 1         194 Carp::croak(
179             "Required glob param '$part->{name}' was not "
180             . "passed when building a path");
181             }
182             }
183              
184 6         9 $path .= $params{$name};
185             }
186             elsif ($type eq 'text') {
187 29         76 $path .= $part->{text};
188             }
189              
190 73         264 $trailing_slash = $part->{trailing_slash};
191             }
192              
193 80 100       219 if ($path ne '') {
194 71         191 push @parts, $path;
195             }
196             }
197              
198 35         73 my $head = q{/};
199              
200 35   66     179 my $parent_pattern = $self->{routes} && $self->{routes}->{parent_pattern};
201 35 100       118 if ($parent_pattern) {
202 3         15 $head = $parent_pattern->build_path(%params);
203 3 50       11 $head .= q{/} unless substr($head, -1) eq q{/};
204             }
205              
206 35         115 my $path = $head . join q{/} => @parts;
207              
208 35 100 66     171 if ($path ne '/' && $trailing_slash) {
209 10         22 $path .= q{/};
210             }
211              
212 35         225 return $path;
213             }
214              
215             sub _match_method {
216 171     171   288 my $self = shift;
217 171         370 my ($value) = @_;
218              
219 171         313 my $methods = $self->{method};
220              
221 171 100       599 return 1 unless defined $methods;
222              
223 29 100       78 return unless defined $value;
224 22         39 $value = uc $value;
225              
226 22         32 return !!scalar grep { $_ eq $value } @{$methods};
  26         106  
  22         40  
227             }
228              
229             sub _prepare_pattern {
230 71     71   124 my $self = shift;
231              
232 71 50       237 return $self->{pattern} if ref $self->{pattern} eq 'Regexp';
233              
234 71         138 my $pattern = $self->{pattern};
235 71 100       409 if ($pattern !~ m{ \A ( / | \(/.+?\)\?/ ) }xms) {
236 4         11 $pattern = q{/} . $pattern;
237             }
238              
239 71         178 $self->{captures} = [];
240              
241 71         143 my $re = q{};
242 71         138 my $par_depth = 0;
243 71         125 my @parts;
244              
245             my $part;
246              
247 71         240 pos $pattern = 0;
248 71         301 while (pos $pattern < length $pattern) {
249 301 100       2502 if ($pattern =~ m{ \G \/ }gcxms) {
    100          
    100          
    100          
    100          
    100          
    50          
250 150 100       406 if ($part) {
251 79         162 push @parts, $part;
252             }
253              
254 150         276 $part = [];
255 150         307 $re .= q{/};
256             }
257             elsif ($pattern =~ m{ \G :($TOKEN) }gcxms) {
258 53         140 my $name = $1;
259 53         102 my $constraint;
260              
261 53 100       142 if (exists $self->{constraints}->{$name}) {
262 4         5 $constraint = $self->{constraints}->{$name};
263 4 100       12 if (ref $constraint eq 'ARRAY') {
264 1         3 $constraint = join('|', @$constraint);
265             }
266 4         10 $re .= "($constraint)";
267             }
268             else {
269 49         102 $re .= '([^\/]+)';
270             }
271              
272 53 100       360 push @$part,
273             { type => 'capture',
274             name => $name,
275             constraint => $constraint ? qr/^ $constraint $/xms : undef,
276             level => $par_depth
277             };
278              
279 53         141 push @{$self->{captures}}, $name;
  53         157  
280             }
281             elsif ($pattern =~ m{ \G \*($TOKEN) }gcxms) {
282 9         22 my $name = $1;
283              
284 9         24 $re .= '(.*)';
285              
286 9         37 push @$part, {type => 'glob', name => $name};
287              
288 9         16 push @{$self->{captures}}, $name;
  9         21  
289             }
290             elsif ($pattern =~ m{ \G ($TOKEN) }gcxms) {
291 61         205 my $text = $1;
292 61         158 $re .= quotemeta $text;
293              
294 61         257 push @$part, {type => 'text', text => $text};
295             }
296             elsif ($pattern =~ m{ \G \( }gcxms) {
297 16         38 $par_depth++;
298 16         32 $re .= '(?: ';
299 16         62 next;
300             }
301             elsif ($pattern =~ m{ \G \)\? }gcxms) {
302 11         33 $part->[-1]->{optional} = 1;
303 11         21 $par_depth--;
304 11         25 $re .= ' )?';
305 11         35 next;
306             }
307             elsif ($pattern =~ m{ \G \) }gcxms) {
308 1         3 $par_depth--;
309 1         2 $re .= ' )';
310 1         2 next;
311             }
312              
313 273 100 100     1329 if ($part->[-1] && substr($pattern, pos($pattern), 1) eq '/') {
314 70         243 $part->[-1]->{trailing_slash} = 1;
315             }
316             }
317              
318 71 100       245 if ($par_depth != 0) {
319 1         164 Carp::croak("Parentheses are not balanced in pattern '$pattern'");
320             }
321              
322 70 50 66     222 if (!$self->{strict_trailing_slash} && !$self->{subroutes}) {
323 6 100       22 if ($re =~ m{/$}) {
    100          
324 2         5 $re .= '?';
325             }
326             elsif ($re =~ m{\)\?$}) {
327 1         4 $re =~ s{\)\?$}{/?)?}
328             }
329             else {
330 3         6 $re .= '/?';
331             }
332             }
333              
334 70 100       186 if ($self->{subroutes}) {
335 13         34 $re = "^ $re";
336             }
337             else {
338 57         215 $re = "^ $re \$";
339             }
340              
341 70 100       192 if ($self->{strict_case}) {
342 68         1180 $re = qr/$re/xms;
343             }
344             else {
345 2         23 $re = qr/$re/xmsi;
346             }
347              
348 70 100 66     425 if ($part && @$part) {
349 43         93 push @parts, $part;
350             }
351              
352 70         205 $self->{parts} = [@parts];
353 70         163 $self->{pattern} = $re;
354              
355 70         177 return $self;
356             }
357              
358 92     92   157 sub _build_match { shift; return Routes::Tiny::Match->new(@_) }
  92         372  
359              
360             1;
361             __END__