File Coverage

blib/lib/Web/Dispatch/Parser.pm
Criterion Covered Total %
statement 115 126 91.2
branch 78 94 82.9
condition 21 29 72.4
subroutine 13 15 86.6
pod 0 2 0.0
total 227 266 85.3


line stmt bran cond sub pod time code
1             package Web::Dispatch::Parser;
2              
3             sub DEBUG () { 0 }
4              
5             BEGIN {
6 16 50   16   345 if ($ENV{WEB_DISPATCH_PARSER_DEBUG}) {
7 16     16   22950 no warnings 'redefine';
  16         34  
  16         10005  
8             *DEBUG = sub () { 1 }
9 0         0 }
10             }
11              
12 16     16   740 use Sub::Quote;
  16         14093  
  16         913  
13 16     16   7659 use Web::Dispatch::Predicates;
  16         40  
  16         1526  
14 16     16   825 use Moo;
  16         4178  
  16         87  
15              
16             has _cache => (
17             is => 'lazy', default => quote_sub q{ {} }
18             );
19              
20 0     0 0 0 sub diag { if (DEBUG) { warn $_[0] } }
21              
22             sub _wtf {
23 0     0   0 my ($self, $error) = @_;
24 0   0     0 my $hat = (' ' x (pos||0)).'^';
25 0         0 warn "Warning parsing dispatch specification: ${error}\n
26             ${_}
27             ${hat} here\n";
28             }
29              
30             sub _blam {
31 1     1   2 my ($self, $error) = @_;
32 1   50     5 my $hat = (' ' x (pos||0)).'^';
33 1         32 die "Error parsing dispatch specification: ${error}\n
34             ${_}
35             ${hat} here\n";
36             }
37              
38             sub parse {
39 252     252 0 135353 my ($self, $spec) = @_;
40 252         579 $spec =~ s/\s+//g; # whitespace is not valid
41 252   66     6176 return $self->_cache->{$spec} ||= $self->_parse_spec($spec);
42             }
43              
44             sub _parse_spec {
45 146     146   7208 my ($self, $spec, $nested) = @_;
46 146 100       349 return match_true() unless length($spec);
47 143         347 for ($_[1]) {
48 143         377 my @match;
49             my $close;
50 143         158 PARSE: { do {
  143         171  
51 212 50       504 push @match, $self->_parse_spec_section($_)
52             or $self->_blam("Unable to work out what the next section is");
53 211 100       568 if (/\G\)/gc) {
54 3 50       6 $self->_blam("Found closing ) with no opening (") unless $nested;
55 3         7 $close = 1;
56 3         5 last PARSE;
57             }
58 208 100       551 last PARSE if (pos == length);
59 73 50       184 $match[-1] = $self->_parse_spec_combinator($_, $match[-1])
60             or $self->_blam('No valid combinator - expected + or |');
61             } until (pos == length) }; # accept trailing whitespace
62 142 100 100     679 if (!$close and $nested and pos == length) {
      66        
63 1         4 pos = $nested - 1;
64 1         5 $self->_blam("No closing ) found for opening (");
65             }
66 141 100       632 return $match[0] if (@match == 1);
67 67         195 return match_and(@match);
68             }
69             }
70              
71             sub _parse_spec_combinator {
72 73     73   119 my ($self, $spec, $match) = @_;
73 73         124 for ($_[1]) {
74              
75 73 100       454 /\G\+/gc and
76             return $match;
77              
78             /\G\|/gc and
79 4 50       12 return do {
80 4         13 my @match = $match;
81 4         5 PARSE: { do {
  4         6  
82 5 50       11 push @match, $self->_parse_spec_section($_)
83             or $self->_blam("Unable to work out what the next section is");
84 5 100       14 last PARSE if (pos == length);
85 1 50       7 last PARSE unless /\G\|/gc; # give up when next thing isn't |
86             } until (pos == length) }; # accept trailing whitespace
87 4         13 return match_or(@match);
88             };
89             }
90 0         0 return;
91             }
92              
93             sub _parse_spec_section {
94 218     218   284 my ($self) = @_;
95 218         346 for ($_[1]) {
96              
97             # ~
98              
99 218 100       564 /\G~/gc and
100             return match_path('^$');
101              
102             # GET POST PUT HEAD ...
103              
104 216 100       642 /\G([A-Z]+)/gc and
105             return match_method($1);
106              
107             # /...
108              
109 191 100       865 /\G(?=\/)/gc and
110             return $self->_url_path_match($_);
111              
112             # .* and .html
113              
114 97 100       214 /\G\.(\*|\w+)/gc and
115             return match_extension($1);
116              
117             # (...)
118              
119 92 100       207 /\G\(/gc and
120             return $self->_parse_spec($_, pos);
121              
122             # !something
123              
124 88 100       191 /\G!/gc and
125             return match_not($self->_parse_spec_section($_));
126              
127             # ?
128 87 100       368 /\G\?/gc and
129             return $self->_parse_param_handler($_, 'query');
130              
131             # %
132 5 100       29 /\G\%/gc and
133             return $self->_parse_param_handler($_, 'body');
134              
135             # *
136 1 50       8 /\G\*/gc and
137             return $self->_parse_param_handler($_, 'uploads');
138             }
139 0         0 return; # () will trigger the blam in our caller
140             }
141              
142             sub _url_path_match {
143 94     94   145 my ($self) = @_;
144 94         147 for ($_[1]) {
145 94         142 my (@path, @names, $seen_nameless);
146 94         112 my $end = '';
147 94         97 my $keep_dot;
148 94         323 PATH: while (/\G\//gc) {
149             /\G\.\.\./gc
150 192 100       402 and do {
151 4         9 $end = '(/.*)';
152 4         10 last PATH;
153             };
154              
155 188 50       454 my ($segment) = $self->_url_path_segment_match($_)
156             or $self->_blam("Couldn't parse path match segment");
157              
158 188 100       373 if (ref($segment)) {
159 17         38 ($segment, $keep_dot, my $name) = @$segment;
160 17 100       46 if (defined($name)) {
161 6 50       11 $self->_blam("Can't mix positional and named captures in path match")
162             if $seen_nameless;
163 6         11 push @names, $name;
164             } else {
165 11 50       25 $self->_blam("Can't mix positional and named captures in path match")
166             if @names;
167 11         18 $seen_nameless = 1;
168             }
169             }
170 188         259 push @path, $segment;
171              
172             /\G\.\.\./gc
173 188 100       361 and do {
174 4         9 $end = '(|/.*)';
175 4         9 last PATH;
176             };
177 184 100       322 /\G\.\*/gc
178             and $keep_dot = 1;
179              
180 184 100       595 last PATH if $keep_dot;
181             }
182 94 100 100     620 if (@path && !$end && !$keep_dot) {
      100        
183 83   66     320 length and $_ .= '(?:\.\w+)?' for $path[-1];
184             }
185 94         288 my $re = '^('.join('/','',@path).')'.$end.'$';
186 94         939 $re = qr/$re/;
187 94 100       208 if ($end) {
188 8 50       48 return match_path_strip($re, @names ? \@names : ());
189             } else {
190 86 100       339 return match_path($re, @names ? \@names : ());
191             }
192             }
193 0         0 return;
194             }
195              
196             sub _url_path_segment_match {
197 188     188   220 my ($self) = @_;
198 188         272 for ($_[1]) {
199             # trailing / -> require / on end of URL
200 188 100       543 /\G(?:(?=[+|\)])|$)/gc and
201             return '';
202             # word chars only -> exact path part match
203 158 100       1059 /
204             \G(
205             (?: # start matching at a space followed by:
206             [\w\-] # word chars or dashes
207             | # OR
208             \. # a period
209             (?!\.) # not followed by another period
210             )
211             + # then grab as far as possible
212             )
213             /gcx and
214             return "\Q$1";
215             # ** -> capture unlimited path parts
216 17 100       69 /\G\*\*(?:(\.\*)?\:(\w+))?/gc and
217             return [ '(.*?[^/])', $1, $2 ];
218             # * -> capture path part
219             # *:name -> capture named path part
220 12 100       84 /\G\*(?:(\.\*)?\:(\w+))?/gc and
221             return [ '([^/]+?)', $1, $2 ];
222              
223             # :name -> capture named path part
224 3 50       26 /\G\:(\w+)/gc and
225             return [ '([^/]+?)', 0, $1 ];
226             }
227 0         0 return ();
228             }
229              
230             sub _parse_param_handler {
231 87     87   152 my ($self, $spec, $type) = @_;
232              
233 87         149 for ($_[1]) {
234 87         98 my (@required, @single, %multi, $star, $multistar, %positional, $have_kw);
235 0         0 my %spec;
236 87         109 my $pos_idx = 0;
237 87         94 PARAM: { do {
  87         288  
238              
239             # ?:foo or ?@:foo
240              
241 110         204 my $is_kw = /\G\:/gc;
242              
243             # ?@foo or ?@*
244              
245 110         179 my $multi = /\G\@/gc;
246              
247             # @* or *
248              
249 110 100       224 if (/\G\*/gc) {
250              
251 17 50       51 $self->_blam("* is always named; no need to supply :") if $is_kw;
252              
253 17 50       34 if ($star) {
254 0         0 $self->_blam("Can only use one * or \@* in a parameter match");
255             }
256              
257 17         86 $spec{star} = { multi => $multi };
258             } else {
259              
260             # @foo= or foo= or @foo~ or foo~
261              
262 93 50       284 /\G([\w.]*)/gc or $self->_blam('Expected parameter name');
263              
264 93         178 my $name = $1;
265              
266             # check for = or ~ on the end
267              
268             /\G\=/gc
269 93 100 100     254 ? push(@{$spec{required}||=[]}, $name)
  79   33     401  
270             : (/\G\~/gc or $self->_blam('Expected = or ~ after parameter name'));
271              
272             # record positional or keyword
273              
274 93 100 100     339 push @{$spec{$is_kw ? 'named' : 'positional'}||=[]},
  93         813  
275             { name => $name, multi => $multi };
276             }
277             } while (/\G\&/gc) }
278              
279 87         660 return Web::Dispatch::Predicates->can("match_${type}")->(\%spec);
280             }
281             }
282              
283             1;