github.com/ipld/go-ipld-prime@v0.21.0/node/bindnode/custom_test.go (about) 1 package bindnode_test 2 3 import ( 4 "bytes" 5 "encoding/hex" 6 "fmt" 7 "math/big" 8 "strings" 9 "testing" 10 11 "github.com/google/go-cmp/cmp" 12 "github.com/ipfs/go-cid" 13 "github.com/ipld/go-ipld-prime" 14 "github.com/ipld/go-ipld-prime/codec/dagcbor" 15 "github.com/ipld/go-ipld-prime/codec/dagjson" 16 "github.com/ipld/go-ipld-prime/datamodel" 17 "github.com/ipld/go-ipld-prime/fluent/qp" 18 basicnode "github.com/ipld/go-ipld-prime/node/basic" 19 "github.com/ipld/go-ipld-prime/node/bindnode" 20 "github.com/ipld/go-ipld-prime/schema" 21 "github.com/multiformats/go-multihash" 22 23 qt "github.com/frankban/quicktest" 24 ) 25 26 type BoolSubst int 27 28 const ( 29 BoolSubst_Yes = 100 30 BoolSubst_No = -100 31 ) 32 33 func BoolSubstFromBool(b bool) (interface{}, error) { 34 if b { 35 return BoolSubst_Yes, nil 36 } 37 return BoolSubst_No, nil 38 } 39 40 func BoolToBoolSubst(b interface{}) (bool, error) { 41 bp, ok := b.(*BoolSubst) 42 if !ok { 43 return true, fmt.Errorf("expected *BoolSubst value") 44 } 45 switch *bp { 46 case BoolSubst_Yes: 47 return true, nil 48 case BoolSubst_No: 49 return false, nil 50 default: 51 return true, fmt.Errorf("bad BoolSubst") 52 } 53 } 54 55 type IntSubst string 56 57 func IntSubstFromInt(i int64) (interface{}, error) { 58 if i == 1000 { 59 return "one thousand", nil 60 } else if i == 2000 { 61 return "two thousand", nil 62 } 63 return nil, fmt.Errorf("unexpected value of IntSubst") 64 } 65 66 func IntToIntSubst(i interface{}) (int64, error) { 67 ip, ok := i.(*IntSubst) 68 if !ok { 69 return 0, fmt.Errorf("expected *IntSubst value") 70 } 71 switch *ip { 72 case "one thousand": 73 return 1000, nil 74 case "two thousand": 75 return 2000, nil 76 default: 77 return 0, fmt.Errorf("bad IntSubst") 78 } 79 } 80 81 type BigFloat struct{ *big.Float } 82 83 func BigFloatFromFloat(f float64) (interface{}, error) { 84 bf := big.NewFloat(f) 85 return &BigFloat{bf}, nil 86 } 87 88 func FloatFromBigFloat(f interface{}) (float64, error) { 89 fp, ok := f.(*BigFloat) 90 if !ok { 91 return 0, fmt.Errorf("expected *BigFloat value") 92 } 93 f64, _ := fp.Float64() 94 return f64, nil 95 } 96 97 type ByteArray [][]byte 98 99 func ByteArrayFromString(s string) (interface{}, error) { 100 sa := strings.Split(s, "|") 101 ba := make([][]byte, 0) 102 for _, a := range sa { 103 ba = append(ba, []byte(a)) 104 } 105 return ba, nil 106 } 107 108 func StringFromByteArray(b interface{}) (string, error) { 109 bap, ok := b.(*ByteArray) 110 if !ok { 111 return "", fmt.Errorf("expected *ByteArray value") 112 } 113 sb := strings.Builder{} 114 for i, b := range *bap { 115 sb.WriteString(string(b)) 116 if i != len(*bap)-1 { 117 sb.WriteString("|") 118 } 119 } 120 return sb.String(), nil 121 } 122 123 // similar to cid/Cid, go-address/Address, go-graphsync/RequestID 124 type Boop struct{ str string } 125 126 func NewBoop(b []byte) *Boop { 127 return &Boop{string(b)} 128 } 129 130 func (b Boop) Bytes() []byte { 131 return []byte(b.str) 132 } 133 134 func (b Boop) String() string { 135 return b.str 136 } 137 138 // similar to go-state-types/big/Int 139 type Frop struct{ *big.Int } 140 141 func NewFropFromString(str string) Frop { 142 v, _ := big.NewInt(0).SetString(str, 10) 143 return Frop{v} 144 } 145 146 func NewFropFromBytes(buf []byte) *Frop { 147 var negative bool 148 switch buf[0] { 149 case 0: 150 negative = false 151 case 1: 152 negative = true 153 default: 154 panic("can't handle this") 155 } 156 157 i := big.NewInt(0).SetBytes(buf[1:]) 158 if negative { 159 i.Neg(i) 160 } 161 162 return &Frop{i} 163 } 164 165 func (b *Frop) Bytes() []byte { 166 switch { 167 case b.Sign() > 0: 168 return append([]byte{0}, b.Int.Bytes()...) 169 case b.Sign() < 0: 170 return append([]byte{1}, b.Int.Bytes()...) 171 default: 172 return []byte{} 173 } 174 } 175 176 func BoopFromBytes(b []byte) (interface{}, error) { 177 return NewBoop(b), nil 178 } 179 180 func BoopToBytes(iface interface{}) ([]byte, error) { 181 if boop, ok := iface.(*Boop); ok { 182 return boop.Bytes(), nil 183 } 184 return nil, fmt.Errorf("did not get expected type") 185 } 186 187 func FropFromBytes(b []byte) (interface{}, error) { 188 return NewFropFromBytes(b), nil 189 } 190 191 func FropToBytes(iface interface{}) ([]byte, error) { 192 if frop, ok := iface.(*Frop); ok { 193 return frop.Bytes(), nil 194 } 195 return nil, fmt.Errorf("did not get expected type") 196 } 197 198 // Bitcoin's version of "links" is a hex form of the dbl-sha2-256 digest reversed 199 type BtcId string 200 201 func FromCidToBtcId(c cid.Cid) (interface{}, error) { 202 if c.Prefix().Codec != cid.BitcoinBlock { // should be able to do BitcoinTx too .. but .. 203 return nil, fmt.Errorf("can only convert IDs for BitcoinBlock codecs") 204 } 205 // and multihash must be dbl-sha2-256 206 dig, err := multihash.Decode(c.Hash()) 207 if err != nil { 208 return nil, err 209 } 210 hid := make([]byte, 0) 211 for i := len(dig.Digest) - 1; i >= 0; i-- { 212 hid = append(hid, dig.Digest[i]) 213 } 214 return BtcId(hex.EncodeToString(hid)), nil 215 } 216 217 func FromBtcIdToCid(iface interface{}) (cid.Cid, error) { 218 bid, ok := iface.(*BtcId) 219 if !ok { 220 return cid.Undef, fmt.Errorf("expected *BtcId value") 221 } 222 dig := make([]byte, 0) 223 hid, err := hex.DecodeString(string(*bid)) 224 if err != nil { 225 return cid.Undef, err 226 } 227 for i := len(hid) - 1; i >= 0; i-- { 228 dig = append(dig, hid[i]) 229 } 230 mh, err := multihash.Encode(dig, multihash.DBL_SHA2_256) 231 if err != nil { 232 return cid.Undef, err 233 } 234 return cid.NewCidV1(cid.BitcoinBlock, mh), nil 235 } 236 237 type Boom struct { 238 S string 239 St ByteArray 240 B Boop 241 Bo BoolSubst 242 Bptr *Boop 243 F Frop 244 Fl BigFloat 245 I int 246 In IntSubst 247 L BtcId 248 } 249 250 const boomSchema = ` 251 type Boom struct { 252 S String 253 St String 254 B Bytes 255 Bo Bool 256 Bptr nullable Bytes 257 F Bytes 258 Fl Float 259 I Int 260 In Int 261 L &Any 262 } representation map 263 ` 264 265 const boomFixtureDagJson = `{"B":{"/":{"bytes":"dGhlc2UgYXJlIGJ5dGVz"}},"Bo":false,"Bptr":{"/":{"bytes":"dGhlc2UgYXJlIHBvaW50ZXIgYnl0ZXM"}},"F":{"/":{"bytes":"AAH3fubjrGlwOMpClAkh/ro13L5Uls4/CtI"}},"Fl":1.12,"I":10101,"In":2000,"L":{"/":"bagyacvra2e6qt2fohajauxceox55t3gedsyqap2phmv7q2qaaaaaaaaaaaaa"},"S":"a string here","St":"a|byte|array"}` 266 267 var boomFixtureInstance = Boom{ 268 B: *NewBoop([]byte("these are bytes")), 269 Bo: BoolSubst_No, 270 Bptr: NewBoop([]byte("these are pointer bytes")), 271 F: NewFropFromString("12345678901234567891234567890123456789012345678901234567890"), 272 Fl: BigFloat{big.NewFloat(1.12)}, 273 I: 10101, 274 In: IntSubst("two thousand"), 275 S: "a string here", 276 St: ByteArray([][]byte{[]byte("a"), []byte("byte"), []byte("array")}), 277 L: BtcId("00000000000000006af82b3b4f3f00b11cc4ecd9fb75445c0a1238aee8093dd1"), 278 } 279 280 func TestCustom(t *testing.T) { 281 opts := []bindnode.Option{ 282 bindnode.TypedBytesConverter(&Boop{}, BoopFromBytes, BoopToBytes), 283 bindnode.TypedBytesConverter(&Frop{}, FropFromBytes, FropToBytes), 284 bindnode.TypedBoolConverter(BoolSubst(0), BoolSubstFromBool, BoolToBoolSubst), 285 bindnode.TypedIntConverter(IntSubst(""), IntSubstFromInt, IntToIntSubst), 286 bindnode.TypedFloatConverter(&BigFloat{}, BigFloatFromFloat, FloatFromBigFloat), 287 bindnode.TypedStringConverter(&ByteArray{}, ByteArrayFromString, StringFromByteArray), 288 bindnode.TypedLinkConverter(BtcId(""), FromCidToBtcId, FromBtcIdToCid), 289 } 290 291 typeSystem, err := ipld.LoadSchemaBytes([]byte(boomSchema)) 292 qt.Assert(t, err, qt.IsNil) 293 schemaType := typeSystem.TypeByName("Boom") 294 proto := bindnode.Prototype(&Boom{}, schemaType, opts...) 295 296 builder := proto.Representation().NewBuilder() 297 err = dagjson.Decode(builder, bytes.NewReader([]byte(boomFixtureDagJson))) 298 qt.Assert(t, err, qt.IsNil) 299 300 typ := bindnode.Unwrap(builder.Build()) 301 inst, ok := typ.(*Boom) 302 qt.Assert(t, ok, qt.IsTrue) 303 304 cmpr := qt.CmpEquals( 305 cmp.Comparer(func(x, y Boop) bool { return x.String() == y.String() }), 306 cmp.Comparer(func(x, y Frop) bool { return x.String() == y.String() }), 307 cmp.Comparer(func(x, y BigFloat) bool { return x.String() == y.String() }), 308 ) 309 qt.Assert(t, *inst, cmpr, boomFixtureInstance) 310 311 tn := bindnode.Wrap(inst, schemaType, opts...) 312 var buf bytes.Buffer 313 err = dagjson.Encode(tn.Representation(), &buf) 314 qt.Assert(t, err, qt.IsNil) 315 316 qt.Assert(t, buf.String(), qt.Equals, boomFixtureDagJson) 317 } 318 319 type AnyExtend struct { 320 Name string 321 Blob AnyExtendBlob 322 Count int 323 Null AnyCborEncoded 324 NullPtr *AnyCborEncoded 325 NullableWith *AnyCborEncoded 326 Bool AnyCborEncoded 327 Int AnyCborEncoded 328 Float AnyCborEncoded 329 String AnyCborEncoded 330 Bytes AnyCborEncoded 331 Link AnyCborEncoded 332 Map AnyCborEncoded 333 List AnyCborEncoded 334 BoolPtr *BoolSubst // included to test that a null entry won't call a non-Any converter 335 XListAny []AnyCborEncoded 336 XMapAny anyMap 337 } 338 339 type anyMap struct { 340 Keys []string 341 Values map[string]*AnyCborEncoded 342 } 343 344 const anyExtendSchema = ` 345 type AnyExtend struct { 346 Name String 347 Blob Any 348 Count Int 349 Null nullable Any 350 NullPtr nullable Any 351 NullableWith nullable Any 352 Bool Any 353 Int Any 354 Float Any 355 String Any 356 Bytes Any 357 Link Any 358 Map Any 359 List Any 360 BoolPtr nullable Bool 361 XListAny [Any] 362 XMapAny {String:Any} 363 } 364 ` 365 366 type AnyExtendBlob struct { 367 f string 368 x int64 369 y int64 370 z int64 371 } 372 373 func AnyExtendBlobFromNode(node datamodel.Node) (interface{}, error) { 374 foo, err := node.LookupByString("foo") 375 if err != nil { 376 return nil, err 377 } 378 fooStr, err := foo.AsString() 379 if err != nil { 380 return nil, err 381 } 382 baz, err := node.LookupByString("baz") 383 if err != nil { 384 return nil, err 385 } 386 x, err := baz.LookupByIndex(0) 387 if err != nil { 388 return nil, err 389 } 390 xi, err := x.AsInt() 391 if err != nil { 392 return nil, err 393 } 394 y, err := baz.LookupByIndex(1) 395 if err != nil { 396 return nil, err 397 } 398 yi, err := y.AsInt() 399 if err != nil { 400 return nil, err 401 } 402 z, err := baz.LookupByIndex(2) 403 if err != nil { 404 return nil, err 405 } 406 zi, err := z.AsInt() 407 if err != nil { 408 return nil, err 409 } 410 return &AnyExtendBlob{f: fooStr, x: xi, y: yi, z: zi}, nil 411 } 412 413 func (aeb AnyExtendBlob) ToNode() (datamodel.Node, error) { 414 return qp.BuildMap(basicnode.Prototype.Any, -1, func(ma datamodel.MapAssembler) { 415 qp.MapEntry(ma, "foo", qp.String(aeb.f)) 416 qp.MapEntry(ma, "baz", qp.List(-1, func(la datamodel.ListAssembler) { 417 qp.ListEntry(la, qp.Int(aeb.x)) 418 qp.ListEntry(la, qp.Int(aeb.y)) 419 qp.ListEntry(la, qp.Int(aeb.z)) 420 })) 421 }) 422 } 423 424 func AnyExtendBlobToNode(ptr interface{}) (datamodel.Node, error) { 425 aeb, ok := ptr.(*AnyExtendBlob) 426 if !ok { 427 return nil, fmt.Errorf("expected *AnyExtendBlob type") 428 } 429 return aeb.ToNode() 430 } 431 432 // take a datamodel.Node, dag-cbor encode it and store it here, do the reverse 433 // to get the datamodel.Node back 434 type AnyCborEncoded struct{ str []byte } 435 436 func AnyCborEncodedFromNode(node datamodel.Node) (interface{}, error) { 437 if tn, ok := node.(schema.TypedNode); ok { 438 node = tn.Representation() 439 } 440 var buf bytes.Buffer 441 err := dagcbor.Encode(node, &buf) 442 if err != nil { 443 return nil, err 444 } 445 acb := AnyCborEncoded{str: buf.Bytes()} 446 return &acb, nil 447 } 448 449 func AnyCborEncodedToNode(ptr interface{}) (datamodel.Node, error) { 450 acb, ok := ptr.(*AnyCborEncoded) 451 if !ok { 452 return nil, fmt.Errorf("expected *AnyCborEncoded type") 453 } 454 na := basicnode.Prototype.Any.NewBuilder() 455 err := dagcbor.Decode(na, bytes.NewReader(acb.str)) 456 if err != nil { 457 return nil, err 458 } 459 return na.Build(), nil 460 } 461 462 const anyExtendDagJson = `{"Blob":{"baz":[2,3,4],"foo":"bar"},"Bool":false,"BoolPtr":null,"Bytes":{"/":{"bytes":"AgMEBQYHCA"}},"Count":101,"Float":2.34,"Int":123456789,"Link":{"/":"bagyacvra2e6qt2fohajauxceox55t3gedsyqap2phmv7q2qaaaaaaaaaaaaa"},"List":[null,"one","two","three",1,2,3,true],"Map":{"foo":"bar","one":1,"three":3,"two":2},"Name":"Any extend test","Null":null,"NullPtr":null,"NullableWith":123456789,"String":"this is a string","XListAny":[1,2,true,null,"bop"],"XMapAny":{"a":1,"b":2,"c":true,"d":null,"e":"bop"}}` 463 464 var anyExtendFixtureInstance = AnyExtend{ 465 Name: "Any extend test", 466 Count: 101, 467 Blob: AnyExtendBlob{f: "bar", x: 2, y: 3, z: 4}, 468 Null: AnyCborEncoded{mustFromHex("f6")}, // normally these two fields would be `nil`, but we now get to decide whether it should be something concrete 469 NullPtr: &AnyCborEncoded{mustFromHex("f6")}, 470 NullableWith: &AnyCborEncoded{mustFromHex("1a075bcd15")}, 471 Bool: AnyCborEncoded{mustFromHex("f4")}, 472 Int: AnyCborEncoded{mustFromHex("1a075bcd15")}, // 123456789 473 Float: AnyCborEncoded{mustFromHex("fb4002b851eb851eb8")}, // 2.34 474 String: AnyCborEncoded{mustFromHex("7074686973206973206120737472696e67")}, // "this is a string" 475 Bytes: AnyCborEncoded{mustFromHex("4702030405060708")}, // [2,3,4,5,6,7,8] 476 Link: AnyCborEncoded{mustFromHex("d82a58260001b0015620d13d09e8ae38120a5c4475fbd9ecc41cb1003f4f3b2bf86a0000000000000000")}, // bagyacvra2e6qt2fohajauxceox55t3gedsyqap2phmv7q2qaaaaaaaaaaaaa 477 Map: AnyCborEncoded{mustFromHex("a463666f6f63626172636f6e65016374776f0265746872656503")}, // {"one":1,"two":2,"three":3,"foo":"bar"} 478 List: AnyCborEncoded{mustFromHex("88f6636f6e656374776f657468726565010203f5")}, // [null,'one','two','three',1,2,3,true] 479 BoolPtr: nil, 480 XListAny: []AnyCborEncoded{{mustFromHex("01")}, {mustFromHex("02")}, {mustFromHex("f5")}, {mustFromHex("f6")}, {mustFromHex("63626f70")}}, // [1,2,true,null,"bop"] 481 XMapAny: anyMap{ 482 Keys: []string{"a", "b", "c", "d", "e"}, 483 Values: map[string]*AnyCborEncoded{ 484 "a": {mustFromHex("01")}, 485 "b": {mustFromHex("02")}, 486 "c": {mustFromHex("f5")}, 487 "d": {mustFromHex("f6")}, 488 "e": {mustFromHex("63626f70")}}}, // {"a":1,"b":2,"c":true,"d":null,"e":"bop"} 489 } 490 491 func TestCustomAny(t *testing.T) { 492 opts := []bindnode.Option{ 493 bindnode.TypedAnyConverter(&AnyExtendBlob{}, AnyExtendBlobFromNode, AnyExtendBlobToNode), 494 bindnode.TypedAnyConverter(&AnyCborEncoded{}, AnyCborEncodedFromNode, AnyCborEncodedToNode), 495 bindnode.TypedBoolConverter(BoolSubst(0), BoolSubstFromBool, BoolToBoolSubst), 496 } 497 498 typeSystem, err := ipld.LoadSchemaBytes([]byte(anyExtendSchema)) 499 qt.Assert(t, err, qt.IsNil) 500 schemaType := typeSystem.TypeByName("AnyExtend") 501 proto := bindnode.Prototype(&AnyExtend{}, schemaType, opts...) 502 503 builder := proto.Representation().NewBuilder() 504 err = dagjson.Decode(builder, bytes.NewReader([]byte(anyExtendDagJson))) 505 qt.Assert(t, err, qt.IsNil) 506 507 typ := bindnode.Unwrap(builder.Build()) 508 inst, ok := typ.(*AnyExtend) 509 qt.Assert(t, ok, qt.IsTrue) 510 511 cmpr := qt.CmpEquals( 512 cmp.Comparer(func(x, y AnyExtendBlob) bool { 513 return x.f == y.f && x.x == y.x && x.y == y.y && x.z == y.z 514 }), 515 cmp.Comparer(func(x, y AnyCborEncoded) bool { 516 return bytes.Equal(x.str, y.str) 517 }), 518 ) 519 qt.Assert(t, *inst, cmpr, anyExtendFixtureInstance) 520 521 tn := bindnode.Wrap(inst, schemaType, opts...) 522 var buf bytes.Buffer 523 err = dagjson.Encode(tn.Representation(), &buf) 524 qt.Assert(t, err, qt.IsNil) 525 526 qt.Assert(t, buf.String(), qt.Equals, anyExtendDagJson) 527 } 528 529 func mustFromHex(hexStr string) []byte { 530 byt, err := hex.DecodeString(hexStr) 531 if err != nil { 532 panic(err) 533 } 534 return byt 535 }