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 }