focus.go 14.5 KB
Newer Older
1 2 3 4 5
package traversal

import (
	"fmt"

tavit ohanian's avatar
tavit ohanian committed
6
	ld "gitlab.dms3.io/ld/go-ld-prime"
7 8
)

Eric Myhre's avatar
Eric Myhre committed
9 10 11 12 13 14 15
// Focus traverses a Node graph according to a path, reaches a single Node,
// and calls the given VisitFn on that reached node.
//
// 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 Focus function on the Progress structure
// for more advanced and configurable walks.
tavit ohanian's avatar
tavit ohanian committed
16
func Focus(n ld.Node, p ld.Path, fn VisitFn) error {
17
	return Progress{}.Focus(n, p, fn)
18 19
}

Eric Myhre's avatar
Eric Myhre committed
20 21 22 23 24 25 26
// 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.
tavit ohanian's avatar
tavit ohanian committed
27
func Get(n ld.Node, p ld.Path) (ld.Node, error) {
Eric Myhre's avatar
Eric Myhre committed
28 29 30
	return Progress{}.Get(n, p)
}

tavit ohanian's avatar
tavit ohanian committed
31
// FocusedTransform traverses an ld.Node graph, reaches a single Node,
Eric Myhre's avatar
Eric Myhre committed
32 33 34 35 36 37 38
// 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).
//
// 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 FocusedTransform function on the Progress structure
// for more advanced and configurable walks.
tavit ohanian's avatar
tavit ohanian committed
39
func FocusedTransform(n ld.Node, p ld.Path, fn TransformFn, createParents bool) (ld.Node, error) {
40
	return Progress{}.FocusedTransform(n, p, fn, createParents)
41 42
}

Eric Myhre's avatar
Eric Myhre committed
43 44
// Focus traverses a Node graph according to a path, reaches a single Node,
// and calls the given VisitFn on that reached node.
45 46 47 48
//
// Focus is a read-only traversal.
// See FocusedTransform if looking for a way to do an "update" to a Node.
//
Eric Myhre's avatar
Eric Myhre committed
49 50 51 52 53 54 55 56 57
// 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.
//
// Focus (and the other traversal functions) can be used again again inside the VisitFn!
// By using the traversal.Progress handed to the VisitFn,
// 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.
tavit ohanian's avatar
tavit ohanian committed
58
func (prog Progress) Focus(n ld.Node, p ld.Path, fn VisitFn) error {
Eric Myhre's avatar
Eric Myhre committed
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
	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.
tavit ohanian's avatar
tavit ohanian committed
77
func (prog Progress) Get(n ld.Node, p ld.Path) (ld.Node, error) {
Eric Myhre's avatar
Eric Myhre committed
78 79 80 81 82 83
	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.
tavit ohanian's avatar
tavit ohanian committed
84
func (prog *Progress) get(n ld.Node, p ld.Path, trackProgress bool) (ld.Node, error) {
Eric Myhre's avatar
Eric Myhre committed
85
	prog.init()
Eric Myhre's avatar
Eric Myhre committed
86
	segments := p.Segments()
tavit ohanian's avatar
tavit ohanian committed
87
	var prev ld.Node // for LinkContext
Eric Myhre's avatar
Eric Myhre committed
88
	for i, seg := range segments {
89
		// Traverse the segment.
90
		switch n.Kind() {
tavit ohanian's avatar
tavit ohanian committed
91
		case ld.Kind_Invalid:
92
			panic(fmt.Errorf("invalid node encountered at %q", p.Truncate(i)))
tavit ohanian's avatar
tavit ohanian committed
93
		case ld.Kind_Map:
94
			next, err := n.LookupByString(seg.String())
95
			if err != nil {
Eric Myhre's avatar
Eric Myhre committed
96
				return nil, fmt.Errorf("error traversing segment %q on node at %q: %s", seg, p.Truncate(i), err)
97
			}
98
			prev, n = n, next
tavit ohanian's avatar
tavit ohanian committed
99
		case ld.Kind_List:
100
			intSeg, err := seg.Index()
101
			if err != nil {
Eric Myhre's avatar
Eric Myhre committed
102
				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))
103
			}
104
			next, err := n.LookupByIndex(intSeg)
105
			if err != nil {
Eric Myhre's avatar
Eric Myhre committed
106
				return nil, fmt.Errorf("error traversing segment %q on node at %q: %s", seg, p.Truncate(i), err)
107
			}
108
			prev, n = n, next
109
		default:
Eric Myhre's avatar
Eric Myhre committed
110
			return nil, fmt.Errorf("cannot traverse node at %q: %s", p.Truncate(i), fmt.Errorf("cannot traverse terminals"))
111 112
		}
		// Dereference any links.
tavit ohanian's avatar
tavit ohanian committed
113
		for n.Kind() == ld.Kind_Link {
114
			lnk, _ := n.AsLink()
tavit ohanian's avatar
tavit ohanian committed
115
			lnkCtx := ld.LinkContext{
Eric Myhre's avatar
Eric Myhre committed
116
				Ctx:        prog.Cfg.Ctx,
117 118 119 120
				LinkPath:   p.Truncate(i),
				LinkNode:   n,
				ParentNode: prev,
			}
121
			// Pick what in-memory format we will build.
122
			np, err := prog.Cfg.LinkTargetNodePrototypeChooser(lnk, lnkCtx)
123
			if err != nil {
Eric Myhre's avatar
Eric Myhre committed
124
				return nil, fmt.Errorf("error traversing node at %q: could not load link %q: %s", p.Truncate(i+1), lnk, err)
125
			}
126
			// Load link!
Eric Myhre's avatar
Eric Myhre committed
127 128
			prev = n
			n, err = prog.Cfg.LinkSystem.Load(lnkCtx, lnk, np)
129
			if err != nil {
Eric Myhre's avatar
Eric Myhre committed
130 131 132 133 134
				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
135
			}
136 137
		}
	}
Eric Myhre's avatar
Eric Myhre committed
138 139 140 141
	if trackProgress {
		prog.Path = prog.Path.Join(p)
	}
	return n, nil
142 143
}

