github.com/ipld/go-ipld-prime@v0.21.0/node/bindnode/infer_test.go (about) 1 package bindnode_test 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "html/template" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "reflect" 12 "runtime/debug" 13 "strings" 14 "testing" 15 16 qt "github.com/frankban/quicktest" 17 "github.com/google/go-cmp/cmp" 18 "github.com/ipfs/go-cid" 19 20 "github.com/ipld/go-ipld-prime" 21 "github.com/ipld/go-ipld-prime/codec/dagjson" 22 "github.com/ipld/go-ipld-prime/datamodel" 23 cidlink "github.com/ipld/go-ipld-prime/linking/cid" 24 "github.com/ipld/go-ipld-prime/node/basicnode" 25 "github.com/ipld/go-ipld-prime/node/bindnode" 26 "github.com/ipld/go-ipld-prime/schema" 27 ) 28 29 type anyScalar struct { 30 Bool *bool 31 Int *int64 32 Float *float64 33 String *string 34 Bytes *[]byte 35 Link *datamodel.Link 36 } 37 38 type anyRecursive struct { 39 List *[]string 40 Map *struct { 41 Keys []string 42 Values map[string]string 43 } 44 } 45 46 var prototypeTests = []struct { 47 name string 48 schemaSrc string 49 ptrType interface{} 50 51 // Prettified for maintainability, valid DAG-JSON when compacted. 52 prettyDagJSON string 53 }{ 54 { 55 name: "Scalars", 56 schemaSrc: `type Root struct { 57 bool Bool 58 int Int 59 uint Int 60 float Float 61 string String 62 bytes Bytes 63 }`, 64 ptrType: (*struct { 65 Bool bool 66 Int int64 67 Uint uint32 68 Float float64 69 String string 70 Bytes []byte 71 })(nil), 72 prettyDagJSON: `{ 73 "bool": true, 74 "bytes": {"/": {"bytes": "34cd"}}, 75 "float": 12.5, 76 "int": 3, 77 "string": "foo", 78 "uint": 50 79 }`, 80 }, 81 { 82 name: "Links", 83 schemaSrc: `type Root struct { 84 linkCID Link 85 linkGeneric Link 86 linkImpl Link 87 }`, 88 ptrType: (*struct { 89 LinkCID cid.Cid 90 LinkGeneric datamodel.Link 91 LinkImpl cidlink.Link 92 })(nil), 93 prettyDagJSON: `{ 94 "linkCID": {"/": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"}, 95 "linkGeneric": {"/": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"}, 96 "linkImpl": {"/": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"} 97 }`, 98 }, 99 { 100 name: "Any", 101 // TODO: also Null 102 schemaSrc: `type Root struct { 103 anyNodeWithBool Any 104 anyNodeWithInt Any 105 anyNodeWithFloat Any 106 anyNodeWithString Any 107 anyNodeWithBytes Any 108 anyNodeWithList Any 109 anyNodeWithMap Any 110 anyNodeWithLink Any 111 112 anyNodeBehindList [Any] 113 anyNodeBehindMap {String:Any} 114 }`, 115 ptrType: (*struct { 116 AnyNodeWithBool datamodel.Node 117 AnyNodeWithInt datamodel.Node 118 AnyNodeWithFloat datamodel.Node 119 AnyNodeWithString datamodel.Node 120 AnyNodeWithBytes datamodel.Node 121 AnyNodeWithList datamodel.Node 122 AnyNodeWithMap datamodel.Node 123 AnyNodeWithLink datamodel.Node 124 125 AnyNodeBehindList []datamodel.Node 126 AnyNodeBehindMap struct { 127 Keys []string 128 Values map[string]datamodel.Node 129 } 130 })(nil), 131 prettyDagJSON: `{ 132 "anyNodeBehindList": [12.5, {"x": false}], 133 "anyNodeBehindMap": {"x": 123, "y": [true, false]}, 134 "anyNodeWithBool": true, 135 "anyNodeWithBytes": {"/": {"bytes": "34cd"}}, 136 "anyNodeWithFloat": 12.5, 137 "anyNodeWithInt": 3, 138 "anyNodeWithLink": {"/": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"}, 139 "anyNodeWithList": [3, 2, 1], 140 "anyNodeWithMap": {"a": "x", "b": "y"}, 141 "anyNodeWithString": "foo" 142 }`, 143 }, 144 { 145 name: "Enums", 146 schemaSrc: `type Root struct { 147 stringAsString EnumAsString 148 stringAsStringCustom EnumAsString 149 stringAsInt EnumAsInt 150 intAsInt EnumAsInt 151 uintAsInt EnumAsInt 152 } 153 type EnumAsString enum { 154 | Nope ("No") 155 | Yep ("Yes") 156 | Maybe 157 } 158 type EnumAsInt enum { 159 | Nope ("10") 160 | Yep ("11") 161 | Maybe ("12") 162 } representation int`, 163 ptrType: (*struct { 164 StringAsString string 165 StringAsStringCustom string 166 StringAsInt string 167 IntAsInt int32 168 UintAsInt uint16 169 })(nil), 170 prettyDagJSON: `{ 171 "intAsInt": 12, 172 "stringAsInt": 10, 173 "stringAsString": "Maybe", 174 "stringAsStringCustom": "Yes", 175 "uintAsInt": 11 176 }`, 177 }, 178 { 179 name: "ScalarKindedUnions", 180 // TODO: should we use an "Any" type from the prelude? 181 schemaSrc: `type Root struct { 182 boolAny AnyScalar 183 intAny AnyScalar 184 floatAny AnyScalar 185 stringAny AnyScalar 186 bytesAny AnyScalar 187 linkAny AnyScalar 188 } 189 190 type AnyScalar union { 191 | Bool bool 192 | Int int 193 | Float float 194 | String string 195 | Bytes bytes 196 | Link link 197 } representation kinded`, 198 ptrType: (*struct { 199 BoolAny anyScalar 200 IntAny anyScalar 201 FloatAny anyScalar 202 StringAny anyScalar 203 BytesAny anyScalar 204 LinkAny anyScalar 205 })(nil), 206 prettyDagJSON: `{ 207 "boolAny": true, 208 "bytesAny": {"/": {"bytes": "34cd"}}, 209 "floatAny": 12.5, 210 "intAny": 3, 211 "linkAny": {"/": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"}, 212 "stringAny": "foo" 213 }`, 214 }, 215 { 216 name: "RecursiveKindedUnions", 217 // TODO: should we use an "Any" type from the prelude? 218 // Especially since we use String map/list element types. 219 // TODO: use inline map/list defs once schema and dsl+dmt support it. 220 schemaSrc: `type Root struct { 221 listAny AnyRecursive 222 mapAny AnyRecursive 223 } 224 225 type List_String [String] 226 type Map_String {String:String} 227 228 type AnyRecursive union { 229 | List_String list 230 | Map_String map 231 } representation kinded`, 232 ptrType: (*struct { 233 ListAny anyRecursive 234 MapAny anyRecursive 235 })(nil), 236 prettyDagJSON: `{ 237 "listAny": ["foo", "bar"], 238 "mapAny": {"a": "x", "b": "y"} 239 }`, 240 }, 241 } 242 243 func compactJSON(t *testing.T, pretty string) string { 244 var buf bytes.Buffer 245 err := json.Compact(&buf, []byte(pretty)) 246 qt.Assert(t, err, qt.IsNil) 247 return buf.String() 248 } 249 250 func dagjsonEncode(t *testing.T, node datamodel.Node) string { 251 var sb strings.Builder 252 err := dagjson.Encode(node, &sb) 253 qt.Assert(t, err, qt.IsNil) 254 return sb.String() 255 } 256 257 func dagjsonDecode(t *testing.T, proto datamodel.NodePrototype, src string) datamodel.Node { 258 nb := proto.NewBuilder() 259 err := dagjson.Decode(nb, strings.NewReader(src)) 260 qt.Assert(t, err, qt.IsNil) 261 return nb.Build() 262 } 263 264 func TestPrototype(t *testing.T) { 265 t.Parallel() 266 267 for _, test := range prototypeTests { 268 test := test // don't reuse the range var 269 270 for _, onlySchema := range []bool{false, true} { 271 onlySchema := onlySchema // don't reuse the range var 272 suffix := "" 273 if onlySchema { 274 suffix = "_onlySchema" 275 } 276 t.Run(test.name+suffix, func(t *testing.T) { 277 t.Parallel() 278 279 ts, err := ipld.LoadSchemaBytes([]byte(test.schemaSrc)) 280 qt.Assert(t, err, qt.IsNil) 281 schemaType := ts.TypeByName("Root") 282 qt.Assert(t, schemaType, qt.Not(qt.IsNil)) 283 284 ptrType := test.ptrType // don't write to the shared test value 285 if onlySchema { 286 ptrType = nil 287 } 288 proto := bindnode.Prototype(ptrType, schemaType) 289 290 wantEncoded := compactJSON(t, test.prettyDagJSON) 291 node := dagjsonDecode(t, proto.Representation(), wantEncoded).(schema.TypedNode) 292 // TODO: assert node type matches ptrType 293 294 encoded := dagjsonEncode(t, node.Representation()) 295 qt.Assert(t, encoded, qt.Equals, wantEncoded) 296 297 // Verify that doing a dag-json encode of the non-repr node works. 298 _ = dagjsonEncode(t, node) 299 }) 300 } 301 } 302 } 303 304 func TestPrototypePointerCombinations(t *testing.T) { 305 t.Parallel() 306 307 // TODO: Null 308 // TODO: cover more schema types and repr strategies. 309 // Some of them are still using w.val directly without "nonPtr" calls. 310 kindTests := []struct { 311 name string 312 schemaType string 313 fieldPtrType interface{} 314 fieldDagJSON string 315 }{ 316 {"Bool", "Bool", (*bool)(nil), `true`}, 317 {"Int", "Int", (*int64)(nil), `23`}, 318 {"Float", "Float", (*float64)(nil), `34.5`}, 319 {"String", "String", (*string)(nil), `"foo"`}, 320 {"Bytes", "Bytes", (*[]byte)(nil), `{"/": {"bytes": "34cd"}}`}, 321 {"Link_CID", "Link", (*cid.Cid)(nil), `{"/": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"}`}, 322 {"Link_Impl", "Link", (*cidlink.Link)(nil), `{"/": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"}`}, 323 {"Link_Generic", "Link", (*datamodel.Link)(nil), `{"/": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"}`}, 324 {"List_String", "[String]", (*[]string)(nil), `["foo", "bar"]`}, 325 {"Any_Node_Int", "Any", (*datamodel.Node)(nil), `23`}, 326 // TODO: reenable once we don't require pointers for nullable 327 // {"Any_Pointer_Int", "{String: nullable Any}", 328 // (*struct { 329 // Keys []string 330 // Values map[string]datamodel.Node 331 // })(nil), `{"x":3,"y":"bar","z":[2.3,4.5]}`}, 332 {"Map_String_Int", "{String:Int}", (*struct { 333 Keys []string 334 Values map[string]int64 335 })(nil), `{"x":3,"y":4}`}, 336 } 337 338 // For each IPLD kind, we test a matrix of combinations for IPLD's optional 339 // and nullable fields alongside pointer usage on the Go field side. 340 modifiers := []struct { 341 schemaField string // "", "optional", "nullable", "optional nullable" 342 goPointers int // 0 (T), 1 (*T), 2 (**T) 343 }{ 344 {"", 0}, // regular IPLD field with Go's T 345 {"", 1}, // regular IPLD field with Go's *T 346 {"optional", 0}, // optional IPLD field with Go's T (skipped unless T is nilable) 347 {"optional", 1}, // optional IPLD field with Go's *T 348 {"nullable", 0}, // nullable IPLD field with Go's T (skipped unless T is nilable) 349 {"nullable", 1}, // nullable IPLD field with Go's *T 350 {"optional nullable", 2}, // optional and nullable IPLD field with Go's **T 351 } 352 for _, kindTest := range kindTests { 353 for _, modifier := range modifiers { 354 // don't reuse range vars 355 kindTest := kindTest 356 modifier := modifier 357 goFieldType := reflect.TypeOf(kindTest.fieldPtrType) 358 switch modifier.goPointers { 359 case 0: 360 goFieldType = goFieldType.Elem() // dereference fieldPtrType 361 case 1: 362 // fieldPtrType already uses one pointer 363 case 2: 364 goFieldType = reflect.PtrTo(goFieldType) // dereference fieldPtrType 365 } 366 if modifier.schemaField != "" && !nilable(goFieldType.Kind()) { 367 continue 368 } 369 t.Run(fmt.Sprintf("%s/%s-%dptr", kindTest.name, modifier.schemaField, modifier.goPointers), func(t *testing.T) { 370 t.Parallel() 371 372 var buf bytes.Buffer 373 err := template.Must(template.New("").Parse(` 374 type Root struct { 375 field {{.Modifier}} {{.Type}} 376 }`)).Execute(&buf, 377 struct { 378 Type, Modifier string 379 }{kindTest.schemaType, modifier.schemaField}) 380 qt.Assert(t, err, qt.IsNil) 381 schemaSrc := buf.String() 382 t.Logf("IPLD schema: %s", schemaSrc) 383 384 // *struct { Field {{.goFieldType}} } 385 goType := reflect.Zero(reflect.PtrTo(reflect.StructOf([]reflect.StructField{ 386 {Name: "Field", Type: goFieldType}, 387 }))).Interface() 388 t.Logf("Go type: %T", goType) 389 390 ts, err := ipld.LoadSchemaBytes([]byte(schemaSrc)) 391 qt.Assert(t, err, qt.IsNil) 392 schemaType := ts.TypeByName("Root") 393 qt.Assert(t, schemaType, qt.Not(qt.IsNil)) 394 395 proto := bindnode.Prototype(goType, schemaType) 396 wantEncodedBytes, err := json.Marshal(map[string]interface{}{"field": json.RawMessage(kindTest.fieldDagJSON)}) 397 qt.Assert(t, err, qt.IsNil) 398 wantEncoded := string(wantEncodedBytes) 399 400 node := dagjsonDecode(t, proto.Representation(), wantEncoded).(schema.TypedNode) 401 402 encoded := dagjsonEncode(t, node.Representation()) 403 qt.Assert(t, encoded, qt.Equals, wantEncoded) 404 405 // Assigning with the missing field should only work with optional. 406 nb := proto.NewBuilder() 407 err = dagjson.Decode(nb, strings.NewReader(`{}`)) 408 switch modifier.schemaField { 409 case "optional", "optional nullable": 410 qt.Assert(t, err, qt.IsNil) 411 node := nb.Build() 412 // The resulting node should be non-nil with a nil field. 413 nodeVal := reflect.ValueOf(bindnode.Unwrap(node)) 414 qt.Assert(t, nodeVal.Elem().FieldByName("Field").IsNil(), qt.IsTrue) 415 default: 416 qt.Assert(t, err, qt.Not(qt.IsNil)) 417 } 418 419 // Assigning with a null field should only work with nullable. 420 nb = proto.NewBuilder() 421 err = dagjson.Decode(nb, strings.NewReader(`{"field":null}`)) 422 switch modifier.schemaField { 423 case "nullable", "optional nullable": 424 qt.Assert(t, err, qt.IsNil) 425 node := nb.Build() 426 // The resulting node should be non-nil with a nil field. 427 nodeVal := reflect.ValueOf(bindnode.Unwrap(node)) 428 if modifier.schemaField == "nullable" { 429 qt.Assert(t, nodeVal.Elem().FieldByName("Field").IsNil(), qt.IsTrue) 430 } else { 431 qt.Assert(t, nodeVal.Elem().FieldByName("Field").Elem().IsNil(), qt.IsTrue) 432 } 433 default: 434 qt.Assert(t, err, qt.Not(qt.IsNil)) 435 } 436 }) 437 } 438 } 439 } 440 441 func nilable(kind reflect.Kind) bool { 442 switch kind { 443 case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 444 return true 445 default: 446 return false 447 } 448 } 449 450 func assembleAsKind(proto datamodel.NodePrototype, schemaType schema.Type, asKind datamodel.Kind) (ipld.Node, error) { 451 nb := proto.NewBuilder() 452 switch asKind { 453 case datamodel.Kind_Bool: 454 if err := nb.AssignBool(true); err != nil { 455 return nil, err 456 } 457 case datamodel.Kind_Int: 458 if err := nb.AssignInt(123); err != nil { 459 return nil, err 460 } 461 case datamodel.Kind_Float: 462 if err := nb.AssignFloat(12.5); err != nil { 463 return nil, err 464 } 465 case datamodel.Kind_String: 466 if err := nb.AssignString("foo"); err != nil { 467 return nil, err 468 } 469 case datamodel.Kind_Bytes: 470 if err := nb.AssignBytes([]byte("\x00bar")); err != nil { 471 return nil, err 472 } 473 case datamodel.Kind_Link: 474 someCid, err := cid.Decode("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi") 475 if err != nil { 476 return nil, err 477 } 478 if err := nb.AssignLink(cidlink.Link{Cid: someCid}); err != nil { 479 return nil, err 480 } 481 case datamodel.Kind_Map: 482 asm, err := nb.BeginMap(-1) 483 if err != nil { 484 return nil, err 485 } 486 // First via AssembleKey. 487 if err := asm.AssembleKey().AssignString("F1"); err != nil { 488 return nil, err 489 } 490 if err := asm.AssembleValue().AssignInt(101); err != nil { 491 return nil, err 492 } 493 // Then via AssembleEntry. 494 entryAsm, err := asm.AssembleEntry("F2") 495 if err != nil { 496 return nil, err 497 } 498 if err := entryAsm.AssignInt(102); err != nil { 499 return nil, err 500 } 501 502 // If this is a struct, using a missing field should error. 503 if _, ok := schemaType.(*schema.TypeStruct); ok { 504 if err := asm.AssembleKey().AssignString("MissingKey"); err != nil { 505 return nil, err 506 } 507 if err := asm.AssembleValue().AssignInt(101); err == nil { 508 return nil, fmt.Errorf("expected error on missing struct key") 509 } 510 } 511 if err := asm.Finish(); err != nil { 512 return nil, err 513 } 514 case datamodel.Kind_List: 515 asm, err := nb.BeginList(-1) 516 if err != nil { 517 return nil, err 518 } 519 // Note that we want the list to have two integer elements, 520 // which matches the map entries above, 521 // so that the struct with tuple repr just works too. 522 if err := asm.AssembleValue().AssignInt(101); err != nil { 523 return nil, err 524 } 525 if err := asm.AssembleValue().AssignInt(102); err != nil { 526 return nil, err 527 } 528 // If this is a struct, assembling one more tuple entry should error. 529 if _, ok := schemaType.(*schema.TypeStruct); ok { 530 if err := asm.AssembleValue().AssignInt(103); err == nil { 531 return nil, fmt.Errorf("expected error on extra tuple entry") 532 } 533 } 534 if err := asm.Finish(); err != nil { 535 return nil, err 536 } 537 } 538 node := nb.Build() 539 if node == nil { 540 // If we succeeded, node must never be nil. 541 return nil, fmt.Errorf("built node is nil") 542 } 543 return node, nil 544 } 545 546 func useNodeAsKind(node datamodel.Node, asKind datamodel.Kind) error { 547 if gotKind := node.Kind(); gotKind != asKind { 548 // Return a dummy error to signal when the kind doesn't match. 549 return datamodel.ErrWrongKind{MethodName: "TestKindMismatches_Dummy_Kind"} 550 } 551 // Just check that IsAbsent and IsNull don't panic, for now. 552 _ = node.IsAbsent() 553 _ = node.IsNull() 554 555 proto := node.Prototype() 556 if proto == nil { 557 return fmt.Errorf("got a null Prototype") 558 } 559 560 // TODO: also check LookupByNode, LookupBySegment 561 switch asKind { 562 case datamodel.Kind_Bool: 563 if _, err := node.AsBool(); err != nil { 564 return err 565 } 566 case datamodel.Kind_Int: 567 if _, err := node.AsInt(); err != nil { 568 return err 569 } 570 case datamodel.Kind_Float: 571 if _, err := node.AsFloat(); err != nil { 572 return err 573 } 574 case datamodel.Kind_String: 575 if _, err := node.AsString(); err != nil { 576 return err 577 } 578 case datamodel.Kind_Bytes: 579 if _, err := node.AsBytes(); err != nil { 580 return err 581 } 582 case datamodel.Kind_Link: 583 if _, err := node.AsLink(); err != nil { 584 return err 585 } 586 case datamodel.Kind_Map: 587 iter := node.MapIterator() 588 if iter == nil { 589 // Return a dummy error to signal whether iter is nil or not. 590 return datamodel.ErrWrongKind{MethodName: "TestKindMismatches_Dummy_MapIterator"} 591 } 592 for !iter.Done() { 593 _, _, err := iter.Next() 594 if err != nil { 595 return err 596 } 597 } 598 599 // valid element 600 entryNode, err := node.LookupByString("F1") 601 if err != nil { 602 return err 603 } 604 if err := useNodeAsKind(entryNode, datamodel.Kind_Int); err != nil { 605 return err 606 } 607 608 // missing element 609 _, missingErr := node.LookupByString("MissingKey") 610 switch err := missingErr.(type) { 611 case nil: 612 return fmt.Errorf("lookup of a missing key succeeded") 613 case datamodel.ErrNotExists: // expected for maps 614 case schema.ErrInvalidKey: // expected for structs 615 default: 616 return err 617 } 618 619 switch l := node.Length(); l { 620 case 2: 621 case -1: 622 // Return a dummy error to signal whether Length failed. 623 return datamodel.ErrWrongKind{MethodName: "TestKindMismatches_Dummy_Length"} 624 default: 625 return fmt.Errorf("unexpected Length: %d", l) 626 } 627 case datamodel.Kind_List: 628 iter := node.ListIterator() 629 if iter == nil { 630 // Return a dummy error to signal whether iter is nil or not. 631 return datamodel.ErrWrongKind{MethodName: "TestKindMismatches_Dummy_ListIterator"} 632 } 633 for !iter.Done() { 634 _, _, err := iter.Next() 635 if err != nil { 636 return err 637 } 638 } 639 640 // valid element 641 entryNode, err := node.LookupByIndex(1) 642 if err != nil { 643 return err 644 } 645 if err := useNodeAsKind(entryNode, datamodel.Kind_Int); err != nil { 646 return err 647 } 648 649 // missing element 650 _, missingErr := node.LookupByIndex(30) 651 switch err := missingErr.(type) { 652 case nil: 653 return fmt.Errorf("lookup of a missing key succeeded") 654 case datamodel.ErrNotExists: // expected for maps 655 case schema.ErrInvalidKey: // expected for structs 656 default: 657 return err 658 } 659 660 switch l := node.Length(); l { 661 case 2: 662 case -1: 663 // Return a dummy error to signal whether Length failed. 664 return datamodel.ErrWrongKind{MethodName: "TestKindMismatches_Dummy_Length"} 665 default: 666 return fmt.Errorf("unexpected Length: %d", l) 667 } 668 } 669 return nil 670 } 671 672 func TestKindMismatches(t *testing.T) { 673 t.Parallel() 674 675 kindTests := []struct { 676 name string 677 schemaSrc string 678 }{ 679 {"Bool", "type Root bool"}, 680 {"Int", "type Root int"}, 681 {"Float", "type Root float"}, 682 {"String", "type Root string"}, 683 {"Bytes", "type Root bytes"}, 684 {"Map", ` 685 type Root {String:Int} 686 `}, 687 {"Struct", ` 688 type Root struct { 689 F1 Int 690 F2 Int 691 } 692 `}, 693 {"Struct_Tuple", ` 694 type Root struct { 695 F1 Int 696 F2 Int 697 } representation tuple 698 `}, 699 {"Enum", ` 700 type Root enum { 701 | Foo ("foo") 702 | Bar ("bar") 703 | Either 704 } 705 `}, 706 {"Union_Kinded_onlyString", ` 707 type Root union { 708 | String string 709 } representation kinded 710 `}, 711 {"Union_Kinded_onlyList", ` 712 type Root union { 713 | List list 714 } representation kinded 715 `}, 716 // TODO: more schema types and repr strategies 717 } 718 719 allKinds := []datamodel.Kind{ 720 // datamodel.Kind_Null, TODO 721 datamodel.Kind_Bool, 722 datamodel.Kind_Int, 723 datamodel.Kind_Float, 724 datamodel.Kind_String, 725 datamodel.Kind_Bytes, 726 datamodel.Kind_Link, 727 datamodel.Kind_Map, 728 datamodel.Kind_List, 729 } 730 731 // TODO: also test for non-repr assemblers and nodes 732 733 for _, kindTest := range kindTests { 734 // don't reuse range vars 735 kindTest := kindTest 736 t.Run(kindTest.name, func(t *testing.T) { 737 t.Parallel() 738 defer func() { 739 if r := recover(); r != nil { 740 // Note that debug.Stack inside the recover will include the 741 // stack trace for the original panic call. 742 t.Errorf("caught panic:\n%v\n%s", r, debug.Stack()) 743 } 744 }() 745 746 ts, err := ipld.LoadSchemaBytes([]byte(kindTest.schemaSrc)) 747 qt.Assert(t, err, qt.IsNil) 748 schemaType := ts.TypeByName("Root") 749 qt.Assert(t, schemaType, qt.Not(qt.IsNil)) 750 751 // Note that the Go type is inferred. 752 proto := bindnode.Prototype(nil, schemaType).Representation() 753 754 reprBehaviorKind := schemaType.RepresentationBehavior() 755 if reprBehaviorKind == datamodel.Kind_Invalid { 756 // For now, this only applies to kinded unions. 757 // We'll need to modify this when we test with Any. 758 members := schemaType.(*schema.TypeUnion).Members() 759 qt.Assert(t, members, qt.HasLen, 1) 760 reprBehaviorKind = members[0].RepresentationBehavior() 761 } 762 qt.Assert(t, reprBehaviorKind, qt.Not(qt.Equals), datamodel.Kind_Invalid) 763 764 for _, kind := range allKinds { 765 _, err := assembleAsKind(proto, schemaType, kind) 766 comment := qt.Commentf("assigned as %v", kind) 767 // Assembling should succed iff we used the right kind. 768 if kind == reprBehaviorKind { 769 qt.Assert(t, err, qt.IsNil, comment) 770 } else { 771 qt.Assert(t, err, qt.Not(qt.IsNil), comment) 772 qt.Assert(t, err, qt.ErrorAs, new(datamodel.ErrWrongKind), comment) 773 } 774 } 775 776 node, err := assembleAsKind(proto, schemaType, reprBehaviorKind) 777 qt.Assert(t, err, qt.IsNil) 778 node = node.(schema.TypedNode).Representation() 779 nodeKind := node.Kind() 780 781 for _, kind := range allKinds { 782 err := useNodeAsKind(node, kind) 783 comment := qt.Commentf("used as %v", kind) 784 // Using the node should succed iff we used the right kind. 785 if kind == nodeKind { 786 qt.Assert(t, err, qt.IsNil, comment) 787 } else { 788 qt.Assert(t, err, qt.Not(qt.IsNil), comment) 789 qt.Assert(t, err, qt.ErrorAs, new(datamodel.ErrWrongKind), comment) 790 } 791 } 792 }) 793 } 794 } 795 796 type verifyBadType struct { 797 ptrType interface{} 798 panicRegexp string 799 } 800 801 type ( 802 namedBool bool 803 namedInt64 int64 804 namedFloat64 float64 805 namedString string 806 namedBytes []byte 807 ) 808 809 var verifyTests = []struct { 810 name string 811 schemaSrc string 812 813 goodTypes []interface{} 814 badTypes []verifyBadType 815 }{ 816 { 817 name: "Bool", 818 schemaSrc: `type Root bool`, 819 goodTypes: []interface{}{ 820 (*bool)(nil), 821 (*namedBool)(nil), 822 }, 823 badTypes: []verifyBadType{ 824 {(*string)(nil), `.*type Root .* type string: kind mismatch;.*`}, 825 }, 826 }, 827 { 828 name: "Int", 829 schemaSrc: `type Root int`, 830 goodTypes: []interface{}{ 831 (*int)(nil), 832 (*namedInt64)(nil), 833 (*int8)(nil), 834 (*int16)(nil), 835 (*int32)(nil), 836 (*int64)(nil), 837 }, 838 badTypes: []verifyBadType{ 839 {(*string)(nil), `.*type Root .* type string: kind mismatch;.*`}, 840 }, 841 }, 842 { 843 name: "Float", 844 schemaSrc: `type Root float`, 845 goodTypes: []interface{}{ 846 (*float64)(nil), 847 (*namedFloat64)(nil), 848 (*float32)(nil), 849 }, 850 badTypes: []verifyBadType{ 851 {(*string)(nil), `.*type Root .* type string: kind mismatch;.*`}, 852 }, 853 }, 854 { 855 name: "String", 856 schemaSrc: `type Root string`, 857 goodTypes: []interface{}{ 858 (*string)(nil), 859 (*namedString)(nil), 860 }, 861 badTypes: []verifyBadType{ 862 {(*int)(nil), `.*type Root .* type int: kind mismatch;.*`}, 863 }, 864 }, 865 { 866 name: "Bytes", 867 schemaSrc: `type Root bytes`, 868 goodTypes: []interface{}{ 869 (*[]byte)(nil), 870 (*namedBytes)(nil), 871 (*[]uint8)(nil), // alias of byte 872 }, 873 badTypes: []verifyBadType{ 874 {(*int)(nil), `.*type Root .* type int: kind mismatch;.*`}, 875 {(*[]int)(nil), `.*type Root .* type \[\]int: kind mismatch;.*`}, 876 }, 877 }, 878 { 879 name: "List", 880 schemaSrc: `type Root [String]`, 881 goodTypes: []interface{}{ 882 (*[]string)(nil), 883 (*[]namedString)(nil), 884 }, 885 badTypes: []verifyBadType{ 886 {(*string)(nil), `.*type Root .* type string: kind mismatch;.*`}, 887 {(*[]int)(nil), `.*type String .* type int: kind mismatch;.*`}, 888 {(*[3]string)(nil), `.*type Root .* type \[3\]string: kind mismatch;.*`}, 889 }, 890 }, 891 { 892 name: "Struct", 893 schemaSrc: `type Root struct { 894 int Int 895 }`, 896 goodTypes: []interface{}{ 897 (*struct{ Int int })(nil), 898 (*struct{ Int namedInt64 })(nil), 899 }, 900 badTypes: []verifyBadType{ 901 {(*string)(nil), `.*type Root .* type string: kind mismatch;.*`}, 902 {(*struct{ Int bool })(nil), `.*type Int .* type bool: kind mismatch;.*`}, 903 {(*struct{ Int1, Int2 int })(nil), `.*type Root .* type struct {.*}: 2 vs 1 fields`}, 904 }, 905 }, 906 { 907 name: "Map", 908 schemaSrc: `type Root {String:Int}`, 909 goodTypes: []interface{}{ 910 (*struct { 911 Keys []string 912 Values map[string]int 913 })(nil), 914 (*struct { 915 Keys []namedString 916 Values map[namedString]namedInt64 917 })(nil), 918 }, 919 badTypes: []verifyBadType{ 920 {(*string)(nil), `.*type Root .* type string: kind mismatch;.*`}, 921 {(*struct{ Keys []string })(nil), `.*type Root .*: 1 vs 2 fields`}, 922 {(*struct{ Values map[string]int })(nil), `.*type Root .*: 1 vs 2 fields`}, 923 {(*struct { 924 Keys string 925 Values map[string]int 926 })(nil), `.*type Root .*: kind mismatch;.*`}, 927 {(*struct { 928 Keys []string 929 Values string 930 })(nil), `.*type Root .*: kind mismatch;.*`}, 931 }, 932 }, 933 { 934 name: "MapNullableAny", 935 schemaSrc: `type Root {String:nullable Any}`, 936 goodTypes: []interface{}{ 937 (*struct { 938 Keys []string 939 Values map[string]*datamodel.Node 940 })(nil), 941 (*struct { 942 Keys []string 943 Values map[string]datamodel.Node 944 })(nil), 945 }, 946 badTypes: []verifyBadType{ 947 {(*string)(nil), `.*type Root .* type string: kind mismatch;.*`}, 948 }, 949 }, 950 { 951 name: "Union", 952 schemaSrc: `type Root union { 953 | List_String list 954 | String string 955 } representation kinded 956 957 type List_String [String] 958 `, 959 goodTypes: []interface{}{ 960 (*struct { 961 List *[]string 962 String *string 963 })(nil), 964 (*struct { 965 List []string 966 String *string 967 })(nil), 968 (*struct { 969 List *[]namedString 970 String *namedString 971 })(nil), 972 }, 973 badTypes: []verifyBadType{ 974 {(*string)(nil), `.*type Root .* type string: kind mismatch;.*`}, 975 {(*struct{ List *[]string })(nil), `.*type Root .*: 1 vs 2 members`}, 976 {(*struct { 977 List []string 978 String string 979 })(nil), `.*type Root .*: union members must be nilable`}, 980 {(*struct { 981 List *[]string 982 String *int 983 })(nil), `.*type String .*: kind mismatch;.*`}, 984 }, 985 }, 986 } 987 988 func TestSchemaVerify(t *testing.T) { 989 t.Parallel() 990 991 for _, test := range verifyTests { 992 test := test // don't reuse the range var 993 994 t.Run(test.name, func(t *testing.T) { 995 t.Parallel() 996 997 ts, err := ipld.LoadSchemaBytes([]byte(test.schemaSrc)) 998 qt.Assert(t, err, qt.IsNil) 999 schemaType := ts.TypeByName("Root") 1000 qt.Assert(t, schemaType, qt.Not(qt.IsNil)) 1001 1002 for _, ptrType := range test.goodTypes { 1003 proto := bindnode.Prototype(ptrType, schemaType) 1004 qt.Assert(t, proto, qt.Not(qt.IsNil)) 1005 1006 ptrVal := reflect.New(reflect.TypeOf(ptrType).Elem()).Interface() 1007 node := bindnode.Wrap(ptrVal, schemaType) 1008 qt.Assert(t, node, qt.Not(qt.IsNil)) 1009 } 1010 1011 for _, bad := range test.badTypes { 1012 qt.Check(t, func() { bindnode.Prototype(bad.ptrType, schemaType) }, 1013 qt.PanicMatches, bad.panicRegexp) 1014 1015 ptrVal := reflect.New(reflect.TypeOf(bad.ptrType).Elem()).Interface() 1016 qt.Check(t, func() { bindnode.Wrap(ptrVal, schemaType) }, 1017 qt.PanicMatches, bad.panicRegexp) 1018 } 1019 }) 1020 } 1021 } 1022 1023 func TestProduceGoTypes(t *testing.T) { 1024 t.Parallel() 1025 1026 for _, test := range prototypeTests { 1027 test := test // don't reuse the range var 1028 1029 t.Run(test.name, func(t *testing.T) { 1030 t.Parallel() 1031 1032 ts, err := ipld.LoadSchemaBytes([]byte(test.schemaSrc)) 1033 qt.Assert(t, err, qt.IsNil) 1034 1035 // Include a package line and the datamodel import. 1036 buf := new(bytes.Buffer) 1037 fmt.Fprintln(buf, `package p`) 1038 fmt.Fprintln(buf, `import "github.com/ipld/go-ipld-prime/datamodel"`) 1039 fmt.Fprintln(buf, `var _ datamodel.Link // always used`) 1040 err = bindnode.ProduceGoTypes(buf, ts) 1041 qt.Assert(t, err, qt.IsNil) 1042 1043 // Ensure that the output builds, i.e. typechecks. 1044 genPath := filepath.Join(t.TempDir(), "gen.go") 1045 err = os.WriteFile(genPath, buf.Bytes(), 0o666) 1046 qt.Assert(t, err, qt.IsNil) 1047 1048 out, err := exec.Command("go", "build", genPath).CombinedOutput() 1049 qt.Assert(t, err, qt.IsNil, qt.Commentf("output: %s", out)) 1050 1051 // TODO: check that the generated types are compatible with the schema. 1052 }) 1053 } 1054 } 1055 1056 func TestRenameAssignNode(t *testing.T) { 1057 type Foo struct{ I int } 1058 1059 ts, _ := ipld.LoadSchemaBytes([]byte(` 1060 type Foo struct { 1061 I Int (rename "J") 1062 } 1063 `)) 1064 FooProto := bindnode.Prototype((*Foo)(nil), ts.TypeByName("Foo")) 1065 1066 // Decode straight into bindnode typed builder 1067 nb := FooProto.Representation().NewBuilder() 1068 err := dagjson.Decode(nb, bytes.NewReader([]byte(`{"J":100}`))) 1069 qt.Assert(t, err, qt.IsNil) 1070 nb.Build() 1071 1072 // decode into basicnode builder 1073 nb = basicnode.Prototype.Any.NewBuilder() 1074 err = dagjson.Decode(nb, bytes.NewReader([]byte(`{"J":100}`))) 1075 qt.Assert(t, err, qt.IsNil) 1076 node := nb.Build() 1077 1078 // AssignNode from the basicnode form 1079 nb = FooProto.Representation().NewBuilder() 1080 err = nb.AssignNode(node) 1081 qt.Assert(t, err, qt.IsNil) 1082 nb.Build() 1083 } 1084 1085 func TestEmptyTypedAssignNode(t *testing.T) { 1086 type Foo struct { 1087 I string 1088 J string 1089 K int 1090 } 1091 type Foo1Optional struct { 1092 I string 1093 J string 1094 K *int 1095 } 1096 type Foo2Optional struct { 1097 I string 1098 J *string 1099 K *int 1100 } 1101 tupleSchema := `type Foo struct { 1102 I String 1103 J String 1104 K Int 1105 } representation tuple` 1106 tuple1OptionalSchema := `type Foo struct { 1107 I String 1108 J String 1109 K optional Int 1110 } representation tuple` 1111 tuple2OptionalSchema := `type Foo struct { 1112 I String 1113 J optional String 1114 K optional Int 1115 } representation tuple` 1116 1117 testCases := map[string]struct { 1118 schema string 1119 typ interface{} 1120 dagJson string 1121 err string 1122 }{ 1123 "tuple": { 1124 schema: tupleSchema, 1125 typ: (*Foo)(nil), 1126 dagJson: `["","",0]`, 1127 }, 1128 "tuple with 2 absents": { 1129 schema: tupleSchema, 1130 typ: (*Foo)(nil), 1131 dagJson: `[""]`, 1132 err: "missing required fields: J,K", 1133 }, 1134 "tuple with 1 optional": { 1135 schema: tuple1OptionalSchema, 1136 typ: (*Foo1Optional)(nil), 1137 dagJson: `["","",0]`, 1138 }, 1139 "tuple with 1 optional and absent": { 1140 schema: tuple1OptionalSchema, 1141 typ: (*Foo1Optional)(nil), 1142 dagJson: `["",""]`, 1143 }, 1144 "tuple with 1 optional and 2 absents": { 1145 schema: tuple1OptionalSchema, 1146 typ: (*Foo1Optional)(nil), 1147 dagJson: `[""]`, 1148 err: "missing required fields: J", 1149 }, 1150 "tuple with 2 optional": { 1151 schema: tuple2OptionalSchema, 1152 typ: (*Foo2Optional)(nil), 1153 dagJson: `["","",0]`, 1154 }, 1155 "tuple with 2 optional and 1 absent": { 1156 schema: tuple2OptionalSchema, 1157 typ: (*Foo2Optional)(nil), 1158 dagJson: `["",""]`, 1159 }, 1160 "tuple with 2 optional and 2 absent": { 1161 schema: tuple2OptionalSchema, 1162 typ: (*Foo2Optional)(nil), 1163 dagJson: `[""]`, 1164 }, 1165 "tuple with 2 optional and 3 absent": { 1166 schema: tuple2OptionalSchema, 1167 typ: (*Foo2Optional)(nil), 1168 dagJson: `[]`, 1169 err: "missing required fields: I", 1170 }, 1171 "map": { 1172 schema: `type Foo struct { 1173 I String 1174 J String 1175 K Int 1176 } representation map 1177 `, 1178 typ: (*Foo)(nil), 1179 dagJson: `{"I":"","J":"","K":0}`, 1180 }, 1181 } 1182 1183 for testCase, data := range testCases { 1184 t.Run(testCase, func(t *testing.T) { 1185 ts, _ := ipld.LoadSchemaBytes([]byte(data.schema)) 1186 FooProto := bindnode.Prototype(data.typ, ts.TypeByName("Foo")) 1187 1188 // decode an "empty" object into Foo, these are all default values 1189 nb := basicnode.Prototype.Any.NewBuilder() 1190 err := dagjson.Decode(nb, bytes.NewReader([]byte(data.dagJson))) 1191 qt.Assert(t, err, qt.IsNil) 1192 node := nb.Build() 1193 1194 // AssignNode from the basicnode form 1195 nb = FooProto.Representation().NewBuilder() 1196 err = nb.AssignNode(node) 1197 if data.err == "" { 1198 qt.Assert(t, err, qt.IsNil) 1199 } else { 1200 qt.Assert(t, err, qt.ErrorMatches, data.err) 1201 } 1202 nb.Build() 1203 1204 // make an "empty" form, although none of the fields are optional so we should end up with defaults 1205 nb = FooProto.Representation().NewBuilder() 1206 empty := nb.Build() 1207 1208 // AssignNode from the representation of the "empty" form, which should pass through default values 1209 nb = FooProto.Representation().NewBuilder() 1210 err = nb.AssignNode(empty.(schema.TypedNode).Representation()) 1211 qt.Assert(t, err, qt.IsNil) 1212 }) 1213 } 1214 } 1215 1216 func TestInferLinksAndAny(t *testing.T) { 1217 link, err := cid.Decode("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi") 1218 qt.Assert(t, err, qt.IsNil) 1219 1220 type Nd struct { 1221 A cid.Cid 1222 B cidlink.Link 1223 C datamodel.Link 1224 D datamodel.Node 1225 } 1226 1227 proto := bindnode.Prototype(&Nd{}, nil) 1228 1229 expected := &Nd{ 1230 A: link, 1231 B: cidlink.Link{Cid: link}, 1232 C: cidlink.Link{Cid: link}, 1233 D: basicnode.NewString("Any Here"), 1234 } 1235 1236 node := bindnode.Wrap(expected, proto.Type()) 1237 1238 byts, err := ipld.Encode(node, dagjson.Encode) 1239 qt.Assert(t, err, qt.IsNil) 1240 1241 qt.Assert(t, string(byts), qt.Equals, `{"A":{"/":"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"},"B":{"/":"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"},"C":{"/":"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"},"D":"Any Here"}`) 1242 1243 nodeRt, err := ipld.DecodeUsingPrototype(byts, dagjson.Decode, proto) 1244 qt.Assert(t, err, qt.IsNil) 1245 1246 if actual, ok := bindnode.Unwrap(nodeRt).(*Nd); ok { 1247 qt.Assert(t, actual, qt.CmpEquals(cmp.Comparer(func(x, y cid.Cid) bool { return x.Equals(y) })), expected) 1248 } else { 1249 t.Error("expected *Nd from Unwrap") 1250 } 1251 }