github.com/ipld/go-ipld-prime@v0.21.0/traversal/focus_test.go (about) 1 package traversal_test 2 3 import ( 4 "reflect" 5 "testing" 6 7 qt "github.com/frankban/quicktest" 8 "github.com/google/go-cmp/cmp" 9 "github.com/ipfs/go-cid" 10 11 _ "github.com/ipld/go-ipld-prime/codec/dagjson" 12 "github.com/ipld/go-ipld-prime/datamodel" 13 "github.com/ipld/go-ipld-prime/fluent" 14 "github.com/ipld/go-ipld-prime/linking" 15 cidlink "github.com/ipld/go-ipld-prime/linking/cid" 16 "github.com/ipld/go-ipld-prime/must" 17 "github.com/ipld/go-ipld-prime/node/basicnode" 18 nodetests "github.com/ipld/go-ipld-prime/node/tests" 19 "github.com/ipld/go-ipld-prime/storage/memstore" 20 "github.com/ipld/go-ipld-prime/traversal" 21 ) 22 23 // Do some fixture fabrication. 24 // We assume all the builders and serialization must Just Work here. 25 26 var deepEqualsAllowAllUnexported = qt.CmpEquals(cmp.Exporter(func(reflect.Type) bool { return true })) 27 28 var store = memstore.Store{} 29 var ( 30 // baguqeeyexkjwnfy 31 leafAlpha, leafAlphaLnk = encode(basicnode.NewString("alpha")) 32 // baguqeeyeqvc7t3a 33 leafBeta, leafBetaLnk = encode(basicnode.NewString("beta")) 34 // baguqeeyezhlahvq 35 middleMapNode, middleMapNodeLnk = encode(fluent.MustBuildMap(basicnode.Prototype.Map, 3, func(na fluent.MapAssembler) { 36 na.AssembleEntry("foo").AssignBool(true) 37 na.AssembleEntry("bar").AssignBool(false) 38 na.AssembleEntry("nested").CreateMap(2, func(na fluent.MapAssembler) { 39 na.AssembleEntry("alink").AssignLink(leafAlphaLnk) 40 na.AssembleEntry("nonlink").AssignString("zoo") 41 }) 42 })) 43 // baguqeeyehfkkfwa 44 middleListNode, middleListNodeLnk = encode(fluent.MustBuildList(basicnode.Prototype.List, 4, func(na fluent.ListAssembler) { 45 na.AssembleValue().AssignLink(leafAlphaLnk) 46 na.AssembleValue().AssignLink(leafAlphaLnk) 47 na.AssembleValue().AssignLink(leafBetaLnk) 48 na.AssembleValue().AssignLink(leafAlphaLnk) 49 })) 50 // note that using `rootNode` directly will have a different field ordering than 51 // the encoded form if you were to load `rootNodeLnk` due to dag-json field 52 // reordering on encode, beware the difference for traversal order between 53 // created, in-memory nodes and those that have passed through a codec with 54 // field ordering rules 55 // baguqeeyeie4ajfy 56 rootNode, rootNodeLnk = encode(fluent.MustBuildMap(basicnode.Prototype.Map, 4, func(na fluent.MapAssembler) { 57 na.AssembleEntry("plain").AssignString("olde string") 58 na.AssembleEntry("linkedString").AssignLink(leafAlphaLnk) 59 na.AssembleEntry("linkedMap").AssignLink(middleMapNodeLnk) 60 na.AssembleEntry("linkedList").AssignLink(middleListNodeLnk) 61 })) 62 ) 63 64 // encode hardcodes some encoding choices for ease of use in fixture generation; 65 // just gimme a link and stuff the bytes in a map. 66 // (also return the node again for convenient assignment.) 67 func encode(n datamodel.Node) (datamodel.Node, datamodel.Link) { 68 lp := cidlink.LinkPrototype{Prefix: cid.Prefix{ 69 Version: 1, 70 Codec: 0x0129, 71 MhType: 0x13, 72 MhLength: 4, 73 }} 74 lsys := cidlink.DefaultLinkSystem() 75 lsys.SetWriteStorage(&store) 76 77 lnk, err := lsys.Store(linking.LinkContext{}, lp, n) 78 if err != nil { 79 panic(err) 80 } 81 return n, lnk 82 } 83 84 // covers Focus used on one already-loaded Node; no link-loading exercised. 85 func TestFocusSingleTree(t *testing.T) { 86 t.Run("empty path on scalar node returns start node", func(t *testing.T) { 87 err := traversal.Focus(basicnode.NewString("x"), datamodel.Path{}, func(prog traversal.Progress, n datamodel.Node) error { 88 qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("x")) 89 qt.Check(t, prog.Path.String(), qt.Equals, datamodel.Path{}.String()) 90 return nil 91 }) 92 qt.Check(t, err, qt.IsNil) 93 }) 94 t.Run("one step path on map node works", func(t *testing.T) { 95 err := traversal.Focus(middleMapNode, datamodel.ParsePath("foo"), func(prog traversal.Progress, n datamodel.Node) error { 96 qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewBool(true)) 97 qt.Check(t, prog.Path, deepEqualsAllowAllUnexported, datamodel.ParsePath("foo")) 98 return nil 99 }) 100 qt.Check(t, err, qt.IsNil) 101 }) 102 t.Run("two step path on map node works", func(t *testing.T) { 103 err := traversal.Focus(middleMapNode, datamodel.ParsePath("nested/nonlink"), func(prog traversal.Progress, n datamodel.Node) error { 104 qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("zoo")) 105 qt.Check(t, prog.Path, deepEqualsAllowAllUnexported, datamodel.ParsePath("nested/nonlink")) 106 return nil 107 }) 108 qt.Check(t, err, qt.IsNil) 109 }) 110 } 111 112 // covers Get used on one already-loaded Node; no link-loading exercised. 113 // same fixtures as the test for Focus; just has fewer assertions, since Get does no progress tracking. 114 func TestGetSingleTree(t *testing.T) { 115 t.Run("empty path on scalar node returns start node", func(t *testing.T) { 116 n, err := traversal.Get(basicnode.NewString("x"), datamodel.Path{}) 117 qt.Check(t, err, qt.IsNil) 118 qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("x")) 119 }) 120 t.Run("one step path on map node works", func(t *testing.T) { 121 n, err := traversal.Get(middleMapNode, datamodel.ParsePath("foo")) 122 qt.Check(t, err, qt.IsNil) 123 qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewBool(true)) 124 }) 125 t.Run("two step path on map node works", func(t *testing.T) { 126 n, err := traversal.Get(middleMapNode, datamodel.ParsePath("nested/nonlink")) 127 qt.Check(t, err, qt.IsNil) 128 qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("zoo")) 129 }) 130 } 131 132 func TestFocusWithLinkLoading(t *testing.T) { 133 t.Run("link traversal with no configured loader should fail", func(t *testing.T) { 134 t.Run("terminal link should fail", func(t *testing.T) { 135 err := traversal.Focus(middleMapNode, datamodel.ParsePath("nested/alink"), func(prog traversal.Progress, n datamodel.Node) error { 136 t.Errorf("should not be reached; no way to load this path") 137 return nil 138 }) 139 qt.Check(t, err.Error(), qt.Equals, `error traversing node at "nested/alink": could not load link "`+leafAlphaLnk.String()+`": no LinkTargetNodePrototypeChooser configured`) 140 }) 141 t.Run("mid-path link should fail", func(t *testing.T) { 142 err := traversal.Focus(rootNode, datamodel.ParsePath("linkedMap/nested/nonlink"), func(prog traversal.Progress, n datamodel.Node) error { 143 t.Errorf("should not be reached; no way to load this path") 144 return nil 145 }) 146 qt.Check(t, err.Error(), qt.Equals, `error traversing node at "linkedMap": could not load link "`+middleMapNodeLnk.String()+`": no LinkTargetNodePrototypeChooser configured`) 147 }) 148 }) 149 t.Run("link traversal with loader should work", func(t *testing.T) { 150 lsys := cidlink.DefaultLinkSystem() 151 lsys.SetReadStorage(&store) 152 err := traversal.Progress{ 153 Cfg: &traversal.Config{ 154 LinkSystem: lsys, 155 LinkTargetNodePrototypeChooser: basicnode.Chooser, 156 }, 157 }.Focus(rootNode, datamodel.ParsePath("linkedMap/nested/nonlink"), func(prog traversal.Progress, n datamodel.Node) error { 158 qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("zoo")) 159 qt.Check(t, prog.Path, deepEqualsAllowAllUnexported, datamodel.ParsePath("linkedMap/nested/nonlink")) 160 qt.Check(t, prog.LastBlock.Link, deepEqualsAllowAllUnexported, middleMapNodeLnk) 161 qt.Check(t, prog.LastBlock.Path, deepEqualsAllowAllUnexported, datamodel.ParsePath("linkedMap")) 162 return nil 163 }) 164 qt.Check(t, err, qt.IsNil) 165 }) 166 } 167 168 func TestGetWithLinkLoading(t *testing.T) { 169 t.Run("link traversal with no configured loader should fail", func(t *testing.T) { 170 t.Run("terminal link should fail", func(t *testing.T) { 171 _, err := traversal.Get(middleMapNode, datamodel.ParsePath("nested/alink")) 172 qt.Check(t, err.Error(), qt.Equals, `error traversing node at "nested/alink": could not load link "`+leafAlphaLnk.String()+`": no LinkTargetNodePrototypeChooser configured`) 173 }) 174 t.Run("mid-path link should fail", func(t *testing.T) { 175 _, err := traversal.Get(rootNode, datamodel.ParsePath("linkedMap/nested/nonlink")) 176 qt.Check(t, err.Error(), qt.Equals, `error traversing node at "linkedMap": could not load link "`+middleMapNodeLnk.String()+`": no LinkTargetNodePrototypeChooser configured`) 177 }) 178 }) 179 t.Run("link traversal with loader should work", func(t *testing.T) { 180 lsys := cidlink.DefaultLinkSystem() 181 lsys.SetReadStorage(&store) 182 n, err := traversal.Progress{ 183 Cfg: &traversal.Config{ 184 LinkSystem: lsys, 185 LinkTargetNodePrototypeChooser: basicnode.Chooser, 186 }, 187 }.Get(rootNode, datamodel.ParsePath("linkedMap/nested/nonlink")) 188 qt.Check(t, err, qt.IsNil) 189 qt.Check(t, n, nodetests.NodeContentEquals, basicnode.NewString("zoo")) 190 }) 191 } 192 193 func TestFocusedTransform(t *testing.T) { 194 t.Run("UpdateMapEntry", func(t *testing.T) { 195 n, err := traversal.FocusedTransform(rootNode, datamodel.ParsePath("plain"), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 196 qt.Check(t, progress.Path.String(), qt.Equals, "plain") 197 qt.Check(t, must.String(prev), qt.Equals, "olde string") 198 nb := prev.Prototype().NewBuilder() 199 nb.AssignString("new string!") 200 return nb.Build(), nil 201 }, false) 202 qt.Check(t, err, qt.IsNil) 203 qt.Check(t, n.Kind(), qt.Equals, datamodel.Kind_Map) 204 // updated value should be there 205 qt.Check(t, must.Node(n.LookupByString("plain")), nodetests.NodeContentEquals, basicnode.NewString("new string!")) 206 // everything else should be there 207 qt.Check(t, must.Node(n.LookupByString("linkedString")), qt.Equals, must.Node(rootNode.LookupByString("linkedString"))) 208 qt.Check(t, must.Node(n.LookupByString("linkedMap")), qt.Equals, must.Node(rootNode.LookupByString("linkedMap"))) 209 qt.Check(t, must.Node(n.LookupByString("linkedList")), qt.Equals, must.Node(rootNode.LookupByString("linkedList"))) 210 // everything should still be in the same order 211 qt.Check(t, keys(n), qt.DeepEquals, []string{"plain", "linkedString", "linkedMap", "linkedList"}) 212 }) 213 t.Run("UpdateDeeperMap", func(t *testing.T) { 214 n, err := traversal.FocusedTransform(middleMapNode, datamodel.ParsePath("nested/alink"), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 215 qt.Check(t, progress.Path.String(), qt.Equals, "nested/alink") 216 qt.Check(t, prev, nodetests.NodeContentEquals, basicnode.NewLink(leafAlphaLnk)) 217 return basicnode.NewString("new string!"), nil 218 }, false) 219 qt.Check(t, err, qt.IsNil) 220 qt.Check(t, n.Kind(), qt.Equals, datamodel.Kind_Map) 221 // updated value should be there 222 qt.Check(t, must.Node(must.Node(n.LookupByString("nested")).LookupByString("alink")), nodetests.NodeContentEquals, basicnode.NewString("new string!")) 223 // everything else in the parent map should should be there! 224 qt.Check(t, must.Node(n.LookupByString("foo")), qt.Equals, must.Node(middleMapNode.LookupByString("foo"))) 225 qt.Check(t, must.Node(n.LookupByString("bar")), qt.Equals, must.Node(middleMapNode.LookupByString("bar"))) 226 // everything should still be in the same order 227 qt.Check(t, keys(n), qt.DeepEquals, []string{"foo", "bar", "nested"}) 228 }) 229 t.Run("AppendIfNotExists", func(t *testing.T) { 230 n, err := traversal.FocusedTransform(rootNode, datamodel.ParsePath("newpart"), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 231 qt.Check(t, progress.Path.String(), qt.Equals, "newpart") 232 qt.Check(t, prev, qt.IsNil) // REVIEW: should datamodel.Absent be used here? I lean towards "no" but am unsure what's least surprising here. 233 // An interesting thing to note about inserting a value this way is that you have no `prev.Prototype().NewBuilder()` to use if you wanted to. 234 // But if that's an issue, then what you do is a focus or walk (transforming or not) to the parent node, get its child prototypes, and go from there. 235 return basicnode.NewString("new string!"), nil 236 }, false) 237 qt.Check(t, err, qt.IsNil) 238 qt.Check(t, n.Kind(), qt.Equals, datamodel.Kind_Map) 239 // updated value should be there 240 qt.Check(t, must.Node(n.LookupByString("newpart")), nodetests.NodeContentEquals, basicnode.NewString("new string!")) 241 // everything should still be in the same order... with the new entry at the end. 242 qt.Check(t, keys(n), qt.DeepEquals, []string{"plain", "linkedString", "linkedMap", "linkedList", "newpart"}) 243 }) 244 t.Run("CreateParents", func(t *testing.T) { 245 n, err := traversal.FocusedTransform(rootNode, datamodel.ParsePath("newsection/newpart"), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 246 qt.Check(t, progress.Path.String(), qt.Equals, "newsection/newpart") 247 qt.Check(t, prev, qt.IsNil) // REVIEW: should datamodel.Absent be used here? I lean towards "no" but am unsure what's least surprising here. 248 return basicnode.NewString("new string!"), nil 249 }, true) 250 qt.Check(t, err, qt.IsNil) 251 qt.Check(t, n.Kind(), qt.Equals, datamodel.Kind_Map) 252 // a new map node in the middle should've been created 253 n2 := must.Node(n.LookupByString("newsection")) 254 qt.Check(t, n2.Kind(), qt.Equals, datamodel.Kind_Map) 255 // updated value should in there 256 qt.Check(t, must.Node(n2.LookupByString("newpart")), nodetests.NodeContentEquals, basicnode.NewString("new string!")) 257 // everything in the root map should still be in the same order... with the new entry at the end. 258 qt.Check(t, keys(n), qt.DeepEquals, []string{"plain", "linkedString", "linkedMap", "linkedList", "newsection"}) 259 // and the created intermediate map of course has just one entry. 260 qt.Check(t, keys(n2), qt.DeepEquals, []string{"newpart"}) 261 }) 262 t.Run("CreateParentsRequiresPermission", func(t *testing.T) { 263 _, err := traversal.FocusedTransform(rootNode, datamodel.ParsePath("newsection/newpart"), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 264 qt.Check(t, true, qt.IsFalse) // ought not be reached 265 return nil, nil 266 }, false) 267 qt.Check(t, err.Error(), qt.Equals, "transform: parent position at \"newsection\" did not exist (and createParents was false)") 268 }) 269 t.Run("UpdateListEntry", func(t *testing.T) { 270 n, err := traversal.FocusedTransform(middleListNode, datamodel.ParsePath("2"), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 271 qt.Check(t, progress.Path.String(), qt.Equals, "2") 272 qt.Check(t, prev, nodetests.NodeContentEquals, basicnode.NewLink(leafBetaLnk)) 273 return basicnode.NewString("new string!"), nil 274 }, false) 275 qt.Check(t, err, qt.IsNil) 276 qt.Check(t, n.Kind(), qt.Equals, datamodel.Kind_List) 277 // updated value should be there 278 qt.Check(t, must.Node(n.LookupByIndex(2)), nodetests.NodeContentEquals, basicnode.NewString("new string!")) 279 // everything else should be there 280 qt.Check(t, n.Length(), qt.Equals, int64(4)) 281 qt.Check(t, must.Node(n.LookupByIndex(0)), nodetests.NodeContentEquals, basicnode.NewLink(leafAlphaLnk)) 282 qt.Check(t, must.Node(n.LookupByIndex(1)), nodetests.NodeContentEquals, basicnode.NewLink(leafAlphaLnk)) 283 qt.Check(t, must.Node(n.LookupByIndex(3)), nodetests.NodeContentEquals, basicnode.NewLink(leafAlphaLnk)) 284 }) 285 t.Run("AppendToList", func(t *testing.T) { 286 n, err := traversal.FocusedTransform(middleListNode, datamodel.ParsePath("-"), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 287 qt.Check(t, progress.Path.String(), qt.Equals, "4") 288 qt.Check(t, prev, qt.IsNil) 289 return basicnode.NewString("new string!"), nil 290 }, false) 291 qt.Check(t, err, qt.IsNil) 292 qt.Check(t, n.Kind(), qt.Equals, datamodel.Kind_List) 293 // updated value should be there 294 qt.Check(t, must.Node(n.LookupByIndex(4)), nodetests.NodeContentEquals, basicnode.NewString("new string!")) 295 // everything else should be there 296 qt.Check(t, n.Length(), qt.Equals, int64(5)) 297 }) 298 t.Run("ListBounds", func(t *testing.T) { 299 _, err := traversal.FocusedTransform(middleListNode, datamodel.ParsePath("4"), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 300 qt.Check(t, true, qt.IsFalse) // ought not be reached 301 return nil, nil 302 }, false) 303 qt.Check(t, err, qt.ErrorMatches, "transform: cannot navigate path segment \"4\" at \"\" because it is beyond the list bounds") 304 }) 305 t.Run("ReplaceRoot", func(t *testing.T) { // a fairly degenerate case and no reason to do this, but should work. 306 n, err := traversal.FocusedTransform(middleListNode, datamodel.ParsePath(""), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 307 qt.Check(t, progress.Path.String(), qt.Equals, "") 308 qt.Check(t, prev, nodetests.NodeContentEquals, middleListNode) 309 nb := basicnode.Prototype.Any.NewBuilder() 310 la, _ := nb.BeginList(0) 311 la.Finish() 312 return nb.Build(), nil 313 }, false) 314 qt.Check(t, err, qt.IsNil) 315 qt.Check(t, n.Kind(), qt.Equals, datamodel.Kind_List) 316 qt.Check(t, n.Length(), qt.Equals, int64(0)) 317 }) 318 } 319 320 func TestFocusedTransformWithLinks(t *testing.T) { 321 var store2 = memstore.Store{} 322 lsys := cidlink.DefaultLinkSystem() 323 lsys.SetReadStorage(&store) 324 lsys.SetWriteStorage(&store2) 325 cfg := traversal.Config{ 326 LinkSystem: lsys, 327 LinkTargetNodePrototypeChooser: basicnode.Chooser, 328 } 329 t.Run("UpdateMapBeyondLink", func(t *testing.T) { 330 n, err := traversal.Progress{ 331 Cfg: &cfg, 332 }.FocusedTransform(rootNode, datamodel.ParsePath("linkedMap/nested/nonlink"), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 333 qt.Check(t, progress.Path.String(), qt.Equals, "linkedMap/nested/nonlink") 334 qt.Check(t, must.String(prev), qt.Equals, "zoo") 335 qt.Check(t, progress.LastBlock.Path.String(), qt.Equals, "linkedMap") 336 qt.Check(t, progress.LastBlock.Link.String(), qt.Equals, "baguqeeyezhlahvq") 337 nb := prev.Prototype().NewBuilder() 338 nb.AssignString("new string!") 339 return nb.Build(), nil 340 }, false) 341 qt.Check(t, err, qt.IsNil) 342 qt.Check(t, n.Kind(), qt.Equals, datamodel.Kind_Map) 343 // there should be a new object in our new storage! 344 qt.Check(t, store2.Bag, qt.HasLen, 1) 345 // cleanup for next test 346 store2 = memstore.Store{} 347 }) 348 t.Run("UpdateNotBeyondLink", func(t *testing.T) { 349 // This is replacing a link with a non-link. Doing so shouldn't hit storage. 350 n, err := traversal.Progress{ 351 Cfg: &cfg, 352 }.FocusedTransform(rootNode, datamodel.ParsePath("linkedMap"), func(progress traversal.Progress, prev datamodel.Node) (datamodel.Node, error) { 353 qt.Check(t, progress.Path.String(), qt.Equals, "linkedMap") 354 nb := prev.Prototype().NewBuilder() 355 nb.AssignString("new string!") 356 return nb.Build(), nil 357 }, false) 358 qt.Check(t, err, qt.IsNil) 359 qt.Check(t, n.Kind(), qt.Equals, datamodel.Kind_Map) 360 // there should be no new objects in our new storage! 361 qt.Check(t, store2.Bag, qt.HasLen, 0) 362 // cleanup for next test 363 store2 = memstore.Store{} 364 }) 365 366 // link traverse to scalar // this is unspecifiable using the current path syntax! you'll just end up replacing the link with the scalar! 367 } 368 369 func keys(n datamodel.Node) []string { 370 v := make([]string, 0, n.Length()) 371 for itr := n.MapIterator(); !itr.Done(); { 372 k, _, _ := itr.Next() 373 v = append(v, must.String(k)) 374 } 375 return v 376 }