diff --git a/x/mango/selector.go b/x/mango/selector.go index b0feb379..1b043ba0 100644 --- a/x/mango/selector.go +++ b/x/mango/selector.go @@ -130,7 +130,7 @@ func (f *fieldNode) Match(doc interface{}) bool { val := doc // Traverse nested fields (e.g. "foo.bar.baz") - segments := strings.Split(f.field, ".") + segments := SplitKeys(f.field) for _, segment := range segments { m, ok := val.(map[string]interface{}) if !ok { @@ -371,3 +371,33 @@ func cmpSelectors(a, b Node) int { } return 0 } + +// SplitKeys splits a field into its component keys. For example, +// "foo.bar" is split into `["foo", "bar"]`. Escaped dots are not treated +// as separators, so `"foo\\.bar"` becomes `["foo.bar"]`. +func SplitKeys(field string) []string { + var escaped bool + result := []string{} + word := make([]byte, 0, len(field)) + for _, ch := range field { + if escaped { + word = append(word, byte(ch)) + escaped = false + continue + } + if ch == '\\' { + escaped = true + continue + } + if ch == '.' { + result = append(result, string(word)) + word = word[:0] + continue + } + word = append(word, byte(ch)) + } + if escaped { + word = append(word, '\\') + } + return append(result, string(word)) +} diff --git a/x/mango/selector_test.go b/x/mango/selector_test.go new file mode 100644 index 00000000..b5ecaf18 --- /dev/null +++ b/x/mango/selector_test.go @@ -0,0 +1,64 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +package mango + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestSplitKeys(t *testing.T) { + tests := []struct { + input string + want []string + }{ + { + input: "foo.bar.baz", + want: []string{"foo", "bar", "baz"}, + }, + { + input: "foo", + want: []string{"foo"}, + }, + { + input: "", + want: []string{""}, + }, + { + input: "foo\\.bar", + want: []string{"foo.bar"}, + }, + { + input: "foo\\\\.bar", + want: []string{"foo\\", "bar"}, + }, + { + input: "foo\\", + want: []string{"foo\\"}, + }, + { + input: "foo.", + want: []string{"foo", ""}, + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := SplitKeys(tt.input) + if d := cmp.Diff(tt.want, got); d != "" { + t.Errorf("unexpected keys (-want, +got): %s", d) + } + }) + } +}