From 2b07355b946769c1204fbb9e78ef923f46923b92 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 20 Aug 2014 11:19:43 -0700 Subject: [PATCH] watch: First pass at query parsing --- watch/watch.go | 160 ++++++++++++++++++++++++++++++++++++++++++++ watch/watch_test.go | 106 +++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 watch/watch.go create mode 100644 watch/watch_test.go diff --git a/watch/watch.go b/watch/watch.go new file mode 100644 index 0000000000..c2785bfe1f --- /dev/null +++ b/watch/watch.go @@ -0,0 +1,160 @@ +package watch + +import ( + "fmt" + "strings" +) + +// WatchPlan is the parsed version of a watch specification. A watch provides +// the details of a query, which generates a view into the Consul data store. +// This view is watched for changes and a handler is invoked to take any +// appropriate actions. +type WatchPlan struct { + Datacenter string + Token string + Type string +} + +// Parse takes a watch query and compiles it into a WatchPlan or an error +func Parse(query string) (*WatchPlan, error) { + tokens, err := tokenize(query) + if err != nil { + return nil, fmt.Errorf("Failed to parse: %v", err) + } + params := collapse(tokens) + plan := &WatchPlan{} + + if err := assignValue(params, "type", &plan.Type); err != nil { + return nil, err + } + if plan.Type == "" { + return nil, fmt.Errorf("Watch type must be specified") + } + if err := assignValue(params, "datacenter", &plan.Datacenter); err != nil { + return nil, err + } + if err := assignValue(params, "token", &plan.Token); err != nil { + return nil, err + } + + return plan, nil +} + +// assignValue is used to extract a value ensuring it is only +// defined once +func assignValue(params map[string][]string, name string, out *string) error { + if vals, ok := params[name]; ok { + if len(vals) != 1 { + return fmt.Errorf("Multiple definitions of %s", name) + } + *out = vals[0] + delete(params, name) + } + return nil +} + +// token is used to represent a "datacenter:foobar" pair, where +// datacenter is the param and foobar is the value +type token struct { + param string + val string +} + +func (t *token) GoString() string { + return fmt.Sprintf("%#v", *t) +} + +// tokenize splits a query string into a slice of tokens +func tokenize(query string) ([]*token, error) { + var tokens []*token + for i := 0; i < len(query); i++ { + char := query[i] + + // Ignore whitespace + if char == ' ' || char == '\t' || char == '\n' { + continue + } + + // Read the next token + next, offset, err := readToken(query[i:]) + if err != nil { + return nil, err + } + + // Store the token + tokens = append(tokens, next) + + // Increment the offset + i += offset + } + return tokens, nil +} + +// readToken is used to read a single token +func readToken(query string) (*token, int, error) { + // Get the token + param, offset, err := readParameter(query) + if err != nil { + return nil, 0, err + } + + // Get the value + query = query[offset:] + val, offset2, err := readValue(query) + if err != nil { + return nil, 0, err + } + + // Return the new token + token := &token{ + param: param, + val: val, + } + return token, offset + offset2, nil +} + +// readParameter scans for the next parameter +func readParameter(query string) (string, int, error) { + for i := 0; i < len(query); i++ { + char := query[i] + if char == ':' { + if i == 0 { + return "", 0, fmt.Errorf("Missing parameter name") + } else { + return query[:i], i + 1, nil + } + } + } + return "", 0, fmt.Errorf("Parameter delimiter not found") +} + +// readValue is used to scan for the next value +func readValue(query string) (string, int, error) { + // Handle quoted values + if query[0] == '\'' || query[0] == '"' { + quoteChar := query[0:1] + endChar := strings.Index(query[1:], quoteChar) + if endChar == -1 { + return "", 0, fmt.Errorf("Missing end of quotation") + } + return query[1 : endChar+1], endChar + 2, nil + } + + // Look for white space + endChar := strings.IndexAny(query, " \t\n") + if endChar == -1 { + return query, len(query), nil + } + return query[:endChar], endChar, nil +} + +// collapse is used to collapse a token stream into a map +// of parameter name to list of values. +func collapse(tokens []*token) map[string][]string { + out := make(map[string][]string) + for _, t := range tokens { + existing := out[t.param] + out[t.param] = append(existing, t.val) + } + return out +} diff --git a/watch/watch_test.go b/watch/watch_test.go new file mode 100644 index 0000000000..35dc6151eb --- /dev/null +++ b/watch/watch_test.go @@ -0,0 +1,106 @@ +package watch + +import ( + "fmt" + "reflect" + "testing" +) + +func TestTokenize(t *testing.T) { + type tcase struct { + in string + out []*token + err error + } + cases := []tcase{ + tcase{ + "", + nil, + nil, + }, + tcase{ + "foo:bar bar:baz zip:zap", + []*token{ + &token{"foo", "bar"}, + &token{"bar", "baz"}, + &token{"zip", "zap"}, + }, + nil, + }, + tcase{ + "foo:\"long input here\" after:this", + []*token{ + &token{"foo", "long input here"}, + &token{"after", "this"}, + }, + nil, + }, + tcase{ + "foo:'long input here' after:this", + []*token{ + &token{"foo", "long input here"}, + &token{"after", "this"}, + }, + nil, + }, + tcase{ + "foo:'long input here after:this", + nil, + fmt.Errorf("Missing end of quotation"), + }, + tcase{ + "foo", + nil, + fmt.Errorf("Parameter delimiter not found"), + }, + tcase{ + ":val", + nil, + fmt.Errorf("Missing parameter name"), + }, + } + + for _, tc := range cases { + tokens, err := tokenize(tc.in) + if err != nil && tc.err == nil { + t.Fatalf("%s: err: %v", tc.in, err) + } else if tc.err != nil && (err == nil || err.Error() != tc.err.Error()) { + t.Fatalf("%s: bad err: %v", tc.in, err) + } + if !reflect.DeepEqual(tokens, tc.out) { + t.Fatalf("%s: bad: %#v %#v", tc.in, tokens, tc.out) + } + } +} + +func TestCollapse(t *testing.T) { + inp := "type:key key:foo key:bar" + tokens, err := tokenize(inp) + if err != nil { + t.Fatalf("err: %v", err) + } + out := collapse(tokens) + expect := map[string][]string{ + "type": []string{"key"}, + "key": []string{"foo", "bar"}, + } + if !reflect.DeepEqual(out, expect) { + t.Fatalf("bad: %#v", out) + } +} + +func TestParseBasic(t *testing.T) { + p, err := Parse("type:key datacenter:dc2 token:12345") + if err != nil { + t.Fatalf("err: %v", err) + } + if p.Datacenter != "dc2" { + t.Fatalf("Bad: %#v", p) + } + if p.Token != "12345" { + t.Fatalf("Bad: %#v", p) + } + if p.Type != "key" { + t.Fatalf("Bad: %#v", p) + } +}