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  }