diff --git a/node/bindnode/api.go b/node/bindnode/api.go index 4b276ae2..062aec4a 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -7,6 +7,7 @@ package bindnode import ( "reflect" + "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/schema" ) @@ -27,11 +28,13 @@ import ( // from it, so its underlying value will typically be nil. For example: // // proto := bindnode.Prototype((*goType)(nil), schemaType) -func Prototype(ptrType interface{}, schemaType schema.Type) schema.TypedPrototype { +func Prototype(ptrType interface{}, schemaType schema.Type, options ...Option) schema.TypedPrototype { if ptrType == nil && schemaType == nil { panic("bindnode: either ptrType or schemaType must not be nil") } + cfg := applyOptions(options...) + // TODO: if both are supplied, verify that they are compatible var goType reflect.Type @@ -50,11 +53,217 @@ func Prototype(ptrType interface{}, schemaType schema.Type) schema.TypedPrototyp if schemaType == nil { schemaType = inferSchema(goType, 0) } else { - verifyCompatibility(make(map[seenEntry]bool), goType, schemaType) + verifyCompatibility(cfg, make(map[seenEntry]bool), goType, schemaType) } } - return &_prototype{schemaType: schemaType, goType: goType} + return &_prototype{cfg: cfg, schemaType: schemaType, goType: goType} +} + +type converter struct { + kind schema.TypeKind + + customFromBool func(bool) (interface{}, error) + customToBool func(interface{}) (bool, error) + + customFromInt func(int64) (interface{}, error) + customToInt func(interface{}) (int64, error) + + customFromFloat func(float64) (interface{}, error) + customToFloat func(interface{}) (float64, error) + + customFromString func(string) (interface{}, error) + customToString func(interface{}) (string, error) + + customFromBytes func([]byte) (interface{}, error) + customToBytes func(interface{}) ([]byte, error) + + customFromLink func(cid.Cid) (interface{}, error) + customToLink func(interface{}) (cid.Cid, error) + + customFromAny func(datamodel.Node) (interface{}, error) + customToAny func(interface{}) (datamodel.Node, error) +} + +type config map[reflect.Type]*converter + +// this mainly exists to short-circuit the nonPtrType() call; the `Type()` variant +// exists for completeness +func (c config) converterFor(val reflect.Value) *converter { + if len(c) == 0 { + return nil + } + return c[nonPtrType(val)] +} + +func (c config) converterForType(typ reflect.Type) *converter { + if len(c) == 0 { + return nil + } + return c[typ] +} + +// Option is able to apply custom options to the bindnode API +type Option func(config) + +// TypedBoolConverter adds custom converter functions for a particular +// type as identified by a pointer in the first argument. +// The fromFunc is of the form: func(bool) (interface{}, error) +// and toFunc is of the form: func(interface{}) (bool, error) +// where interface{} is a pointer form of the type we are converting. +// +// TypedBoolConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func TypedBoolConverter(ptrVal interface{}, from func(bool) (interface{}, error), to func(interface{}) (bool, error)) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Bool, + customFromBool: from, + customToBool: to, + } + return func(cfg config) { + cfg[customType] = converter + } +} + +// TypedIntConverter adds custom converter functions for a particular +// type as identified by a pointer in the first argument. +// The fromFunc is of the form: func(int64) (interface{}, error) +// and toFunc is of the form: func(interface{}) (int64, error) +// where interface{} is a pointer form of the type we are converting. +// +// TypedIntConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func TypedIntConverter(ptrVal interface{}, from func(int64) (interface{}, error), to func(interface{}) (int64, error)) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Int, + customFromInt: from, + customToInt: to, + } + return func(cfg config) { + cfg[customType] = converter + } +} + +// TypedFloatConverter adds custom converter functions for a particular +// type as identified by a pointer in the first argument. +// The fromFunc is of the form: func(float64) (interface{}, error) +// and toFunc is of the form: func(interface{}) (float64, error) +// where interface{} is a pointer form of the type we are converting. +// +// TypedFloatConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func TypedFloatConverter(ptrVal interface{}, from func(float64) (interface{}, error), to func(interface{}) (float64, error)) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Float, + customFromFloat: from, + customToFloat: to, + } + return func(cfg config) { + cfg[customType] = converter + } +} + +// TypedStringConverter adds custom converter functions for a particular +// type as identified by a pointer in the first argument. +// The fromFunc is of the form: func(string) (interface{}, error) +// and toFunc is of the form: func(interface{}) (string, error) +// where interface{} is a pointer form of the type we are converting. +// +// TypedStringConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func TypedStringConverter(ptrVal interface{}, from func(string) (interface{}, error), to func(interface{}) (string, error)) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_String, + customFromString: from, + customToString: to, + } + return func(cfg config) { + cfg[customType] = converter + } +} + +// TypedBytesConverter adds custom converter functions for a particular +// type as identified by a pointer in the first argument. +// The fromFunc is of the form: func([]byte) (interface{}, error) +// and toFunc is of the form: func(interface{}) ([]byte, error) +// where interface{} is a pointer form of the type we are converting. +// +// TypedBytesConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func TypedBytesConverter(ptrVal interface{}, from func([]byte) (interface{}, error), to func(interface{}) ([]byte, error)) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Bytes, + customFromBytes: from, + customToBytes: to, + } + return func(cfg config) { + cfg[customType] = converter + } +} + +// TypedLinkConverter adds custom converter functions for a particular +// type as identified by a pointer in the first argument. +// The fromFunc is of the form: func([]byte) (interface{}, error) +// and toFunc is of the form: func(interface{}) ([]byte, error) +// where interface{} is a pointer form of the type we are converting. +// +// Beware that this API is only compatible with cidlink.Link types in the data +// model and may result in errors if attempting to convert from other +// datamodel.Link types. +// +// TypedLinkConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func TypedLinkConverter(ptrVal interface{}, from func(cid.Cid) (interface{}, error), to func(interface{}) (cid.Cid, error)) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Link, + customFromLink: from, + customToLink: to, + } + return func(cfg config) { + cfg[customType] = converter + } +} + +// TypedAnyConverter adds custom converter functions for a particular +// type as identified by a pointer in the first argument. +// The fromFunc is of the form: func(datamodel.Node) (interface{}, error) +// and toFunc is of the form: func(interface{}) (datamodel.Node, error) +// where interface{} is a pointer form of the type we are converting. +// +// This method should be able to deal with all forms of Any and return an error +// if the expected data forms don't match the expected. +// +// TypedAnyConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func TypedAnyConverter(ptrVal interface{}, from func(datamodel.Node) (interface{}, error), to func(interface{}) (datamodel.Node, error)) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Any, + customFromAny: from, + customToAny: to, + } + return func(cfg config) { + cfg[customType] = converter + } +} + +func applyOptions(opt ...Option) config { + if len(opt) == 0 { + // no need to allocate, we access it via converterFor and converterForType + // which are safe for nil maps + return nil + } + cfg := make(map[reflect.Type]*converter) + for _, o := range opt { + o(cfg) + } + return cfg } // Wrap implements a schema.TypedNode given a non-nil pointer to a Go value and an @@ -65,7 +274,7 @@ func Prototype(ptrType interface{}, schemaType schema.Type) schema.TypedPrototyp // // Similar to Prototype, if schemaType is non-nil it is assumed to be compatible // with the Go type, and otherwise it's inferred from the Go type. -func Wrap(ptrVal interface{}, schemaType schema.Type) schema.TypedNode { +func Wrap(ptrVal interface{}, schemaType schema.Type, options ...Option) schema.TypedNode { if ptrVal == nil { panic("bindnode: ptrVal must not be nil") } @@ -77,6 +286,7 @@ func Wrap(ptrVal interface{}, schemaType schema.Type) schema.TypedNode { // Note that this can happen if ptrVal was a typed nil. panic("bindnode: ptrVal must not be nil") } + cfg := applyOptions(options...) goVal := goPtrVal.Elem() if goVal.Kind() == reflect.Ptr { panic("bindnode: ptrVal must not be a pointer to a pointer") @@ -84,9 +294,14 @@ func Wrap(ptrVal interface{}, schemaType schema.Type) schema.TypedNode { if schemaType == nil { schemaType = inferSchema(goVal.Type(), 0) } else { - verifyCompatibility(make(map[seenEntry]bool), goVal.Type(), schemaType) + // TODO(rvagg): explore ways to make this skippable by caching in the schema.Type + // passed in to this function; e.g. if you call Prototype(), then you've gone through + // this already, then calling .Type() on that could return a bindnode version of + // schema.Type that has the config cached and can be assumed to have been checked or + // inferred. + verifyCompatibility(cfg, make(map[seenEntry]bool), goVal.Type(), schemaType) } - return &_node{val: goVal, schemaType: schemaType} + return newNode(cfg, schemaType, goVal) } // TODO: consider making our own Node interface, like: diff --git a/node/bindnode/api_test.go b/node/bindnode/api_test.go index 5c8fa7ea..624c2a9e 100644 --- a/node/bindnode/api_test.go +++ b/node/bindnode/api_test.go @@ -2,6 +2,7 @@ package bindnode_test import ( "encoding/hex" + "math" "testing" qt "github.com/frankban/quicktest" @@ -197,3 +198,77 @@ func TestSubNodeWalkAndUnwrap(t *testing.T) { verifyMap(node) }) } + +func TestUint64Struct(t *testing.T) { + t.Run("in struct", func(t *testing.T) { + type IntHolder struct { + Int32 int32 + Int64 int64 + Uint64 uint64 + } + schema := ` + type IntHolder struct { + Int32 Int + Int64 Int + Uint64 Int + } + ` + + maxExpectedHex := "a365496e7433321a7fffffff65496e7436341b7fffffffffffffff6655696e7436341bffffffffffffffff" + maxExpected, err := hex.DecodeString(maxExpectedHex) + qt.Assert(t, err, qt.IsNil) + + typeSystem, err := ipld.LoadSchemaBytes([]byte(schema)) + qt.Assert(t, err, qt.IsNil) + schemaType := typeSystem.TypeByName("IntHolder") + proto := bindnode.Prototype(&IntHolder{}, schemaType) + + node, err := ipld.DecodeUsingPrototype([]byte(maxExpected), dagcbor.Decode, proto) + qt.Assert(t, err, qt.IsNil) + + typ := bindnode.Unwrap(node) + inst, ok := typ.(*IntHolder) + qt.Assert(t, ok, qt.IsTrue) + + qt.Assert(t, *inst, qt.DeepEquals, IntHolder{ + Int32: math.MaxInt32, + Int64: math.MaxInt64, + Uint64: math.MaxUint64, + }) + + node = bindnode.Wrap(inst, schemaType).Representation() + byt, err := ipld.Encode(node, dagcbor.Encode) + qt.Assert(t, err, qt.IsNil) + + qt.Assert(t, hex.EncodeToString(byt), qt.Equals, maxExpectedHex) + }) + + t.Run("plain", func(t *testing.T) { + type IntHolder uint64 + schema := `type IntHolder int` + + maxExpectedHex := "1bffffffffffffffff" + maxExpected, err := hex.DecodeString(maxExpectedHex) + qt.Assert(t, err, qt.IsNil) + + typeSystem, err := ipld.LoadSchemaBytes([]byte(schema)) + qt.Assert(t, err, qt.IsNil) + schemaType := typeSystem.TypeByName("IntHolder") + proto := bindnode.Prototype((*IntHolder)(nil), schemaType) + + node, err := ipld.DecodeUsingPrototype([]byte(maxExpected), dagcbor.Decode, proto) + qt.Assert(t, err, qt.IsNil) + + typ := bindnode.Unwrap(node) + inst, ok := typ.(*IntHolder) + qt.Assert(t, ok, qt.IsTrue) + + qt.Assert(t, *inst, qt.Equals, IntHolder(math.MaxUint64)) + + node = bindnode.Wrap(inst, schemaType).Representation() + byt, err := ipld.Encode(node, dagcbor.Encode) + qt.Assert(t, err, qt.IsNil) + + qt.Assert(t, hex.EncodeToString(byt), qt.Equals, maxExpectedHex) + }) +} diff --git a/node/bindnode/custom_test.go b/node/bindnode/custom_test.go new file mode 100644 index 00000000..adfc84b1 --- /dev/null +++ b/node/bindnode/custom_test.go @@ -0,0 +1,535 @@ +package bindnode_test + +import ( + "bytes" + "encoding/hex" + "fmt" + "math/big" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + basicnode "github.com/ipld/go-ipld-prime/node/basic" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/ipld/go-ipld-prime/schema" + "github.com/multiformats/go-multihash" + + qt "github.com/frankban/quicktest" +) + +type BoolSubst int + +const ( + BoolSubst_Yes = 100 + BoolSubst_No = -100 +) + +func BoolSubstFromBool(b bool) (interface{}, error) { + if b { + return BoolSubst_Yes, nil + } + return BoolSubst_No, nil +} + +func BoolToBoolSubst(b interface{}) (bool, error) { + bp, ok := b.(*BoolSubst) + if !ok { + return true, fmt.Errorf("expected *BoolSubst value") + } + switch *bp { + case BoolSubst_Yes: + return true, nil + case BoolSubst_No: + return false, nil + default: + return true, fmt.Errorf("bad BoolSubst") + } +} + +type IntSubst string + +func IntSubstFromInt(i int64) (interface{}, error) { + if i == 1000 { + return "one thousand", nil + } else if i == 2000 { + return "two thousand", nil + } + return nil, fmt.Errorf("unexpected value of IntSubst") +} + +func IntToIntSubst(i interface{}) (int64, error) { + ip, ok := i.(*IntSubst) + if !ok { + return 0, fmt.Errorf("expected *IntSubst value") + } + switch *ip { + case "one thousand": + return 1000, nil + case "two thousand": + return 2000, nil + default: + return 0, fmt.Errorf("bad IntSubst") + } +} + +type BigFloat struct{ *big.Float } + +func BigFloatFromFloat(f float64) (interface{}, error) { + bf := big.NewFloat(f) + return &BigFloat{bf}, nil +} + +func FloatFromBigFloat(f interface{}) (float64, error) { + fp, ok := f.(*BigFloat) + if !ok { + return 0, fmt.Errorf("expected *BigFloat value") + } + f64, _ := fp.Float64() + return f64, nil +} + +type ByteArray [][]byte + +func ByteArrayFromString(s string) (interface{}, error) { + sa := strings.Split(s, "|") + ba := make([][]byte, 0) + for _, a := range sa { + ba = append(ba, []byte(a)) + } + return ba, nil +} + +func StringFromByteArray(b interface{}) (string, error) { + bap, ok := b.(*ByteArray) + if !ok { + return "", fmt.Errorf("expected *ByteArray value") + } + sb := strings.Builder{} + for i, b := range *bap { + sb.WriteString(string(b)) + if i != len(*bap)-1 { + sb.WriteString("|") + } + } + return sb.String(), nil +} + +// similar to cid/Cid, go-address/Address, go-graphsync/RequestID +type Boop struct{ str string } + +func NewBoop(b []byte) *Boop { + return &Boop{string(b)} +} + +func (b Boop) Bytes() []byte { + return []byte(b.str) +} + +func (b Boop) String() string { + return b.str +} + +// similar to go-state-types/big/Int +type Frop struct{ *big.Int } + +func NewFropFromString(str string) Frop { + v, _ := big.NewInt(0).SetString(str, 10) + return Frop{v} +} + +func NewFropFromBytes(buf []byte) *Frop { + var negative bool + switch buf[0] { + case 0: + negative = false + case 1: + negative = true + default: + panic("can't handle this") + } + + i := big.NewInt(0).SetBytes(buf[1:]) + if negative { + i.Neg(i) + } + + return &Frop{i} +} + +func (b *Frop) Bytes() []byte { + switch { + case b.Sign() > 0: + return append([]byte{0}, b.Int.Bytes()...) + case b.Sign() < 0: + return append([]byte{1}, b.Int.Bytes()...) + default: + return []byte{} + } +} + +func BoopFromBytes(b []byte) (interface{}, error) { + return NewBoop(b), nil +} + +func BoopToBytes(iface interface{}) ([]byte, error) { + if boop, ok := iface.(*Boop); ok { + return boop.Bytes(), nil + } + return nil, fmt.Errorf("did not get expected type") +} + +func FropFromBytes(b []byte) (interface{}, error) { + return NewFropFromBytes(b), nil +} + +func FropToBytes(iface interface{}) ([]byte, error) { + if frop, ok := iface.(*Frop); ok { + return frop.Bytes(), nil + } + return nil, fmt.Errorf("did not get expected type") +} + +// Bitcoin's version of "links" is a hex form of the dbl-sha2-256 digest reversed +type BtcId string + +func FromCidToBtcId(c cid.Cid) (interface{}, error) { + if c.Prefix().Codec != cid.BitcoinBlock { // should be able to do BitcoinTx too .. but .. + return nil, fmt.Errorf("can only convert IDs for BitcoinBlock codecs") + } + // and multihash must be dbl-sha2-256 + dig, err := multihash.Decode(c.Hash()) + if err != nil { + return nil, err + } + hid := make([]byte, 0) + for i := len(dig.Digest) - 1; i >= 0; i-- { + hid = append(hid, dig.Digest[i]) + } + return BtcId(hex.EncodeToString(hid)), nil +} + +func FromBtcIdToCid(iface interface{}) (cid.Cid, error) { + bid, ok := iface.(*BtcId) + if !ok { + return cid.Undef, fmt.Errorf("expected *BtcId value") + } + dig := make([]byte, 0) + hid, err := hex.DecodeString(string(*bid)) + if err != nil { + return cid.Undef, err + } + for i := len(hid) - 1; i >= 0; i-- { + dig = append(dig, hid[i]) + } + mh, err := multihash.Encode(dig, multihash.DBL_SHA2_256) + if err != nil { + return cid.Undef, err + } + return cid.NewCidV1(cid.BitcoinBlock, mh), nil +} + +type Boom struct { + S string + St ByteArray + B Boop + Bo BoolSubst + Bptr *Boop + F Frop + Fl BigFloat + I int + In IntSubst + L BtcId +} + +const boomSchema = ` +type Boom struct { + S String + St String + B Bytes + Bo Bool + Bptr nullable Bytes + F Bytes + Fl Float + I Int + In Int + L &Any +} representation map +` + +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"}` + +var boomFixtureInstance = Boom{ + B: *NewBoop([]byte("these are bytes")), + Bo: BoolSubst_No, + Bptr: NewBoop([]byte("these are pointer bytes")), + F: NewFropFromString("12345678901234567891234567890123456789012345678901234567890"), + Fl: BigFloat{big.NewFloat(1.12)}, + I: 10101, + In: IntSubst("two thousand"), + S: "a string here", + St: ByteArray([][]byte{[]byte("a"), []byte("byte"), []byte("array")}), + L: BtcId("00000000000000006af82b3b4f3f00b11cc4ecd9fb75445c0a1238aee8093dd1"), +} + +func TestCustom(t *testing.T) { + opts := []bindnode.Option{ + bindnode.TypedBytesConverter(&Boop{}, BoopFromBytes, BoopToBytes), + bindnode.TypedBytesConverter(&Frop{}, FropFromBytes, FropToBytes), + bindnode.TypedBoolConverter(BoolSubst(0), BoolSubstFromBool, BoolToBoolSubst), + bindnode.TypedIntConverter(IntSubst(""), IntSubstFromInt, IntToIntSubst), + bindnode.TypedFloatConverter(&BigFloat{}, BigFloatFromFloat, FloatFromBigFloat), + bindnode.TypedStringConverter(&ByteArray{}, ByteArrayFromString, StringFromByteArray), + bindnode.TypedLinkConverter(BtcId(""), FromCidToBtcId, FromBtcIdToCid), + } + + typeSystem, err := ipld.LoadSchemaBytes([]byte(boomSchema)) + qt.Assert(t, err, qt.IsNil) + schemaType := typeSystem.TypeByName("Boom") + proto := bindnode.Prototype(&Boom{}, schemaType, opts...) + + builder := proto.Representation().NewBuilder() + err = dagjson.Decode(builder, bytes.NewReader([]byte(boomFixtureDagJson))) + qt.Assert(t, err, qt.IsNil) + + typ := bindnode.Unwrap(builder.Build()) + inst, ok := typ.(*Boom) + qt.Assert(t, ok, qt.IsTrue) + + cmpr := qt.CmpEquals( + cmp.Comparer(func(x, y Boop) bool { return x.String() == y.String() }), + cmp.Comparer(func(x, y Frop) bool { return x.String() == y.String() }), + cmp.Comparer(func(x, y BigFloat) bool { return x.String() == y.String() }), + ) + qt.Assert(t, *inst, cmpr, boomFixtureInstance) + + tn := bindnode.Wrap(inst, schemaType, opts...) + var buf bytes.Buffer + err = dagjson.Encode(tn.Representation(), &buf) + qt.Assert(t, err, qt.IsNil) + + qt.Assert(t, buf.String(), qt.Equals, boomFixtureDagJson) +} + +type AnyExtend struct { + Name string + Blob AnyExtendBlob + Count int + Null AnyCborEncoded + NullPtr *AnyCborEncoded + NullableWith *AnyCborEncoded + Bool AnyCborEncoded + Int AnyCborEncoded + Float AnyCborEncoded + String AnyCborEncoded + Bytes AnyCborEncoded + Link AnyCborEncoded + Map AnyCborEncoded + List AnyCborEncoded + BoolPtr *BoolSubst // included to test that a null entry won't call a non-Any converter + XListAny []AnyCborEncoded + XMapAny anyMap +} + +type anyMap struct { + Keys []string + Values map[string]*AnyCborEncoded +} + +const anyExtendSchema = ` +type AnyExtend struct { + Name String + Blob Any + Count Int + Null nullable Any + NullPtr nullable Any + NullableWith nullable Any + Bool Any + Int Any + Float Any + String Any + Bytes Any + Link Any + Map Any + List Any + BoolPtr nullable Bool + XListAny [Any] + XMapAny {String:Any} +} +` + +type AnyExtendBlob struct { + f string + x int64 + y int64 + z int64 +} + +func AnyExtendBlobFromNode(node datamodel.Node) (interface{}, error) { + foo, err := node.LookupByString("foo") + if err != nil { + return nil, err + } + fooStr, err := foo.AsString() + if err != nil { + return nil, err + } + baz, err := node.LookupByString("baz") + if err != nil { + return nil, err + } + x, err := baz.LookupByIndex(0) + if err != nil { + return nil, err + } + xi, err := x.AsInt() + if err != nil { + return nil, err + } + y, err := baz.LookupByIndex(1) + if err != nil { + return nil, err + } + yi, err := y.AsInt() + if err != nil { + return nil, err + } + z, err := baz.LookupByIndex(2) + if err != nil { + return nil, err + } + zi, err := z.AsInt() + if err != nil { + return nil, err + } + return &AnyExtendBlob{f: fooStr, x: xi, y: yi, z: zi}, nil +} + +func (aeb AnyExtendBlob) ToNode() (datamodel.Node, error) { + return qp.BuildMap(basicnode.Prototype.Any, -1, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "foo", qp.String(aeb.f)) + qp.MapEntry(ma, "baz", qp.List(-1, func(la datamodel.ListAssembler) { + qp.ListEntry(la, qp.Int(aeb.x)) + qp.ListEntry(la, qp.Int(aeb.y)) + qp.ListEntry(la, qp.Int(aeb.z)) + })) + }) +} + +func AnyExtendBlobToNode(ptr interface{}) (datamodel.Node, error) { + aeb, ok := ptr.(*AnyExtendBlob) + if !ok { + return nil, fmt.Errorf("expected *AnyExtendBlob type") + } + return aeb.ToNode() +} + +// take a datamodel.Node, dag-cbor encode it and store it here, do the reverse +// to get the datamodel.Node back +type AnyCborEncoded struct{ str []byte } + +func AnyCborEncodedFromNode(node datamodel.Node) (interface{}, error) { + if tn, ok := node.(schema.TypedNode); ok { + node = tn.Representation() + } + var buf bytes.Buffer + err := dagcbor.Encode(node, &buf) + if err != nil { + return nil, err + } + acb := AnyCborEncoded{str: buf.Bytes()} + return &acb, nil +} + +func AnyCborEncodedToNode(ptr interface{}) (datamodel.Node, error) { + acb, ok := ptr.(*AnyCborEncoded) + if !ok { + return nil, fmt.Errorf("expected *AnyCborEncoded type") + } + na := basicnode.Prototype.Any.NewBuilder() + err := dagcbor.Decode(na, bytes.NewReader(acb.str)) + if err != nil { + return nil, err + } + return na.Build(), nil +} + +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"}}` + +var anyExtendFixtureInstance = AnyExtend{ + Name: "Any extend test", + Count: 101, + Blob: AnyExtendBlob{f: "bar", x: 2, y: 3, z: 4}, + Null: AnyCborEncoded{mustFromHex("f6")}, // normally these two fields would be `nil`, but we now get to decide whether it should be something concrete + NullPtr: &AnyCborEncoded{mustFromHex("f6")}, + NullableWith: &AnyCborEncoded{mustFromHex("1a075bcd15")}, + Bool: AnyCborEncoded{mustFromHex("f4")}, + Int: AnyCborEncoded{mustFromHex("1a075bcd15")}, // 123456789 + Float: AnyCborEncoded{mustFromHex("fb4002b851eb851eb8")}, // 2.34 + String: AnyCborEncoded{mustFromHex("7074686973206973206120737472696e67")}, // "this is a string" + Bytes: AnyCborEncoded{mustFromHex("4702030405060708")}, // [2,3,4,5,6,7,8] + Link: AnyCborEncoded{mustFromHex("d82a58260001b0015620d13d09e8ae38120a5c4475fbd9ecc41cb1003f4f3b2bf86a0000000000000000")}, // bagyacvra2e6qt2fohajauxceox55t3gedsyqap2phmv7q2qaaaaaaaaaaaaa + Map: AnyCborEncoded{mustFromHex("a463666f6f63626172636f6e65016374776f0265746872656503")}, // {"one":1,"two":2,"three":3,"foo":"bar"} + List: AnyCborEncoded{mustFromHex("88f6636f6e656374776f657468726565010203f5")}, // [null,'one','two','three',1,2,3,true] + BoolPtr: nil, + XListAny: []AnyCborEncoded{{mustFromHex("01")}, {mustFromHex("02")}, {mustFromHex("f5")}, {mustFromHex("f6")}, {mustFromHex("63626f70")}}, // [1,2,true,null,"bop"] + XMapAny: anyMap{ + Keys: []string{"a", "b", "c", "d", "e"}, + Values: map[string]*AnyCborEncoded{ + "a": {mustFromHex("01")}, + "b": {mustFromHex("02")}, + "c": {mustFromHex("f5")}, + "d": {mustFromHex("f6")}, + "e": {mustFromHex("63626f70")}}}, // {"a":1,"b":2,"c":true,"d":null,"e":"bop"} +} + +func TestCustomAny(t *testing.T) { + opts := []bindnode.Option{ + bindnode.TypedAnyConverter(&AnyExtendBlob{}, AnyExtendBlobFromNode, AnyExtendBlobToNode), + bindnode.TypedAnyConverter(&AnyCborEncoded{}, AnyCborEncodedFromNode, AnyCborEncodedToNode), + bindnode.TypedBoolConverter(BoolSubst(0), BoolSubstFromBool, BoolToBoolSubst), + } + + typeSystem, err := ipld.LoadSchemaBytes([]byte(anyExtendSchema)) + qt.Assert(t, err, qt.IsNil) + schemaType := typeSystem.TypeByName("AnyExtend") + proto := bindnode.Prototype(&AnyExtend{}, schemaType, opts...) + + builder := proto.Representation().NewBuilder() + err = dagjson.Decode(builder, bytes.NewReader([]byte(anyExtendDagJson))) + qt.Assert(t, err, qt.IsNil) + + typ := bindnode.Unwrap(builder.Build()) + inst, ok := typ.(*AnyExtend) + qt.Assert(t, ok, qt.IsTrue) + + cmpr := qt.CmpEquals( + cmp.Comparer(func(x, y AnyExtendBlob) bool { + return x.f == y.f && x.x == y.x && x.y == y.y && x.z == y.z + }), + cmp.Comparer(func(x, y AnyCborEncoded) bool { + return bytes.Equal(x.str, y.str) + }), + ) + qt.Assert(t, *inst, cmpr, anyExtendFixtureInstance) + + tn := bindnode.Wrap(inst, schemaType, opts...) + var buf bytes.Buffer + err = dagjson.Encode(tn.Representation(), &buf) + qt.Assert(t, err, qt.IsNil) + + qt.Assert(t, buf.String(), qt.Equals, anyExtendDagJson) +} + +func mustFromHex(hexStr string) []byte { + byt, err := hex.DecodeString(hexStr) + if err != nil { + panic(err) + } + return byt +} diff --git a/node/bindnode/infer.go b/node/bindnode/infer.go index fc2f7134..6b1c4849 100644 --- a/node/bindnode/infer.go +++ b/node/bindnode/infer.go @@ -39,7 +39,12 @@ type seenEntry struct { schemaType schema.Type } -func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaType schema.Type) { +// verifyCompatibility is the primary way we check that the schema type(s) +// matches the Go type(s); so we do this before we can proceed operating on it. +// verifyCompatibility doesn't return an error, it panics—the errors here are +// not runtime errors, they're programmer errors because your schema doesn't +// match your Go type +func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Type, schemaType schema.Type) { // TODO(mvdan): support **T as well? if goType.Kind() == reflect.Ptr { goType = goType.Elem() @@ -66,30 +71,51 @@ func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaTyp } switch schemaType := schemaType.(type) { case *schema.TypeBool: - if goType.Kind() != reflect.Bool { + if customConverter := cfg.converterForType(goType); customConverter != nil { + if customConverter.kind != schema.TypeKind_Bool { + doPanic("kind mismatch; custom converter for type is not for Bool") + } + } else if goType.Kind() != reflect.Bool { doPanic("kind mismatch; need boolean") } case *schema.TypeInt: - if kind := goType.Kind(); !kindInt[kind] && !kindUint[kind] { + if customConverter := cfg.converterForType(goType); customConverter != nil { + if customConverter.kind != schema.TypeKind_Int { + doPanic("kind mismatch; custom converter for type is not for Int") + } + } else if kind := goType.Kind(); !kindInt[kind] && !kindUint[kind] { doPanic("kind mismatch; need integer") } case *schema.TypeFloat: - switch goType.Kind() { - case reflect.Float32, reflect.Float64: - default: - doPanic("kind mismatch; need float") + if customConverter := cfg.converterForType(goType); customConverter != nil { + if customConverter.kind != schema.TypeKind_Float { + doPanic("kind mismatch; custom converter for type is not for Float") + } + } else { + switch goType.Kind() { + case reflect.Float32, reflect.Float64: + default: + doPanic("kind mismatch; need float") + } } case *schema.TypeString: // TODO: allow []byte? - if goType.Kind() != reflect.String { + if customConverter := cfg.converterForType(goType); customConverter != nil { + if customConverter.kind != schema.TypeKind_String { + doPanic("kind mismatch; custom converter for type is not for String") + } + } else if goType.Kind() != reflect.String { doPanic("kind mismatch; need string") } case *schema.TypeBytes: // TODO: allow string? - if goType.Kind() != reflect.Slice { + if customConverter := cfg.converterForType(goType); customConverter != nil { + if customConverter.kind != schema.TypeKind_Bytes { + doPanic("kind mismatch; custom converter for type is not for Bytes") + } + } else if goType.Kind() != reflect.Slice { doPanic("kind mismatch; need slice of bytes") - } - if goType.Elem().Kind() != reflect.Uint8 { + } else if goType.Elem().Kind() != reflect.Uint8 { doPanic("kind mismatch; need slice of bytes") } case *schema.TypeEnum: @@ -114,7 +140,7 @@ func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaTyp goType = goType.Elem() } } - verifyCompatibility(seen, goType, schemaType.ValueType()) + verifyCompatibility(cfg, seen, goType, schemaType.ValueType()) case *schema.TypeMap: // struct { // Keys []K @@ -131,14 +157,14 @@ func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaTyp if fieldKeys.Type.Kind() != reflect.Slice { doPanic("kind mismatch; need struct{Keys []K; Values map[K]V}") } - verifyCompatibility(seen, fieldKeys.Type.Elem(), schemaType.KeyType()) + verifyCompatibility(cfg, seen, fieldKeys.Type.Elem(), schemaType.KeyType()) fieldValues := goType.Field(1) if fieldValues.Type.Kind() != reflect.Map { doPanic("kind mismatch; need struct{Keys []K; Values map[K]V}") } keyType := fieldValues.Type.Key() - verifyCompatibility(seen, keyType, schemaType.KeyType()) + verifyCompatibility(cfg, seen, keyType, schemaType.KeyType()) elemType := fieldValues.Type.Elem() if schemaType.ValueIsNullable() { @@ -148,7 +174,7 @@ func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaTyp elemType = elemType.Elem() } } - verifyCompatibility(seen, elemType, schemaType.ValueType()) + verifyCompatibility(cfg, seen, elemType, schemaType.ValueType()) case *schema.TypeStruct: if goType.Kind() != reflect.Struct { doPanic("kind mismatch; need struct") @@ -166,6 +192,7 @@ func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaTyp // TODO: https://github.com/ipld/go-ipld-prime/issues/340 will // help here, to avoid the double pointer. We can't use nilable // but non-pointer types because that's just one "nil" state. + // TODO: deal with custom converters in this case if goType.Kind() != reflect.Ptr { doPanic("optional and nullable fields must use double pointers (**)") } @@ -182,12 +209,14 @@ func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaTyp } case schemaField.IsNullable(): if ptr, nilable := ptrOrNilable(goType.Kind()); !nilable { - doPanic("nullable fields must be nilable") + if customConverter := cfg.converterForType(goType); customConverter == nil { + doPanic("nullable fields must be nilable") + } } else if ptr { goType = goType.Elem() } } - verifyCompatibility(seen, goType, schemaType) + verifyCompatibility(cfg, seen, goType, schemaType) } case *schema.TypeUnion: if goType.Kind() != reflect.Struct { @@ -206,15 +235,22 @@ func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaTyp } else if ptr { goType = goType.Elem() } - verifyCompatibility(seen, goType, schemaType) + verifyCompatibility(cfg, seen, goType, schemaType) } case *schema.TypeLink: - if goType != goTypeLink && goType != goTypeCidLink && goType != goTypeCid { + if customConverter := cfg.converterForType(goType); customConverter != nil { + if customConverter.kind != schema.TypeKind_Link { + doPanic("kind mismatch; custom converter for type is not for Link") + } + } else if goType != goTypeLink && goType != goTypeCidLink && goType != goTypeCid { doPanic("links in Go must be datamodel.Link, cidlink.Link, or cid.Cid") } case *schema.TypeAny: - // TODO: support some other option for Any, such as deferred decode - if goType != goTypeNode { + if customConverter := cfg.converterForType(goType); customConverter != nil { + if customConverter.kind != schema.TypeKind_Any { + doPanic("kind mismatch; custom converter for type is not for Any") + } + } else if goType != goTypeNode { doPanic("Any in Go must be datamodel.Node") } default: @@ -247,6 +283,7 @@ const ( inferringDone ) +// inferGoType can build a Go type given a schema func inferGoType(typ schema.Type, status map[schema.TypeName]inferredStatus, level int) reflect.Type { if level > maxRecursionLevel { panic(fmt.Sprintf("inferGoType: refusing to recurse past %d levels", maxRecursionLevel)) @@ -376,6 +413,7 @@ func init() { // TODO: we should probably avoid re-spawning the same types if the TypeSystem // has them, and test that that works as expected +// inferSchema can build a schema from a Go type func inferSchema(typ reflect.Type, level int) schema.Type { if level > maxRecursionLevel { panic(fmt.Sprintf("inferSchema: refusing to recurse past %d levels", maxRecursionLevel)) diff --git a/node/bindnode/node.go b/node/bindnode/node.go index 8bf2c6bc..7930fa10 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -2,6 +2,7 @@ package bindnode import ( "fmt" + "math" "reflect" "runtime" "strings" @@ -10,6 +11,7 @@ import ( "github.com/ipld/go-ipld-prime/datamodel" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/ipld/go-ipld-prime/node/mixins" "github.com/ipld/go-ipld-prime/schema" ) @@ -24,6 +26,12 @@ var ( _ schema.TypedNode = (*_node)(nil) _ datamodel.Node = (*_nodeRepr)(nil) + _ datamodel.Node = (*_uintNode)(nil) + _ schema.TypedNode = (*_uintNode)(nil) + _ datamodel.UintNode = (*_uintNode)(nil) + _ datamodel.Node = (*_uintNodeRepr)(nil) + _ datamodel.UintNode = (*_uintNodeRepr)(nil) + _ datamodel.NodeBuilder = (*_builder)(nil) _ datamodel.NodeBuilder = (*_builderRepr)(nil) _ datamodel.NodeAssembler = (*_assembler)(nil) @@ -46,12 +54,14 @@ var ( ) type _prototype struct { + cfg config schemaType schema.Type goType reflect.Type // non-pointer } func (w *_prototype) NewBuilder() datamodel.NodeBuilder { return &_builder{_assembler{ + cfg: w.cfg, schemaType: w.schemaType, val: reflect.New(w.goType).Elem(), }} @@ -66,6 +76,7 @@ func (w *_prototype) Representation() datamodel.NodePrototype { } type _node struct { + cfg config schemaType schema.Type val reflect.Value // non-pointer @@ -76,6 +87,20 @@ type _node struct { // _node // } +func newNode(cfg config, schemaType schema.Type, val reflect.Value) schema.TypedNode { + if schemaType.TypeKind() == schema.TypeKind_Int && nonPtrVal(val).Kind() == reflect.Uint64 { + // special case for uint64 values so we can handle the >int64 range + // we give this treatment to all uint64s, regardless of current value + // because we have no guarantees the value won't change underneath us + return &_uintNode{ + cfg: cfg, + schemaType: schemaType, + val: val, + } + } + return &_node{cfg, schemaType, val} +} + func (w *_node) Type() schema.Type { return w.schemaType } @@ -88,15 +113,19 @@ func (w *_node) Kind() datamodel.Kind { return actualKind(w.schemaType) } +// matching schema level types to data model kinds, since our Node and Builder +// interfaces operate on kinds func compatibleKind(schemaType schema.Type, kind datamodel.Kind) error { switch sch := schemaType.(type) { case *schema.TypeAny: return nil default: - actual := actualKind(sch) + actual := actualKind(sch) // ActsLike data model if actual == kind { return nil } + + // Error methodName := "" if pc, _, _, ok := runtime.Caller(1); ok { if fn := runtime.FuncForPC(pc); fn != nil { @@ -105,7 +134,6 @@ func compatibleKind(schemaType schema.Type, kind datamodel.Kind) error { methodName = methodName[strings.LastIndexByte(methodName, '.')+1:] } } - return datamodel.ErrWrongKind{ TypeName: schemaType.Name(), MethodName: methodName, @@ -131,6 +159,31 @@ func nonPtrVal(val reflect.Value) reflect.Value { return val } +func ptrVal(val reflect.Value) reflect.Value { + if val.Kind() == reflect.Ptr { + return val + } + return val.Addr() +} + +func nonPtrType(val reflect.Value) reflect.Type { + typ := val.Type() + if typ.Kind() == reflect.Ptr { + return typ.Elem() + } + return typ +} + +// where we need to cal Set(), ensure the Value we're setting is a pointer or +// not, depending on the field we're setting into. +func matchSettable(val interface{}, to reflect.Value) reflect.Value { + setVal := nonPtrVal(reflect.ValueOf(val)) + if !setVal.Type().AssignableTo(to.Type()) && setVal.Type().ConvertibleTo(to.Type()) { + setVal = setVal.Convert(to.Type()) + } + return setVal +} + func (w *_node) LookupByString(key string) (datamodel.Node, error) { switch typ := w.schemaType.(type) { case *schema.TypeStruct: @@ -162,21 +215,28 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { } } if _, ok := field.Type().(*schema.TypeAny); ok { + if customConverter := w.cfg.converterFor(fval); customConverter != nil { + // field is an Any and we have a custom type converter for the type + return customConverter.customToAny(ptrVal(fval).Interface()) + } + // field is an Any, safely assume a Node in fval return nonPtrVal(fval).Interface().(datamodel.Node), nil } - node := &_node{ - schemaType: field.Type(), - val: fval, - } - return node, nil + return newNode(w.cfg, field.Type(), fval), nil case *schema.TypeMap: + // maps can only be structs with a Values map var kval reflect.Value valuesVal := nonPtrVal(w.val).FieldByName("Values") switch ktyp := typ.KeyType().(type) { case *schema.TypeString: + // plain String keys, so safely use the map key as is kval = reflect.ValueOf(key) default: + // key is something other than a string that we need to assemble via + // the string representation form, use _assemblerRepr to reverse from + // string to the type that indexes the map asm := &_assembler{ + cfg: w.cfg, schemaType: ktyp, val: reflect.New(valuesVal.Type().Key()).Elem(), } @@ -200,14 +260,17 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { fval = fval.Elem() } if _, ok := typ.ValueType().(*schema.TypeAny); ok { + if customConverter := w.cfg.converterFor(fval); customConverter != nil { + // value is an Any and we have a custom type converter for the type + return customConverter.customToAny(ptrVal(fval).Interface()) + } + // value is an Any, safely assume a Node in fval return nonPtrVal(fval).Interface().(datamodel.Node), nil } - node := &_node{ - schemaType: typ.ValueType(), - val: fval, - } - return node, nil + return newNode(w.cfg, typ.ValueType(), fval), nil case *schema.TypeUnion: + // treat a union similar to a struct, but we have the member names more + // easily accessible to match to 'key' var idx int var mtyp schema.Type for i, member := range typ.Members() { @@ -225,11 +288,7 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { if haveIdx != idx { // mismatching type return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfString(key)} } - node := &_node{ - schemaType: mtyp, - val: mval, - } - return node, nil + return newNode(w.cfg, mtyp, mval), nil } return nil, datamodel.ErrWrongKind{ TypeName: w.schemaType.Name(), @@ -269,20 +328,31 @@ func (w *_node) LookupByIndex(idx int64) (datamodel.Node, error) { switch typ := w.schemaType.(type) { case *schema.TypeList: val := nonPtrVal(w.val) + // we should be able assume that val is something we can Len() and Index() if idx < 0 || int(idx) >= val.Len() { return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfInt(idx)} } val = val.Index(int(idx)) + _, isAny := typ.ValueType().(*schema.TypeAny) + if isAny { + if customConverter := w.cfg.converterFor(val); customConverter != nil { + // values are Any and we have a converter for this type that will give us + // a datamodel.Node + return customConverter.customToAny(ptrVal(val).Interface()) + } + } if typ.ValueIsNullable() { if val.IsNil() { return datamodel.Null, nil } + // nullable elements are assumed to be pointers val = val.Elem() } - if _, ok := typ.ValueType().(*schema.TypeAny); ok { + if isAny { + // Any always yields a plain datamodel.Node return nonPtrVal(val).Interface().(datamodel.Node), nil } - return &_node{schemaType: typ.ValueType(), val: val}, nil + return newNode(w.cfg, typ.ValueType(), val), nil } return nil, datamodel.ErrWrongKind{ TypeName: w.schemaType.Name(), @@ -336,21 +406,28 @@ func (w *_node) LookupByNode(key datamodel.Node) (datamodel.Node, error) { func (w *_node) MapIterator() datamodel.MapIterator { val := nonPtrVal(w.val) + // structs, unions and maps can all iterate but they each have different + // access semantics for the underlying type, so we need a different iterator + // for each switch typ := w.schemaType.(type) { case *schema.TypeStruct: return &_structIterator{ + cfg: w.cfg, schemaType: typ, fields: typ.Fields(), val: val, } case *schema.TypeUnion: return &_unionIterator{ + cfg: w.cfg, schemaType: typ, members: typ.Members(), val: val, } case *schema.TypeMap: + // we can assume a: struct{Keys []string, Values map[x]y} return &_mapIterator{ + cfg: w.cfg, schemaType: typ, keysVal: val.FieldByName("Keys"), valuesVal: val.FieldByName("Values"), @@ -363,7 +440,7 @@ func (w *_node) ListIterator() datamodel.ListIterator { val := nonPtrVal(w.val) switch typ := w.schemaType.(type) { case *schema.TypeList: - return &_listIterator{schemaType: typ, val: val} + return &_listIterator{cfg: w.cfg, schemaType: typ, val: val} } return nil } @@ -395,10 +472,19 @@ func (w *_node) IsNull() bool { return false } +// The AsX methods are matter of fetching the non-pointer form of the underlying +// value and returning the appropriate Go type. The user may have registered +// custom converters for the kind being converted, in which case the underlying +// type may not be the type we need, but the converter will supply it for us. + func (w *_node) AsBool() (bool, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Bool); err != nil { return false, err } + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { + // user has registered a converter that takes the underlying type and returns a bool + return customConverter.customToBool(ptrVal(w.val).Interface()) + } return nonPtrVal(w.val).Bool(), nil } @@ -406,10 +492,17 @@ func (w *_node) AsInt() (int64, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Int); err != nil { return 0, err } + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { + // user has registered a converter that takes the underlying type and returns an int + return customConverter.customToInt(ptrVal(w.val).Interface()) + } val := nonPtrVal(w.val) if kindUint[val.Kind()] { - // TODO: check for overflow - return int64(val.Uint()), nil + u := val.Uint() + if u > math.MaxInt64 { + return 0, fmt.Errorf("bindnode: integer overflow, %d is too large for an int64", u) + } + return int64(u), nil } return val.Int(), nil } @@ -418,6 +511,10 @@ func (w *_node) AsFloat() (float64, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Float); err != nil { return 0, err } + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { + // user has registered a converter that takes the underlying type and returns a float + return customConverter.customToFloat(ptrVal(w.val).Interface()) + } return nonPtrVal(w.val).Float(), nil } @@ -425,6 +522,10 @@ func (w *_node) AsString() (string, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_String); err != nil { return "", err } + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { + // user has registered a converter that takes the underlying type and returns a string + return customConverter.customToString(ptrVal(w.val).Interface()) + } return nonPtrVal(w.val).String(), nil } @@ -432,6 +533,10 @@ func (w *_node) AsBytes() ([]byte, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Bytes); err != nil { return nil, err } + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { + // user has registered a converter that takes the underlying type and returns a []byte + return customConverter.customToBytes(ptrVal(w.val).Interface()) + } return nonPtrVal(w.val).Bytes(), nil } @@ -439,6 +544,14 @@ func (w *_node) AsLink() (datamodel.Link, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Link); err != nil { return nil, err } + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { + // user has registered a converter that takes the underlying type and returns a cid.Cid + cid, err := customConverter.customToLink(ptrVal(w.val).Interface()) + if err != nil { + return nil, err + } + return cidlink.Link{Cid: cid}, nil + } switch val := nonPtrVal(w.val).Interface().(type) { case datamodel.Link: return val, nil @@ -450,7 +563,7 @@ func (w *_node) AsLink() (datamodel.Link, error) { } func (w *_node) Prototype() datamodel.NodePrototype { - return &_prototype{schemaType: w.schemaType, goType: w.val.Type()} + return &_prototype{cfg: w.cfg, schemaType: w.schemaType, goType: w.val.Type()} } type _builder struct { @@ -459,7 +572,7 @@ type _builder struct { func (w *_builder) Build() datamodel.Node { // TODO: should we panic if no Assign call was made, just like codegen? - return &_node{schemaType: w.schemaType, val: w.val} + return newNode(w.cfg, w.schemaType, w.val) } func (w *_builder) Reset() { @@ -467,6 +580,7 @@ func (w *_builder) Reset() { } type _assembler struct { + cfg config schemaType schema.Type val reflect.Value // non-pointer @@ -479,6 +593,7 @@ type _assembler struct { nullable bool // true if field or map value is nullable } +// createNonPtrVal is used for Set() operations on the underlying value func (w *_assembler) createNonPtrVal() reflect.Value { val := w.val // TODO: if val is not a pointer, we reuse its value. @@ -501,11 +616,14 @@ func (w *_assembler) Representation() datamodel.NodeAssembler { return (*_assemblerRepr)(w) } +// basicMapAssembler is for assembling basicnode values, it's only use is for +// Any fields that end up needing a BeginMap() type basicMapAssembler struct { datamodel.MapAssembler - builder datamodel.NodeBuilder - parent *_assembler + builder datamodel.NodeBuilder + parent *_assembler + converter *converter } func (w *basicMapAssembler) Finish() error { @@ -513,7 +631,18 @@ func (w *basicMapAssembler) Finish() error { return err } basicNode := w.builder.Build() - w.parent.createNonPtrVal().Set(reflect.ValueOf(basicNode)) + if w.converter != nil { + // we can assume an Any converter because basicMapAssembler is only for Any + // the user has registered the ability to convert a datamodel.Node to the + // underlying Go type which may not be a datamodel.Node + typ, err := w.converter.customFromAny(basicNode) + if err != nil { + return err + } + w.parent.createNonPtrVal().Set(matchSettable(typ, reflect.ValueOf(basicNode))) + } else { + w.parent.createNonPtrVal().Set(reflect.ValueOf(basicNode)) + } if w.parent.finish != nil { if err := w.parent.finish(); err != nil { return err @@ -530,17 +659,25 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { if err != nil { return nil, err } - return &basicMapAssembler{MapAssembler: mapAsm, builder: basicBuilder, parent: w}, nil + converter := w.cfg.converterFor(w.val) + return &basicMapAssembler{MapAssembler: mapAsm, builder: basicBuilder, parent: w, converter: converter}, nil case *schema.TypeStruct: val := w.createNonPtrVal() + // _structAssembler walks through the fields in order as the entries are + // assembled, verifyCompatibility() should mean it's safe to assume that + // they match the schema, but we need to keep track of the fields that are + // set in case of premature Finish() doneFields := make([]bool, val.NumField()) return &_structAssembler{ + cfg: w.cfg, schemaType: typ, val: val, doneFields: doneFields, finish: w.finish, }, nil case *schema.TypeMap: + // assume a struct{Keys []string, Values map[x]y} that we can fill with + // _mapAssembler val := w.createNonPtrVal() keysVal := val.FieldByName("Keys") valuesVal := val.FieldByName("Values") @@ -548,14 +685,18 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { valuesVal.Set(reflect.MakeMap(valuesVal.Type())) } return &_mapAssembler{ + cfg: w.cfg, schemaType: typ, keysVal: keysVal, valuesVal: valuesVal, finish: w.finish, }, nil case *schema.TypeUnion: + // we can use _unionAssembler to assemble a union as if it were a map with + // a single entry val := w.createNonPtrVal() return &_unionAssembler{ + cfg: w.cfg, schemaType: typ, val: val, finish: w.finish, @@ -569,11 +710,14 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { } } +// basicListAssembler is for assembling basicnode values, it's only use is for +// Any fields that end up needing a BeginList() type basicListAssembler struct { datamodel.ListAssembler - builder datamodel.NodeBuilder - parent *_assembler + builder datamodel.NodeBuilder + parent *_assembler + converter *converter } func (w *basicListAssembler) Finish() error { @@ -581,7 +725,18 @@ func (w *basicListAssembler) Finish() error { return err } basicNode := w.builder.Build() - w.parent.createNonPtrVal().Set(reflect.ValueOf(basicNode)) + if w.converter != nil { + // we can assume an Any converter because basicListAssembler is only for Any + // the user has registered the ability to convert a datamodel.Node to the + // underlying Go type which may not be a datamodel.Node + typ, err := w.converter.customFromAny(basicNode) + if err != nil { + return err + } + w.parent.createNonPtrVal().Set(matchSettable(typ, reflect.ValueOf(basicNode))) + } else { + w.parent.createNonPtrVal().Set(reflect.ValueOf(basicNode)) + } if w.parent.finish != nil { if err := w.parent.finish(); err != nil { return err @@ -598,10 +753,14 @@ func (w *_assembler) BeginList(sizeHint int64) (datamodel.ListAssembler, error) if err != nil { return nil, err } - return &basicListAssembler{ListAssembler: listAsm, builder: basicBuilder, parent: w}, nil + converter := w.cfg.converterFor(w.val) + return &basicListAssembler{ListAssembler: listAsm, builder: basicBuilder, parent: w, converter: converter}, nil case *schema.TypeList: + // we should be able to safely assume we're dealing with a Go slice here, + // so _listAssembler can append to that val := w.createNonPtrVal() return &_listAssembler{ + cfg: w.cfg, schemaType: typ, val: val, finish: w.finish, @@ -616,14 +775,26 @@ func (w *_assembler) BeginList(sizeHint int64) (datamodel.ListAssembler, error) } func (w *_assembler) AssignNull() error { - if !w.nullable { - return datamodel.ErrWrongKind{ - TypeName: w.schemaType.Name(), - MethodName: "AssignNull", - // TODO + _, isAny := w.schemaType.(*schema.TypeAny) + if customConverter := w.cfg.converterFor(w.val); customConverter != nil && isAny { + // an Any field that is being assigned a Null, we pass the Null directly to + // the converter, regardless of whether this field is nullable or not + typ, err := customConverter.customFromAny(datamodel.Null) + if err != nil { + return err } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) + } else { + if !w.nullable { + return datamodel.ErrWrongKind{ + TypeName: w.schemaType.Name(), + MethodName: "AssignNull", + // TODO + } + } + // set the zero value for the underlying type as a stand-in for Null + w.val.Set(reflect.Zero(w.val.Type())) } - w.val.Set(reflect.Zero(w.val.Type())) if w.finish != nil { if err := w.finish(); err != nil { return err @@ -636,10 +807,61 @@ func (w *_assembler) AssignBool(b bool) error { if err := compatibleKind(w.schemaType, datamodel.Kind_Bool); err != nil { return err } - if _, ok := w.schemaType.(*schema.TypeAny); ok { - w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewBool(b))) + customConverter := w.cfg.converterFor(w.val) + _, isAny := w.schemaType.(*schema.TypeAny) + if customConverter != nil { + var typ interface{} + var err error + if isAny { + // field is an Any, so the converter will be an Any converter that wants + // a datamodel.Node to convert to whatever the underlying Go type is + if typ, err = customConverter.customFromAny(basicnode.NewBool(b)); err != nil { + return err + } + } else { + // field is a Bool, but the user has registered a converter from a bool to + // whatever the underlying Go type is + if typ, err = customConverter.customFromBool(b); err != nil { + return err + } + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { - w.createNonPtrVal().SetBool(b) + if isAny { + // Any means the Go type must receive a datamodel.Node + w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewBool(b))) + } else { + w.createNonPtrVal().SetBool(b) + } + } + if w.finish != nil { + if err := w.finish(); err != nil { + return err + } + } + return nil +} + +func (w *_assembler) assignUInt(uin datamodel.UintNode) error { + if err := compatibleKind(w.schemaType, datamodel.Kind_Int); err != nil { + return err + } + _, isAny := w.schemaType.(*schema.TypeAny) + // TODO: customConverter for uint?? + if isAny { + // Any means the Go type must receive a datamodel.Node + w.createNonPtrVal().Set(reflect.ValueOf(uin)) + } else { + i, err := uin.AsUint() + if err != nil { + return err + } + if kindUint[w.val.Kind()] { + w.createNonPtrVal().SetUint(i) + } else { + // TODO: check for overflow + w.createNonPtrVal().SetInt(int64(i)) + } } if w.finish != nil { if err := w.finish(); err != nil { @@ -654,16 +876,38 @@ func (w *_assembler) AssignInt(i int64) error { return err } // TODO: check for overflow - if _, ok := w.schemaType.(*schema.TypeAny); ok { - w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewInt(i))) - } else if kindUint[w.val.Kind()] { - if i < 0 { - // TODO: write a test - return fmt.Errorf("bindnode: cannot assign negative integer to %s", w.val.Type()) + customConverter := w.cfg.converterFor(w.val) + _, isAny := w.schemaType.(*schema.TypeAny) + if customConverter != nil { + var typ interface{} + var err error + if isAny { + // field is an Any, so the converter will be an Any converter that wants + // a datamodel.Node to convert to whatever the underlying Go type is + if typ, err = customConverter.customFromAny(basicnode.NewInt(i)); err != nil { + return err + } + } else { + // field is an Int, but the user has registered a converter from an int to + // whatever the underlying Go type is + if typ, err = customConverter.customFromInt(i); err != nil { + return err + } } - w.createNonPtrVal().SetUint(uint64(i)) + w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { - w.createNonPtrVal().SetInt(i) + if isAny { + // Any means the Go type must receive a datamodel.Node + w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewInt(i))) + } else if kindUint[w.val.Kind()] { + if i < 0 { + // TODO: write a test + return fmt.Errorf("bindnode: cannot assign negative integer to %s", w.val.Type()) + } + w.createNonPtrVal().SetUint(uint64(i)) + } else { + w.createNonPtrVal().SetInt(i) + } } if w.finish != nil { if err := w.finish(); err != nil { @@ -677,10 +921,32 @@ func (w *_assembler) AssignFloat(f float64) error { if err := compatibleKind(w.schemaType, datamodel.Kind_Float); err != nil { return err } - if _, ok := w.schemaType.(*schema.TypeAny); ok { - w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewFloat(f))) + customConverter := w.cfg.converterFor(w.val) + _, isAny := w.schemaType.(*schema.TypeAny) + if customConverter != nil { + var typ interface{} + var err error + if isAny { + // field is an Any, so the converter will be an Any converter that wants + // a datamodel.Node to convert to whatever the underlying Go type is + if typ, err = customConverter.customFromAny(basicnode.NewFloat(f)); err != nil { + return err + } + } else { + // field is a Float, but the user has registered a converter from a float + // to whatever the underlying Go type is + if typ, err = customConverter.customFromFloat(f); err != nil { + return err + } + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { - w.createNonPtrVal().SetFloat(f) + if isAny { + // Any means the Go type must receive a datamodel.Node + w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewFloat(f))) + } else { + w.createNonPtrVal().SetFloat(f) + } } if w.finish != nil { if err := w.finish(); err != nil { @@ -694,10 +960,32 @@ func (w *_assembler) AssignString(s string) error { if err := compatibleKind(w.schemaType, datamodel.Kind_String); err != nil { return err } - if _, ok := w.schemaType.(*schema.TypeAny); ok { - w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewString(s))) + customConverter := w.cfg.converterFor(w.val) + _, isAny := w.schemaType.(*schema.TypeAny) + if customConverter != nil { + var typ interface{} + var err error + if isAny { + // field is an Any, so the converter will be an Any converter that wants + // a datamodel.Node to convert to whatever the underlying Go type is + if typ, err = customConverter.customFromAny(basicnode.NewString(s)); err != nil { + return err + } + } else { + // field is a String, but the user has registered a converter from a + // string to whatever the underlying Go type is + if typ, err = customConverter.customFromString(s); err != nil { + return err + } + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { - w.createNonPtrVal().SetString(s) + if isAny { + // Any means the Go type must receive a datamodel.Node + w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewString(s))) + } else { + w.createNonPtrVal().SetString(s) + } } if w.finish != nil { if err := w.finish(); err != nil { @@ -711,10 +999,32 @@ func (w *_assembler) AssignBytes(p []byte) error { if err := compatibleKind(w.schemaType, datamodel.Kind_Bytes); err != nil { return err } - if _, ok := w.schemaType.(*schema.TypeAny); ok { - w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewBytes(p))) + customConverter := w.cfg.converterFor(w.val) + _, isAny := w.schemaType.(*schema.TypeAny) + if customConverter != nil { + var typ interface{} + var err error + if isAny { + // field is an Any, so the converter will be an Any converter that wants + // a datamodel.Node to convert to whatever the underlying Go type is + if typ, err = customConverter.customFromAny(basicnode.NewBytes(p)); err != nil { + return err + } + } else { + // field is a Bytes, but the user has registered a converter from a []byte + // to whatever the underlying Go type is + if typ, err = customConverter.customFromBytes(p); err != nil { + return err + } + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { - w.createNonPtrVal().SetBytes(p) + if isAny { + // Any means the Go type must receive a datamodel.Node + w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewBytes(p))) + } else { + w.createNonPtrVal().SetBytes(p) + } } if w.finish != nil { if err := w.finish(); err != nil { @@ -727,8 +1037,32 @@ func (w *_assembler) AssignBytes(p []byte) error { func (w *_assembler) AssignLink(link datamodel.Link) error { val := w.createNonPtrVal() // TODO: newVal.Type() panics if link==nil; add a test and fix. + customConverter := w.cfg.converterFor(w.val) if _, ok := w.schemaType.(*schema.TypeAny); ok { - val.Set(reflect.ValueOf(basicnode.NewLink(link))) + if customConverter != nil { + // field is an Any, so the converter will be an Any converter that wants + // a datamodel.Node to convert to whatever the underlying Go type is + typ, err := customConverter.customFromAny(basicnode.NewLink(link)) + if err != nil { + return err + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) + } else { + // Any means the Go type must receive a datamodel.Node + val.Set(reflect.ValueOf(basicnode.NewLink(link))) + } + } else if customConverter != nil { + if cl, ok := link.(cidlink.Link); ok { + // field is a Link, but the user has registered a converter from a cid.Cid + // to whatever the underlying Go type is + typ, err := customConverter.customFromLink(cl.Cid) + if err != nil { + return err + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) + } else { + return fmt.Errorf("bindnode: custom converter can only receive a cidlink.Link through AssignLink") + } } else if newVal := reflect.ValueOf(link); newVal.Type().AssignableTo(val.Type()) { // Directly assignable. val.Set(newVal) @@ -747,7 +1081,6 @@ func (w *_assembler) AssignLink(link datamodel.Link) error { } else { // The schema type is a Link, but we somehow can't assign to the Go value. // Almost certainly a bug; we should have verified for compatibility upfront. - // fmt.Println(newVal.Type().ConvertibleTo(val.Type())) return fmt.Errorf("bindnode bug: AssignLink with %s argument can't be used on Go type %s", newVal.Type(), val.Type()) } @@ -766,16 +1099,22 @@ func (w *_assembler) AssignNode(node datamodel.Node) error { // w.val.Set(newVal) // return nil // } + if uintNode, ok := node.(datamodel.UintNode); ok { + return w.assignUInt(uintNode) + } return datamodel.Copy(node, w) } func (w *_assembler) Prototype() datamodel.NodePrototype { - return &_prototype{schemaType: w.schemaType, goType: w.val.Type()} + return &_prototype{cfg: w.cfg, schemaType: w.schemaType, goType: w.val.Type()} } +// _structAssembler is used for Struct assembling via BeginMap() type _structAssembler struct { // TODO: embed _assembler? + cfg config + schemaType *schema.TypeStruct val reflect.Value // non-pointer finish func() error @@ -796,6 +1135,7 @@ type _structAssembler struct { func (w *_structAssembler) AssembleKey() datamodel.NodeAssembler { w.curKey = _assembler{ + cfg: w.cfg, schemaType: schemaTypeString, val: reflect.New(goTypeString).Elem(), } @@ -837,6 +1177,7 @@ func (w *_structAssembler) AssembleValue() datamodel.NodeAssembler { } // TODO: reuse same assembler for perf? return &_assembler{ + cfg: w.cfg, schemaType: field.Type(), val: fval, nullable: field.IsNullable(), @@ -873,7 +1214,7 @@ func (w *_structAssembler) Finish() error { func (w *_structAssembler) KeyPrototype() datamodel.NodePrototype { // TODO: if the user provided their own schema with their own typesystem, // the schemaTypeString here may be using the wrong typesystem. - return &_prototype{schemaType: schemaTypeString, goType: goTypeString} + return &_prototype{cfg: w.cfg, schemaType: schemaTypeString, goType: goTypeString} } func (w *_structAssembler) ValuePrototype(k string) datamodel.NodePrototype { @@ -896,7 +1237,10 @@ func (w _errorAssembler) AssignLink(datamodel.Link) error { ret func (w _errorAssembler) AssignNode(datamodel.Node) error { return w.err } func (w _errorAssembler) Prototype() datamodel.NodePrototype { return nil } +// used for Maps which we can assume are of type: struct{Keys []string, Values map[x]y}, +// where we have Keys in keysVal and Values in valuesVal type _mapAssembler struct { + cfg config schemaType *schema.TypeMap keysVal reflect.Value // non-pointer valuesVal reflect.Value // non-pointer @@ -909,6 +1253,7 @@ type _mapAssembler struct { func (w *_mapAssembler) AssembleKey() datamodel.NodeAssembler { w.curKey = _assembler{ + cfg: w.cfg, schemaType: w.schemaType.KeyType(), val: reflect.New(w.valuesVal.Type().Key()).Elem(), } @@ -919,8 +1264,6 @@ func (w *_mapAssembler) AssembleValue() datamodel.NodeAssembler { kval := w.curKey.val val := reflect.New(w.valuesVal.Type().Elem()).Elem() finish := func() error { - // fmt.Println(kval.Interface(), val.Interface()) - // TODO: check for duplicates in keysVal w.keysVal.Set(reflect.Append(w.keysVal, kval)) @@ -928,6 +1271,7 @@ func (w *_mapAssembler) AssembleValue() datamodel.NodeAssembler { return nil } return &_assembler{ + cfg: w.cfg, schemaType: w.schemaType.ValueType(), val: val, nullable: w.schemaType.ValueIsNullable(), @@ -953,14 +1297,16 @@ func (w *_mapAssembler) Finish() error { } func (w *_mapAssembler) KeyPrototype() datamodel.NodePrototype { - return &_prototype{schemaType: w.schemaType.KeyType(), goType: w.valuesVal.Type().Key()} + return &_prototype{cfg: w.cfg, schemaType: w.schemaType.KeyType(), goType: w.valuesVal.Type().Key()} } func (w *_mapAssembler) ValuePrototype(k string) datamodel.NodePrototype { - return &_prototype{schemaType: w.schemaType.ValueType(), goType: w.valuesVal.Type().Elem()} + return &_prototype{cfg: w.cfg, schemaType: w.schemaType.ValueType(), goType: w.valuesVal.Type().Elem()} } +// _listAssembler is for operating directly on slices, which we have in val type _listAssembler struct { + cfg config schemaType *schema.TypeList val reflect.Value // non-pointer finish func() error @@ -971,6 +1317,7 @@ func (w *_listAssembler) AssembleValue() datamodel.NodeAssembler { // TODO: use a finish func to append w.val.Set(reflect.Append(w.val, reflect.New(goType).Elem())) return &_assembler{ + cfg: w.cfg, schemaType: w.schemaType.ValueType(), val: w.val.Index(w.val.Len() - 1), nullable: w.schemaType.ValueIsNullable(), @@ -987,10 +1334,13 @@ func (w *_listAssembler) Finish() error { } func (w *_listAssembler) ValuePrototype(idx int64) datamodel.NodePrototype { - return &_prototype{schemaType: w.schemaType.ValueType(), goType: w.val.Type().Elem()} + return &_prototype{cfg: w.cfg, schemaType: w.schemaType.ValueType(), goType: w.val.Type().Elem()} } +// when assembling as a Map but we anticipate a single value, which we need to +// look up in the union members type _unionAssembler struct { + cfg config schemaType *schema.TypeUnion val reflect.Value // non-pointer finish func() error @@ -1002,6 +1352,7 @@ type _unionAssembler struct { func (w *_unionAssembler) AssembleKey() datamodel.NodeAssembler { w.curKey = _assembler{ + cfg: w.cfg, schemaType: schemaTypeString, val: reflect.New(goTypeString).Elem(), } @@ -1030,11 +1381,11 @@ func (w *_unionAssembler) AssembleValue() datamodel.NodeAssembler { goType := w.val.Field(idx).Type().Elem() valPtr := reflect.New(goType) finish := func() error { - // fmt.Println(kval.Interface(), val.Interface()) unionSetMember(w.val, idx, valPtr) return nil } return &_assembler{ + cfg: w.cfg, schemaType: mtyp, val: valPtr.Elem(), finish: finish, @@ -1050,6 +1401,8 @@ func (w *_unionAssembler) AssembleEntry(k string) (datamodel.NodeAssembler, erro } func (w *_unionAssembler) Finish() error { + // TODO(rvagg): I think this might allow setting multiple members of the union + // we need a test for this. haveIdx, _ := unionMember(w.val) if haveIdx < 0 { return schema.ErrNotUnionStructure{TypeName: w.schemaType.Name(), Detail: "a union must have exactly one entry"} @@ -1063,15 +1416,20 @@ func (w *_unionAssembler) Finish() error { } func (w *_unionAssembler) KeyPrototype() datamodel.NodePrototype { - return &_prototype{schemaType: schemaTypeString, goType: goTypeString} + return &_prototype{cfg: w.cfg, schemaType: schemaTypeString, goType: goTypeString} } func (w *_unionAssembler) ValuePrototype(k string) datamodel.NodePrototype { panic("bindnode TODO: union ValuePrototype") } +// _structIterator is for iterating over Struct types which operate over Go +// structs. The iteration order is dictated by Go field declaration order which +// should match the schema for this type. type _structIterator struct { // TODO: support embedded fields? + cfg config + schemaType *schema.TypeStruct fields []schema.StructField val reflect.Value // non-pointer @@ -1097,6 +1455,18 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { val = val.Elem() } } + _, isAny := field.Type().(*schema.TypeAny) + if isAny { + if customConverter := w.cfg.converterFor(val); customConverter != nil { + // field is an Any and we have an Any converter which takes the underlying + // struct field value and returns a datamodel.Node + v, err := customConverter.customToAny(ptrVal(val).Interface()) + if err != nil { + return nil, nil, err + } + return key, v, nil + } + } if field.IsNullable() { if val.IsNil() { return key, datamodel.Null, nil @@ -1105,21 +1475,21 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { val = val.Elem() } } - if _, ok := field.Type().(*schema.TypeAny); ok { + if isAny { + // field holds a datamodel.Node return key, nonPtrVal(val).Interface().(datamodel.Node), nil } - node := &_node{ - schemaType: field.Type(), - val: val, - } - return key, node, nil + return key, newNode(w.cfg, field.Type(), val), nil } func (w *_structIterator) Done() bool { return w.nextIndex >= len(w.fields) } +// _mapIterator is for iterating over a struct{Keys []string, Values map[x]y}, +// where we have the Keys in keysVal and Values in valuesVal type _mapIterator struct { + cfg config schemaType *schema.TypeMap keysVal reflect.Value // non-pointer valuesVal reflect.Value // non-pointer @@ -1134,31 +1504,40 @@ func (w *_mapIterator) Next() (key, value datamodel.Node, _ error) { val := w.valuesVal.MapIndex(goKey) w.nextIndex++ - key = &_node{ - schemaType: w.schemaType.KeyType(), - val: goKey, + key = newNode(w.cfg, w.schemaType.KeyType(), goKey) + _, isAny := w.schemaType.ValueType().(*schema.TypeAny) + if isAny { + if customConverter := w.cfg.converterFor(val); customConverter != nil { + // values of this map are Any and we have an Any converter which takes the + // underlying map value and returns a datamodel.Node + + // TODO(rvagg): can't call ptrVal on a map value that's not a pointer + // so only map[string]*foo will work for the Values map and an Any + // converter. Should we check in infer.go? + val, err := customConverter.customToAny(ptrVal(val).Interface()) + return key, val, err + } } if w.schemaType.ValueIsNullable() { if val.IsNil() { return key, datamodel.Null, nil } - val = val.Elem() + val = val.Elem() // nullable entries are pointers } - if _, ok := w.schemaType.ValueType().(*schema.TypeAny); ok { + if isAny { + // Values holds datamodel.Nodes return key, nonPtrVal(val).Interface().(datamodel.Node), nil } - node := &_node{ - schemaType: w.schemaType.ValueType(), - val: val, - } - return key, node, nil + return key, newNode(w.cfg, w.schemaType.ValueType(), val), nil } func (w *_mapIterator) Done() bool { return w.nextIndex >= w.keysVal.Len() } +// _listIterator is for iterating over slices, which is held in val type _listIterator struct { + cfg config schemaType *schema.TypeList val reflect.Value // non-pointer nextIndex int @@ -1175,12 +1554,19 @@ func (w *_listIterator) Next() (index int64, value datamodel.Node, _ error) { if val.IsNil() { return idx, datamodel.Null, nil } - val = val.Elem() + val = val.Elem() // nullable values are pointers } if _, ok := w.schemaType.ValueType().(*schema.TypeAny); ok { + if customConverter := w.cfg.converterFor(val); customConverter != nil { + // values are Any and we have an Any converter which can take whatever + // the underlying Go type in this slice is and return a datamodel.Node + val, err := customConverter.customToAny(ptrVal(val).Interface()) + return idx, val, err + } + // values are Any, assume that they are datamodel.Nodes return idx, nonPtrVal(val).Interface().(datamodel.Node), nil } - return idx, &_node{schemaType: w.schemaType.ValueType(), val: val}, nil + return idx, newNode(w.cfg, w.schemaType.ValueType(), val), nil } func (w *_listIterator) Done() bool { @@ -1189,6 +1575,7 @@ func (w *_listIterator) Done() bool { type _unionIterator struct { // TODO: support embedded fields? + cfg config schemaType *schema.TypeUnion members []schema.Type val reflect.Value // non-pointer @@ -1197,6 +1584,8 @@ type _unionIterator struct { } func (w *_unionIterator) Next() (key, value datamodel.Node, _ error) { + // we can only call this once for a union since a union can only have one + // entry even though it behaves like a Map if w.Done() { return nil, nil, datamodel.ErrIteratorOverread{} } @@ -1208,10 +1597,7 @@ func (w *_unionIterator) Next() (key, value datamodel.Node, _ error) { } mtyp := w.members[haveIdx] - node := &_node{ - schemaType: mtyp, - val: mval, - } + node := newNode(w.cfg, mtyp, mval) key = basicnode.NewString(mtyp.Name()) return key, node, nil } @@ -1219,3 +1605,150 @@ func (w *_unionIterator) Next() (key, value datamodel.Node, _ error) { func (w *_unionIterator) Done() bool { return w.done } + +// --- uint64 special case handling + +type _uintNode struct { + cfg config + schemaType schema.Type + + val reflect.Value // non-pointer +} + +func (tu *_uintNode) Type() schema.Type { + return tu.schemaType +} +func (tu *_uintNode) Representation() datamodel.Node { + return (*_uintNodeRepr)(tu) +} +func (_uintNode) Kind() datamodel.Kind { + return datamodel.Kind_Int +} +func (_uintNode) LookupByString(string) (datamodel.Node, error) { + return mixins.Int{TypeName: "int"}.LookupByString("") +} +func (_uintNode) LookupByNode(key datamodel.Node) (datamodel.Node, error) { + return mixins.Int{TypeName: "int"}.LookupByNode(nil) +} +func (_uintNode) LookupByIndex(idx int64) (datamodel.Node, error) { + return mixins.Int{TypeName: "int"}.LookupByIndex(0) +} +func (_uintNode) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) { + return mixins.Int{TypeName: "int"}.LookupBySegment(seg) +} +func (_uintNode) MapIterator() datamodel.MapIterator { + return nil +} +func (_uintNode) ListIterator() datamodel.ListIterator { + return nil +} +func (_uintNode) Length() int64 { + return -1 +} +func (_uintNode) IsAbsent() bool { + return false +} +func (_uintNode) IsNull() bool { + return false +} +func (_uintNode) AsBool() (bool, error) { + return mixins.Int{TypeName: "int"}.AsBool() +} +func (tu *_uintNode) AsInt() (int64, error) { + return (*_uintNodeRepr)(tu).AsInt() +} +func (tu *_uintNode) AsUint() (uint64, error) { + return (*_uintNodeRepr)(tu).AsUint() +} +func (_uintNode) AsFloat() (float64, error) { + return mixins.Int{TypeName: "int"}.AsFloat() +} +func (_uintNode) AsString() (string, error) { + return mixins.Int{TypeName: "int"}.AsString() +} +func (_uintNode) AsBytes() ([]byte, error) { + return mixins.Int{TypeName: "int"}.AsBytes() +} +func (_uintNode) AsLink() (datamodel.Link, error) { + return mixins.Int{TypeName: "int"}.AsLink() +} +func (_uintNode) Prototype() datamodel.NodePrototype { + return basicnode.Prototype__Int{} +} + +// we need this for _uintNode#Representation() so we don't return a TypeNode +type _uintNodeRepr _uintNode + +func (_uintNodeRepr) Kind() datamodel.Kind { + return datamodel.Kind_Int +} +func (_uintNodeRepr) LookupByString(string) (datamodel.Node, error) { + return mixins.Int{TypeName: "int"}.LookupByString("") +} +func (_uintNodeRepr) LookupByNode(key datamodel.Node) (datamodel.Node, error) { + return mixins.Int{TypeName: "int"}.LookupByNode(nil) +} +func (_uintNodeRepr) LookupByIndex(idx int64) (datamodel.Node, error) { + return mixins.Int{TypeName: "int"}.LookupByIndex(0) +} +func (_uintNodeRepr) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) { + return mixins.Int{TypeName: "int"}.LookupBySegment(seg) +} +func (_uintNodeRepr) MapIterator() datamodel.MapIterator { + return nil +} +func (_uintNodeRepr) ListIterator() datamodel.ListIterator { + return nil +} +func (_uintNodeRepr) Length() int64 { + return -1 +} +func (_uintNodeRepr) IsAbsent() bool { + return false +} +func (_uintNodeRepr) IsNull() bool { + return false +} +func (_uintNodeRepr) AsBool() (bool, error) { + return mixins.Int{TypeName: "int"}.AsBool() +} +func (tu *_uintNodeRepr) AsInt() (int64, error) { + if err := compatibleKind(tu.schemaType, datamodel.Kind_Int); err != nil { + return 0, err + } + if customConverter := tu.cfg.converterFor(tu.val); customConverter != nil { + // user has registered a converter that takes the underlying type and returns an int + return customConverter.customToInt(ptrVal(tu.val).Interface()) + } + val := nonPtrVal(tu.val) + // we can assume it's a uint64 at this point + u := val.Uint() + if u > math.MaxInt64 { + return 0, fmt.Errorf("bindnode: integer overflow, %d is too large for an int64", u) + } + return int64(u), nil +} +func (tu *_uintNodeRepr) AsUint() (uint64, error) { + if err := compatibleKind(tu.schemaType, datamodel.Kind_Int); err != nil { + return 0, err + } + // TODO(rvagg): do we want a converter option for uint values? do we combine it + // with int converters? + // we can assume it's a uint64 at this point + return nonPtrVal(tu.val).Uint(), nil +} +func (_uintNodeRepr) AsFloat() (float64, error) { + return mixins.Int{TypeName: "int"}.AsFloat() +} +func (_uintNodeRepr) AsString() (string, error) { + return mixins.Int{TypeName: "int"}.AsString() +} +func (_uintNodeRepr) AsBytes() ([]byte, error) { + return mixins.Int{TypeName: "int"}.AsBytes() +} +func (_uintNodeRepr) AsLink() (datamodel.Link, error) { + return mixins.Int{TypeName: "int"}.AsLink() +} +func (_uintNodeRepr) Prototype() datamodel.NodePrototype { + return basicnode.Prototype__Int{} +} diff --git a/node/bindnode/registry/registry.go b/node/bindnode/registry/registry.go new file mode 100644 index 00000000..b619bab9 --- /dev/null +++ b/node/bindnode/registry/registry.go @@ -0,0 +1,142 @@ +package registry + +import ( + "fmt" + "io" + "reflect" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/ipld/go-ipld-prime/schema" +) + +type prototypeData struct { + proto schema.TypedPrototype + options []bindnode.Option +} + +// BindnodeRegistry holds TypedPrototype and bindnode options for Go types and +// will use that data for conversion operations. +type BindnodeRegistry map[reflect.Type]prototypeData + +// NewRegistry creates a new BindnodeRegistry +func NewRegistry() BindnodeRegistry { + return make(BindnodeRegistry) +} + +func typeOf(ptrValue interface{}) reflect.Type { + val := reflect.ValueOf(ptrValue).Type() + for val.Kind() == reflect.Ptr { + val = val.Elem() + } + return val +} + +func (br BindnodeRegistry) prototypeDataFor(ptrType interface{}) prototypeData { + typ := typeOf(ptrType) + proto, ok := br[typ] + if !ok { + panic(fmt.Sprintf("bindnode utils: type has not been registered: %s", typ.Name())) + } + return proto +} + +// RegisterType registers ptrType with schema such that it can be wrapped and +// unwrapped without needing the schema, Type, or TypedPrototype. +// Typically the typeName will match the Go type name, but it can be whatever +// is defined in the schema for the type being registered. +// Registering the same type twice on this registry will cause a panic. Use +// IsRegistered() if this is a possibility that should be avoided. +// This call may also panic if the schema is invalid or the type doesn't match +// the schema. +func (br BindnodeRegistry) RegisterType(ptrType interface{}, schema string, typeName string, options ...bindnode.Option) { + typ := typeOf(ptrType) + if _, ok := br[typ]; ok { + panic(fmt.Sprintf("bindnode utils: type already registered: %s", typ.Name())) + } + typeSystem, err := ipld.LoadSchemaBytes([]byte(schema)) + if err != nil { + panic(fmt.Sprintf("bindnode utils: failed to load schema: %s", err.Error())) + } + schemaType := typeSystem.TypeByName(typeName) + if schemaType == nil { + panic(fmt.Sprintf("bindnode utils: schema for [%T] does not contain that named type [%s]", ptrType, typ.Name())) + } + br[typ] = prototypeData{ + bindnode.Prototype(ptrType, schemaType, options...), + options, + } +} + +// IsRegistered can be used to determine if the type has already been registered +// within this registry. +// Using RegisterType on an already registered type will cause a panic, so where +// this may be the case, IsRegistered can be used to check. +func (br BindnodeRegistry) IsRegistered(ptrType interface{}) bool { + _, ok := br[typeOf(ptrType)] + return ok +} + +// TypeFromReader deserializes bytes using the given codec from a Reader and +// instantiates the Go type that's provided as a pointer via the ptrValue +// argument. +func (br BindnodeRegistry) TypeFromReader(r io.Reader, ptrValue interface{}, decoder codec.Decoder) (interface{}, error) { + protoData := br.prototypeDataFor(ptrValue) + node, err := ipld.DecodeStreamingUsingPrototype(r, decoder, protoData.proto) + if err != nil { + return nil, err + } + typ := bindnode.Unwrap(node) + return typ, nil +} + +// TypeFromBytes deserializes bytes using the given codec from its bytes and +// instantiates the Go type that's provided as a pointer via the ptrValue +// argument. +func (br BindnodeRegistry) TypeFromBytes(byts []byte, ptrValue interface{}, decoder codec.Decoder) (interface{}, error) { + protoData := br.prototypeDataFor(ptrValue) + node, err := ipld.DecodeUsingPrototype(byts, decoder, protoData.proto) + if err != nil { + return nil, err + } + typ := bindnode.Unwrap(node) + return typ, nil +} + +// TypeFromNode converts an datamodel.Node into an appropriate Go type that's +// provided as a pointer via the ptrValue argument. +func (br BindnodeRegistry) TypeFromNode(node datamodel.Node, ptrValue interface{}) (interface{}, error) { + protoData := br.prototypeDataFor(ptrValue) + if tn, ok := node.(schema.TypedNode); ok { + node = tn.Representation() + } + builder := protoData.proto.Representation().NewBuilder() + err := builder.AssignNode(node) + if err != nil { + return nil, err + } + typ := bindnode.Unwrap(builder.Build()) + return typ, nil +} + +// TypeToNode converts a Go type that's provided as a pointer via the ptrValue +// argument to an schema.TypedNode. +func (br BindnodeRegistry) TypeToNode(ptrValue interface{}) schema.TypedNode { + protoData := br.prototypeDataFor(ptrValue) + return bindnode.Wrap(ptrValue, protoData.proto.Type(), protoData.options...) +} + +// TypeToWriter is a utility method that serializes a Go type that's provided as +// a pointer via the ptrValue argument through the given codec to a Writer. +func (br BindnodeRegistry) TypeToWriter(ptrValue interface{}, w io.Writer, encoder codec.Encoder) error { + return ipld.EncodeStreaming(w, br.TypeToNode(ptrValue), encoder) +} + +// TypeToBytes is a utility method that serializes a Go type that's provided as +// a pointer via the ptrValue argument through the given codec and returns the +// bytes. +func (br BindnodeRegistry) TypeToBytes(ptrValue interface{}, encoder codec.Encoder) ([]byte, error) { + return ipld.Encode(br.TypeToNode(ptrValue), encoder) +} diff --git a/node/bindnode/repr.go b/node/bindnode/repr.go index 02b2f058..993defbd 100644 --- a/node/bindnode/repr.go +++ b/node/bindnode/repr.go @@ -39,6 +39,7 @@ type _prototypeRepr _prototype func (w *_prototypeRepr) NewBuilder() datamodel.NodeBuilder { return &_builderRepr{_assemblerRepr{ + cfg: w.cfg, schemaType: w.schemaType, val: reflect.New(w.goType).Elem(), }} @@ -268,7 +269,7 @@ func (w *_nodeRepr) ListIterator() datamodel.ListIterator { switch reprStrategy(w.schemaType).(type) { case schema.StructRepresentation_Tuple: typ := w.schemaType.(*schema.TypeStruct) - iter := _tupleIteratorRepr{schemaType: typ, fields: typ.Fields(), val: w.val} + iter := _tupleIteratorRepr{cfg: w.cfg, schemaType: typ, fields: typ.Fields(), val: w.val} iter.reprEnd = int(w.lengthMinusTrailingAbsents()) return &iter default: @@ -307,6 +308,7 @@ func (w *_nodeRepr) lengthMinusAbsents() int64 { type _tupleIteratorRepr struct { // TODO: support embedded fields? + cfg config schemaType *schema.TypeStruct fields []schema.StructField val reflect.Value // non-pointer @@ -535,7 +537,7 @@ type _builderRepr struct { func (w *_builderRepr) Build() datamodel.Node { // TODO: see the notes above. // return &_nodeRepr{schemaType: w.schemaType, val: w.val} - return &_node{schemaType: w.schemaType, val: w.val} + return &_node{cfg: w.cfg, schemaType: w.schemaType, val: w.val} } func (w *_builderRepr) Reset() { @@ -543,6 +545,7 @@ func (w *_builderRepr) Reset() { } type _assemblerRepr struct { + cfg config schemaType schema.Type val reflect.Value // non-pointer finish func() error @@ -653,6 +656,21 @@ func (w *_assemblerRepr) AssignBool(b bool) error { } } +func (w *_assemblerRepr) assignUInt(uin datamodel.UintNode) error { + switch stg := reprStrategy(w.schemaType).(type) { + case schema.UnionRepresentation_Kinded: + return w.asKinded(stg, datamodel.Kind_Int).(*_assemblerRepr).assignUInt(uin) + case schema.EnumRepresentation_Int: + uin, err := uin.AsUint() + if err != nil { + return err + } + return fmt.Errorf("AssignInt: %d is not a valid member of enum %s", uin, w.schemaType.Name()) + default: + return (*_assembler)(w).assignUInt(uin) + } +} + func (w *_assemblerRepr) AssignInt(i int64) error { switch stg := reprStrategy(w.schemaType).(type) { case schema.UnionRepresentation_Kinded: @@ -817,6 +835,9 @@ func (w *_assemblerRepr) AssignLink(link datamodel.Link) error { func (w *_assemblerRepr) AssignNode(node datamodel.Node) error { // TODO: attempt to take a shortcut, like assembler.AssignNode + if uintNode, ok := node.(datamodel.UintNode); ok { + return w.assignUInt(uintNode) + } return datamodel.Copy(node, w) }