tavit ohanian's avatar
tavit ohanian committed
144
// FocusedTransform traverses an ld.Node graph, reaches a single Node,
Eric Myhre's avatar
Eric Myhre committed
145 146
// 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).
147
//
Eric Myhre's avatar
Eric Myhre committed
148 149
// If the TransformFn returns the same Node which it was called with,
// then the transform is a no-op, and the Node returned from the
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
// FocusedTransform call as a whole will also be the same as its starting Node.
//
// Otherwise, the reached node will be "replaced" with the new Node -- meaning
// that new intermediate nodes will be constructed to also replace each
// parent Node that was traversed to get here, thus propagating the changes in
// a copy-on-write fashion -- and the FocusedTransform function as a whole will
// return a new Node containing identical children except for those replaced.
//
// FocusedTransform can be used again inside the applied function!
// This kind of composition can be useful for doing batches of updates.
// E.g. if have a large Node graph which contains a 100-element list, and
// you want to replace elements 12, 32, and 95 of that list:
// then you should FocusedTransform to the list first, and inside that
// TransformFn's body, you can replace the entire list with a new one
// that is composed of copies of everything but those elements -- including
// using more TransformFn calls as desired to produce the replacement elements
// if it so happens that those replacement elements are easiest to construct
// by regarding them as incremental updates to the previous values.
168 169 170 171 172 173 174
// (This approach can also be used when doing other modifications like insertion
// or reordering -- which would otherwise be tricky to define, since
// each operation could change the meaning of subsequently used indexes.)
//
// As a special case, list appending is supported by using the path segment "-".
// (This is determined by the node it applies to -- if that path segment
// is applied to a map, it's just a regular map key of the string of dash.)
175 176 177 178 179
//
// Note that anything you can do with the Transform function, you can also
// do with regular Node and NodeBuilder usage directly.  Transform just
// does a large amount of the intermediate bookkeeping that's useful when
// creating new values which are partial updates to existing values.
Eric Myhre's avatar
Eric Myhre committed
180
//
tavit ohanian's avatar
tavit ohanian committed
181
func (prog Progress) FocusedTransform(n ld.Node, p ld.Path, fn TransformFn, createParents bool) (ld.Node, error) {
182 183 184 185 186 187 188 189 190 191 192 193
	prog.init()
	nb := n.Prototype().NewBuilder()
	if err := prog.focusedTransform(n, nb, p, fn, createParents); err != nil {
		return nil, err
	}
	return nb.Build(), nil
}

