Commit 31c9bb76 authored by hannahhoward's avatar hannahhoward

feat(traversal): support lists and low cardinality

Improve the traverse function so that it only explores relevant fields for low cardinality selectors
and traverses high cardinality selectors for lists correctly
parent fcc1cc53
......@@ -107,3 +107,48 @@ func (ps PathSegmentInt) String() string {
func (ps PathSegmentInt) Index() (int, error) {
return ps.I, nil
}
// SegmentIterator iterates either a list or a map, generating PathSegments
// instead of indexes or keys
type SegmentIterator interface {
Next() (pathSegment PathSegment, value ipld.Node, err error)
Done() bool
}
// NewSegmentIterator generates a new iterator based on the node type
func NewSegmentIterator(n ipld.Node) SegmentIterator {
if n.ReprKind() == ipld.ReprKind_List {
return listSegmentIterator{n.ListIterator()}
}
return mapSegmentIterator{n.MapIterator()}
}
type listSegmentIterator struct {
ipld.ListIterator
}
func (lsi listSegmentIterator) Next() (pathSegment PathSegment, value ipld.Node, err error) {
i, v, err := lsi.ListIterator.Next()
return PathSegmentInt{i}, v, err
}
func (lsi listSegmentIterator) Done() bool {
return lsi.ListIterator.Done()
}
type mapSegmentIterator struct {
ipld.MapIterator
}
func (msi mapSegmentIterator) Next() (pathSegment PathSegment, value ipld.Node, err error) {
k, v, err := msi.MapIterator.Next()
if err != nil {
return nil, v, err
}
kstr, _ := k.AsString()
return PathSegmentString{kstr}, v, err
}
func (msi mapSegmentIterator) Done() bool {
return msi.MapIterator.Done()
}
......@@ -50,46 +50,67 @@ func (tp TraversalProgress) traverseInformatively(n ipld.Node, s selector.Select
default:
return nil
}
// TODO: should only do this full loop if high-cardinality indicated.
// attn := s.Interests()
// if attn == nil {
// FIXME need another kind switch here, and list support!
for itr := n.MapIterator(); !itr.Done(); {
k, v, err := itr.Next()
if err != nil {
return err
}
kstr, _ := k.AsString()
sNext := s.Explore(n, selector.PathSegmentString{kstr})
if sNext != nil {
tpNext := tp
tpNext.Path = tp.Path.AppendSegment(kstr)
if v.ReprKind() == ipld.ReprKind_Link {
lnk, _ := v.AsLink()
// Assemble the LinkContext in case the Loader or NBChooser want it.
lnkCtx := ipld.LinkContext{
LinkPath: tpNext.Path,
LinkNode: v,
ParentNode: n,
attn := s.Interests()
if attn == nil {
for itr := selector.NewSegmentIterator(n); !itr.Done(); {
ps, v, err := itr.Next()
if err != nil {
return err
}
sNext := s.Explore(n, ps)
if sNext != nil {
err = tp.traverseChild(n, v, ps, sNext, fn)
if err != nil {
return err
}
// Load link!
v, err = lnk.Load(
tpNext.Cfg.Ctx,
lnkCtx,
tpNext.Cfg.LinkNodeBuilderChooser(lnk, lnkCtx),
tpNext.Cfg.LinkLoader,
)
}
}
} else {
for _, ps := range attn {
// TODO: Probably not the most efficient way to be doing this...
v, err := n.TraverseField(ps.String())
if err != nil {
continue
}
sNext := s.Explore(n, ps)
if sNext != nil {
err = tp.traverseChild(n, v, ps, sNext, fn)
if err != nil {
return fmt.Errorf("error traversing node at %q: could not load link %q: %s", tpNext.Path, lnk, err)
return err
}
}
// TODO when link load is implemented, it should go roughly here.
}
}
return nil
}
if err := tpNext.traverseInformatively(v, sNext, fn); err != nil {
return err
}
func (tp TraversalProgress) traverseChild(n ipld.Node, v ipld.Node, ps selector.PathSegment, sNext selector.Selector, fn AdvVisitFn) error {
var err error
tpNext := tp
tpNext.Path = tp.Path.AppendSegment(ps.String())
if v.ReprKind() == ipld.ReprKind_Link {
lnk, _ := v.AsLink()
// Assemble the LinkContext in case the Loader or NBChooser want it.
lnkCtx := ipld.LinkContext{
LinkPath: tpNext.Path,
LinkNode: v,
ParentNode: n,
}
// Load link!
v, err = lnk.Load(
tpNext.Cfg.Ctx,
lnkCtx,
tpNext.Cfg.LinkNodeBuilderChooser(lnk, lnkCtx),
tpNext.Cfg.LinkLoader,
)
if err != nil {
return fmt.Errorf("error traversing node at %q: could not load link %q: %s", tpNext.Path, lnk, err)
}
}
if err := tpNext.traverseInformatively(v, sNext, fn); err != nil {
return err
}
return nil
}
......
......@@ -14,7 +14,6 @@ import (
"github.com/ipld/go-ipld-prime/traversal"
"github.com/ipld/go-ipld-prime/traversal/selector"
"github.com/ipld/go-ipld-prime/traversal/selector/builder"
)
/* Remember, we've got the following fixtures in scope:
......@@ -156,4 +155,80 @@ func TestTraverse(t *testing.T) {
Wish(t, err, ShouldEqual, nil)
Wish(t, order, ShouldEqual, 6)
})
t.Run("traversing lists should work", func(t *testing.T) {
ss := ssb.ExploreRange(0, 3, ssb.Matcher())
s, err := ss.Selector()
var order int
err = traversal.TraversalProgress{
Cfg: &traversal.TraversalConfig{
LinkLoader: func(lnk ipld.Link, _ ipld.LinkContext) (io.Reader, error) {
return bytes.NewBuffer(storage[lnk]), nil
},
},
}.Traverse(middleListNode, s, func(tp traversal.TraversalProgress, n ipld.Node) error {
switch order {
case 0:
Wish(t, n, ShouldEqual, fnb.CreateString("alpha"))
Wish(t, tp.Path.String(), ShouldEqual, "0")
case 1:
Wish(t, n, ShouldEqual, fnb.CreateString("alpha"))
Wish(t, tp.Path.String(), ShouldEqual, "1")
case 2:
Wish(t, n, ShouldEqual, fnb.CreateString("beta"))
Wish(t, tp.Path.String(), ShouldEqual, "2")
}
order++
return nil
})
Wish(t, err, ShouldEqual, nil)
Wish(t, order, ShouldEqual, 3)
})
t.Run("multiple layers of link traversal should work", func(t *testing.T) {
ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
efsb.Insert("linkedList", ssb.ExploreAll(ssb.Matcher()))
efsb.Insert("linkedMap", ssb.ExploreRecursive(3, ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
efsb.Insert("foo", ssb.Matcher())
efsb.Insert("nonlink", ssb.Matcher())
efsb.Insert("alink", ssb.Matcher())
efsb.Insert("nested", ssb.ExploreRecursiveEdge())
})))
})
s, err := ss.Selector()
var order int
err = traversal.TraversalProgress{
Cfg: &traversal.TraversalConfig{
LinkLoader: func(lnk ipld.Link, _ ipld.LinkContext) (io.Reader, error) {
return bytes.NewBuffer(storage[lnk]), nil
},
},
}.Traverse(rootNode, s, func(tp traversal.TraversalProgress, n ipld.Node) error {
switch order {
case 0:
Wish(t, n, ShouldEqual, fnb.CreateString("alpha"))
Wish(t, tp.Path.String(), ShouldEqual, "linkedList/0")
case 1:
Wish(t, n, ShouldEqual, fnb.CreateString("alpha"))
Wish(t, tp.Path.String(), ShouldEqual, "linkedList/1")
case 2:
Wish(t, n, ShouldEqual, fnb.CreateString("beta"))
Wish(t, tp.Path.String(), ShouldEqual, "linkedList/2")
case 3:
Wish(t, n, ShouldEqual, fnb.CreateString("alpha"))
Wish(t, tp.Path.String(), ShouldEqual, "linkedList/3")
case 4:
Wish(t, n, ShouldEqual, fnb.CreateBool(true))
Wish(t, tp.Path.String(), ShouldEqual, "linkedMap/foo")
case 5:
Wish(t, n, ShouldEqual, fnb.CreateString("zoo"))
Wish(t, tp.Path.String(), ShouldEqual, "linkedMap/nested/nonlink")
case 6:
Wish(t, n, ShouldEqual, fnb.CreateString("alpha"))
Wish(t, tp.Path.String(), ShouldEqual, "linkedMap/nested/alink")
}
order++
return nil
})
Wish(t, err, ShouldEqual, nil)
Wish(t, order, ShouldEqual, 7)
})
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment