handle ) as last character of composite spec
[catagits/Web-Simple.git] / lib / Web / Dispatch / Parser.pm
index ae9aa23..cee4e1b 100644 (file)
@@ -19,6 +19,14 @@ has _cache => (
 
 sub diag { if (DEBUG) { warn $_[0] } }
 
+sub _wtf {
+  my ($self, $error) = @_;
+  my $hat = (' ' x (pos||0)).'^';
+  warn "Warning parsing dispatch specification: ${error}\n
+${_}
+${hat} here\n";
+}
+
 sub _blam {
   my ($self, $error) = @_;
   my $hat = (' ' x (pos||0)).'^';
@@ -29,26 +37,29 @@ ${hat} here\n";
 
 sub parse {
   my ($self, $spec) = @_;
+  $spec =~ s/\s+//g; # whitespace is not valid
   return $self->_cache->{$spec} ||= $self->_parse_spec($spec);
 }
 
 sub _parse_spec {
   my ($self, $spec, $nested) = @_;
+  return match_true() unless length($spec);
   for ($_[1]) {
     my @match;
-    /^\G\s*/; # eat leading whitespace
+    my $close;
     PARSE: { do {
       push @match, $self->_parse_spec_section($_)
         or $self->_blam("Unable to work out what the next section is");
       if (/\G\)/gc) {
         $self->_blam("Found closing ) with no opening (") unless $nested;
+        $close = 1;
         last PARSE;
       }
       last PARSE if (pos == length);
       $match[-1] = $self->_parse_spec_combinator($_, $match[-1])
         or $self->_blam('No valid combinator - expected + or |');
     } until (pos == length) }; # accept trailing whitespace
-    if ($nested and pos == length) {
+    if (!$close and $nested and pos == length) {
       pos = $nested - 1;
       $self->_blam("No closing ) found for opening (");
     }
@@ -83,10 +94,15 @@ sub _parse_spec_section {
   my ($self) = @_;
   for ($_[1]) {
 
+    # ~
+
+    /\G~/gc and
+      return match_path('^$');
+
     # GET POST PUT HEAD ...
 
     /\G([A-Z]+)/gc and
-      return $self->_http_method_match($_, $1);
+      return match_method($1);
 
     # /...
 
@@ -96,7 +112,7 @@ sub _parse_spec_section {
     # .* and .html
 
     /\G\.(\*|\w+)/gc and
-      return $self->_url_extension_match($_, $1);
+      return match_extension($1);
 
     # (...)
 
@@ -106,13 +122,7 @@ sub _parse_spec_section {
     # !something
 
     /\G!/gc and
-      return do {
-        my $match = $self->_parse_spec_section($_);
-        return sub {
-          return {} unless $match->(@_);
-          return;
-        };
-      };
+      return match_not($self->_parse_spec_section($_));
 
     # ?<param spec>
     /\G\?/gc and
@@ -121,35 +131,64 @@ sub _parse_spec_section {
     # %<param spec>
     /\G\%/gc and
       return $self->_parse_param_handler($_, 'body');
+
+    # *<param spec>
+    /\G\*/gc and
+      return $self->_parse_param_handler($_, 'uploads');
   }
   return; # () will trigger the blam in our caller
 }
 
-sub _http_method_match {
-  my ($self, $str, $method) = @_;
-  match_method($method);
-}
-
 sub _url_path_match {
   my ($self) = @_;
   for ($_[1]) {
-    my @path;
-    my $full_path = '$';
+    my (@path, @names, $seen_nameless);
+    my $end = '';
+    my $keep_dot;
     PATH: while (/\G\//gc) {
       /\G\.\.\./gc
         and do {
-          $full_path = '';
+          $end = '(/.*)';
           last PATH;
         };
-      push @path, $self->_url_path_segment_match($_)
+
+      my ($segment) = $self->_url_path_segment_match($_)
         or $self->_blam("Couldn't parse path match segment");
+
+      if (ref($segment)) {
+        ($segment, $keep_dot, my $name) = @$segment;
+        if (defined($name)) {
+          $self->_blam("Can't mix positional and named captures in path match")
+            if $seen_nameless;
+          push @names, $name;
+        } else {
+          $self->_blam("Can't mix positional and named captures in path match")
+            if @names;
+          $seen_nameless = 1;
+        }
+      }
+      push @path, $segment;
+
+      /\G\.\.\./gc
+        and do {
+          $end = '(|/.*)';
+          last PATH;
+        };
+      /\G\.\*/gc
+        and $keep_dot = 1;
+
+      last PATH if $keep_dot;
     }
-    my $re = '^('.join('/','',@path).')'.($full_path ? '$' : '(/.*)$');
+    if (@path && !$end && !$keep_dot) {
+      length and $_ .= '(?:\.\w+)?' for $path[-1];
+    }
+    my $re = '^('.join('/','',@path).')'.$end.'$';
     $re = qr/$re/;
-    if ($full_path) {
-      return match_path($re);
+    if ($end) {
+      return match_path_strip($re, @names ? \@names : ());
+    } else {
+      return match_path($re, @names ? \@names : ());
     }
-    return match_path_strip($re);
   }
   return;
 }
@@ -159,49 +198,41 @@ sub _url_path_segment_match {
   for ($_[1]) {
     # trailing / -> require / on end of URL
     /\G(?:(?=[+|\)])|$)/gc and
-      return '$';
+      return '';
     # word chars only -> exact path part match
-    /\G(\w+)/gc and
+    /
+        \G(
+            (?:             # start matching at a space followed by:
+                    [\w\-]  # word chars or dashes
+                |           # OR
+                    \.      # a period
+                    (?!\.)  # not followed by another period
+            )
+            +               # then grab as far as possible
+        )
+    /gcx and
       return "\Q$1";
     # ** -> capture unlimited path parts
-    /\G\*\*/gc and
-      return '(.*?[^/])';
+    /\G\*\*(?:(\.\*)?\:(\w+))?/gc and
+      return [ '(.*?[^/])', $1, $2 ];
     # * -> capture path part
-    /\G\*/gc and
-      return '([^/]+)';
-  }
-  return ();
-}
+    # *:name -> capture named path part
+    /\G\*(?:(\.\*)?\:(\w+))?/gc and
+      return [ '([^/]+?)', $1, $2 ];
 
-sub _url_extension_match {
-  my ($self, $str, $extension) = @_;
-  if ($extension eq '*') {
-    sub {
-      if ((my $tmp = shift->{PATH_INFO}) =~ s/\.(\w+)$//) {
-        ({ PATH_INFO => $tmp }, $1);
-      } else {
-        ();
-      }
-    };
-  } else {
-    sub {
-      if ((my $tmp = shift->{PATH_INFO}) =~ s/\.\Q${extension}\E$//) {
-        ({ PATH_INFO => $tmp });
-      } else {
-        ();
-      }
-    };
+    # :name -> capture named path part
+    /\G\:(\w+)/gc and
+      return [ '([^/]+?)', 0, $1 ];
   }
+  return ();
 }
 
 sub _parse_param_handler {
   my ($self, $spec, $type) = @_;
 
-  require Web::Simple::ParamParser;
-  my $unpacker = Web::Simple::ParamParser->can("get_unpacked_${type}_from");
-
   for ($_[1]) {
     my (@required, @single, %multi, $star, $multistar, %positional, $have_kw);
+    my %spec;
     my $pos_idx = 0;
     PARAM: { do {
 
@@ -219,76 +250,33 @@ sub _parse_param_handler {
 
         $self->_blam("* is always named; no need to supply :") if $is_kw;
 
-        $multi ? ($multistar = 1) : ($star = 1);
-
-        $have_kw = 1;
-
-        if ($star && $multistar) {
-          $self->_blam("Can't use * and \@* in the same parameter match");
+        if ($star) {
+          $self->_blam("Can only use one * or \@* in a parameter match");
         }
+
+        $spec{star} = { multi => $multi };
       } else {
 
         # @foo= or foo= or @foo~ or foo~
 
-        /\G(\w+)/gc or $self->_blam('Expected parameter name');
+        /\G([\w.]*)/gc or $self->_blam('Expected parameter name');
 
         my $name = $1;
 
         # check for = or ~ on the end
 
         /\G\=/gc
-          ? push(@required, $name)
+          ? push(@{$spec{required}||=[]}, $name)
           : (/\G\~/gc or $self->_blam('Expected = or ~ after parameter name'));
 
-        # record the key in the right category depending on the multi (@) flag
-
-        $multi ? ($multi{$name} = 1) : (push @single, $name);
-
         # record positional or keyword
 
-        $is_kw ? ($have_kw = 1) : ($positional{$name} = $pos_idx++);
+        push @{$spec{$is_kw ? 'named' : 'positional'}||=[]},
+          { name => $name, multi => $multi };
       }
     } while (/\G\&/gc) }
 
-    return sub {
-      my $raw = $unpacker->($_[0]);
-      foreach my $name (@required) {
-        return unless exists $raw->{$name};
-      }
-      my (%p, %done);
-      my @p = (undef) x $pos_idx;
-      foreach my $name (
-        @single,
-        ($star
-          ? (grep { !exists $multi{$_} } keys %$raw)
-          : ()
-        )
-      ) {
-        if (exists $raw->{$name}) {
-          if (exists $positional{$name}) {
-            $p[$positional{$name}] = $raw->{$name}->[-1];
-          } else {
-            $p{$name} = $raw->{$name}->[-1];
-          }
-        }
-        $done{$name} = 1;
-      }
-      foreach my $name (
-        keys %multi,
-        ($multistar
-          ? (grep { !exists $done{$_} && !exists $multi{$_} } keys %$raw)
-          : ()
-        )
-      ) {
-        if (exists $positional{$name}) {
-          $p[$positional{$name}] = $raw->{$name}||[];
-        } else {
-          $p{$name} = $raw->{$name}||[];
-        }
-      }
-      $p[$pos_idx] = \%p if $have_kw;
-      return ({}, @p);
-    };
+    return Web::Dispatch::Predicates->can("match_${type}")->(\%spec);
   }
 }