// focusedTransform assumes that an update will actually happen, and as it recurses deeper,
// begins building an updated node tree.
//
// As implemented, this is not actually efficient if the update will be a no-op; it won't notice until it gets there.
tavit ohanian's avatar
tavit ohanian committed
194
func (prog Progress) focusedTransform(n ld.Node, na ld.NodeAssembler, p ld.Path, fn TransformFn, createParents bool) error {
195 196 197 198 199
	if p.Len() == 0 {
		n2, err := fn(prog, n)
		if err != nil {
			return err
		}
200
		return na.AssignNode(n2)
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
	}
	seg, p2 := p.Shift()
	// Special branch for if we've entered createParent mode in an earlier step.
	//  This needs slightly different logic because there's no prior node to reference
	//   (and we wouldn't want to waste time creating a dummy one).
	if n == nil {
		ma, err := na.BeginMap(1)
		if err != nil {
			return err
		}
		prog.Path = prog.Path.AppendSegment(seg)
		if err := ma.AssembleKey().AssignString(seg.String()); err != nil {
			return err
		}
		if err := prog.focusedTransform(nil, ma.AssembleValue(), p2, fn, createParents); err != nil {
			return err
		}
		return ma.Finish()
	}
	// Handle node based on kind.
	//  If it's a recursive kind (map or list), we'll be recursing on it.
	//  If it's a link, load it!  And recurse on it.
	//  If it's a scalar kind (any of the rest), we'll... be erroring, actually;
	//   if we're at the end, it was already handled at the top of the function,
	//   so we only get to this case if we were expecting to go deeper.
	switch n.Kind() {
tavit ohanian's avatar
tavit ohanian committed
227
	case ld.Kind_Map:
228 229 230 231 232
		ma, err := na.BeginMap(n.Length())
		if err != nil {
			return err
		}
		// Copy children over.  Replace the target (preserving its current position!) while doing this, if found.
233
		//  Note that we don't recurse into copying children (assuming AssignNode doesn't); this is as shallow/COW as the AssignNode implementation permits.
234 235 236 237 238 239
		var replaced bool
		for itr := n.MapIterator(); !itr.Done(); {
			k, v, err := itr.Next()
			if err != nil {
				return err
			}
240
			if err := ma.AssembleKey().AssignNode(k); err != nil {
241 242 243 244 245 246 247 248 249
				return err
			}
			if asPathSegment(k).Equals(seg) {
				prog.Path = prog.Path.AppendSegment(seg)
				if err := prog.focusedTransform(v, ma.AssembleValue(), p2, fn, createParents); err != nil {
					return err
				}
				replaced = true
			} else {
250
				if err := ma.AssembleValue().AssignNode(v); err != nil {
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
					return err
				}
			}
		}
		if replaced {
			return ma.Finish()
		}
		// If we didn't find the target yet: append it.
		//  If we're at the end, always do this;
		//  if we're in the middle, only do this if createParents mode is enabled.
		prog.Path = prog.Path.AppendSegment(seg)
		if p.Len() > 1 && !createParents {
			return fmt.Errorf("transform: parent position at %q did not exist (and createParents was false)", prog.Path)
		}
		if err := ma.AssembleKey().AssignString(seg.String()); err != nil {
			return err
		}
		if err := prog.focusedTransform(nil, ma.AssembleValue(), p2, fn, createParents); err != nil {
			return err
		}
		return ma.Finish()
tavit ohanian's avatar
tavit ohanian committed
272
	case ld.Kind_List:
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
		la, err := na.BeginList(n.Length())
		if err != nil {
			return err
		}
		// First figure out if this path segment can apply to a list sanely at all.
		//  Simultaneously, get it in numeric format, so subsequent operations are cheaper.
		ti, err := seg.Index()
		if err != nil {
			if seg.String() == "-" {
				ti = -1
			} else {
				return fmt.Errorf("transform: cannot navigate path segment %q at %q because a list is here", seg, prog.Path)
			}
		}
		// Copy children over.  Replace the target (preserving its current position!) while doing this, if found.
288
		//  Note that we don't recurse into copying children (assuming AssignNode doesn't); this is as shallow/COW as the AssignNode implementation permits.
289 290 291 292 293 294 295 296 297 298 299 300 301
		var replaced bool
		for itr := n.ListIterator(); !itr.Done(); {
			i, v, err := itr.Next()
			if err != nil {
				return err
			}
			if ti == i {
				prog.Path = prog.Path.AppendSegment(seg)
				if err := prog.focusedTransform(v, la.AssembleValue(), p2, fn, createParents); err != nil {
					return err
				}
				replaced = true
			} else {
302
				if err := la.AssembleValue().AssignNode(v); err != nil {
303 304 305 306 307 308 309 310 311 312 313 314
					return err
				}
			}
		}
		if replaced {
			return la.Finish()
		}
		// If we didn't find the target yet: hopefully this was an append operation;
		//  if it wasn't, then it's index out of bounds.  We don't arbitrarily extend lists with filler.
		if ti >= 0 {
			return fmt.Errorf("transform: cannot navigate path segment %q at %q because it is beyond the list bounds", seg, prog.Path)
		}
tavit ohanian's avatar
tavit ohanian committed
315
		prog.Path = prog.Path.AppendSegment(ld.PathSegmentOfInt(n.Length()))
316 317 318 319
		if err := prog.focusedTransform(nil, la.AssembleValue(), p2, fn, createParents); err != nil {
			return err
		}
		return la.Finish()
tavit ohanian's avatar
tavit ohanian committed
320 321
	case ld.Kind_Link:
		lnkCtx := ld.LinkContext{
Eric Myhre's avatar
Eric Myhre committed
322
			Ctx:        prog.Cfg.Ctx,
323 324 325 326 327 328 329 330 331 332 333
			LinkPath:   prog.Path,
			LinkNode:   n,
			ParentNode: nil, // TODO inconvenient that we don't have this.  maybe this whole case should be a helper function.
		}
		lnk, _ := n.AsLink()
		// Pick what in-memory format we will build.
		np, err := prog.Cfg.LinkTargetNodePrototypeChooser(lnk, lnkCtx)
		if err != nil {
			return fmt.Errorf("transform: error traversing node at %q: could not load link %q: %s", prog.Path, lnk, err)
		}
		// Load link!
Eric Myhre's avatar
Eric Myhre committed
334 335 336 337
		//  We'll use LinkSystem.Fill here rather than Load,
		//   because there's a nice opportunity to reuse the builder shortly.
		nb := np.NewBuilder()
		err = prog.Cfg.LinkSystem.Fill(lnkCtx, lnk, nb)
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
		if err != nil {
			return fmt.Errorf("transform: error traversing node at %q: could not load link %q: %s", prog.Path, lnk, err)
		}
		prog.LastBlock.Path = prog.Path
		prog.LastBlock.Link = lnk
		n = nb.Build()
		// Recurse.
		//  Start a new builder for this, using the same prototype we just used for loading the link.
		//   (Or more specifically: this is an opportunity for just resetting a builder and reusing memory!)
		//  When we come back... we'll have to engage serialization and storage on the new node!
		//  Path isn't updated here (neither progress nor to-go).
		nb.Reset()
		if err := prog.focusedTransform(n, nb, p, fn, createParents); err != nil {
			return err
		}
		n = nb.Build()
Eric Myhre's avatar
Eric Myhre committed
354
		lnk, err = prog.Cfg.LinkSystem.Store(lnkCtx, lnk.Prototype(), n)
355 356 357 358 359 360 361
		if err != nil {
			return fmt.Errorf("transform: error storing transformed node at %q: %s", prog.Path, err)
		}
		return na.AssignLink(lnk)
	default:
		return fmt.Errorf("transform: parent position at %q was a scalar, cannot go deeper", prog.Path)
	}
362
}