diff --git a/pkg/policy/selector/parsing.go b/pkg/policy/selector/parsing.go index 16843e2..20b89d4 100644 --- a/pkg/policy/selector/parsing.go +++ b/pkg/policy/selector/parsing.go @@ -2,6 +2,7 @@ package selector import ( "fmt" + "math" "regexp" "strconv" "strings" @@ -33,54 +34,64 @@ func Parse(str string) (Selector, error) { if opt { seg = tok[0 : len(tok)-1] } - switch seg { - case ".": + switch { + case seg == ".": if len(sel) > 0 && sel[len(sel)-1].Identity() { return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok) } sel = append(sel, segment{str: ".", identity: true}) - case "[]": - sel = append(sel, segment{str: tok, optional: opt, iterator: true}) - default: - if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") { - lookup := seg[1 : len(seg)-1] - if indexRegex.MatchString(lookup) { // index - idx, err := strconv.Atoi(lookup) - if err != nil { - return nil, newParseError("invalid index", str, col, tok) - } - sel = append(sel, segment{str: tok, optional: opt, index: idx}) - } else if strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\"") { // explicit field - sel = append(sel, segment{str: tok, optional: opt, field: lookup[1 : len(lookup)-1]}) - } else if sliceRegex.MatchString(lookup) { // slice [3:5] or [:5] or [3:] - var rng []int - splt := strings.Split(lookup, ":") - if splt[0] == "" { - rng = append(rng, 0) - } else { - i, err := strconv.Atoi(splt[0]) - if err != nil { - return nil, newParseError("invalid slice index", str, col, tok) - } - rng = append(rng, i) - } - if splt[1] != "" { - i, err := strconv.Atoi(splt[1]) - if err != nil { - return nil, newParseError("invalid slice index", str, col, tok) - } - rng = append(rng, i) - } - sel = append(sel, segment{str: tok, optional: opt, slice: rng}) - } else { - return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok) + case seg == "[]": + sel = append(sel, segment{str: tok, optional: opt, iterator: true}) + + case strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]"): + lookup := seg[1 : len(seg)-1] + + switch { + // index, [123] + case indexRegex.MatchString(lookup): + idx, err := strconv.Atoi(lookup) + if err != nil { + return nil, newParseError("invalid index", str, col, tok) } - } else if fieldRegex.MatchString(seg) { - sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]}) - } else { + sel = append(sel, segment{str: tok, optional: opt, index: idx}) + + // explicit field, ["abcd"] + case strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\""): + sel = append(sel, segment{str: tok, optional: opt, field: lookup[1 : len(lookup)-1]}) + + // slice [3:5] or [:5] or [3:], also negative numbers + case sliceRegex.MatchString(lookup): + var rng [2]int64 + splt := strings.Split(lookup, ":") + if splt[0] == "" { + rng[0] = math.MinInt + } else { + i, err := strconv.ParseInt(splt[0], 10, 0) + if err != nil { + return nil, newParseError("invalid slice index", str, col, tok) + } + rng[0] = i + } + if splt[1] == "" { + rng[1] = math.MaxInt + } else { + i, err := strconv.ParseInt(splt[1], 10, 0) + if err != nil { + return nil, newParseError("invalid slice index", str, col, tok) + } + rng[1] = i + } + sel = append(sel, segment{str: tok, optional: opt, slice: rng[:]}) + + default: return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok) } + + case fieldRegex.MatchString(seg): + sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]}) + default: + return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok) } col += len(tok) } diff --git a/pkg/policy/selector/parsing_test.go b/pkg/policy/selector/parsing_test.go new file mode 100644 index 0000000..dc5c1cc --- /dev/null +++ b/pkg/policy/selector/parsing_test.go @@ -0,0 +1,427 @@ +package selector + +import ( + "fmt" + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParse(t *testing.T) { + t.Run("identity", func(t *testing.T) { + sel, err := Parse(".") + require.NoError(t, err) + require.Equal(t, 1, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + }) + + t.Run("dotted field name", func(t *testing.T) { + sel, err := Parse(".foo") + require.NoError(t, err) + require.Equal(t, 1, len(sel)) + require.False(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Equal(t, sel[0].Field(), "foo") + require.Empty(t, sel[0].Index()) + }) + + t.Run("explicit field", func(t *testing.T) { + sel, err := Parse(`.["foo"]`) + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Equal(t, sel[1].Field(), "foo") + require.Empty(t, sel[1].Index()) + }) + + t.Run("iterator, collection value", func(t *testing.T) { + sel, err := Parse(".[]") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.True(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + }) + + t.Run("index", func(t *testing.T) { + sel, err := Parse(".[138]") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Equal(t, sel[1].Index(), 138) + }) + + t.Run("negative index", func(t *testing.T) { + sel, err := Parse(".[-138]") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Equal(t, sel[1].Index(), -138) + }) + + t.Run("List slice", func(t *testing.T) { + sel, err := Parse(".[7:11]") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Equal(t, sel[1].Slice(), []int64{7, 11}) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + + sel, err = Parse(".[2:]") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Equal(t, sel[1].Slice(), []int64{2, math.MaxInt}) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + + sel, err = Parse(".[:42]") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Equal(t, sel[1].Slice(), []int64{math.MinInt, 42}) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + + sel, err = Parse(".[0:-2]") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Equal(t, sel[1].Slice(), []int64{0, -2}) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + }) + + t.Run("optional identity", func(t *testing.T) { + sel, err := Parse(".?") + require.NoError(t, err) + require.Equal(t, 1, len(sel)) + require.True(t, sel[0].Identity()) + require.True(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + }) + + t.Run("optional dotted field name", func(t *testing.T) { + sel, err := Parse(".foo?") + require.NoError(t, err) + require.Equal(t, 1, len(sel)) + require.False(t, sel[0].Identity()) + require.True(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Equal(t, sel[0].Field(), "foo") + require.Empty(t, sel[0].Index()) + }) + + t.Run("optional explicit field", func(t *testing.T) { + sel, err := Parse(`.["foo"]?`) + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Equal(t, sel[1].Field(), "foo") + require.Empty(t, sel[1].Index()) + }) + + t.Run("optional iterator", func(t *testing.T) { + sel, err := Parse(".[]?") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.True(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + }) + + t.Run("optional index", func(t *testing.T) { + sel, err := Parse(".[138]?") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Equal(t, sel[1].Index(), 138) + }) + + t.Run("optional negative index", func(t *testing.T) { + sel, err := Parse(".[-138]?") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Equal(t, sel[1].Index(), -138) + }) + + t.Run("optional list slice", func(t *testing.T) { + sel, err := Parse(".[7:11]?") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Equal(t, sel[1].Slice(), []int64{7, 11}) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + + sel, err = Parse(".[2:]?") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Equal(t, sel[1].Slice(), []int64{2, math.MaxInt}) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + + sel, err = Parse(".[:42]?") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Equal(t, sel[1].Slice(), []int64{math.MinInt, 42}) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + + sel, err = Parse(".[0:-2]?") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Equal(t, sel[1].Slice(), []int64{0, -2}) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + }) + + t.Run("idempotent optional", func(t *testing.T) { + sel, err := Parse(".foo???") + require.NoError(t, err) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Equal(t, sel[1].Field(), "foo") + require.Empty(t, sel[1].Index()) + }) + + t.Run("deny multi dot", func(t *testing.T) { + _, err := Parse("..") + require.Error(t, err) + }) + + t.Run("nesting", func(t *testing.T) { + str := `.foo.["bar"].[138]?.baz[1:]` + sel, err := Parse(str) + require.NoError(t, err) + printSegments(sel) + require.Equal(t, str, sel.String()) + require.Equal(t, 7, len(sel)) + require.False(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Equal(t, sel[0].Field(), "foo") + require.Empty(t, sel[0].Index()) + require.True(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + require.False(t, sel[2].Identity()) + require.False(t, sel[2].Optional()) + require.False(t, sel[2].Iterator()) + require.Empty(t, sel[2].Slice()) + require.Equal(t, sel[2].Field(), "bar") + require.Empty(t, sel[2].Index()) + require.True(t, sel[3].Identity()) + require.False(t, sel[3].Optional()) + require.False(t, sel[3].Iterator()) + require.Empty(t, sel[3].Slice()) + require.Empty(t, sel[3].Field()) + require.Empty(t, sel[3].Index()) + require.False(t, sel[4].Identity()) + require.True(t, sel[4].Optional()) + require.False(t, sel[4].Iterator()) + require.Empty(t, sel[4].Slice()) + require.Empty(t, sel[4].Field()) + require.Equal(t, sel[4].Index(), 138) + require.False(t, sel[5].Identity()) + require.False(t, sel[5].Optional()) + require.False(t, sel[5].Iterator()) + require.Empty(t, sel[5].Slice()) + require.Equal(t, sel[5].Field(), "baz") + require.Empty(t, sel[5].Index()) + require.False(t, sel[6].Identity()) + require.False(t, sel[6].Optional()) + require.False(t, sel[6].Iterator()) + require.Equal(t, sel[6].Slice(), []int64{1, math.MaxInt}) + require.Empty(t, sel[6].Field()) + require.Empty(t, sel[6].Index()) + }) + + t.Run("non dotted", func(t *testing.T) { + _, err := Parse("foo") + require.NotNil(t, err) + fmt.Println(err) + }) + + t.Run("non quoted", func(t *testing.T) { + _, err := Parse(".[foo]") + require.NotNil(t, err) + fmt.Println(err) + }) +} + +func printSegments(s Selector) { + for i, seg := range s { + fmt.Printf("%d: %s\n", i, seg.String()) + } +} diff --git a/pkg/policy/selector/selector.go b/pkg/policy/selector/selector.go index b0c49ac..f60aa5c 100644 --- a/pkg/policy/selector/selector.go +++ b/pkg/policy/selector/selector.go @@ -1,11 +1,15 @@ package selector import ( + "errors" "fmt" + "math" + "strconv" "strings" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/schema" ) @@ -15,16 +19,20 @@ import ( type Selector []segment // Select perform the selection described by the selector on the input IPLD DAG. -// If no error, Select returns either one ipld.Node or a []ipld.Node. -func (s Selector) Select(subject ipld.Node) (ipld.Node, []ipld.Node, error) { +// Select can return: +// - exactly one matched IPLD node +// - an error that can be checked with IsResolutionErr(err) to distinguish not being able to resolve to a node, +// or another type of error. +// - nil and no error, if the selector couldn't match on an optional segment (with ?). +func (s Selector) Select(subject ipld.Node) (ipld.Node, error) { return resolve(s, subject, nil) } -// MatchPath tells if the selector operates on the given (string only) path segments. -// It returns the segments that didn't get consumed by the matching. -func (s Selector) MatchPath(pathSegment ...string) (bool, []string) { - return matchPath(s, pathSegment) -} +// // MatchPath tells if the selector operates on the given (string only) path segments. +// // It returns the segments that didn't get consumed by the matching. +// func (s Selector) MatchPath(pathSegment ...string) (bool, []string) { +// return matchPath(s, pathSegment) +// } func (s Selector) String() string { var res strings.Builder @@ -39,7 +47,7 @@ type segment struct { identity bool optional bool iterator bool - slice []int + slice []int64 // either 0-length or 2-length field string index int } @@ -65,7 +73,7 @@ func (s segment) Iterator() bool { } // Slice flags that this segment targets a range of a slice. -func (s segment) Slice() []int { +func (s segment) Slice() []int64 { return s.slice } @@ -79,337 +87,222 @@ func (s segment) Index() int { return s.index } -func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.Node, error) { - cur := subject - for i, seg := range sel { - if seg.Identity() { - continue - } - - // 1st level: handle the different segment types (iterator, field, slice, index) - // 2nd level: handle different node kinds (list, map, string, bytes) - switch { - case seg.Iterator(): - if cur == nil || cur.Kind() == datamodel.Kind_Null { - if seg.Optional() { - // build empty list - nb := basicnode.Prototype.List.NewBuilder() - assembler, err := nb.BeginList(0) - if err != nil { - return nil, nil, err - } - - if err = assembler.Finish(); err != nil { - return nil, nil, err - } - - return nb.Build(), nil, nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at) - } - } else { - var many []ipld.Node - switch cur.Kind() { - case datamodel.Kind_List: - it := cur.ListIterator() - for !it.Done() { - _, v, err := it.Next() - if err != nil { - return nil, nil, err - } - - // check if there are more iterator segments - if len(sel) > i+1 && sel[i+1].Iterator() { - if v.Kind() == datamodel.Kind_List { - // recursively resolve the remaining selector segments - var o ipld.Node - var m []ipld.Node - o, m, err = resolve(sel[i+1:], v, at) - if err != nil { - // if the segment is optional and an error occurs, skip the current iteration. - if seg.Optional() { - continue - } else { - return nil, nil, err - } - } - if m != nil { - many = append(many, m...) - } else if o != nil { - many = append(many, o) - } - } else { - // if the current value is not a list and the next segment is optional, skip the current iteration - if sel[i+1].Optional() { - continue - } else { - return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(v)), at) - } - } - } else { - // if there are no more iterator segments, append the current value to the result - many = append(many, v) - } - } - case datamodel.Kind_Map: - it := cur.MapIterator() - for !it.Done() { - _, v, err := it.Next() - if err != nil { - return nil, nil, err - } - - if len(sel) > i+1 && sel[i+1].Iterator() { - if v.Kind() == datamodel.Kind_List { - var o ipld.Node - var m []ipld.Node - o, m, err = resolve(sel[i+1:], v, at) - if err != nil { - if seg.Optional() { - continue - } else { - return nil, nil, err - } - } - if m != nil { - many = append(many, m...) - } else if o != nil { - many = append(many, o) - } - } else { - if sel[i+1].Optional() { - continue - } else { - return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(v)), at) - } - } - } else { - many = append(many, v) - } - } - default: - return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at) - } - - return nil, many, nil - } - - case seg.Field() != "": - at = append(at, seg.Field()) - if cur == nil { - if seg.Optional() { - cur = nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at) - } - } else { - switch cur.Kind() { - case datamodel.Kind_Map: - n, err := cur.LookupByString(seg.Field()) - if err != nil { - if isMissing(err) { - if seg.Optional() { - cur = nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at) - } - } else { - return nil, nil, err - } - } else { - cur = n - } - case datamodel.Kind_List: - var many []ipld.Node - it := cur.ListIterator() - for !it.Done() { - _, v, err := it.Next() - if err != nil { - return nil, nil, err - } - if v.Kind() == datamodel.Kind_Map { - n, err := v.LookupByString(seg.Field()) - if err == nil { - many = append(many, n) - } - } - } - if len(many) > 0 { - cur = nil - return nil, many, nil - } else if seg.Optional() { - cur = nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("no elements in list have field named: %s", seg.Field()), at) - } - default: - if seg.Optional() { - cur = nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at) - } - } - } - - case seg.Slice() != nil: - if cur == nil { - if seg.Optional() { - cur = nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at) - } - } else { - slice := seg.Slice() - var start, end, length int64 - switch cur.Kind() { - case datamodel.Kind_List: - length = cur.Length() - start, end = resolveSliceIndices(slice, length) - case datamodel.Kind_Bytes: - b, _ := cur.AsBytes() - length = int64(len(b)) - start, end = resolveSliceIndices(slice, length) - case datamodel.Kind_String: - str, _ := cur.AsString() - length = int64(len(str)) - start, end = resolveSliceIndices(slice, length) - default: - return nil, nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at) - } - - if start < 0 || end < start || end > length { - if seg.Optional() { - cur = nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("slice out of bounds: [%d:%d]", start, end), at) - } - } else { - switch cur.Kind() { - case datamodel.Kind_List: - if end > cur.Length() { - end = cur.Length() - } - nb := basicnode.Prototype.List.NewBuilder() - assembler, _ := nb.BeginList(int64(end - start)) - for i := start; i < end; i++ { - item, _ := cur.LookupByIndex(int64(i)) - assembler.AssembleValue().AssignNode(item) - } - assembler.Finish() - cur = nb.Build() - case datamodel.Kind_Bytes: - b, _ := cur.AsBytes() - l := int64(len(b)) - if end > l { - end = l - } - cur = basicnode.NewBytes(b[start:end]) - case datamodel.Kind_String: - str, _ := cur.AsString() - l := int64(len(str)) - if end > l { - end = l - } - cur = basicnode.NewString(str[start:end]) - } - } - } - - default: // Index() - at = append(at, fmt.Sprintf("%d", seg.Index())) - if cur == nil { - if seg.Optional() { - cur = nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at) - } - } else { - idx := seg.Index() - switch cur.Kind() { - case datamodel.Kind_List: - if idx < 0 { - idx = int(cur.Length()) + idx - } - if idx < 0 || idx >= int(cur.Length()) { - if seg.Optional() { - cur = nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at) - } - } else { - cur, _ = cur.LookupByIndex(int64(idx)) - } - case datamodel.Kind_String: - str, _ := cur.AsString() - if idx < 0 { - idx = len(str) + idx - } - if idx < 0 || idx >= len(str) { - if seg.Optional() { - cur = nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at) - } - } else { - cur = basicnode.NewString(string(str[idx])) - } - case datamodel.Kind_Bytes: - b, _ := cur.AsBytes() - if idx < 0 { - idx = len(b) + idx - } - if idx < 0 || idx >= len(b) { - if seg.Optional() { - cur = nil - } else { - return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at) - } - } else { - cur = basicnode.NewInt(int64(b[idx])) - } - default: - return nil, nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at) - } - } +func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, error) { + errIfNotOptional := func(s segment, err error) error { + if !s.Optional() { + return err } + return nil } - return cur, nil, nil -} - -func matchPath(sel Selector, path []string) (bool, []string) { + cur := subject for _, seg := range sel { - if len(path) == 0 { - return true, path - } + // 1st level: handle the different segment types (iterator, field, slice, index) + // 2nd level: handle different node kinds (list, map, string, bytes) switch { case seg.Identity(): continue case seg.Iterator(): - // we have reached a [] iterator, it should have matched earlier - return false, nil + switch { + case cur == nil || cur.Kind() == datamodel.Kind_Null: + if seg.Optional() { + // build an empty list + n, err := qp.BuildList(basicnode.Prototype.Any, 0, func(_ datamodel.ListAssembler) {}) + return n, err + } + return nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at) + + case cur.Kind() == datamodel.Kind_List: + // iterators are no-op on list + continue + + case cur.Kind() == datamodel.Kind_Map: + // iterators on maps collect the values + return qp.BuildList(basicnode.Prototype.Any, cur.Length(), func(l datamodel.ListAssembler) { + it := cur.MapIterator() + for !it.Done() { + _, v, err := it.Next() + if err != nil { + panic(err) // recovered by BuildList + } + qp.ListEntry(l, qp.Node(v)) + } + }) + + default: + return nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at) + } case seg.Field() != "": - // if exact match on the segment, we continue - if path[0] == seg.Field() { - path = path[1:] - continue - } - return false, nil + at = append(at, seg.Field()) + switch { + case cur == nil: + err := newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at) + return nil, errIfNotOptional(seg, err) - case seg.Slice() != nil: - // we have reached a [:] slicing, it should have matched earlier - return false, nil + case cur.Kind() == datamodel.Kind_Map: + n, err := cur.LookupByString(seg.Field()) + if err != nil { + if !isMissing(err) { + return nil, err + } + if seg.Optional() { + cur = nil + } else { + return nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at) + } + } else { + cur = n + } + + default: + err := newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at) + return nil, errIfNotOptional(seg, err) + } + + case len(seg.Slice()) > 0: + if cur == nil { + err := newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at) + return nil, errIfNotOptional(seg, err) + } + + slice := seg.Slice() + var start, end, length int64 + switch cur.Kind() { + case datamodel.Kind_List: + length = cur.Length() + start, end = resolveSliceIndices(slice, length) + case datamodel.Kind_Bytes: + b, _ := cur.AsBytes() + length = int64(len(b)) + start, end = resolveSliceIndices(slice, length) + + // TODO: cleanup if confirmed that slicing/indexing on string is not legal + // case datamodel.Kind_String: + // str, _ := cur.AsString() + // length = int64(len(str)) + // start, end = resolveSliceIndices(slice, length) + default: + return nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at) + } + + if start < 0 || end < start || end > length { + err := newResolutionError(fmt.Sprintf("slice out of bounds: [%d:%d]", start, end), at) + return nil, errIfNotOptional(seg, err) + } + + switch cur.Kind() { + case datamodel.Kind_List: + sliced, err := qp.BuildList(basicnode.Prototype.Any, end-start, func(l datamodel.ListAssembler) { + for i := start; i <= end; i++ { + item, err := cur.LookupByIndex(i) + if err != nil { + panic(err) // recovered by BuildList + } + qp.ListEntry(l, qp.Node(item)) + } + }) + if err != nil { + return nil, errIfNotOptional(seg, err) + } + cur = sliced + case datamodel.Kind_Bytes: + b, _ := cur.AsBytes() + cur = basicnode.NewBytes(b[start:end]) + // TODO: cleanup if confirmed that slicing/indexing on string is not legal + // case datamodel.Kind_String: + // str, _ := cur.AsString() + // cur = basicnode.NewString(str[start:end]) + } + return cur, nil default: // Index() - // we have reached a [] indexing, it should have matched earlier - return false, nil + at = append(at, strconv.Itoa(seg.Index())) + + if cur == nil { + err := newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at) + return nil, errIfNotOptional(seg, err) + } + + idx := seg.Index() + switch cur.Kind() { + case datamodel.Kind_List: + if idx < 0 { + idx = int(cur.Length()) + idx + } + if idx < 0 || idx >= int(cur.Length()) { + err := newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at) + return nil, errIfNotOptional(seg, err) + } + cur, _ = cur.LookupByIndex(int64(idx)) + + // TODO: cleanup if confirmed that slicing/indexing on string is not legal + // case datamodel.Kind_String: + // str, _ := cur.AsString() + // if idx < 0 { + // idx = len(str) + idx + // } + // if idx < 0 || idx >= len(str) { + // err := newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at) + // return nil, errIfNotOptional(seg, err) + // } + // cur = basicnode.NewString(string(str[idx])) + + case datamodel.Kind_Bytes: + b, _ := cur.AsBytes() + if idx < 0 { + idx = len(b) + idx + } + if idx < 0 || idx >= len(b) { + err := newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at) + return nil, errIfNotOptional(seg, err) + } + cur = basicnode.NewInt(int64(b[idx])) + + default: + return nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at) + } } } - return true, path + + // segment exhausted, we return where we are + return cur, nil } +// func matchPath(sel Selector, path []string) (bool, []string) { +// for _, seg := range sel { +// if len(path) == 0 { +// return true, path +// } +// switch { +// case seg.Identity(): +// continue +// +// case seg.Iterator(): +// // we have reached a [] iterator, it should have matched earlier +// return false, nil +// +// case seg.Field() != "": +// // if exact match on the segment, we continue +// if path[0] == seg.Field() { +// path = path[1:] +// continue +// } +// return false, nil +// +// case seg.Slice() != nil: +// // we have reached a [:] slicing, it should have matched earlier +// return false, nil +// +// default: // Index() +// // we have reached a [] indexing, it should have matched earlier +// return false, nil +// } +// } +// return true, path +// } + // resolveSliceIndices resolves the start and end indices for slicing a list or byte array. // // It takes the slice indices from the selector segment and the length of the list or byte array, @@ -422,26 +315,30 @@ func matchPath(sel Selector, path []string) (bool, []string) { // Returns: // - start: The resolved start index for slicing. // - end: The resolved end index for slicing. -func resolveSliceIndices(slice []int, length int64) (int64, int64) { - start, end := int64(0), length - if len(slice) > 0 { - start = int64(slice[0]) - if start < 0 { - start = length + start - if start < 0 { - start = 0 - } - } +func resolveSliceIndices(slice []int64, length int64) (start int64, end int64) { + if len(slice) != 2 { + panic("should always be 2-length") } - if len(slice) > 1 { - end = int64(slice[1]) - if end <= 0 { - end = length + end - if end < start { - end = start - } - } + + start, end = slice[0], slice[1] + + // adjust boundaries + switch { + case slice[0] == math.MinInt: + start = 0 + case slice[0] < 0: + start = length + slice[0] } + switch { + case slice[1] == math.MaxInt: + end = length - 1 + case slice[1] < 0: + end = length + slice[1] + } + + // TODO: is backward iteration allowed? + // if yes, we need to return a +1 or -1 "step" + // if no, we need to error return start, end } @@ -466,6 +363,10 @@ func isMissing(err error) bool { return false } +func IsResolutionErr(err error) bool { + return errors.As(err, &resolutionerr{}) +} + type resolutionerr struct { msg string at []string diff --git a/pkg/policy/selector/selector_test.go b/pkg/policy/selector/selector_test.go index 93e7fa0..f5470ca 100644 --- a/pkg/policy/selector/selector_test.go +++ b/pkg/policy/selector/selector_test.go @@ -14,239 +14,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestParse(t *testing.T) { - t.Run("identity", func(t *testing.T) { - sel, err := Parse(".") - require.NoError(t, err) - require.Equal(t, 1, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - }) - - t.Run("field", func(t *testing.T) { - sel, err := Parse(".foo") - require.NoError(t, err) - require.Equal(t, 1, len(sel)) - require.False(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Equal(t, sel[0].Field(), "foo") - require.Empty(t, sel[0].Index()) - }) - - t.Run("explicit field", func(t *testing.T) { - sel, err := Parse(`.["foo"]`) - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.False(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Equal(t, sel[1].Field(), "foo") - require.Empty(t, sel[1].Index()) - }) - - t.Run("index", func(t *testing.T) { - sel, err := Parse(".[138]") - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.False(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Equal(t, sel[1].Index(), 138) - }) - - t.Run("negative index", func(t *testing.T) { - sel, err := Parse(".[-138]") - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.False(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Equal(t, sel[1].Index(), -138) - }) - - t.Run("iterator", func(t *testing.T) { - sel, err := Parse(".[]") - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.False(t, sel[1].Optional()) - require.True(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Empty(t, sel[1].Index()) - }) - - t.Run("optional field", func(t *testing.T) { - sel, err := Parse(".foo?") - require.NoError(t, err) - require.Equal(t, 1, len(sel)) - require.False(t, sel[0].Identity()) - require.True(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Equal(t, sel[0].Field(), "foo") - require.Empty(t, sel[0].Index()) - }) - - t.Run("optional explicit field", func(t *testing.T) { - sel, err := Parse(`.["foo"]?`) - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.True(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Equal(t, sel[1].Field(), "foo") - require.Empty(t, sel[1].Index()) - }) - - t.Run("optional index", func(t *testing.T) { - sel, err := Parse(".[138]?") - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.True(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Equal(t, sel[1].Index(), 138) - }) - - t.Run("optional iterator", func(t *testing.T) { - sel, err := Parse(".[]?") - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.True(t, sel[1].Optional()) - require.True(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Empty(t, sel[1].Index()) - }) - - t.Run("nesting", func(t *testing.T) { - str := `.foo.["bar"].[138]?.baz[1:]` - sel, err := Parse(str) - require.NoError(t, err) - printSegments(sel) - require.Equal(t, str, sel.String()) - require.Equal(t, 7, len(sel)) - require.False(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Equal(t, sel[0].Field(), "foo") - require.Empty(t, sel[0].Index()) - require.True(t, sel[1].Identity()) - require.False(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Empty(t, sel[1].Index()) - require.False(t, sel[2].Identity()) - require.False(t, sel[2].Optional()) - require.False(t, sel[2].Iterator()) - require.Empty(t, sel[2].Slice()) - require.Equal(t, sel[2].Field(), "bar") - require.Empty(t, sel[2].Index()) - require.True(t, sel[3].Identity()) - require.False(t, sel[3].Optional()) - require.False(t, sel[3].Iterator()) - require.Empty(t, sel[3].Slice()) - require.Empty(t, sel[3].Field()) - require.Empty(t, sel[3].Index()) - require.False(t, sel[4].Identity()) - require.True(t, sel[4].Optional()) - require.False(t, sel[4].Iterator()) - require.Empty(t, sel[4].Slice()) - require.Empty(t, sel[4].Field()) - require.Equal(t, sel[4].Index(), 138) - require.False(t, sel[5].Identity()) - require.False(t, sel[5].Optional()) - require.False(t, sel[5].Iterator()) - require.Empty(t, sel[5].Slice()) - require.Equal(t, sel[5].Field(), "baz") - require.Empty(t, sel[5].Index()) - require.False(t, sel[6].Identity()) - require.False(t, sel[6].Optional()) - require.False(t, sel[6].Iterator()) - require.Equal(t, sel[6].Slice(), []int{1}) - require.Empty(t, sel[6].Field()) - require.Empty(t, sel[6].Index()) - }) - - t.Run("non dotted", func(t *testing.T) { - _, err := Parse("foo") - require.NotNil(t, err) - fmt.Println(err) - }) - - t.Run("non quoted", func(t *testing.T) { - _, err := Parse(".[foo]") - require.NotNil(t, err) - fmt.Println(err) - }) -} - -func printSegments(s Selector) { - for i, seg := range s { - fmt.Printf("%d: %s\n", i, seg.String()) - } -} - func TestSelect(t *testing.T) { type name struct { First string @@ -313,14 +80,13 @@ func TestSelect(t *testing.T) { sel, err := Parse(".") require.NoError(t, err) - one, many, err := sel.Select(anode) + res, err := sel.Select(anode) require.NoError(t, err) - require.NotEmpty(t, one) - require.Empty(t, many) + require.NotEmpty(t, res) - fmt.Println(printer.Sprint(one)) + fmt.Println(printer.Sprint(res)) - age := must.Int(must.Node(one.LookupByString("age"))) + age := must.Int(must.Node(res.LookupByString("age"))) require.Equal(t, int64(alice.Age), age) }) @@ -328,24 +94,22 @@ func TestSelect(t *testing.T) { sel, err := Parse(".name.first") require.NoError(t, err) - one, many, err := sel.Select(anode) + res, err := sel.Select(anode) require.NoError(t, err) - require.NotEmpty(t, one) - require.Empty(t, many) + require.NotEmpty(t, res) - fmt.Println(printer.Sprint(one)) + fmt.Println(printer.Sprint(res)) - name := must.String(one) + name := must.String(res) require.Equal(t, alice.Name.First, name) - one, many, err = sel.Select(bnode) + res, err = sel.Select(bnode) require.NoError(t, err) - require.NotEmpty(t, one) - require.Empty(t, many) + require.NotEmpty(t, res) - fmt.Println(printer.Sprint(one)) + fmt.Println(printer.Sprint(res)) - name = must.String(one) + name = must.String(res) require.Equal(t, bob.Name.First, name) }) @@ -353,109 +117,88 @@ func TestSelect(t *testing.T) { sel, err := Parse(".name.middle?") require.NoError(t, err) - one, many, err := sel.Select(anode) + res, err := sel.Select(anode) require.NoError(t, err) - require.NotEmpty(t, one) - require.Empty(t, many) + require.NotEmpty(t, res) - fmt.Println(printer.Sprint(one)) + fmt.Println(printer.Sprint(res)) - name := must.String(one) + name := must.String(res) require.Equal(t, *alice.Name.Middle, name) - one, many, err = sel.Select(bnode) + res, err = sel.Select(bnode) require.NoError(t, err) - require.Empty(t, one) - require.Empty(t, many) + require.Empty(t, res) }) t.Run("not exists", func(t *testing.T) { sel, err := Parse(".name.foo") require.NoError(t, err) - one, many, err := sel.Select(anode) + res, err := sel.Select(anode) require.Error(t, err) - require.Empty(t, one) - require.Empty(t, many) + require.Empty(t, res) fmt.Println(err) - require.ErrorAs(t, err, &resolutionerr{}, "error was not a resolution error") + require.ErrorAs(t, err, &resolutionerr{}, "error should be a resolution error") + require.True(t, IsResolutionErr(err)) }) t.Run("optional not exists", func(t *testing.T) { sel, err := Parse(".name.foo?") require.NoError(t, err) - one, many, err := sel.Select(anode) + one, err := sel.Select(anode) require.NoError(t, err) require.Empty(t, one) - require.Empty(t, many) }) t.Run("iterator", func(t *testing.T) { sel, err := Parse(".interests[]") require.NoError(t, err) - one, many, err := sel.Select(anode) + res, err := sel.Select(anode) require.NoError(t, err) - require.Empty(t, one) - require.NotEmpty(t, many) + require.NotEmpty(t, res) - for _, n := range many { - fmt.Println(printer.Sprint(n)) - } + fmt.Println(printer.Sprint(res)) - iname := must.String(must.Node(many[0].LookupByString("name"))) + iname := must.String(must.Node(must.Node(res.LookupByIndex(0)).LookupByString("name"))) require.Equal(t, alice.Interests[0].Name, iname) - iname = must.String(must.Node(many[1].LookupByString("name"))) + iname = must.String(must.Node(must.Node(res.LookupByIndex(1)).LookupByString("name"))) require.Equal(t, alice.Interests[1].Name, iname) }) - t.Run("map iterator", func(t *testing.T) { - sel, err := Parse(".interests[0][]") - require.NoError(t, err) - - one, many, err := sel.Select(anode) - require.NoError(t, err) - require.Empty(t, one) - require.NotEmpty(t, many) - - for _, n := range many { - fmt.Println(printer.Sprint(n)) - } - - require.Equal(t, alice.Interests[0].Name, must.String(many[0])) - require.Equal(t, alice.Interests[0].Experience, int(must.Int(many[2]))) - }) + // TODO: fully test slicing + negative numbers } -func TestMatch(t *testing.T) { - for _, tc := range []struct { - sel string - path []string - want bool - remaining []string - }{ - {sel: ".foo.bar", path: []string{"foo", "bar"}, want: true, remaining: []string{}}, - {sel: ".foo.bar", path: []string{"foo"}, want: true, remaining: []string{}}, - {sel: ".foo.bar", path: []string{"foo", "bar", "baz"}, want: true, remaining: []string{"baz"}}, - {sel: ".foo.bar", path: []string{"foo", "faa"}, want: false}, - {sel: ".foo.[]", path: []string{"foo", "faa"}, want: false}, - {sel: ".foo.[]", path: []string{"foo"}, want: true, remaining: []string{}}, - {sel: ".foo.bar?", path: []string{"foo"}, want: true, remaining: []string{}}, - {sel: ".foo.bar?", path: []string{"foo", "bar"}, want: true, remaining: []string{}}, - {sel: ".foo.bar?", path: []string{"foo", "baz"}, want: false}, - } { - t.Run(tc.sel, func(t *testing.T) { - sel := MustParse(tc.sel) - res, remain := sel.MatchPath(tc.path...) - require.Equal(t, tc.want, res) - require.EqualValues(t, tc.remaining, remain) - }) - } -} +// func TestMatch(t *testing.T) { +// for _, tc := range []struct { +// sel string +// path []string +// want bool +// remaining []string +// }{ +// {sel: ".foo.bar", path: []string{"foo", "bar"}, want: true, remaining: []string{}}, +// {sel: ".foo.bar", path: []string{"foo"}, want: true, remaining: []string{}}, +// {sel: ".foo.bar", path: []string{"foo", "bar", "baz"}, want: true, remaining: []string{"baz"}}, +// {sel: ".foo.bar", path: []string{"foo", "faa"}, want: false}, +// {sel: ".foo.[]", path: []string{"foo", "faa"}, want: false}, +// {sel: ".foo.[]", path: []string{"foo"}, want: true, remaining: []string{}}, +// {sel: ".foo.bar?", path: []string{"foo"}, want: true, remaining: []string{}}, +// {sel: ".foo.bar?", path: []string{"foo", "bar"}, want: true, remaining: []string{}}, +// {sel: ".foo.bar?", path: []string{"foo", "baz"}, want: false}, +// } { +// t.Run(tc.sel, func(t *testing.T) { +// sel := MustParse(tc.sel) +// res, remain := sel.MatchPath(tc.path...) +// require.Equal(t, tc.want, res) +// require.EqualValues(t, tc.remaining, remain) +// }) +// } +// } func FuzzParse(f *testing.F) { selectorCorpus := []string{ @@ -520,6 +263,6 @@ func FuzzParseAndSelect(f *testing.F) { } // look for panic() - _, _, _ = sel.Select(node) + _, _ = sel.Select(node) }) } diff --git a/pkg/policy/selector/supported_test.go b/pkg/policy/selector/supported_test.go index dcb41ec..ee0a159 100644 --- a/pkg/policy/selector/supported_test.go +++ b/pkg/policy/selector/supported_test.go @@ -26,7 +26,7 @@ func TestSupportedForms(t *testing.T) { Output string } - // Pass + // Pass and return a node for _, testcase := range []Testcase{ {Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`}, {Name: "Iterator", Selector: `.[]`, Input: `[1, 2]`, Output: `[1, 2]`}, @@ -52,35 +52,16 @@ func TestSupportedForms(t *testing.T) { require.NoError(t, err) // attempt to select - node, nodes, err := sel.Select(makeNode(t, tc.Input)) + res, err := sel.Select(makeNode(t, tc.Input)) require.NoError(t, err) - require.NotEqual(t, node != nil, len(nodes) > 0) // XOR (only one of node or nodes should be set) - - // make an IPLD List node from a []datamodel.Node - if node == nil { - nb := basicnode.Prototype.List.NewBuilder() - la, err := nb.BeginList(int64(len(nodes))) - require.NoError(t, err) - - for _, n := range nodes { - // TODO: This code is probably not needed if the Select operation properly prunes nil values - e.g.: Optional Iterator - if n == nil { - n = datamodel.Null - } - - require.NoError(t, la.AssembleValue().AssignNode(n)) - } - require.NoError(t, la.Finish()) - - node = nb.Build() - } + require.NotNil(t, res) exp := makeNode(t, tc.Output) - equalIPLD(t, exp, node) + equalIPLD(t, exp, res) }) } - // null + // No error and return null, as optional for _, testcase := range []Testcase{ {Name: "Optional Missing Key", Selector: `.x?`, Input: `{}`}, {Name: "Optional Null Key", Selector: `.x?`, Input: `null`}, @@ -97,16 +78,13 @@ func TestSupportedForms(t *testing.T) { require.NoError(t, err) // attempt to select - node, nodes, err := sel.Select(makeNode(t, tc.Input)) + res, err := sel.Select(makeNode(t, tc.Input)) require.NoError(t, err) - // TODO: should Select return a single node which is sometimes a list or null? - // require.Equal(t, datamodel.Null, node) - assert.Nil(t, node) - assert.Empty(t, nodes) + require.Nil(t, res) }) } - // error + // fail to select and return an error for _, testcase := range []Testcase{ {Name: "Null Iterator", Selector: `.[]`, Input: `null`}, {Name: "Nested Iterator", Selector: `.[][]`, Input: `[[1], 2, [3]]`}, @@ -124,10 +102,10 @@ func TestSupportedForms(t *testing.T) { require.NoError(t, err) // attempt to select - node, nodes, err := sel.Select(makeNode(t, tc.Input)) + res, err := sel.Select(makeNode(t, tc.Input)) require.Error(t, err) - assert.Nil(t, node) - assert.Empty(t, nodes) + require.True(t, selector.IsResolutionErr(err)) + require.Nil(t, res) }) } }