File Coverage

blib/lib/Gears/Router/Pattern/SigilMatch.pm
Criterion Covered Total %
statement 93 94 98.9
branch 23 28 82.1
condition 9 10 90.0
subroutine 12 12 100.0
pod 2 3 66.6
total 139 147 94.5


line stmt bran cond sub pod time code
1             package Gears::Router::Pattern::SigilMatch;
2             $Gears::Router::Pattern::SigilMatch::VERSION = '0.101';
3 2     2   22 use v5.40;
  2         6  
4 2     2   8 use Mooish::Base -standard;
  2         2  
  2         13  
5              
6 2     2   20027 use Gears::X;
  2         9  
  2         98  
7 2     2   1433 use URI::Escape;
  2         3997  
  2         3019  
8              
9             extends 'Gears::Router::Pattern';
10              
11             has extended 'location' => (
12             handles => [
13             qw(
14             checks
15             defaults
16             )
17             ],
18             );
19              
20             has field '_regex' => (
21             isa => RegexpRef,
22             lazy => 1,
23             );
24              
25             has field 'tokens' => (
26             isa => ArrayRef,
27             default => sub { [] },
28             );
29              
30             # helpers for matching different types of wildcards
31             my sub noslash ($sigil)
32 61     61   89 {
  61         101  
  61         97  
33 61         132 return 1 == grep { $sigil eq $_ } ':', '?';
  122         384  
34             }
35              
36             my sub matchall ($sigil)
37 11     11   16 {
  11         20  
  11         21  
38 11         20 return 1 == grep { $sigil eq $_ } '*', '>';
  22         60  
39             }
40              
41             my sub optional ($sigil)
42 56     56   91 {
  56         98  
  56         92  
43 56         109 return 1 == grep { $sigil eq $_ } '?', '>';
  112         295  
44             }
45              
46             sub _rep_regex
47             {
48 52     52   277 my ($self, $char, $switch, $token, $out) = @_;
49 52         107 my $qchar = quotemeta $char;
50 52         75 my $re;
51              
52 52         234 push $self->tokens->@*, {
53             sigil => $switch,
54             label => $token,
55             };
56              
57 52         169 my ($prefix, $suffix) = ("(?<$token>", ')');
58 52 100       128 if (noslash($switch)) {
    50          
59 41   100     941 $re = $qchar . $prefix . ($self->checks->{$token} // '[^\/]+') . $suffix;
60             }
61             elsif (matchall($switch)) {
62 11   100     219 $re = $qchar . $prefix . ($self->checks->{$token} // '.+') . $suffix;
63             }
64              
65 52 100       1718 if (optional($switch)) {
66 15 100       49 $re = "(?:$re)" if $char eq '/';
67 15         32 $re .= '?';
68             }
69              
70 52         115 push $out->@*, $re;
71 52         249 return '{}';
72             }
73              
74             sub _build_regex ($self)
75 33     33   318 {
  33         60  
  33         49  
76 33         673 my $pattern = $self->pattern;
77              
78 33         1449 my $placeholder_pattern = qr{
79             ( [^\0]? ) # preceding char, may change behavior of some placeholders
80             ( [:*?>] ) # placeholder sigil
81             ( \w+ ) # placeholder label
82             }x;
83              
84             # Curly braces and brackets are only used for separation.
85             # We replace all of them with \0, then convert the pattern
86             # into a regular expression. This way if the regular expression
87             # contains curlies, they won't be removed.
88 33         116 $pattern =~ s/[{}]/\0/g;
89              
90 33         80 my @rep_regex_parts;
91 33         459 $pattern =~ s{
92             $placeholder_pattern
93             }{
94 52         185 $self->_rep_regex($1, $2, $3, \@rep_regex_parts)
95             }egx;
96              
97             # Now remove all curlies remembered as \0 - We will use curlies again for
98             # special behavior in a moment
99 33         128 $pattern =~ s/\0//g;
100              
101             # remember if the pattern has a trailing slash before we quote it
102 33         107 my $trailing_slash = $pattern =~ m{/$};
103              
104             # _rep_regex reused curies for {} placeholders, so we want to split the
105             # string by that (and include them in the result by capturing the
106             # separator)
107 33         204 my @parts = split /(\Q{}\E)/, $pattern, -1;
108              
109             # If we have a placeholder, replace it with next part. If not, quote it to
110             # avoid misusing regex in patterns.
111 33         83 foreach my $part (@parts) {
112 137 100       248 if ($part eq '{}') {
113 52         103 $part = shift @rep_regex_parts;
114             }
115             else {
116 85         159 $part = quotemeta $part;
117             }
118             }
119              
120 33         1088 $pattern = join '', @parts;
121 33 50       771 if ($self->is_bridge) {
122              
123             # bridge must be followed by a slash or end of string, so that:
124             # - /test matches
125             # - /test/ matches
126             # - /test/something matches
127             # - /testsomething does not match
128             # if the bridge is already followed by a trailing slash, it's not a
129             # concern
130 0 0       0 $pattern .= '(?:/|$)' unless $trailing_slash;
131             }
132             else {
133              
134             # regular pattern must end immediately
135 33 50       128 $pattern .= quotemeta('/') . '?' unless $trailing_slash;
136 33         67 $pattern .= '$';
137             }
138              
139 33         2192 return qr{^$pattern};
140             }
141              
142 33         85 sub BUILD ($self, $)
143 33     33 0 842 {
  33         54  
144 33         623 $self->_regex; # ensure tokens are created
145             }
146              
147 150         203 sub compare ($self, $request_path)
148 150     150 1 2851 {
  150         217  
  150         193  
149 150 100       3536 return undef unless $request_path =~ $self->_regex;
150              
151             # initialize the named parameters hash and its default values
152 97         2733 my %named = ($self->defaults->%*, %+);
153              
154             # transform into a list of parameters
155 97         4714 return [map { $named{$_->{label}} } $self->tokens->@*];
  174         848  
156             }
157              
158 12         20 sub build ($self, %args)
159 12     12 1 840 {
  12         24  
  12         20  
160 12         273 my $pattern = $self->pattern;
161 12         649 my $checks = $self->checks;
162 12         642 %args = ($self->defaults->%*, %args);
163              
164 12         433 foreach my $token ($self->tokens->@*) {
165 13         37 my $value = $args{$token->{label}};
166              
167             Gears::X->raise("no value for placeholder $token->{sigil}$token->{label}")
168 13 100 100     52 unless defined $value || optional $token->{sigil};
169              
170 12 100       33 if (defined $value) {
171 9         18 my $safe = '^A-Za-z0-9\-\._~';
172 9 100       31 $safe .= '/' unless noslash $token->{sigil};
173 9         40 $value = uri_escape_utf8 $value, "^$safe";
174             }
175              
176 12         849 my $to_replace = qr{
177             \Q$token->{sigil}\E
178             $token->{label}
179             }x;
180              
181 12 100       42 if (defined $value) {
182 9         22 my $check = $checks->{$token->{label}};
183 9 100 66     61 Gears::X->raise("bad value for placeholder $token->{sigil}$token->{label}")
184             if $check && $value !~ /^$check$/;
185              
186 7         205 $pattern =~ s{\{?$to_replace\}?}{$value};
187             }
188             else {
189             # slash should be removed as well for optional placeholders (if no brackets)
190 3         100 $pattern =~ s{/$to_replace|\{?$to_replace\}?}{};
191             }
192             }
193              
194 9         74 return $pattern;
195             }
196              
197             __END__