added match_true and match_false
[catagits/Web-Simple.git] / lib / Web / Dispatch / Parser.pm
CommitLineData
d63bcdae 1package Web::Dispatch::Parser;
920d6222 2
a5917caa 3sub DEBUG () { 0 }
4
5BEGIN {
d63bcdae 6 if ($ENV{WEB_DISPATCH_PARSER_DEBUG}) {
a5917caa 7 no warnings 'redefine';
8 *DEBUG = sub () { 1 }
9 }
10}
11
d63bcdae 12use Sub::Quote;
13use Web::Dispatch::Predicates;
14use Moo;
15
16has _cache => (
17 is => 'lazy', default => quote_sub q{ {} }
18);
a5917caa 19
d63bcdae 20sub diag { if (DEBUG) { warn $_[0] } }
920d6222 21
22sub _blam {
23 my ($self, $error) = @_;
a4ec359d 24 my $hat = (' ' x (pos||0)).'^';
920d6222 25 die "Error parsing dispatch specification: ${error}\n
26${_}
27${hat} here\n";
28}
29
d63bcdae 30sub parse {
920d6222 31 my ($self, $spec) = @_;
69aaa28a 32 $spec =~ s/\s+//g; # whitespace is not valid
d63bcdae 33 return $self->_cache->{$spec} ||= $self->_parse_spec($spec);
c6ea9542 34}
35
36sub _parse_spec {
b0420ad6 37 my ($self, $spec, $nested) = @_;
bc878dde 38 return match_true() unless length($spec);
c6ea9542 39 for ($_[1]) {
920d6222 40 my @match;
920d6222 41 PARSE: { do {
c6ea9542 42 push @match, $self->_parse_spec_section($_)
920d6222 43 or $self->_blam("Unable to work out what the next section is");
b0420ad6 44 if (/\G\)/gc) {
45 $self->_blam("Found closing ) with no opening (") unless $nested;
46 last PARSE;
47 }
920d6222 48 last PARSE if (pos == length);
c6ea9542 49 $match[-1] = $self->_parse_spec_combinator($_, $match[-1])
50 or $self->_blam('No valid combinator - expected + or |');
920d6222 51 } until (pos == length) }; # accept trailing whitespace
b0420ad6 52 if ($nested and pos == length) {
a4ec359d 53 pos = $nested - 1;
b0420ad6 54 $self->_blam("No closing ) found for opening (");
55 }
920d6222 56 return $match[0] if (@match == 1);
d63bcdae 57 return match_and(@match);
920d6222 58 }
59}
60
9b9866ae 61sub _parse_spec_combinator {
62 my ($self, $spec, $match) = @_;
63 for ($_[1]) {
64
65 /\G\+/gc and
66 return $match;
67
68 /\G\|/gc and
69 return do {
70 my @match = $match;
71 PARSE: { do {
72 push @match, $self->_parse_spec_section($_)
73 or $self->_blam("Unable to work out what the next section is");
74 last PARSE if (pos == length);
75 last PARSE unless /\G\|/gc; # give up when next thing isn't |
76 } until (pos == length) }; # accept trailing whitespace
d63bcdae 77 return match_or(@match);
9b9866ae 78 };
79 }
80 return;
81}
82
920d6222 83sub _parse_spec_section {
84 my ($self) = @_;
85 for ($_[1]) {
86
a96dd5cb 87 # ~
88
89 /\G~/gc and
90 return match_path('^$');
91
920d6222 92 # GET POST PUT HEAD ...
93
94 /\G([A-Z]+)/gc and
e954644c 95 return match_method($1);
920d6222 96
97 # /...
98
99 /\G(?=\/)/gc and
100 return $self->_url_path_match($_);
101
c6ea9542 102 # .* and .html
103
104 /\G\.(\*|\w+)/gc and
e954644c 105 return match_extension($1);
b0420ad6 106
2ee4ab06 107 # (...)
b0420ad6 108
109 /\G\(/gc and
110 return $self->_parse_spec($_, pos);
2ee4ab06 111
112 # !something
113
114 /\G!/gc and
ce573717 115 return match_not($self->_parse_spec_section($_));
920d6222 116
9b9866ae 117 # ?<param spec>
118 /\G\?/gc and
119 return $self->_parse_param_handler($_, 'query');
53d47b78 120
121 # %<param spec>
122 /\G\%/gc and
123 return $self->_parse_param_handler($_, 'body');
05aafc1a 124
125 # *<param spec>
126 /\G\*/gc and
127 return $self->_parse_param_handler($_, 'uploads');
c6ea9542 128 }
9b9866ae 129 return; # () will trigger the blam in our caller
c6ea9542 130}
131
920d6222 132sub _url_path_match {
133 my ($self) = @_;
920d6222 134 for ($_[1]) {
135 my @path;
e7dd1c4b 136 my $end = '';
15e679c1 137 my $keep_dot;
da8429c9 138 PATH: while (/\G\//gc) {
139 /\G\.\.\./gc
140 and do {
e7dd1c4b 141 $end = '(/.*)';
da8429c9 142 last PATH;
143 };
920d6222 144 push @path, $self->_url_path_segment_match($_)
145 or $self->_blam("Couldn't parse path match segment");
8c51c01a 146 /\G\.\.\./gc
147 and do {
bb0dbe7c 148 $end = '(|/.*)';
8c51c01a 149 last PATH;
150 };
15e679c1 151 /\G\.\*/gc
152 and do {
153 $keep_dot = 1;
154 last PATH;
155 };
920d6222 156 }
15e679c1 157 if (@path && !$end && !$keep_dot) {
4ed4fb42 158 length and $_ .= '(?:\.\w+)?' for $path[-1];
159 }
e7dd1c4b 160 my $re = '^('.join('/','',@path).')'.$end.'$';
da8429c9 161 $re = qr/$re/;
e7dd1c4b 162 if ($end) {
163 return match_path_strip($re);
164 } else {
d63bcdae 165 return match_path($re);
da8429c9 166 }
920d6222 167 }
168 return;
169}
170
171sub _url_path_segment_match {
172 my ($self) = @_;
173 for ($_[1]) {
174 # trailing / -> require / on end of URL
6c0f599a 175 /\G(?:(?=[+|\)])|$)/gc and
e7dd1c4b 176 return '';
920d6222 177 # word chars only -> exact path part match
c2cf0534 178 /
179 \G(
180 (?: # start matching at a space followed by:
181 [\w\-] # word chars or dashes
182 | # OR
183 \. # a period
184 (?!\.) # not followed by another period
185 )
186 + # then grab as far as possible
187 )
188 /gcx and
920d6222 189 return "\Q$1";
28f3dfd5 190 # ** -> capture unlimited path parts
191 /\G\*\*/gc and
192 return '(.*?[^/])';
920d6222 193 # * -> capture path part
194 /\G\*/gc and
4ed4fb42 195 return '([^/]+?)';
920d6222 196 }
197 return ();
198}
199
9b9866ae 200sub _parse_param_handler {
201 my ($self, $spec, $type) = @_;
202
9b9866ae 203 for ($_[1]) {
eb9e0e25 204 my (@required, @single, %multi, $star, $multistar, %positional, $have_kw);
052bdd54 205 my %spec;
eb9e0e25 206 my $pos_idx = 0;
9b9866ae 207 PARAM: { do {
208
eb9e0e25 209 # ?:foo or ?@:foo
9b9866ae 210
eb9e0e25 211 my $is_kw = /\G\:/gc;
9b9866ae 212
213 # ?@foo or ?@*
214
eb9e0e25 215 my $multi = /\G\@/gc;
9b9866ae 216
217 # @* or *
218
a5917caa 219 if (/\G\*/gc) {
9b9866ae 220
eb9e0e25 221 $self->_blam("* is always named; no need to supply :") if $is_kw;
222
052bdd54 223 if ($star) {
224 $self->_blam("Can only use one * or \@* in a parameter match");
a5917caa 225 }
052bdd54 226
227 $spec{star} = { multi => $multi };
9b9866ae 228 } else {
229
230 # @foo= or foo= or @foo~ or foo~
6c0f599a 231
9b9866ae 232 /\G(\w+)/gc or $self->_blam('Expected parameter name');
233
234 my $name = $1;
235
236 # check for = or ~ on the end
237
238 /\G\=/gc
052bdd54 239 ? push(@{$spec{required}||=[]}, $name)
9b9866ae 240 : (/\G\~/gc or $self->_blam('Expected = or ~ after parameter name'));
241
eb9e0e25 242 # record positional or keyword
243
052bdd54 244 push @{$spec{$is_kw ? 'named' : 'positional'}||=[]},
245 { name => $name, multi => $multi };
9b9866ae 246 }
247 } while (/\G\&/gc) }
248
b6bf9ed3 249 return Web::Dispatch::Predicates->can("match_${type}")->(\%spec);
9b9866ae 250 }
251}
252
920d6222 2531;