added failing todo test for empty dispatch prototype
[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) = @_;
c6ea9542 38 for ($_[1]) {
920d6222 39 my @match;
920d6222 40 PARSE: { do {
c6ea9542 41 push @match, $self->_parse_spec_section($_)
920d6222 42 or $self->_blam("Unable to work out what the next section is");
b0420ad6 43 if (/\G\)/gc) {
44 $self->_blam("Found closing ) with no opening (") unless $nested;
45 last PARSE;
46 }
920d6222 47 last PARSE if (pos == length);
c6ea9542 48 $match[-1] = $self->_parse_spec_combinator($_, $match[-1])
49 or $self->_blam('No valid combinator - expected + or |');
920d6222 50 } until (pos == length) }; # accept trailing whitespace
b0420ad6 51 if ($nested and pos == length) {
a4ec359d 52 pos = $nested - 1;
b0420ad6 53 $self->_blam("No closing ) found for opening (");
54 }
920d6222 55 return $match[0] if (@match == 1);
d63bcdae 56 return match_and(@match);
920d6222 57 }
58}
59
9b9866ae 60sub _parse_spec_combinator {
61 my ($self, $spec, $match) = @_;
62 for ($_[1]) {
63
64 /\G\+/gc and
65 return $match;
66
67 /\G\|/gc and
68 return do {
69 my @match = $match;
70 PARSE: { do {
71 push @match, $self->_parse_spec_section($_)
72 or $self->_blam("Unable to work out what the next section is");
73 last PARSE if (pos == length);
74 last PARSE unless /\G\|/gc; # give up when next thing isn't |
75 } until (pos == length) }; # accept trailing whitespace
d63bcdae 76 return match_or(@match);
9b9866ae 77 };
78 }
79 return;
80}
81
920d6222 82sub _parse_spec_section {
83 my ($self) = @_;
84 for ($_[1]) {
85
86 # GET POST PUT HEAD ...
87
88 /\G([A-Z]+)/gc and
e954644c 89 return match_method($1);
920d6222 90
91 # /...
92
93 /\G(?=\/)/gc and
94 return $self->_url_path_match($_);
95
c6ea9542 96 # .* and .html
97
98 /\G\.(\*|\w+)/gc and
e954644c 99 return match_extension($1);
b0420ad6 100
2ee4ab06 101 # (...)
b0420ad6 102
103 /\G\(/gc and
104 return $self->_parse_spec($_, pos);
2ee4ab06 105
106 # !something
107
108 /\G!/gc and
ce573717 109 return match_not($self->_parse_spec_section($_));
920d6222 110
9b9866ae 111 # ?<param spec>
112 /\G\?/gc and
113 return $self->_parse_param_handler($_, 'query');
53d47b78 114
115 # %<param spec>
116 /\G\%/gc and
117 return $self->_parse_param_handler($_, 'body');
05aafc1a 118
119 # *<param spec>
120 /\G\*/gc and
121 return $self->_parse_param_handler($_, 'uploads');
c6ea9542 122 }
9b9866ae 123 return; # () will trigger the blam in our caller
c6ea9542 124}
125
920d6222 126sub _url_path_match {
127 my ($self) = @_;
920d6222 128 for ($_[1]) {
129 my @path;
e7dd1c4b 130 my $end = '';
15e679c1 131 my $keep_dot;
da8429c9 132 PATH: while (/\G\//gc) {
133 /\G\.\.\./gc
134 and do {
e7dd1c4b 135 $end = '(/.*)';
da8429c9 136 last PATH;
137 };
920d6222 138 push @path, $self->_url_path_segment_match($_)
139 or $self->_blam("Couldn't parse path match segment");
15e679c1 140 /\G\.\*/gc
141 and do {
142 $keep_dot = 1;
143 last PATH;
144 };
920d6222 145 }
15e679c1 146 if (@path && !$end && !$keep_dot) {
4ed4fb42 147 length and $_ .= '(?:\.\w+)?' for $path[-1];
148 }
e7dd1c4b 149 my $re = '^('.join('/','',@path).')'.$end.'$';
da8429c9 150 $re = qr/$re/;
e7dd1c4b 151 if ($end) {
152 return match_path_strip($re);
153 } else {
d63bcdae 154 return match_path($re);
da8429c9 155 }
920d6222 156 }
157 return;
158}
159
160sub _url_path_segment_match {
161 my ($self) = @_;
162 for ($_[1]) {
163 # trailing / -> require / on end of URL
6c0f599a 164 /\G(?:(?=[+|\)])|$)/gc and
e7dd1c4b 165 return '';
920d6222 166 # word chars only -> exact path part match
1c4f4b78 167 /\G([\w\-]+)/gc and
920d6222 168 return "\Q$1";
28f3dfd5 169 # ** -> capture unlimited path parts
170 /\G\*\*/gc and
171 return '(.*?[^/])';
920d6222 172 # * -> capture path part
173 /\G\*/gc and
4ed4fb42 174 return '([^/]+?)';
920d6222 175 }
176 return ();
177}
178
9b9866ae 179sub _parse_param_handler {
180 my ($self, $spec, $type) = @_;
181
9b9866ae 182 for ($_[1]) {
eb9e0e25 183 my (@required, @single, %multi, $star, $multistar, %positional, $have_kw);
052bdd54 184 my %spec;
eb9e0e25 185 my $pos_idx = 0;
9b9866ae 186 PARAM: { do {
187
eb9e0e25 188 # ?:foo or ?@:foo
9b9866ae 189
eb9e0e25 190 my $is_kw = /\G\:/gc;
9b9866ae 191
192 # ?@foo or ?@*
193
eb9e0e25 194 my $multi = /\G\@/gc;
9b9866ae 195
196 # @* or *
197
a5917caa 198 if (/\G\*/gc) {
9b9866ae 199
eb9e0e25 200 $self->_blam("* is always named; no need to supply :") if $is_kw;
201
052bdd54 202 if ($star) {
203 $self->_blam("Can only use one * or \@* in a parameter match");
a5917caa 204 }
052bdd54 205
206 $spec{star} = { multi => $multi };
9b9866ae 207 } else {
208
209 # @foo= or foo= or @foo~ or foo~
6c0f599a 210
9b9866ae 211 /\G(\w+)/gc or $self->_blam('Expected parameter name');
212
213 my $name = $1;
214
215 # check for = or ~ on the end
216
217 /\G\=/gc
052bdd54 218 ? push(@{$spec{required}||=[]}, $name)
9b9866ae 219 : (/\G\~/gc or $self->_blam('Expected = or ~ after parameter name'));
220
eb9e0e25 221 # record positional or keyword
222
052bdd54 223 push @{$spec{$is_kw ? 'named' : 'positional'}||=[]},
224 { name => $name, multi => $multi };
9b9866ae 225 }
226 } while (/\G\&/gc) }
227
b6bf9ed3 228 return Web::Dispatch::Predicates->can("match_${type}")->(\%spec);
9b9866ae 229 }
230}
231
920d6222 2321;