File Coverage

blib/lib/Routes/Tiny/Pattern.pm
Criterion Covered Total %
statement 188 189 99.4
branch 105 116 90.5
condition 28 41 68.2
subroutine 11 11 100.0
pod 5 5 100.0
total 337 362 93.0


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