Skip to content

Commit

Permalink
Merge pull request #65 from ipld/traversal-get
Browse files Browse the repository at this point in the history
Add traversal.Get function
  • Loading branch information
warpfork authored Aug 22, 2020
2 parents d3ccd3c + 0ab9c70 commit 46f2852
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 10 deletions.
61 changes: 51 additions & 10 deletions traversal/focus.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ func Focus(n ipld.Node, p ipld.Path, fn VisitFn) error {
return Progress{}.Focus(n, p, fn)
}

// Get is the equivalent of Focus, but returns the reached node (rather than invoking a callback at the target),
// and does not yield Progress information.
//
// This function is a helper function which starts a new traversal with default configuration.
// It cannot cross links automatically (since this requires configuration).
// Use the equivalent Get function on the Progress structure
// for more advanced and configurable walks.
func Get(n ipld.Node, p ipld.Path) (ipld.Node, error) {
return Progress{}.Get(n, p)
}

// FocusedTransform traverses an ipld.Node graph, reaches a single Node,
// and calls the given TransformFn to decide what new node to replace the visited node with.
// A new Node tree will be returned (the original is unchanged).
Expand Down Expand Up @@ -45,6 +56,32 @@ func FocusedTransform(n ipld.Node, p ipld.Path, fn TransformFn) (ipld.Node, erro
// the Path recorded of the traversal so far will continue to be extended,
// and thus continued nested uses of Walk and Focus will see the fully contextualized Path.
func (prog Progress) Focus(n ipld.Node, p ipld.Path, fn VisitFn) error {
n, err := prog.get(n, p, true)
if err != nil {
return err
}
return fn(prog, n)
}

// Get is the equivalent of Focus, but returns the reached node (rather than invoking a callback at the target),
// and does not yield Progress information.
//
// Provide configuration to this process using the Config field in the Progress object.
//
// This walk will automatically cross links, but requires some configuration
// with link loading functions to do so.
//
// If doing several traversals which are nested, consider using the Focus funcion in preference to Get;
// the Focus functions provide updated Progress objects which can be used to do nested traversals while keeping consistent track of progress,
// such that continued nested uses of Walk or Focus or Get will see the fully contextualized Path.
func (prog Progress) Get(n ipld.Node, p ipld.Path) (ipld.Node, error) {
return prog.get(n, p, false)
}

// get is the internal implementation for Focus and Get.
// It *mutates* the Progress object it's called on, and returns reached nodes.
// For Get calls, trackProgress=false, which avoids some allocations for state tracking that's not needed by that call.
func (prog *Progress) get(n ipld.Node, p ipld.Path, trackProgress bool) (ipld.Node, error) {
prog.init()
segments := p.Segments()
var prev ipld.Node // for LinkContext
Expand All @@ -56,21 +93,21 @@ func (prog Progress) Focus(n ipld.Node, p ipld.Path, fn VisitFn) error {
case ipld.ReprKind_Map:
next, err := n.LookupByString(seg.String())
if err != nil {
return fmt.Errorf("error traversing segment %q on node at %q: %s", seg, p.Truncate(i), err)
return nil, fmt.Errorf("error traversing segment %q on node at %q: %s", seg, p.Truncate(i), err)
}
prev, n = n, next
case ipld.ReprKind_List:
intSeg, err := seg.Index()
if err != nil {
return fmt.Errorf("error traversing segment %q on node at %q: the segment cannot be parsed as a number and the node is a list", seg, p.Truncate(i))
return nil, fmt.Errorf("error traversing segment %q on node at %q: the segment cannot be parsed as a number and the node is a list", seg, p.Truncate(i))
}
next, err := n.LookupByIndex(intSeg)
if err != nil {
return fmt.Errorf("error traversing segment %q on node at %q: %s", seg, p.Truncate(i), err)
return nil, fmt.Errorf("error traversing segment %q on node at %q: %s", seg, p.Truncate(i), err)
}
prev, n = n, next
default:
return fmt.Errorf("cannot traverse node at %q: %s", p.Truncate(i), fmt.Errorf("cannot traverse terminals"))
return nil, fmt.Errorf("cannot traverse node at %q: %s", p.Truncate(i), fmt.Errorf("cannot traverse terminals"))
}
// Dereference any links.
for n.ReprKind() == ipld.ReprKind_Link {
Expand All @@ -84,7 +121,7 @@ func (prog Progress) Focus(n ipld.Node, p ipld.Path, fn VisitFn) error {
// Pick what in-memory format we will build.
np, err := prog.Cfg.LinkTargetNodePrototypeChooser(lnk, lnkCtx)
if err != nil {
return fmt.Errorf("error traversing node at %q: could not load link %q: %s", p.Truncate(i+1), lnk, err)
return nil, fmt.Errorf("error traversing node at %q: could not load link %q: %s", p.Truncate(i+1), lnk, err)
}
nb := np.NewBuilder()
// Load link!
Expand All @@ -95,15 +132,19 @@ func (prog Progress) Focus(n ipld.Node, p ipld.Path, fn VisitFn) error {
prog.Cfg.LinkLoader,
)
if err != nil {
return fmt.Errorf("error traversing node at %q: could not load link %q: %s", p.Truncate(i+1), lnk, err)
return nil, fmt.Errorf("error traversing node at %q: could not load link %q: %s", p.Truncate(i+1), lnk, err)
}
if trackProgress {
prog.LastBlock.Path = p.Truncate(i + 1)
prog.LastBlock.Link = lnk
}
prog.LastBlock.Path = p.Truncate(i + 1)
prog.LastBlock.Link = lnk
prev, n = n, nb.Build()
}
}
prog.Path = prog.Path.Join(p)
return fn(prog, n)
if trackProgress {
prog.Path = prog.Path.Join(p)
}
return n, nil
}

// FocusedTransform traverses an ipld.Node graph, reaches a single Node,
Expand Down
47 changes: 47 additions & 0 deletions traversal/focus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,26 @@ func TestFocusSingleTree(t *testing.T) {
})
}

