| package httprule |
| |
| import ( |
| "fmt" |
| "strings" |
| |
| "github.com/golang/glog" |
| ) |
| |
| // InvalidTemplateError indicates that the path template is not valid. |
| type InvalidTemplateError struct { |
| tmpl string |
| msg string |
| } |
| |
| func (e InvalidTemplateError) Error() string { |
| return fmt.Sprintf("%s: %s", e.msg, e.tmpl) |
| } |
| |
| // Parse parses the string representation of path template |
| func Parse(tmpl string) (Compiler, error) { |
| if !strings.HasPrefix(tmpl, "/") { |
| return template{}, InvalidTemplateError{tmpl: tmpl, msg: "no leading /"} |
| } |
| tokens, verb := tokenize(tmpl[1:]) |
| |
| p := parser{tokens: tokens} |
| segs, err := p.topLevelSegments() |
| if err != nil { |
| return template{}, InvalidTemplateError{tmpl: tmpl, msg: err.Error()} |
| } |
| |
| return template{ |
| segments: segs, |
| verb: verb, |
| template: tmpl, |
| }, nil |
| } |
| |
| func tokenize(path string) (tokens []string, verb string) { |
| if path == "" { |
| return []string{eof}, "" |
| } |
| |
| const ( |
| init = iota |
| field |
| nested |
| ) |
| var ( |
| st = init |
| ) |
| for path != "" { |
| var idx int |
| switch st { |
| case init: |
| idx = strings.IndexAny(path, "/{") |
| case field: |
| idx = strings.IndexAny(path, ".=}") |
| case nested: |
| idx = strings.IndexAny(path, "/}") |
| } |
| if idx < 0 { |
| tokens = append(tokens, path) |
| break |
| } |
| switch r := path[idx]; r { |
| case '/', '.': |
| case '{': |
| st = field |
| case '=': |
| st = nested |
| case '}': |
| st = init |
| } |
| if idx == 0 { |
| tokens = append(tokens, path[idx:idx+1]) |
| } else { |
| tokens = append(tokens, path[:idx], path[idx:idx+1]) |
| } |
| path = path[idx+1:] |
| } |
| |
| l := len(tokens) |
| t := tokens[l-1] |
| if idx := strings.LastIndex(t, ":"); idx == 0 { |
| tokens, verb = tokens[:l-1], t[1:] |
| } else if idx > 0 { |
| tokens[l-1], verb = t[:idx], t[idx+1:] |
| } |
| tokens = append(tokens, eof) |
| return tokens, verb |
| } |
| |
| // parser is a parser of the template syntax defined in github.com/googleapis/googleapis/google/api/http.proto. |
| type parser struct { |
| tokens []string |
| accepted []string |
| } |
| |
| // topLevelSegments is the target of this parser. |
| func (p *parser) topLevelSegments() ([]segment, error) { |
| glog.V(1).Infof("Parsing %q", p.tokens) |
| segs, err := p.segments() |
| if err != nil { |
| return nil, err |
| } |
| glog.V(2).Infof("accept segments: %q; %q", p.accepted, p.tokens) |
| if _, err := p.accept(typeEOF); err != nil { |
| return nil, fmt.Errorf("unexpected token %q after segments %q", p.tokens[0], strings.Join(p.accepted, "")) |
| } |
| glog.V(2).Infof("accept eof: %q; %q", p.accepted, p.tokens) |
| return segs, nil |
| } |
| |
| func (p *parser) segments() ([]segment, error) { |
| s, err := p.segment() |
| if err != nil { |
| return nil, err |
| } |
| glog.V(2).Infof("accept segment: %q; %q", p.accepted, p.tokens) |
| |
| segs := []segment{s} |
| for { |
| if _, err := p.accept("/"); err != nil { |
| return segs, nil |
| } |
| s, err := p.segment() |
| if err != nil { |
| return segs, err |
| } |
| segs = append(segs, s) |
| glog.V(2).Infof("accept segment: %q; %q", p.accepted, p.tokens) |
| } |
| } |
| |
| func (p *parser) segment() (segment, error) { |
| if _, err := p.accept("*"); err == nil { |
| return wildcard{}, nil |
| } |
| if _, err := p.accept("**"); err == nil { |
| return deepWildcard{}, nil |
| } |
| if l, err := p.literal(); err == nil { |
| return l, nil |
| } |
| |
| v, err := p.variable() |
| if err != nil { |
| return nil, fmt.Errorf("segment neither wildcards, literal or variable: %v", err) |
| } |
| return v, err |
| } |
| |
| func (p *parser) literal() (segment, error) { |
| lit, err := p.accept(typeLiteral) |
| if err != nil { |
| return nil, err |
| } |
| return literal(lit), nil |
| } |
| |
| func (p *parser) variable() (segment, error) { |
| if _, err := p.accept("{"); err != nil { |
| return nil, err |
| } |
| |
| path, err := p.fieldPath() |
| if err != nil { |
| return nil, err |
| } |
| |
| var segs []segment |
| if _, err := p.accept("="); err == nil { |
| segs, err = p.segments() |
| if err != nil { |
| return nil, fmt.Errorf("invalid segment in variable %q: %v", path, err) |
| } |
| } else { |
| segs = []segment{wildcard{}} |
| } |
| |
| if _, err := p.accept("}"); err != nil { |
| return nil, fmt.Errorf("unterminated variable segment: %s", path) |
| } |
| return variable{ |
| path: path, |
| segments: segs, |
| }, nil |
| } |
| |
| func (p *parser) fieldPath() (string, error) { |
| c, err := p.accept(typeIdent) |
| if err != nil { |
| return "", err |
| } |
| components := []string{c} |
| for { |
| if _, err = p.accept("."); err != nil { |
| return strings.Join(components, "."), nil |
| } |
| c, err := p.accept(typeIdent) |
| if err != nil { |
| return "", fmt.Errorf("invalid field path component: %v", err) |
| } |
| components = append(components, c) |
| } |
| } |
| |
| // A termType is a type of terminal symbols. |
| type termType string |
| |
| // These constants define some of valid values of termType. |
| // They improve readability of parse functions. |
| // |
| // You can also use "/", "*", "**", "." or "=" as valid values. |
| const ( |
| typeIdent = termType("ident") |
| typeLiteral = termType("literal") |
| typeEOF = termType("$") |
| ) |
| |
| const ( |
| // eof is the terminal symbol which always appears at the end of token sequence. |
| eof = "\u0000" |
| ) |
| |
| // accept tries to accept a token in "p". |
| // This function consumes a token and returns it if it matches to the specified "term". |
| // If it doesn't match, the function does not consume any tokens and return an error. |
| func (p *parser) accept(term termType) (string, error) { |
| t := p.tokens[0] |
| switch term { |
| case "/", "*", "**", ".", "=", "{", "}": |
| if t != string(term) && t != "/" { |
| return "", fmt.Errorf("expected %q but got %q", term, t) |
| } |
| case typeEOF: |
| if t != eof { |
| return "", fmt.Errorf("expected EOF but got %q", t) |
| } |
| case typeIdent: |
| if err := expectIdent(t); err != nil { |
| return "", err |
| } |
| case typeLiteral: |
| if err := expectPChars(t); err != nil { |
| return "", err |
| } |
| default: |
| return "", fmt.Errorf("unknown termType %q", term) |
| } |
| p.tokens = p.tokens[1:] |
| p.accepted = append(p.accepted, t) |
| return t, nil |
| } |
| |
| // expectPChars determines if "t" consists of only pchars defined in RFC3986. |
| // |
| // https://www.ietf.org/rfc/rfc3986.txt, P.49 |
| // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
| // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" |
| // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" |
| // / "*" / "+" / "," / ";" / "=" |
| // pct-encoded = "%" HEXDIG HEXDIG |
| func expectPChars(t string) error { |
| const ( |
| init = iota |
| pct1 |
| pct2 |
| ) |
| st := init |
| for _, r := range t { |
| if st != init { |
| if !isHexDigit(r) { |
| return fmt.Errorf("invalid hexdigit: %c(%U)", r, r) |
| } |
| switch st { |
| case pct1: |
| st = pct2 |
| case pct2: |
| st = init |
| } |
| continue |
| } |
| |
| // unreserved |
| switch { |
| case 'A' <= r && r <= 'Z': |
| continue |
| case 'a' <= r && r <= 'z': |
| continue |
| case '0' <= r && r <= '9': |
| continue |
| } |
| switch r { |
| case '-', '.', '_', '~': |
| // unreserved |
| case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': |
| // sub-delims |
| case ':', '@': |
| // rest of pchar |
| case '%': |
| // pct-encoded |
| st = pct1 |
| default: |
| return fmt.Errorf("invalid character in path segment: %q(%U)", r, r) |
| } |
| } |
| if st != init { |
| return fmt.Errorf("invalid percent-encoding in %q", t) |
| } |
| return nil |
| } |
| |
| // expectIdent determines if "ident" is a valid identifier in .proto schema ([[:alpha:]_][[:alphanum:]_]*). |
| func expectIdent(ident string) error { |
| if ident == "" { |
| return fmt.Errorf("empty identifier") |
| } |
| for pos, r := range ident { |
| switch { |
| case '0' <= r && r <= '9': |
| if pos == 0 { |
| return fmt.Errorf("identifier starting with digit: %s", ident) |
| } |
| continue |
| case 'A' <= r && r <= 'Z': |
| continue |
| case 'a' <= r && r <= 'z': |
| continue |
| case r == '_': |
| continue |
| default: |
| return fmt.Errorf("invalid character %q(%U) in identifier: %s", r, r, ident) |
| } |
| } |
| return nil |
| } |
| |
| func isHexDigit(r rune) bool { |
| switch { |
| case '0' <= r && r <= '9': |
| return true |
| case 'A' <= r && r <= 'F': |
| return true |
| case 'a' <= r && r <= 'f': |
| return true |
| } |
| return false |
| } |