File Coverage

blib/lib/Routes/Tiny/Pattern.pm
Criterion Covered Total %
statement 188 189 99.4
branch 105 116 90.5
condition 32 41 78.0
subroutine 11 11 100.0
pod 5 5 100.0
total 341 362 94.2


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