// covers Get used on one already-loaded Node; no link-loading exercised.
// same fixtures as the test for Focus; just has fewer assertions, since Get does no progress tracking.
func TestGetSingleTree(t *testing.T) {
t.Run("empty path on scalar node returns start node", func(t *testing.T) {
n, err := traversal.Get(basicnode.NewString("x"), ipld.Path{})
Wish(t, err, ShouldEqual, nil)
Wish(t, n, ShouldEqual, basicnode.NewString("x"))
})
t.Run("one step path on map node works", func(t *testing.T) {
n, err := traversal.Get(middleMapNode, ipld.ParsePath("foo"))
Wish(t, err, ShouldEqual, nil)
Wish(t, n, ShouldEqual, basicnode.NewBool(true))
})
t.Run("two step path on map node works", func(t *testing.T) {
n, err := traversal.Get(middleMapNode, ipld.ParsePath("nested/nonlink"))
Wish(t, err, ShouldEqual, nil)
Wish(t, n, ShouldEqual, basicnode.NewString("zoo"))
})
}

func TestFocusWithLinkLoading(t *testing.T) {
t.Run("link traversal with no configured loader should fail", func(t *testing.T) {
t.Run("terminal link should fail", func(t *testing.T) {
Expand Down Expand Up @@ -160,3 +180,30 @@ func TestFocusWithLinkLoading(t *testing.T) {
Wish(t, err, ShouldEqual, nil)
})
}

func TestGetWithLinkLoading(t *testing.T) {
t.Run("link traversal with no configured loader should fail", func(t *testing.T) {
t.Run("terminal link should fail", func(t *testing.T) {
_, err := traversal.Get(middleMapNode, ipld.ParsePath("nested/alink"))
Wish(t, err.Error(), ShouldEqual, `error traversing node at "nested/alink": could not load link "`+leafAlphaLnk.String()+`": no LinkTargetNodePrototypeChooser configured`)
})
t.Run("mid-path link should fail", func(t *testing.T) {
_, err := traversal.Get(rootNode, ipld.ParsePath("linkedMap/nested/nonlink"))
Wish(t, err.Error(), ShouldEqual, `error traversing node at "linkedMap": could not load link "`+middleMapNodeLnk.String()+`": no LinkTargetNodePrototypeChooser configured`)
})
})
t.Run("link traversal with loader should work", func(t *testing.T) {
n, err := traversal.Progress{
Cfg: &traversal.Config{
LinkLoader: func(lnk ipld.Link, _ ipld.LinkContext) (io.Reader, error) {
return bytes.NewBuffer(storage[lnk]), nil
},
LinkTargetNodePrototypeChooser: func(_ ipld.Link, _ ipld.LinkContext) (ipld.NodePrototype, error) {
return basicnode.Prototype__Any{}, nil
},
},
}.Get(rootNode, ipld.ParsePath("linkedMap/nested/nonlink"))
Wish(t, err, ShouldEqual, nil)
Wish(t, n, ShouldEqual, basicnode.NewString("zoo"))
})
}

0 comments on commit 46f2852

Please sign in to comment.