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 14 50   14   255 if ($ENV{WEB_DISPATCH_PARSER_DEBUG}) {
7 14     14   23110 no warnings 'redefine';
  14         20  
  14         817  
8             *DEBUG = sub () { 1 }
9 0         0 }
10             }
11              
12 14     14   767 use Sub::Quote;
  14         6470  
  14         704  
13 14     14   4533 use Web::Dispatch::Predicates;
  14         23  
  14         1131  
14 14     14   888 use Moo;
  14         21909  
  14         70  
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     10 my $hat = (' ' x (pos||0)).'^';
33 1         33 die "Error parsing dispatch specification: ${error}\n
34             ${_}
35             ${hat} here\n";
36             }
37              
38             sub parse {
39 250     250 0 182511 my ($self, $spec) = @_;
40 250         627 $spec =~ s/\s+//g; # whitespace is not valid
41 250   66     5917 return $self->_cache->{$spec} ||= $self->_parse_spec($spec);
42             }
43              
44             sub _parse_spec {
45 145     145   1886 my ($self, $spec, $nested) = @_;
46 145 100       471 return match_true() unless length($spec);
47 142         314 for ($_[1]) {
48 142         167 my @match;
49             my $close;
50 142         372 PARSE: { do {
  142         160  
51 211 50       502 push @match, $self->_parse_spec_section($_)
52             or $self->_blam("Unable to work out what the next section is");
53 210 100       557 if (/\G\)/gc) {
54 3 50       7 $self->_blam("Found closing ) with no opening (") unless $nested;
55 3         4 $close = 1;
56 3         8 last PARSE;
57             }
58 207 100       607 last PARSE if (pos == length);
59 73 50       210 $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 141 100 100     754 if (!$close and $nested and pos == length) {
      66        
63 1         4 pos = $nested - 1;
64 1         8 $self->_blam("No closing ) found for opening (");
65             }
66 140 100       770 return $match[0] if (@match == 1);
67 67         229 return match_and(@match);
68             }
69             }
70              
71             sub _parse_spec_combinator {
72 73     73   138 my ($self, $spec, $match) = @_;
73 73         140 for ($_[1]) {
74              
75 73 100       590 /\G\+/gc and
76             return $match;
77              
78             /\G\|/gc and
79 4 50       20 return do {
80 4         11 my @match = $match;
81 4         6 PARSE: { do {
  4         6  
82 5 50       12 push @match, $self->_parse_spec_section($_)
83             or $self->_blam("Unable to work out what the next section is");
84 5 100       20 last PARSE if (pos == length);
85 1 50       10 last PARSE unless /\G\|/gc; # give up when next thing isn't |
86             } until (pos == length) }; # accept trailing whitespace
87 4         14 return match_or(@match);
88             };
89             }
90 0         0 return;
91             }
92              
93             sub _parse_spec_section {
94 217     217   252 my ($self) = @_;
95 217         294 for ($_[1]) {
96              
97             # ~
98              
99 217 100       582 /\G~/gc and
100             return match_path('^$');
101              
102             # GET POST PUT HEAD ...
103              
104 215 100       796 /\G([A-Z]+)/gc and
105             return match_method($1);
106              
107             # /...
108              
109 191 100       827 /\G(?=\/)/gc and
110             return $self->_url_path_match($_);
111              
112             # .* and .html
113              
114 97 100       304 /\G\.(\*|\w+)/gc and
115             return match_extension($1);
116              
117             # (...)
118              
119 92 100       245 /\G\(/gc and
120             return $self->_parse_spec($_, pos);
121              
122             # !something
123              
124 88 100       280 /\G!/gc and
125             return match_not($self->_parse_spec_section($_));
126              
127             # ?
128 87 100       492 /\G\?/gc and
129             return $self->_parse_param_handler($_, 'query');
130              
131             # %
132 5 100       19 /\G\%/gc and
133             return $self->_parse_param_handler($_, 'body');
134              
135             # *
136 1 50       6 /\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   144 my ($self) = @_;
144 94         156 for ($_[1]) {
145 94         115 my (@path, @names, $seen_nameless);
146 94         117 my $end = '';
147 94         77 my $keep_dot;
148 94         327 PATH: while (/\G\//gc) {
149             /\G\.\.\./gc
150 192 100       403 and do {
151 4         8 $end = '(/.*)';
152 4         12 last PATH;
153             };
154              
155 188 50       412 my ($segment) = $self->_url_path_segment_match($_)
156             or $self->_blam("Couldn't parse path match segment");
157              
158 188 100       398 if (ref($segment)) {
159 17         38 ($segment, $keep_dot, my $name) = @$segment;
160 17 100       112 if (defined($name)) {
161 6 50       14 $self->_blam("Can't mix positional and named captures in path match")
162             if $seen_nameless;
163 6         12 push @names, $name;
164             } else {
165 11 50       33 $self->_blam("Can't mix positional and named captures in path match")
166             if @names;
167 11         21 $seen_nameless = 1;
168             }
169             }
170 188         293 push @path, $segment;
171              
172             /\G\.\.\./gc
173 188 100       431 and do {
174 4         6 $end = '(|/.*)';
175 4         8 last PATH;
176             };
177 184 100       337 /\G\.\*/gc
178             and $keep_dot = 1;
179              
180 184 100       654 last PATH if $keep_dot;
181             }
182 94 100 100     660 if (@path && !$end && !$keep_dot) {
      100        
183 83   66     309 length and $_ .= '(?:\.\w+)?' for $path[-1];
184             }
185 94         362 my $re = '^('.join('/','',@path).')'.$end.'$';
186 94         1132 $re = qr/$re/;
187 94 100       210 if ($end) {
188 8 50       45 return match_path_strip($re, @names ? \@names : ());
189             } else {
190 86 100       499 return match_path($re, @names ? \@names : ());
191             }
192             }
193 0         0 return;
194             }
195              
196             sub _url_path_segment_match {
197 188     188   273 my ($self) = @_;
198 188         245 for ($_[1]) {
199             # trailing / -> require / on end of URL
200 188 100       581 /\G(?:(?=[+|\)])|$)/gc and
201             return '';
202             # word chars only -> exact path part match
203 158 100       1076 /
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       87 /\G\*\*(?:(\.\*)?\:(\w+))?/gc and
217             return [ '(.*?[^/])', $1, $2 ];
218             # * -> capture path part
219             # *:name -> capture named path part
220 12 100       166 /\G\*(?:(\.\*)?\:(\w+))?/gc and
221             return [ '([^/]+?)', $1, $2 ];
222              
223             # :name -> capture named path part
224 3 50       35 /\G\:(\w+)/gc and
225             return [ '([^/]+?)', 0, $1 ];
226             }
227 0         0 return ();
228             }
229              
230             sub _parse_param_handler {
231 87     87   171 my ($self, $spec, $type) = @_;
232              
233 87         155 for ($_[1]) {
234 87         112 my (@required, @single, %multi, $star, $multistar, %positional, $have_kw);
235 0         0 my %spec;
236 87         106 my $pos_idx = 0;
237 87         117 PARAM: { do {
  87         95  
238              
239             # ?:foo or ?@:foo
240              
241 110         269 my $is_kw = /\G\:/gc;
242              
243             # ?@foo or ?@*
244              
245 110         191 my $multi = /\G\@/gc;
246              
247             # @* or *
248              
249 110 100       286 if (/\G\*/gc) {
250              
251 17 50       48 $self->_blam("* is always named; no need to supply :") if $is_kw;
252              
253 17 50       47 if ($star) {
254 0         0 $self->_blam("Can only use one * or \@* in a parameter match");
255             }
256              
257 17         110 $spec{star} = { multi => $multi };
258             } else {
259              
260             # @foo= or foo= or @foo~ or foo~
261              
262 93 50       349 /\G([\w.]*)/gc or $self->_blam('Expected parameter name');
263              
264 93         227 my $name = $1;
265              
266             # check for = or ~ on the end
267              
268             /\G\=/gc
269 93 100 100     362 ? push(@{$spec{required}||=[]}, $name)
  79   33     537  
270             : (/\G\~/gc or $self->_blam('Expected = or ~ after parameter name'));
271              
272             # record positional or keyword
273              
274 93 100 100     99 push @{$spec{$is_kw ? 'named' : 'positional'}||=[]},
  93         1022  
275             { name => $name, multi => $multi };
276             }
277             } while (/\G\&/gc) }
278              
279 87         1002 return Web::Dispatch::Predicates->can("match_${type}")->(\%spec);
280             }
281             }
282              
283             1;