Skip to content

Commit 39cddf7

Browse files
committed
Implement traversal.FocusedTransform.
And a few new accessors for Path that are helpful and reasonable.
1 parent 8fa241e commit 39cddf7

File tree

4 files changed

+433
-6
lines changed

4 files changed

+433
-6
lines changed

path.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,21 @@ func (p Path) String() string {
147147
return sb.String()
148148
}
149149

150-
// Segements returns a slice of the path segment strings.
150+
// Segments returns a slice of the path segment strings.
151151
//
152152
// It is not lawful to mutate nor append the returned slice.
153153
func (p Path) Segments() []PathSegment {
154154
return p.segments
155155
}
156156

157+
// Len returns the number of segments in this path.
158+
//
159+
// Zero segments means the path refers to "the current node".
160+
// One segment means it refers to a child of the current node; etc.
161+
func (p Path) Len() int {
162+
return len(p.segments)
163+
}
164+
157165
// Join creates a new path composed of the concatenation of this and the given path's segments.
158166
func (p Path) Join(p2 Path) Path {
159167
combinedSegments := make([]PathSegment, len(p.segments)+len(p2.segments))
@@ -191,3 +199,20 @@ func (p Path) Parent() Path {
191199
func (p Path) Truncate(i int) Path {
192200
return Path{p.segments[0:i]}
193201
}
202+
203+
// Last returns the trailing segment of the path.
204+
func (p Path) Last() PathSegment {
205+
if len(p.segments) < 1 {
206+
return PathSegment{}
207+
}
208+
return p.segments[len(p.segments)-1]
209+
}
210+
211+
// Shift returns the first segment of the path together with the remaining path after that first segment.
212+
// If applied to a zero-length path, it returns an empty segment and the same zero-length path.
213+
func (p Path) Shift() (PathSegment, Path) {
214+
if len(p.segments) < 1 {
215+
return PathSegment{}, Path{}
216+
}
217+
return p.segments[0], Path{p.segments[1:]}
218+
}

traversal/common.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,24 @@ func (prog *Progress) init() {
4545
}
4646
prog.Cfg.init()
4747
}
48+
49+
// asPathSegment figures out how to coerce a node into a PathSegment.
50+
// If it's a typed node: we take its representation. (Could be a struct with some string representation.)
51+
// If it's a string or an int, that's it.
52+
// Any other case will panic. (If you're using this one keys returned by a MapIterator, though, you can ignore this possibility;
53+
// any compliant map implementation should've already rejected that data long ago, and should not be able to yield it to you from an iterator.)
54+
func asPathSegment(n ipld.Node) ipld.PathSegment {
55+
if n2, ok := n.(schema.TypedNode); ok {
56+
n = n2.Representation()
57+
}
58+
switch n.Kind() {
59+
case ipld.Kind_String:
60+
s, _ := n.AsString()
61+
return ipld.PathSegmentOfString(s)
62+
case ipld.Kind_Int:
63+
i, _ := n.AsInt()
64+
return ipld.PathSegmentOfInt(i)
65+
default:
66+
panic(fmt.Errorf("cannot get pathsegment from a %s", n.Kind()))
67+
}
68+
}

traversal/focus.go

Lines changed: 192 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ func Get(n ipld.Node, p ipld.Path) (ipld.Node, error) {
3636
// It cannot cross links automatically (since this requires configuration).
3737
// Use the equivalent FocusedTransform function on the Progress structure
3838
// for more advanced and configurable walks.
39-
func FocusedTransform(n ipld.Node, p ipld.Path, fn TransformFn) (ipld.Node, error) {
40-
return Progress{}.FocusedTransform(n, p, fn)
39+
func FocusedTransform(n ipld.Node, p ipld.Path, fn TransformFn, createParents bool) (ipld.Node, error) {
40+
return Progress{}.FocusedTransform(n, p, fn, createParents)
4141
}
4242

4343
// Focus traverses a Node graph according to a path, reaches a single Node,
@@ -171,13 +171,200 @@ func (prog *Progress) get(n ipld.Node, p ipld.Path, trackProgress bool) (ipld.No
171171
// using more TransformFn calls as desired to produce the replacement elements
172172
// if it so happens that those replacement elements are easiest to construct
173173
// by regarding them as incremental updates to the previous values.
174+
// (This approach can also be used when doing other modifications like insertion
175+
// or reordering -- which would otherwise be tricky to define, since
176+
// each operation could change the meaning of subsequently used indexes.)
177+
//
178+
// As a special case, list appending is supported by using the path segment "-".
179+
// (This is determined by the node it applies to -- if that path segment
180+
// is applied to a map, it's just a regular map key of the string of dash.)
174181
//
175182
// Note that anything you can do with the Transform function, you can also
176183
// do with regular Node and NodeBuilder usage directly. Transform just
177184
// does a large amount of the intermediate bookkeeping that's useful when
178185
// creating new values which are partial updates to existing values.
179186
//
180-
// This feature is not yet implemented.
181-
func (prog Progress) FocusedTransform(n ipld.Node, p ipld.Path, fn TransformFn) (ipld.Node, error) {
182-
panic("TODO") // TODO surprisingly different from Focus -- need to store nodes we traversed, and able do building.
187+
func (prog Progress) FocusedTransform(n ipld.Node, p ipld.Path, fn TransformFn, createParents bool) (ipld.Node, error) {
188+
prog.init()
189+
nb := n.Prototype().NewBuilder()
190+
if err := prog.focusedTransform(n, nb, p, fn, createParents); err != nil {
191+
return nil, err
192+
}
193+
return nb.Build(), nil
194+
}
195+
196+
// focusedTransform assumes that an update will actually happen, and as it recurses deeper,
197+
// begins building an updated node tree.
198+
//
199+
// As implemented, this is not actually efficient if the update will be a no-op; it won't notice until it gets there.
200+
func (prog Progress) focusedTransform(n ipld.Node, na ipld.NodeAssembler, p ipld.Path, fn TransformFn, createParents bool) error {
201+
if p.Len() == 0 {
202+
n2, err := fn(prog, n)
203+
if err != nil {
204+
return err
205+
}
206+
return na.ConvertFrom(n2)
207+
}
208+
seg, p2 := p.Shift()
209+
// Special branch for if we've entered createParent mode in an earlier step.
210+
// This needs slightly different logic because there's no prior node to reference
211+
// (and we wouldn't want to waste time creating a dummy one).
212+
if n == nil {
213+
ma, err := na.BeginMap(1)
214+
if err != nil {
215+
return err
216+
}
217+
prog.Path = prog.Path.AppendSegment(seg)
218+
if err := ma.AssembleKey().AssignString(seg.String()); err != nil {
219+
return err
220+
}
221+
if err := prog.focusedTransform(nil, ma.AssembleValue(), p2, fn, createParents); err != nil {
222+
return err
223+
}
224+
return ma.Finish()
225+
}
226+
// Handle node based on kind.
227+
// If it's a recursive kind (map or list), we'll be recursing on it.
228+
// If it's a link, load it! And recurse on it.
229+
// If it's a scalar kind (any of the rest), we'll... be erroring, actually;
230+
// if we're at the end, it was already handled at the top of the function,
231+
// so we only get to this case if we were expecting to go deeper.
232+
switch n.Kind() {
233+
case ipld.Kind_Map:
234+
ma, err := na.BeginMap(n.Length())
235+
if err != nil {
236+
return err
237+
}
238+
// Copy children over. Replace the target (preserving its current position!) while doing this, if found.
239+
// Note that we don't recurse into copying children (assuming ConvertFrom doesn't); this is as shallow/COW as the ConvertFrom implementation permits.
240+
var replaced bool
241+
for itr := n.MapIterator(); !itr.Done(); {
242+
k, v, err := itr.Next()
243+
if err != nil {
244+
return err
245+
}
246+
if err := ma.AssembleKey().ConvertFrom(k); err != nil {
247+
return err
248+
}
249+
if asPathSegment(k).Equals(seg) {
250+
prog.Path = prog.Path.AppendSegment(seg)
251+
if err := prog.focusedTransform(v, ma.AssembleValue(), p2, fn, createParents); err != nil {
252+
return err
253+
}
254+
replaced = true
255+
} else {
256+
if err := ma.AssembleValue().ConvertFrom(v); err != nil {
257+
return err
258+
}
259+
}
260+
}
261+
if replaced {
262+
return ma.Finish()
263+
}
264+
// If we didn't find the target yet: append it.
265+
// If we're at the end, always do this;
266+
// if we're in the middle, only do this if createParents mode is enabled.
267+
prog.Path = prog.Path.AppendSegment(seg)
268+
if p.Len() > 1 && !createParents {
269+
return fmt.Errorf("transform: parent position at %q did not exist (and createParents was false)", prog.Path)
270+
}
271+
if err := ma.AssembleKey().AssignString(seg.String()); err != nil {
272+
return err
273+
}
274+
if err := prog.focusedTransform(nil, ma.AssembleValue(), p2, fn, createParents); err != nil {
275+
return err
276+
}
277+
return ma.Finish()
278+
case ipld.Kind_List:
279+
la, err := na.BeginList(n.Length())
280+
if err != nil {
281+
return err
282+
}
283+
// First figure out if this path segment can apply to a list sanely at all.
284+
// Simultaneously, get it in numeric format, so subsequent operations are cheaper.
285+
ti, err := seg.Index()
286+
if err != nil {
287+
if seg.String() == "-" {
288+
ti = -1
289+
} else {
290+
return fmt.Errorf("transform: cannot navigate path segment %q at %q because a list is here", seg, prog.Path)
291+
}
292+
}
293+
// Copy children over. Replace the target (preserving its current position!) while doing this, if found.
294+
// Note that we don't recurse into copying children (assuming ConvertFrom doesn't); this is as shallow/COW as the ConvertFrom implementation permits.
295+
var replaced bool
296+
for itr := n.ListIterator(); !itr.Done(); {
297+
i, v, err := itr.Next()
298+
if err != nil {
299+
return err
300+
}
301+
if ti == i {
302+
prog.Path = prog.Path.AppendSegment(seg)
303+
if err := prog.focusedTransform(v, la.AssembleValue(), p2, fn, createParents); err != nil {
304+
return err
305+
}
306+
replaced = true
307+
} else {
308+
if err := la.AssembleValue().ConvertFrom(v); err != nil {
309+
return err
310+
}
311+
}
312+
}
313+
if replaced {
314+
return la.Finish()
315+
}
316+
// If we didn't find the target yet: hopefully this was an append operation;
317+
// if it wasn't, then it's index out of bounds. We don't arbitrarily extend lists with filler.
318+
if ti >= 0 {
319+
return fmt.Errorf("transform: cannot navigate path segment %q at %q because it is beyond the list bounds", seg, prog.Path)
320+
}
321+
prog.Path = prog.Path.AppendSegment(ipld.PathSegmentOfInt(n.Length()))
322+
if err := prog.focusedTransform(nil, la.AssembleValue(), p2, fn, createParents); err != nil {
323+
return err
324+
}
325+
return la.Finish()
326+
case ipld.Kind_Link:
327+
lnkCtx := ipld.LinkContext{
328+
LinkPath: prog.Path,
329+
LinkNode: n,
330+
ParentNode: nil, // TODO inconvenient that we don't have this. maybe this whole case should be a helper function.
331+
}
332+
lnk, _ := n.AsLink()
333+
// Pick what in-memory format we will build.
334+
np, err := prog.Cfg.LinkTargetNodePrototypeChooser(lnk, lnkCtx)
335+
if err != nil {
336+
return fmt.Errorf("transform: error traversing node at %q: could not load link %q: %s", prog.Path, lnk, err)
337+
}
338+
nb := np.NewBuilder()
339+
// Load link!
340+
err = lnk.Load(
341+
prog.Cfg.Ctx,
342+
lnkCtx,
343+
nb,
344+
prog.Cfg.LinkLoader,
345+
)
346+
if err != nil {
347+
return fmt.Errorf("transform: error traversing node at %q: could not load link %q: %s", prog.Path, lnk, err)
348+
}
349+
prog.LastBlock.Path = prog.Path
350+
prog.LastBlock.Link = lnk
351+
n = nb.Build()
352+
// Recurse.
353+
// Start a new builder for this, using the same prototype we just used for loading the link.
354+
// (Or more specifically: this is an opportunity for just resetting a builder and reusing memory!)
355+
// When we come back... we'll have to engage serialization and storage on the new node!
356+
// Path isn't updated here (neither progress nor to-go).
357+
nb.Reset()
358+
if err := prog.focusedTransform(n, nb, p, fn, createParents); err != nil {
359+
return err
360+
}
361+
n = nb.Build()
362+
lnk, err = lnk.LinkBuilder().Build(prog.Cfg.Ctx, lnkCtx, n, prog.Cfg.LinkStorer)
363+
if err != nil {
364+
return fmt.Errorf("transform: error storing transformed node at %q: %s", prog.Path, err)
365+
}
366+
return na.AssignLink(lnk)
367+
default:
368+
return fmt.Errorf("transform: parent position at %q was a scalar, cannot go deeper", prog.Path)
369+
}
183370
}

0 commit comments

Comments
 (0)