github.com/ipld/go-ipld-prime@v0.21.0/traversal/focus.go (about)

     1  package traversal
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/ipld/go-ipld-prime/datamodel"
     7  	"github.com/ipld/go-ipld-prime/linking"
     8  )
     9  
    10  // Focus traverses a Node graph according to a path, reaches a single Node,
    11  // and calls the given VisitFn on that reached node.
    12  //
    13  // This function is a helper function which starts a new traversal with default configuration.
    14  // It cannot cross links automatically (since this requires configuration).
    15  // Use the equivalent Focus function on the Progress structure
    16  // for more advanced and configurable walks.
    17  func Focus(n datamodel.Node, p datamodel.Path, fn VisitFn) error {
    18  	return Progress{}.Focus(n, p, fn)
    19  }
    20  
    21  // Get is the equivalent of Focus, but returns the reached node (rather than invoking a callback at the target),
    22  // and does not yield Progress information.
    23  //
    24  // This function is a helper function which starts a new traversal with default configuration.
    25  // It cannot cross links automatically (since this requires configuration).
    26  // Use the equivalent Get function on the Progress structure
    27  // for more advanced and configurable walks.
    28  func Get(n datamodel.Node, p datamodel.Path) (datamodel.Node, error) {
    29  	return Progress{}.Get(n, p)
    30  }
    31  
    32  // FocusedTransform traverses a datamodel.Node graph, reaches a single Node,
    33  // and calls the given TransformFn to decide what new node to replace the visited node with.
    34  // A new Node tree will be returned (the original is unchanged).
    35  //
    36  // This function is a helper function which starts a new traversal with default configuration.
    37  // It cannot cross links automatically (since this requires configuration).
    38  // Use the equivalent FocusedTransform function on the Progress structure
    39  // for more advanced and configurable walks.
    40  func FocusedTransform(n datamodel.Node, p datamodel.Path, fn TransformFn, createParents bool) (datamodel.Node, error) {
    41  	return Progress{}.FocusedTransform(n, p, fn, createParents)
    42  }
    43  
    44  // Focus traverses a Node graph according to a path, reaches a single Node,
    45  // and calls the given VisitFn on that reached node.
    46  //
    47  // Focus is a read-only traversal.
    48  // See FocusedTransform if looking for a way to do an "update" to a Node.
    49  //
    50  // Provide configuration to this process using the Config field in the Progress object.
    51  //
    52  // This walk will automatically cross links, but requires some configuration
    53  // with link loading functions to do so.
    54  //
    55  // Focus (and the other traversal functions) can be used again again inside the VisitFn!
    56  // By using the traversal.Progress handed to the VisitFn,
    57  // the Path recorded of the traversal so far will continue to be extended,
    58  // and thus continued nested uses of Walk and Focus will see the fully contextualized Path.
    59  func (prog Progress) Focus(n datamodel.Node, p datamodel.Path, fn VisitFn) error {
    60  	n, err := prog.get(n, p, true)
    61  	if err != nil {
    62  		return err
    63  	}
    64  	return fn(prog, n)
    65  }
    66  
    67  // Get is the equivalent of Focus, but returns the reached node (rather than invoking a callback at the target),
    68  // and does not yield Progress information.
    69  //
    70  // Provide configuration to this process using the Config field in the Progress object.
    71  //
    72  // This walk will automatically cross links, but requires some configuration
    73  // with link loading functions to do so.
    74  //
    75  // If doing several traversals which are nested, consider using the Focus funcion in preference to Get;
    76  // the Focus functions provide updated Progress objects which can be used to do nested traversals while keeping consistent track of progress,
    77  // such that continued nested uses of Walk or Focus or Get will see the fully contextualized Path.
    78  func (prog Progress) Get(n datamodel.Node, p datamodel.Path) (datamodel.Node, error) {
    79  	return prog.get(n, p, false)
    80  }
    81  
    82  // get is the internal implementation for Focus and Get.
    83  // It *mutates* the Progress object it's called on, and returns reached nodes.
    84  // For Get calls, trackProgress=false, which avoids some allocations for state tracking that's not needed by that call.
    85  func (prog *Progress) get(n datamodel.Node, p datamodel.Path, trackProgress bool) (datamodel.Node, error) {
    86  	prog.init()
    87  	segments := p.Segments()
    88  	var prev datamodel.Node // for LinkContext
    89  	for i, seg := range segments {
    90  		// Check the budget!
    91  		if prog.Budget != nil {
    92  			prog.Budget.NodeBudget--
    93  			if prog.Budget.NodeBudget <= 0 {
    94  				return nil, &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path}
    95  			}
    96  		}
    97  		// Traverse the segment.
    98  		switch n.Kind() {
    99  		case datamodel.Kind_Invalid:
   100  			panic(fmt.Errorf("invalid node encountered at %q", p.Truncate(i)))
   101  		case datamodel.Kind_Map:
   102  			next, err := n.LookupByString(seg.String())
   103  			if err != nil {
   104  				return nil, fmt.Errorf("error traversing segment %q on node at %q: %w", seg, p.Truncate(i), err)
   105  			}
   106  			prev, n = n, next
   107  		case datamodel.Kind_List:
   108  			intSeg, err := seg.Index()
   109  			if err != nil {
   110  				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))
   111  			}
   112  			next, err := n.LookupByIndex(intSeg)
   113  			if err != nil {
   114  				return nil, fmt.Errorf("error traversing segment %q on node at %q: %w", seg, p.Truncate(i), err)
   115  			}
   116  			prev, n = n, next
   117  		default:
   118  			return nil, fmt.Errorf("cannot traverse node at %q: %w", p.Truncate(i), fmt.Errorf("cannot traverse terminals"))
   119  		}
   120  		// Dereference any links.
   121  		for n.Kind() == datamodel.Kind_Link {
   122  			lnk, _ := n.AsLink()
   123  			// Check the budget!
   124  			if prog.Budget != nil {
   125  				if prog.Budget.LinkBudget <= 0 {
   126  					return nil, &ErrBudgetExceeded{BudgetKind: "link", Path: prog.Path, Link: lnk}
   127  				}
   128  				prog.Budget.LinkBudget--
   129  			}
   130  			// Put together the context info we'll offer to the loader and prototypeChooser.
   131  			lnkCtx := linking.LinkContext{
   132  				Ctx:        prog.Cfg.Ctx,
   133  				LinkPath:   p.Truncate(i),
   134  				LinkNode:   n,
   135  				ParentNode: prev,
   136  			}
   137  			// Pick what in-memory format we will build.
   138  			np, err := prog.Cfg.LinkTargetNodePrototypeChooser(lnk, lnkCtx)
   139  			if err != nil {
   140  				return nil, fmt.Errorf("error traversing node at %q: could not load link %q: %w", p.Truncate(i+1), lnk, err)
   141  			}
   142  			// Load link!
   143  			prev = n
   144  			n, err = prog.Cfg.LinkSystem.Load(lnkCtx, lnk, np)
   145  			if err != nil {
   146  				return nil, fmt.Errorf("error traversing node at %q: could not load link %q: %w", p.Truncate(i+1), lnk, err)
   147  			}
   148  			if trackProgress {
   149  				prog.LastBlock.Path = p.Truncate(i + 1)
   150  				prog.LastBlock.Link = lnk
   151  			}
   152  		}
   153  	}
   154  	if trackProgress {
   155  		prog.Path = prog.Path.Join(p)
   156  	}
   157  	return n, nil
   158  }
   159  
   160  // FocusedTransform traverses a datamodel.Node graph, reaches a single Node,
   161  // and calls the given TransformFn to decide what new node to replace the visited node with.
   162  // A new Node tree will be returned (the original is unchanged).
   163  //
   164  // If the TransformFn returns the same Node which it was called with,
   165  // then the transform is a no-op, and the Node returned from the
   166  // FocusedTransform call as a whole will also be the same as its starting Node.
   167  //
   168  // Otherwise, the reached node will be "replaced" with the new Node -- meaning
   169  // that new intermediate nodes will be constructed to also replace each
   170  // parent Node that was traversed to get here, thus propagating the changes in
   171  // a copy-on-write fashion -- and the FocusedTransform function as a whole will
   172  // return a new Node containing identical children except for those replaced.
   173  //
   174  // Returning nil from the TransformFn as the replacement node means "remove this".
   175  //
   176  // FocusedTransform can be used again inside the applied function!
   177  // This kind of composition can be useful for doing batches of updates.
   178  // E.g. if have a large Node graph which contains a 100-element list, and
   179  // you want to replace elements 12, 32, and 95 of that list:
   180  // then you should FocusedTransform to the list first, and inside that
   181  // TransformFn's body, you can replace the entire list with a new one
   182  // that is composed of copies of everything but those elements -- including
   183  // using more TransformFn calls as desired to produce the replacement elements
   184  // if it so happens that those replacement elements are easiest to construct
   185  // by regarding them as incremental updates to the previous values.
   186  // (This approach can also be used when doing other modifications like insertion
   187  // or reordering -- which would otherwise be tricky to define, since
   188  // each operation could change the meaning of subsequently used indexes.)
   189  //
   190  // As a special case, list appending is supported by using the path segment "-".
   191  // (This is determined by the node it applies to -- if that path segment
   192  // is applied to a map, it's just a regular map key of the string of dash.)
   193  //
   194  // Note that anything you can do with the Transform function, you can also
   195  // do with regular Node and NodeBuilder usage directly.  Transform just
   196  // does a large amount of the intermediate bookkeeping that's useful when
   197  // creating new values which are partial updates to existing values.
   198  func (prog Progress) FocusedTransform(n datamodel.Node, p datamodel.Path, fn TransformFn, createParents bool) (datamodel.Node, error) {
   199  	prog.init()
   200  	nb := n.Prototype().NewBuilder()
   201  	if err := prog.focusedTransform(n, nb, p, fn, createParents); err != nil {
   202  		return nil, err
   203  	}
   204  	return nb.Build(), nil
   205  }
   206  
   207  // focusedTransform assumes that an update will actually happen, and as it recurses deeper,
   208  // begins building an updated node tree.
   209  //
   210  // As implemented, this is not actually efficient if the update will be a no-op; it won't notice until it gets there.
   211  func (prog Progress) focusedTransform(n datamodel.Node, na datamodel.NodeAssembler, p datamodel.Path, fn TransformFn, createParents bool) error {
   212  	at := prog.Path
   213  	// Base case: if we've reached the end of the path, do the replacement here.
   214  	//  (Note: in some cases within maps, there is another branch that is the base case, for reasons involving removes.)
   215  	if p.Len() == 0 {
   216  		n2, err := fn(prog, n)
   217  		if err != nil {
   218  			return err
   219  		}
   220  		return na.AssignNode(n2)
   221  	}
   222  	seg, p2 := p.Shift()
   223  	// Check the budget!
   224  	if prog.Budget != nil {
   225  		if prog.Budget.NodeBudget <= 0 {
   226  			return &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path}
   227  		}
   228  		prog.Budget.NodeBudget--
   229  	}
   230  	// Special branch for if we've entered createParent mode in an earlier step.
   231  	//  This needs slightly different logic because there's no prior node to reference
   232  	//   (and we wouldn't want to waste time creating a dummy one).
   233  	if n == nil {
   234  		ma, err := na.BeginMap(1)
   235  		if err != nil {
   236  			return err
   237  		}
   238  		prog.Path = at.AppendSegment(seg)
   239  		if err := ma.AssembleKey().AssignString(seg.String()); err != nil {
   240  			return err
   241  		}
   242  		if err := prog.focusedTransform(nil, ma.AssembleValue(), p2, fn, createParents); err != nil {
   243  			return err
   244  		}
   245  		return ma.Finish()
   246  	}
   247  	// Handle node based on kind.
   248  	//  If it's a recursive kind (map or list), we'll be recursing on it.
   249  	//  If it's a link, load it!  And recurse on it.
   250  	//  If it's a scalar kind (any of the rest), we'll... be erroring, actually;
   251  	//   if we're at the end, it was already handled at the top of the function,
   252  	//   so we only get to this case if we were expecting to go deeper.
   253  	switch n.Kind() {
   254  	case datamodel.Kind_Map:
   255  		ma, err := na.BeginMap(n.Length())
   256  		if err != nil {
   257  			return err
   258  		}
   259  		// If we're approaching the end of the path, call the TransformFunc.
   260  		//  We need to know if it returns nil (meaning: do a deletion) _before_ we do the AssembleKey step.
   261  		//  (This results in the entire map branch having a different base case.)
   262  		var end bool
   263  		var n2 datamodel.Node
   264  		if p2.Len() == 0 {
   265  			end = true
   266  			n3, err := n.LookupBySegment(seg)
   267  			if n3 != datamodel.Absent && err != nil { // TODO badly need to simplify the standard treatment of "not found" here.  Can't even fit it all in one line!  See https://github.com/ipld/go-ipld-prime/issues/360.
   268  				if _, ok := err.(datamodel.ErrNotExists); !ok {
   269  					return err
   270  				}
   271  			}
   272  			prog.Path = at.AppendSegment(seg)
   273  			n2, err = fn(prog, n3)
   274  			if err != nil {
   275  				return err
   276  			}
   277  		}
   278  		// Copy children over.  Replace the target (preserving its current position!) while doing this, if found.
   279  		//  Note that we don't recurse into copying children (assuming AssignNode doesn't); this is as shallow/COW as the AssignNode implementation permits.
   280  		var replaced bool
   281  		for itr := n.MapIterator(); !itr.Done(); {
   282  			k, v, err := itr.Next()
   283  			if err != nil {
   284  				return err
   285  			}
   286  			if asPathSegment(k).Equals(seg) { // for the segment that's either update, update within, or being removed:
   287  				if end { // the last path segment in the overall instruction gets a different case because it may need to handle deletion
   288  					if n2 == nil {
   289  						replaced = true
   290  						continue // replace with nil means delete, which means continue early here: don't even copy the key.
   291  					}
   292  				}
   293  				// as long as we're not deleting, then this key will exist in the new data.
   294  				if err := ma.AssembleKey().AssignNode(k); err != nil {
   295  					return err
   296  				}
   297  				replaced = true
   298  				if n2 != nil { // if we already produced the replacement because we're at the end...
   299  					if err := ma.AssembleValue().AssignNode(n2); err != nil {
   300  						return err
   301  					}
   302  				} else { // ... otherwise, recurse:
   303  					prog.Path = at.AppendSegment(seg)
   304  					if err := prog.focusedTransform(v, ma.AssembleValue(), p2, fn, createParents); err != nil {
   305  						return err
   306  					}
   307  				}
   308  			} else { // for any other siblings of the target: just copy.
   309  				if err := ma.AssembleKey().AssignNode(k); err != nil {
   310  					return err
   311  				}
   312  				if err := ma.AssembleValue().AssignNode(v); err != nil {
   313  					return err
   314  				}
   315  			}
   316  		}
   317  		if replaced {
   318  			return ma.Finish()
   319  		}
   320  		// If we didn't find the target yet: append it.
   321  		//  If we're at the end, always do this;
   322  		//  if we're in the middle, only do this if createParents mode is enabled.
   323  		prog.Path = at.AppendSegment(seg)
   324  		if p.Len() > 1 && !createParents {
   325  			return fmt.Errorf("transform: parent position at %q did not exist (and createParents was false)", prog.Path)
   326  		}
   327  		if err := ma.AssembleKey().AssignString(seg.String()); err != nil {
   328  			return err
   329  		}
   330  		if err := prog.focusedTransform(nil, ma.AssembleValue(), p2, fn, createParents); err != nil {
   331  			return err
   332  		}
   333  		return ma.Finish()
   334  	case datamodel.Kind_List:
   335  		la, err := na.BeginList(n.Length())
   336  		if err != nil {
   337  			return err
   338  		}
   339  		// First figure out if this path segment can apply to a list sanely at all.
   340  		//  Simultaneously, get it in numeric format, so subsequent operations are cheaper.
   341  		ti, err := seg.Index()
   342  		if err != nil {
   343  			if seg.String() == "-" {
   344  				ti = -1
   345  			} else {
   346  				return fmt.Errorf("transform: cannot navigate path segment %q at %q because a list is here", seg, prog.Path)
   347  			}
   348  		}
   349  		// Copy children over.  Replace the target (preserving its current position!) while doing this, if found.
   350  		//  Note that we don't recurse into copying children (assuming AssignNode doesn't); this is as shallow/COW as the AssignNode implementation permits.
   351  		var replaced bool
   352  		for itr := n.ListIterator(); !itr.Done(); {
   353  			i, v, err := itr.Next()
   354  			if err != nil {
   355  				return err
   356  			}
   357  			if ti == i {
   358  				prog.Path = prog.Path.AppendSegment(seg)
   359  				if err := prog.focusedTransform(v, la.AssembleValue(), p2, fn, createParents); err != nil {
   360  					return err
   361  				}
   362  				replaced = true
   363  			} else {
   364  				if err := la.AssembleValue().AssignNode(v); err != nil {
   365  					return err
   366  				}
   367  			}
   368  		}
   369  		if replaced {
   370  			return la.Finish()
   371  		}
   372  		// If we didn't find the target yet: hopefully this was an append operation;
   373  		//  if it wasn't, then it's index out of bounds.  We don't arbitrarily extend lists with filler.
   374  		if ti >= 0 {
   375  			return fmt.Errorf("transform: cannot navigate path segment %q at %q because it is beyond the list bounds", seg, prog.Path)
   376  		}
   377  		prog.Path = prog.Path.AppendSegment(datamodel.PathSegmentOfInt(n.Length()))
   378  		if err := prog.focusedTransform(nil, la.AssembleValue(), p2, fn, createParents); err != nil {
   379  			return err
   380  		}
   381  		return la.Finish()
   382  	case datamodel.Kind_Link:
   383  		lnk, _ := n.AsLink()
   384  		// Check the budget!
   385  		if prog.Budget != nil {
   386  			if prog.Budget.LinkBudget <= 0 {
   387  				return &ErrBudgetExceeded{BudgetKind: "link", Path: prog.Path, Link: lnk}
   388  			}
   389  			prog.Budget.LinkBudget--
   390  		}
   391  		// Put together the context info we'll offer to the loader and prototypeChooser.
   392  		lnkCtx := linking.LinkContext{
   393  			Ctx:        prog.Cfg.Ctx,
   394  			LinkPath:   prog.Path,
   395  			LinkNode:   n,
   396  			ParentNode: nil, // TODO inconvenient that we don't have this.  maybe this whole case should be a helper function.
   397  		}
   398  		// Pick what in-memory format we will build.
   399  		np, err := prog.Cfg.LinkTargetNodePrototypeChooser(lnk, lnkCtx)
   400  		if err != nil {
   401  			return fmt.Errorf("transform: error traversing node at %q: could not load link %q: %w", prog.Path, lnk, err)
   402  		}
   403  		// Load link!
   404  		//  We'll use LinkSystem.Fill here rather than Load,
   405  		//   because there's a nice opportunity to reuse the builder shortly.
   406  		nb := np.NewBuilder()
   407  		err = prog.Cfg.LinkSystem.Fill(lnkCtx, lnk, nb)
   408  		if err != nil {
   409  			return fmt.Errorf("transform: error traversing node at %q: could not load link %q: %w", prog.Path, lnk, err)
   410  		}
   411  		prog.LastBlock.Path = prog.Path
   412  		prog.LastBlock.Link = lnk
   413  		n = nb.Build()
   414  		// Recurse.
   415  		//  Start a new builder for this, using the same prototype we just used for loading the link.
   416  		//   (Or more specifically: this is an opportunity for just resetting a builder and reusing memory!)
   417  		//  When we come back... we'll have to engage serialization and storage on the new node!
   418  		//  Path isn't updated here (neither progress nor to-go).
   419  		nb.Reset()
   420  		if err := prog.focusedTransform(n, nb, p, fn, createParents); err != nil {
   421  			return err
   422  		}
   423  		n = nb.Build()
   424  		lnk, err = prog.Cfg.LinkSystem.Store(lnkCtx, lnk.Prototype(), n)
   425  		if err != nil {
   426  			return fmt.Errorf("transform: error storing transformed node at %q: %w", prog.Path, err)
   427  		}
   428  		return na.AssignLink(lnk)
   429  	default:
   430  		return fmt.Errorf("transform: parent position at %q was a scalar, cannot go deeper", prog.Path)
   431  	}
   432  }