From 5dfad59fa5ade45621f85eea038ddb9bd189b224 Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Thu, 20 Aug 2020 14:31:48 +0200 Subject: [PATCH 1/2] Add traversal.Get function. --- traversal/focus.go | 61 ++++++++++++++++++++++++++++++++++------- traversal/focus_test.go | 20 ++++++++++++++ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/traversal/focus.go b/traversal/focus.go index 9a940433..27c6d04c 100644 --- a/traversal/focus.go +++ b/traversal/focus.go @@ -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). @@ -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 @@ -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 { @@ -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! @@ -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, diff --git a/traversal/focus_test.go b/traversal/focus_test.go index 3877509a..80fbe5e9 100644 --- a/traversal/focus_test.go +++ b/traversal/focus_test.go @@ -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) { From 0ab9c70f0cd609dc8fb1a33ce0b340e951fbb266 Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Sat, 22 Aug 2020 18:20:03 +0200 Subject: [PATCH 2/2] Also test traversal.Get linkloader configuration. This doesn't add much in the way of coverage, since it's internally quickly convergenced with what the Focus tests cover, but in testing, explicitness is good. --- traversal/focus_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/traversal/focus_test.go b/traversal/focus_test.go index 80fbe5e9..6dfe9a3d 100644 --- a/traversal/focus_test.go +++ b/traversal/focus_test.go @@ -180,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")) + }) +}