From d9e3bd2bed58d648bb079d2799f4797046c4f6e0 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 13 May 2022 14:37:13 +1000 Subject: [PATCH 01/13] feat(bindnode): allow custom type conversions with options --- node/bindnode/api.go | 64 +++++++++++-- node/bindnode/custom_test.go | 168 +++++++++++++++++++++++++++++++++++ node/bindnode/infer.go | 31 ++++--- node/bindnode/node.go | 96 +++++++++++++++++--- node/bindnode/repr.go | 7 +- 5 files changed, 334 insertions(+), 32 deletions(-) create mode 100644 node/bindnode/custom_test.go diff --git a/node/bindnode/api.go b/node/bindnode/api.go index 4b276ae2..87618efd 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -27,11 +27,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 +52,60 @@ 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} +} + +// CustomTypeConverter is an empty interface intended as a parent for the +// various type converters +type CustomTypeConverter interface { +} + +// CustomTypeBytesConverter is able to convert byte slices to and from a +// specific type that can't otherwise be handled by bindnode. Such a type will +// likely not be instantiated by plain reflection and therefore need custom +// logic to decode and/or instantiate. +type CustomTypeBytesConverter interface { + FromBytes([]byte) (interface{}, error) + ToBytes(interface{}) ([]byte, error) +} + +type config struct { + customConverters map[reflect.Type]CustomTypeConverter +} + +// Option is able to apply custom options to the bindnode API +type Option func(*config) + +// AddCustomTypeBytesConverter adds a CustomTypeConverter for a particular +// type as referenced by ptrType. The CustomTypeConverter must be able to +// handle that specific type, and the data model kind it's converting must be +// present at the schema location it's encountered. +func AddCustomTypeBytesConverter(ptrType interface{}, converter CustomTypeBytesConverter) Option { + val := reflect.ValueOf(ptrType) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + name := val.Type().Name() + if name == "" { + panic("not a named type") + } + return func(cfg *config) { + cfg.customConverters[val.Type()] = converter + } +} + +func applyOptions(opt ...Option) config { + cfg := config{ + customConverters: make(map[reflect.Type]CustomTypeConverter), + } + 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 +116,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 +128,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 +136,9 @@ 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) + verifyCompatibility(cfg, make(map[seenEntry]bool), goVal.Type(), schemaType) } - return &_node{val: goVal, schemaType: schemaType} + return &_node{cfg: cfg, val: goVal, schemaType: schemaType} } // TODO: consider making our own Node interface, like: diff --git a/node/bindnode/custom_test.go b/node/bindnode/custom_test.go new file mode 100644 index 00000000..ee4ea96d --- /dev/null +++ b/node/bindnode/custom_test.go @@ -0,0 +1,168 @@ +package bindnode_test + +import ( + "bytes" + "fmt" + "math/big" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/ipld/go-ipld-prime/node/bindnode" + + qt "github.com/frankban/quicktest" +) + +// 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 Blop struct{ *big.Int } + +func NewBlopFromString(str string) Blop { + v, _ := big.NewInt(0).SetString(str, 10) + return Blop{v} +} + +func NewBlopFromBytes(buf []byte) Blop { + 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 Blop{i} +} + +func (b *Blop) 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{} + } +} + +type Boom struct { + S string + B Boop + Bptr *Boop + BI Blop + I int +} + +const boomSchema = ` +type Boom struct { + S String + B Bytes + Bptr nullable Bytes + BI Bytes + I Int +} representation map +` + +const boomFixtureDagJson = `{"B":{"/":{"bytes":"dGhlc2UgYXJlIGJ5dGVz"}},"BI":{"/":{"bytes":"AAH3fubjrGlwOMpClAkh/ro13L5Uls4/CtI"}},"Bptr":{"/":{"bytes":"dGhlc2UgYXJlIHBvaW50ZXIgYnl0ZXM"}},"I":10101,"S":"a string here"}` + +var boomFixtureInstance = Boom{ + S: "a string here", + B: *NewBoop([]byte("these are bytes")), + BI: NewBlopFromString("12345678901234567891234567890123456789012345678901234567890"), + Bptr: NewBoop([]byte("these are pointer bytes")), + I: 10101, +} + +type BoopConverter struct { +} + +func (bc BoopConverter) FromBytes(b []byte) (interface{}, error) { + return NewBoop(b), nil +} + +func (bc BoopConverter) ToBytes(typ interface{}) ([]byte, error) { + if boop, ok := typ.(*Boop); ok { + return boop.Bytes(), nil + } + return nil, fmt.Errorf("did not get a Boop type") +} + +type BlopConverter struct { +} + +func (bc BlopConverter) FromBytes(b []byte) (interface{}, error) { + return NewBlopFromBytes(b), nil +} + +func (bc BlopConverter) ToBytes(typ interface{}) ([]byte, error) { + if blop, ok := typ.(*Blop); ok { + return blop.Bytes(), nil + } + return nil, fmt.Errorf("did not get a Blop type") +} + +var ( + _ bindnode.CustomTypeBytesConverter = (*BoopConverter)(nil) + _ bindnode.CustomTypeBytesConverter = (*BlopConverter)(nil) +) + +func TestCustom(t *testing.T) { + opts := []bindnode.Option{ + bindnode.AddCustomTypeBytesConverter(Boop{}, BoopConverter{}), + bindnode.AddCustomTypeBytesConverter(Blop{}, BlopConverter{}), + } + + nb := basicnode.Prototype.Any.NewBuilder() + err := dagjson.Decode(nb, bytes.NewReader([]byte(boomFixtureDagJson))) + qt.Assert(t, err, qt.IsNil) + + typeSystem, err := ipld.LoadSchemaBytes([]byte(boomSchema)) + qt.Assert(t, err, qt.IsNil) + schemaType := typeSystem.TypeByName("Boom") + proto := bindnode.Prototype(&Boom{}, schemaType, opts...) + + node := nb.Build() + builder := proto.Representation().NewBuilder() + err = builder.AssignNode(node) + 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 Blop) bool { return x.String() == y.String() }), + ) + qt.Assert(t, *inst, cmpr, boomFixtureInstance) + + tn := bindnode.Wrap(&boomFixtureInstance, 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) +} diff --git a/node/bindnode/infer.go b/node/bindnode/infer.go index fc2f7134..369ea066 100644 --- a/node/bindnode/infer.go +++ b/node/bindnode/infer.go @@ -39,7 +39,7 @@ type seenEntry struct { schemaType schema.Type } -func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaType schema.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() @@ -86,11 +86,18 @@ func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaTyp } case *schema.TypeBytes: // TODO: allow string? - if goType.Kind() != reflect.Slice { - doPanic("kind mismatch; need slice of bytes") - } - if goType.Elem().Kind() != reflect.Uint8 { - doPanic("kind mismatch; need slice of bytes") + customConverter, ok := cfg.customConverters[goType] + if ok { + if _, ok := customConverter.(CustomTypeBytesConverter); !ok { + doPanic("kind mismatch; custom converter for type is not a CustomTypeBytesConverter") + } + } else { + if goType.Kind() != reflect.Slice { + doPanic("kind mismatch; need slice of bytes") + } + if goType.Elem().Kind() != reflect.Uint8 { + doPanic("kind mismatch; need slice of bytes") + } } case *schema.TypeEnum: if _, ok := schemaType.RepresentationStrategy().(schema.EnumRepresentation_Int); ok { @@ -114,7 +121,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 +138,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 +155,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") @@ -187,7 +194,7 @@ func verifyCompatibility(seen map[seenEntry]bool, goType reflect.Type, schemaTyp goType = goType.Elem() } } - verifyCompatibility(seen, goType, schemaType) + verifyCompatibility(cfg, seen, goType, schemaType) } case *schema.TypeUnion: if goType.Kind() != reflect.Struct { @@ -206,7 +213,7 @@ 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 { diff --git a/node/bindnode/node.go b/node/bindnode/node.go index 8bf2c6bc..35c4679a 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -46,12 +46,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 +68,7 @@ func (w *_prototype) Representation() datamodel.NodePrototype { } type _node struct { + cfg config schemaType schema.Type val reflect.Value // non-pointer @@ -165,6 +168,7 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { return nonPtrVal(fval).Interface().(datamodel.Node), nil } node := &_node{ + cfg: w.cfg, schemaType: field.Type(), val: fval, } @@ -177,6 +181,7 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { kval = reflect.ValueOf(key) default: asm := &_assembler{ + cfg: w.cfg, schemaType: ktyp, val: reflect.New(valuesVal.Type().Key()).Elem(), } @@ -203,6 +208,7 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { return nonPtrVal(fval).Interface().(datamodel.Node), nil } node := &_node{ + cfg: w.cfg, schemaType: typ.ValueType(), val: fval, } @@ -226,6 +232,7 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfString(key)} } node := &_node{ + cfg: w.cfg, schemaType: mtyp, val: mval, } @@ -282,7 +289,7 @@ func (w *_node) LookupByIndex(idx int64) (datamodel.Node, error) { if _, ok := typ.ValueType().(*schema.TypeAny); ok { return nonPtrVal(val).Interface().(datamodel.Node), nil } - return &_node{schemaType: typ.ValueType(), val: val}, nil + return &_node{cfg: w.cfg, schemaType: typ.ValueType(), val: val}, nil } return nil, datamodel.ErrWrongKind{ TypeName: w.schemaType.Name(), @@ -339,18 +346,21 @@ func (w *_node) MapIterator() datamodel.MapIterator { 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: return &_mapIterator{ + cfg: w.cfg, schemaType: typ, keysVal: val.FieldByName("Keys"), valuesVal: val.FieldByName("Values"), @@ -363,7 +373,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 } @@ -432,6 +442,19 @@ func (w *_node) AsBytes() ([]byte, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Bytes); err != nil { return nil, err } + + typ := w.val.Type() + if w.val.Kind() == reflect.Ptr { + typ = typ.Elem() + } + customConverter, ok := w.cfg.customConverters[typ] + if ok { + cbc, ok := customConverter.(CustomTypeBytesConverter) + if !ok { + panic("kind mismatch; custom converter for type is not a CustomTypeBytesConverter") + } + return cbc.ToBytes(w.val.Addr().Interface()) + } return nonPtrVal(w.val).Bytes(), nil } @@ -450,7 +473,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 +482,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 &_node{cfg: w.cfg, schemaType: w.schemaType, val: w.val} } func (w *_builder) Reset() { @@ -467,6 +490,7 @@ func (w *_builder) Reset() { } type _assembler struct { + cfg config schemaType schema.Type val reflect.Value // non-pointer @@ -535,6 +559,7 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { val := w.createNonPtrVal() doneFields := make([]bool, val.NumField()) return &_structAssembler{ + cfg: w.cfg, schemaType: typ, val: val, doneFields: doneFields, @@ -548,6 +573,7 @@ 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, @@ -556,6 +582,7 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { case *schema.TypeUnion: val := w.createNonPtrVal() return &_unionAssembler{ + cfg: w.cfg, schemaType: typ, val: val, finish: w.finish, @@ -602,6 +629,7 @@ func (w *_assembler) BeginList(sizeHint int64) (datamodel.ListAssembler, error) case *schema.TypeList: val := w.createNonPtrVal() return &_listAssembler{ + cfg: w.cfg, schemaType: typ, val: val, finish: w.finish, @@ -714,7 +742,30 @@ func (w *_assembler) AssignBytes(p []byte) error { if _, ok := w.schemaType.(*schema.TypeAny); ok { w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewBytes(p))) } else { - w.createNonPtrVal().SetBytes(p) + typ := w.val.Type() + if w.val.Kind() == reflect.Ptr { + typ = typ.Elem() + } + customConverter, ok := w.cfg.customConverters[typ] + if ok { + cbc, ok := customConverter.(CustomTypeBytesConverter) + if !ok { + panic("kind mismatch; custom converter for type is not a CustomTypeBytesConverter") + } + val, err := cbc.FromBytes(p) + if err != nil { + return err + } + rval := reflect.ValueOf(val) + if w.val.Kind() == reflect.Ptr && rval.Kind() != reflect.Ptr { + rval = rval.Addr() + } else if w.val.Kind() != reflect.Ptr && rval.Kind() == reflect.Ptr { + rval = rval.Elem() + } + w.val.Set(rval) + } else { + w.createNonPtrVal().SetBytes(p) + } } if w.finish != nil { if err := w.finish(); err != nil { @@ -770,12 +821,14 @@ func (w *_assembler) AssignNode(node datamodel.Node) error { } 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()} } type _structAssembler struct { // TODO: embed _assembler? + cfg config + schemaType *schema.TypeStruct val reflect.Value // non-pointer finish func() error @@ -796,6 +849,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 +891,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 +928,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 { @@ -897,6 +952,7 @@ func (w _errorAssembler) AssignNode(datamodel.Node) error { ret func (w _errorAssembler) Prototype() datamodel.NodePrototype { return nil } type _mapAssembler struct { + cfg config schemaType *schema.TypeMap keysVal reflect.Value // non-pointer valuesVal reflect.Value // non-pointer @@ -909,6 +965,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(), } @@ -928,6 +985,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 +1011,15 @@ 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()} } type _listAssembler struct { + cfg config schemaType *schema.TypeList val reflect.Value // non-pointer finish func() error @@ -971,6 +1030,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 +1047,11 @@ 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()} } type _unionAssembler struct { + cfg config schemaType *schema.TypeUnion val reflect.Value // non-pointer finish func() error @@ -1002,6 +1063,7 @@ type _unionAssembler struct { func (w *_unionAssembler) AssembleKey() datamodel.NodeAssembler { w.curKey = _assembler{ + cfg: w.cfg, schemaType: schemaTypeString, val: reflect.New(goTypeString).Elem(), } @@ -1035,6 +1097,7 @@ func (w *_unionAssembler) AssembleValue() datamodel.NodeAssembler { return nil } return &_assembler{ + cfg: w.cfg, schemaType: mtyp, val: valPtr.Elem(), finish: finish, @@ -1063,7 +1126,7 @@ 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 { @@ -1072,6 +1135,8 @@ func (w *_unionAssembler) ValuePrototype(k string) datamodel.NodePrototype { type _structIterator struct { // TODO: support embedded fields? + cfg config + schemaType *schema.TypeStruct fields []schema.StructField val reflect.Value // non-pointer @@ -1109,6 +1174,7 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { return key, nonPtrVal(val).Interface().(datamodel.Node), nil } node := &_node{ + cfg: w.cfg, schemaType: field.Type(), val: val, } @@ -1120,6 +1186,7 @@ func (w *_structIterator) Done() bool { } type _mapIterator struct { + cfg config schemaType *schema.TypeMap keysVal reflect.Value // non-pointer valuesVal reflect.Value // non-pointer @@ -1135,6 +1202,7 @@ func (w *_mapIterator) Next() (key, value datamodel.Node, _ error) { w.nextIndex++ key = &_node{ + cfg: w.cfg, schemaType: w.schemaType.KeyType(), val: goKey, } @@ -1148,6 +1216,7 @@ func (w *_mapIterator) Next() (key, value datamodel.Node, _ error) { return key, nonPtrVal(val).Interface().(datamodel.Node), nil } node := &_node{ + cfg: w.cfg, schemaType: w.schemaType.ValueType(), val: val, } @@ -1159,6 +1228,7 @@ func (w *_mapIterator) Done() bool { } type _listIterator struct { + cfg config schemaType *schema.TypeList val reflect.Value // non-pointer nextIndex int @@ -1180,7 +1250,7 @@ func (w *_listIterator) Next() (index int64, value datamodel.Node, _ error) { if _, ok := w.schemaType.ValueType().(*schema.TypeAny); ok { return idx, nonPtrVal(val).Interface().(datamodel.Node), nil } - return idx, &_node{schemaType: w.schemaType.ValueType(), val: val}, nil + return idx, &_node{cfg: w.cfg, schemaType: w.schemaType.ValueType(), val: val}, nil } func (w *_listIterator) Done() bool { @@ -1189,6 +1259,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 @@ -1209,6 +1280,7 @@ func (w *_unionIterator) Next() (key, value datamodel.Node, _ error) { mtyp := w.members[haveIdx] node := &_node{ + cfg: w.cfg, schemaType: mtyp, val: mval, } diff --git a/node/bindnode/repr.go b/node/bindnode/repr.go index 02b2f058..22eef924 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 From 872976a6c923d1368b9409ff570f8ffc833027cb Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 13 May 2022 17:12:33 +1000 Subject: [PATCH 02/13] feat(bindnode): switch to converter functions instead of type --- node/bindnode/api.go | 82 +++++++++++++++++++++--------------- node/bindnode/custom_test.go | 68 ++++++++++-------------------- node/bindnode/infer.go | 6 +-- node/bindnode/node.go | 32 ++++++++------ 4 files changed, 93 insertions(+), 95 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index 87618efd..3a09fa35 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -59,51 +59,65 @@ func Prototype(ptrType interface{}, schemaType schema.Type, options ...Option) s return &_prototype{cfg: cfg, schemaType: schemaType, goType: goType} } -// CustomTypeConverter is an empty interface intended as a parent for the -// various type converters -type CustomTypeConverter interface { +type converter struct { + kindType reflect.Type + fromFunc reflect.Value // func(Kind) (*Custom, error) + toFunc reflect.Value // func(*Custom) (Kind, error) } -// CustomTypeBytesConverter is able to convert byte slices to and from a -// specific type that can't otherwise be handled by bindnode. Such a type will -// likely not be instantiated by plain reflection and therefore need custom -// logic to decode and/or instantiate. -type CustomTypeBytesConverter interface { - FromBytes([]byte) (interface{}, error) - ToBytes(interface{}) ([]byte, error) -} - -type config struct { - customConverters map[reflect.Type]CustomTypeConverter -} +type config map[reflect.Type]converter // Option is able to apply custom options to the bindnode API -type Option func(*config) - -// AddCustomTypeBytesConverter adds a CustomTypeConverter for a particular -// type as referenced by ptrType. The CustomTypeConverter must be able to -// handle that specific type, and the data model kind it's converting must be -// present at the schema location it's encountered. -func AddCustomTypeBytesConverter(ptrType interface{}, converter CustomTypeBytesConverter) Option { - val := reflect.ValueOf(ptrType) - if val.Kind() == reflect.Ptr { - val = val.Elem() +type Option func(config) + +var byteSliceType = reflect.TypeOf([]byte{}) +var errorType = reflect.TypeOf((*error)(nil)).Elem() + +// AddCustomTypeBytesConverter adds custom converter functions for a particular +// type. The fromBytesFunc is of the form: func([]byte) (interface{}, error) +// and toBytesFunc is of the form: func(interface{}) ([]byte, error) +// where interface{} is a pointer form of the type we are converting. +// +// AddCustomTypeBytesConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func AddCustomTypeBytesConverter(fromBytesFunc interface{}, toBytesFunc interface{}) Option { + fbfVal := reflect.ValueOf(fromBytesFunc) + fbfType := fbfVal.Type() + if fbfType == nil || fbfType.Kind() != reflect.Func || fbfType.IsVariadic() { + panic("fromBytesFunc must be a (non-varadic) function") } - name := val.Type().Name() - if name == "" { - panic("not a named type") + ni, no := fbfType.NumIn(), fbfType.NumOut() + if ni != 1 || no != 2 || fbfType.In(0) != byteSliceType || fbfType.Out(1) != errorType { + panic("fromBytesFunc must be of the form func([]byte) (interface{}, error)") } - return func(cfg *config) { - cfg.customConverters[val.Type()] = converter + + tbfVal := reflect.ValueOf(toBytesFunc) + tbfType := tbfVal.Type() + if tbfType == nil || tbfType.Kind() != reflect.Func || tbfType.IsVariadic() { + panic("toBytesFunc must be a (non-varadic) function") + } + ni, no = tbfType.NumIn(), tbfType.NumOut() + if ni != 1 || no != 2 || tbfType.Out(0) != byteSliceType || tbfType.Out(1) != errorType { + panic("toBytesFunc must be of the form func(interface{}) ([]byte, error)") + } + + if fbfType.Out(0) != tbfType.In(0) { + panic("toBytesFunc must be of the form func(interface{}) ([]byte, error)") + } + + customType := fbfType.Out(0) + if customType.Kind() == reflect.Ptr { + customType = customType.Elem() + } + return func(cfg config) { + cfg[customType] = converter{byteSliceType, fbfVal, tbfVal} } } func applyOptions(opt ...Option) config { - cfg := config{ - customConverters: make(map[reflect.Type]CustomTypeConverter), - } + cfg := make(map[reflect.Type]converter) for _, o := range opt { - o(&cfg) + o(cfg) } return cfg } diff --git a/node/bindnode/custom_test.go b/node/bindnode/custom_test.go index ee4ea96d..e63f5ef3 100644 --- a/node/bindnode/custom_test.go +++ b/node/bindnode/custom_test.go @@ -2,14 +2,12 @@ package bindnode_test import ( "bytes" - "fmt" "math/big" "testing" "github.com/google/go-cmp/cmp" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagjson" - "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/node/bindnode" qt "github.com/frankban/quicktest" @@ -31,14 +29,14 @@ func (b Boop) String() string { } // similar to go-state-types/big/Int -type Blop struct{ *big.Int } +type Frop struct{ *big.Int } -func NewBlopFromString(str string) Blop { +func NewFropFromString(str string) Frop { v, _ := big.NewInt(0).SetString(str, 10) - return Blop{v} + return Frop{v} } -func NewBlopFromBytes(buf []byte) Blop { +func NewFropFromBytes(buf []byte) *Frop { var negative bool switch buf[0] { case 0: @@ -54,10 +52,10 @@ func NewBlopFromBytes(buf []byte) Blop { i.Neg(i) } - return Blop{i} + return &Frop{i} } -func (b *Blop) Bytes() []byte { +func (b *Frop) Bytes() []byte { switch { case b.Sign() > 0: return append([]byte{0}, b.Int.Bytes()...) @@ -72,7 +70,7 @@ type Boom struct { S string B Boop Bptr *Boop - BI Blop + F Frop I int } @@ -81,72 +79,50 @@ type Boom struct { S String B Bytes Bptr nullable Bytes - BI Bytes + F Bytes I Int } representation map ` -const boomFixtureDagJson = `{"B":{"/":{"bytes":"dGhlc2UgYXJlIGJ5dGVz"}},"BI":{"/":{"bytes":"AAH3fubjrGlwOMpClAkh/ro13L5Uls4/CtI"}},"Bptr":{"/":{"bytes":"dGhlc2UgYXJlIHBvaW50ZXIgYnl0ZXM"}},"I":10101,"S":"a string here"}` +const boomFixtureDagJson = `{"B":{"/":{"bytes":"dGhlc2UgYXJlIGJ5dGVz"}},"Bptr":{"/":{"bytes":"dGhlc2UgYXJlIHBvaW50ZXIgYnl0ZXM"}},"F":{"/":{"bytes":"AAH3fubjrGlwOMpClAkh/ro13L5Uls4/CtI"}},"I":10101,"S":"a string here"}` var boomFixtureInstance = Boom{ S: "a string here", B: *NewBoop([]byte("these are bytes")), - BI: NewBlopFromString("12345678901234567891234567890123456789012345678901234567890"), Bptr: NewBoop([]byte("these are pointer bytes")), + F: NewFropFromString("12345678901234567891234567890123456789012345678901234567890"), I: 10101, } -type BoopConverter struct { -} - -func (bc BoopConverter) FromBytes(b []byte) (interface{}, error) { +func BoopFromBytes(b []byte) (*Boop, error) { return NewBoop(b), nil } -func (bc BoopConverter) ToBytes(typ interface{}) ([]byte, error) { - if boop, ok := typ.(*Boop); ok { - return boop.Bytes(), nil - } - return nil, fmt.Errorf("did not get a Boop type") -} - -type BlopConverter struct { +func BoopToBytes(boop *Boop) ([]byte, error) { + return boop.Bytes(), nil } -func (bc BlopConverter) FromBytes(b []byte) (interface{}, error) { - return NewBlopFromBytes(b), nil +func FropFromBytes(b []byte) (*Frop, error) { + return NewFropFromBytes(b), nil } -func (bc BlopConverter) ToBytes(typ interface{}) ([]byte, error) { - if blop, ok := typ.(*Blop); ok { - return blop.Bytes(), nil - } - return nil, fmt.Errorf("did not get a Blop type") +func FropToBytes(frop *Frop) ([]byte, error) { + return frop.Bytes(), nil } -var ( - _ bindnode.CustomTypeBytesConverter = (*BoopConverter)(nil) - _ bindnode.CustomTypeBytesConverter = (*BlopConverter)(nil) -) - func TestCustom(t *testing.T) { opts := []bindnode.Option{ - bindnode.AddCustomTypeBytesConverter(Boop{}, BoopConverter{}), - bindnode.AddCustomTypeBytesConverter(Blop{}, BlopConverter{}), + bindnode.AddCustomTypeBytesConverter(BoopFromBytes, BoopToBytes), + bindnode.AddCustomTypeBytesConverter(FropFromBytes, FropToBytes), } - nb := basicnode.Prototype.Any.NewBuilder() - err := dagjson.Decode(nb, bytes.NewReader([]byte(boomFixtureDagJson))) - qt.Assert(t, err, qt.IsNil) - typeSystem, err := ipld.LoadSchemaBytes([]byte(boomSchema)) qt.Assert(t, err, qt.IsNil) schemaType := typeSystem.TypeByName("Boom") proto := bindnode.Prototype(&Boom{}, schemaType, opts...) - node := nb.Build() builder := proto.Representation().NewBuilder() - err = builder.AssignNode(node) + err = dagjson.Decode(builder, bytes.NewReader([]byte(boomFixtureDagJson))) qt.Assert(t, err, qt.IsNil) typ := bindnode.Unwrap(builder.Build()) @@ -155,11 +131,11 @@ func TestCustom(t *testing.T) { cmpr := qt.CmpEquals( cmp.Comparer(func(x, y Boop) bool { return x.String() == y.String() }), - cmp.Comparer(func(x, y Blop) bool { return x.String() == y.String() }), + cmp.Comparer(func(x, y Frop) bool { return x.String() == y.String() }), ) qt.Assert(t, *inst, cmpr, boomFixtureInstance) - tn := bindnode.Wrap(&boomFixtureInstance, schemaType, opts...) + tn := bindnode.Wrap(inst, schemaType, opts...) var buf bytes.Buffer err = dagjson.Encode(tn.Representation(), &buf) qt.Assert(t, err, qt.IsNil) diff --git a/node/bindnode/infer.go b/node/bindnode/infer.go index 369ea066..1c83f12a 100644 --- a/node/bindnode/infer.go +++ b/node/bindnode/infer.go @@ -86,10 +86,10 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } case *schema.TypeBytes: // TODO: allow string? - customConverter, ok := cfg.customConverters[goType] + customConverter, ok := cfg[goType] if ok { - if _, ok := customConverter.(CustomTypeBytesConverter); !ok { - doPanic("kind mismatch; custom converter for type is not a CustomTypeBytesConverter") + if customConverter.kindType != byteSliceType { + doPanic("kind mismatch; custom converter for type is not for Bytes") } } else { if goType.Kind() != reflect.Slice { diff --git a/node/bindnode/node.go b/node/bindnode/node.go index 35c4679a..9e958e8c 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -447,13 +447,21 @@ func (w *_node) AsBytes() ([]byte, error) { if w.val.Kind() == reflect.Ptr { typ = typ.Elem() } - customConverter, ok := w.cfg.customConverters[typ] + customConverter, ok := w.cfg[typ] if ok { - cbc, ok := customConverter.(CustomTypeBytesConverter) + res := customConverter.toFunc.Call([]reflect.Value{w.val.Addr()}) + if !res[1].IsNil() { + err, ok := res[1].Interface().(error) + if !ok { + return nil, fmt.Errorf("converter function did not return expected error: %v", res[1].Interface()) + } + return nil, err + } + byts, ok := res[0].Interface().([]byte) if !ok { - panic("kind mismatch; custom converter for type is not a CustomTypeBytesConverter") + return nil, fmt.Errorf("converter function did not return expected []byte") } - return cbc.ToBytes(w.val.Addr().Interface()) + return byts, nil } return nonPtrVal(w.val).Bytes(), nil } @@ -746,17 +754,17 @@ func (w *_assembler) AssignBytes(p []byte) error { if w.val.Kind() == reflect.Ptr { typ = typ.Elem() } - customConverter, ok := w.cfg.customConverters[typ] + customConverter, ok := w.cfg[typ] if ok { - cbc, ok := customConverter.(CustomTypeBytesConverter) - if !ok { - panic("kind mismatch; custom converter for type is not a CustomTypeBytesConverter") - } - val, err := cbc.FromBytes(p) - if err != nil { + res := customConverter.fromFunc.Call([]reflect.Value{reflect.ValueOf(p)}) + if !res[1].IsNil() { + err, ok := res[1].Interface().(error) + if !ok { + return fmt.Errorf("converter function did not return expected error: %v", res[1].Interface()) + } return err } - rval := reflect.ValueOf(val) + rval := res[0] if w.val.Kind() == reflect.Ptr && rval.Kind() != reflect.Ptr { rval = rval.Addr() } else if w.val.Kind() != reflect.Ptr && rval.Kind() == reflect.Ptr { From d6f9afcfac30632e53f35523cbef31678bb11085 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Mon, 16 May 2022 18:25:25 +1000 Subject: [PATCH 03/13] chore(bindnode): back out of reflection for converters --- node/bindnode/api.go | 139 ++++++++++++++++++++++++----------- node/bindnode/custom_test.go | 23 ++++-- node/bindnode/infer.go | 2 +- node/bindnode/node.go | 37 ++++------ 4 files changed, 126 insertions(+), 75 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index 3a09fa35..a42f91a9 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" ) @@ -59,10 +60,47 @@ func Prototype(ptrType interface{}, schemaType schema.Type, options ...Option) s return &_prototype{cfg: cfg, schemaType: schemaType, goType: goType} } +// scalar kinds excluding Null + +type CustomBool struct { + From func(bool) (interface{}, error) + To func(interface{}) (bool, error) +} + +type CustomInt struct { + From func(int64) (interface{}, error) + To func(interface{}) (int64, error) +} + +type CustomFloat struct { + From func(float64) (interface{}, error) + To func(interface{}) (float64, error) +} + +type CustomString struct { + From func(string) (interface{}, error) + To func(interface{}) (string, error) +} + +type CustomBytes struct { + From func([]byte) (interface{}, error) + To func(interface{}) ([]byte, error) +} + +type CustomLink struct { + From func(cid.Cid) (interface{}, error) + To func(interface{}) (cid.Cid, error) +} + type converter struct { - kindType reflect.Type - fromFunc reflect.Value // func(Kind) (*Custom, error) - toFunc reflect.Value // func(*Custom) (Kind, error) + kind datamodel.Kind + + customBool *CustomBool + customInt *CustomInt + customFloat *CustomFloat + customString *CustomString + customBytes *CustomBytes + customLink *CustomLink } type config map[reflect.Type]converter @@ -70,47 +108,64 @@ type config map[reflect.Type]converter // Option is able to apply custom options to the bindnode API type Option func(config) -var byteSliceType = reflect.TypeOf([]byte{}) -var errorType = reflect.TypeOf((*error)(nil)).Elem() - -// AddCustomTypeBytesConverter adds custom converter functions for a particular -// type. The fromBytesFunc is of the form: func([]byte) (interface{}, error) -// and toBytesFunc is of the form: func(interface{}) ([]byte, error) -// where interface{} is a pointer form of the type we are converting. +// AddCustomTypeConverter adds custom converter functions for a particular +// type as identified by a pointer in the first argument. +// The fromFunc is of the form: func(kind) (interface{}, error) +// and toFunc is of the form: func(interface{}) (kind, error) +// where interface{} is a pointer form of the type we are converting and "kind" +// is a Go form of the kind being converted (bool, int64, float64, string, +// []byte, cid.Cid). // -// AddCustomTypeBytesConverter is an EXPERIMENTAL API and may be removed or +// AddCustomTypeConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func AddCustomTypeBytesConverter(fromBytesFunc interface{}, toBytesFunc interface{}) Option { - fbfVal := reflect.ValueOf(fromBytesFunc) - fbfType := fbfVal.Type() - if fbfType == nil || fbfType.Kind() != reflect.Func || fbfType.IsVariadic() { - panic("fromBytesFunc must be a (non-varadic) function") - } - ni, no := fbfType.NumIn(), fbfType.NumOut() - if ni != 1 || no != 2 || fbfType.In(0) != byteSliceType || fbfType.Out(1) != errorType { - panic("fromBytesFunc must be of the form func([]byte) (interface{}, error)") - } - - tbfVal := reflect.ValueOf(toBytesFunc) - tbfType := tbfVal.Type() - if tbfType == nil || tbfType.Kind() != reflect.Func || tbfType.IsVariadic() { - panic("toBytesFunc must be a (non-varadic) function") - } - ni, no = tbfType.NumIn(), tbfType.NumOut() - if ni != 1 || no != 2 || tbfType.Out(0) != byteSliceType || tbfType.Out(1) != errorType { - panic("toBytesFunc must be of the form func(interface{}) ([]byte, error)") - } - - if fbfType.Out(0) != tbfType.In(0) { - panic("toBytesFunc must be of the form func(interface{}) ([]byte, error)") - } - - customType := fbfType.Out(0) - if customType.Kind() == reflect.Ptr { - customType = customType.Elem() - } - return func(cfg config) { - cfg[customType] = converter{byteSliceType, fbfVal, tbfVal} +func AddCustomTypeConverter(ptrValue interface{}, customConverter interface{}) Option { + customType := reflect.ValueOf(ptrValue).Elem().Type() + + switch typedCustomConverter := customConverter.(type) { + case CustomBool: + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_Bool, + customBool: &typedCustomConverter, + } + } + case CustomInt: + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_Int, + customInt: &typedCustomConverter, + } + } + case CustomFloat: + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_Float, + customFloat: &typedCustomConverter, + } + } + case CustomString: + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_String, + customString: &typedCustomConverter, + } + } + case CustomBytes: + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_Bytes, + customBytes: &typedCustomConverter, + } + } + case CustomLink: + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_Link, + customLink: &typedCustomConverter, + } + } + default: + panic("bindnode: fromFunc for Link must match one of the CustomFromX types") } } diff --git a/node/bindnode/custom_test.go b/node/bindnode/custom_test.go index e63f5ef3..6facd9fe 100644 --- a/node/bindnode/custom_test.go +++ b/node/bindnode/custom_test.go @@ -2,6 +2,7 @@ package bindnode_test import ( "bytes" + "fmt" "math/big" "testing" @@ -94,26 +95,32 @@ var boomFixtureInstance = Boom{ I: 10101, } -func BoopFromBytes(b []byte) (*Boop, error) { +func BoopFromBytes(b []byte) (interface{}, error) { return NewBoop(b), nil } -func BoopToBytes(boop *Boop) ([]byte, error) { - return boop.Bytes(), 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) (*Frop, error) { +func FropFromBytes(b []byte) (interface{}, error) { return NewFropFromBytes(b), nil } -func FropToBytes(frop *Frop) ([]byte, error) { - return frop.Bytes(), 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") } func TestCustom(t *testing.T) { opts := []bindnode.Option{ - bindnode.AddCustomTypeBytesConverter(BoopFromBytes, BoopToBytes), - bindnode.AddCustomTypeBytesConverter(FropFromBytes, FropToBytes), + bindnode.AddCustomTypeConverter(&Boop{}, bindnode.CustomBytes{From: BoopFromBytes, To: BoopToBytes}), + bindnode.AddCustomTypeConverter(&Frop{}, bindnode.CustomBytes{From: FropFromBytes, To: FropToBytes}), } typeSystem, err := ipld.LoadSchemaBytes([]byte(boomSchema)) diff --git a/node/bindnode/infer.go b/node/bindnode/infer.go index 1c83f12a..20105cdb 100644 --- a/node/bindnode/infer.go +++ b/node/bindnode/infer.go @@ -88,7 +88,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ // TODO: allow string? customConverter, ok := cfg[goType] if ok { - if customConverter.kindType != byteSliceType { + if customConverter.kind != datamodel.Kind_Bytes { doPanic("kind mismatch; custom converter for type is not for Bytes") } } else { diff --git a/node/bindnode/node.go b/node/bindnode/node.go index 9e958e8c..a0bce495 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -134,6 +134,13 @@ 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 (w *_node) LookupByString(key string) (datamodel.Node, error) { switch typ := w.schemaType.(type) { case *schema.TypeStruct: @@ -447,21 +454,8 @@ func (w *_node) AsBytes() ([]byte, error) { if w.val.Kind() == reflect.Ptr { typ = typ.Elem() } - customConverter, ok := w.cfg[typ] - if ok { - res := customConverter.toFunc.Call([]reflect.Value{w.val.Addr()}) - if !res[1].IsNil() { - err, ok := res[1].Interface().(error) - if !ok { - return nil, fmt.Errorf("converter function did not return expected error: %v", res[1].Interface()) - } - return nil, err - } - byts, ok := res[0].Interface().([]byte) - if !ok { - return nil, fmt.Errorf("converter function did not return expected []byte") - } - return byts, nil + if customConverter, ok := w.cfg[typ]; ok { + return customConverter.customBytes.To(ptrVal(w.val).Interface()) } return nonPtrVal(w.val).Bytes(), nil } @@ -754,17 +748,12 @@ func (w *_assembler) AssignBytes(p []byte) error { if w.val.Kind() == reflect.Ptr { typ = typ.Elem() } - customConverter, ok := w.cfg[typ] - if ok { - res := customConverter.fromFunc.Call([]reflect.Value{reflect.ValueOf(p)}) - if !res[1].IsNil() { - err, ok := res[1].Interface().(error) - if !ok { - return fmt.Errorf("converter function did not return expected error: %v", res[1].Interface()) - } + if customConverter, ok := w.cfg[typ]; ok { + typ, err := customConverter.customBytes.From(p) + if err != nil { return err } - rval := res[0] + rval := reflect.ValueOf(typ) if w.val.Kind() == reflect.Ptr && rval.Kind() != reflect.Ptr { rval = rval.Addr() } else if w.val.Kind() != reflect.Ptr && rval.Kind() == reflect.Ptr { From d64b833829810300fb976dce739184ac83b8e1db Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Mon, 16 May 2022 21:40:20 +1000 Subject: [PATCH 04/13] feat(bindnode): add AddCustomTypeXConverter() options for most scalar kinds also adds tests, and typed functions that must be used in the interface --- node/bindnode/api.go | 244 +++++++++++++++++++++++------------ node/bindnode/custom_test.go | 213 ++++++++++++++++++++++++++---- node/bindnode/infer.go | 52 +++++--- node/bindnode/node.go | 98 +++++++++++--- 4 files changed, 464 insertions(+), 143 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index a42f91a9..8ca74df2 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -62,45 +62,74 @@ func Prototype(ptrType interface{}, schemaType schema.Type, options ...Option) s // scalar kinds excluding Null -type CustomBool struct { - From func(bool) (interface{}, error) - To func(interface{}) (bool, error) -} +// CustomFromBool is a custom converter function that takes a bool and returns a +// custom type +type CustomFromBool func(bool) (interface{}, error) -type CustomInt struct { - From func(int64) (interface{}, error) - To func(interface{}) (int64, error) -} +// CustomToBool is a custom converter function that takes a custom type and +// returns a bool +type CustomToBool func(interface{}) (bool, error) -type CustomFloat struct { - From func(float64) (interface{}, error) - To func(interface{}) (float64, error) -} +// CustomFromInt is a custom converter function that takes an int and returns a +// custom type +type CustomFromInt func(int64) (interface{}, error) -type CustomString struct { - From func(string) (interface{}, error) - To func(interface{}) (string, error) -} +// CustomToInt is a custom converter function that takes a custom type and +// returns an int +type CustomToInt func(interface{}) (int64, error) -type CustomBytes struct { - From func([]byte) (interface{}, error) - To func(interface{}) ([]byte, error) -} +// CustomFromFloat is a custom converter function that takes a float and returns +// a custom type +type CustomFromFloat func(float64) (interface{}, error) -type CustomLink struct { - From func(cid.Cid) (interface{}, error) - To func(interface{}) (cid.Cid, error) -} +// CustomToFloat is a custom converter function that takes a custom type and +// returns a float +type CustomToFloat func(interface{}) (float64, error) + +// CustomFromString is a custom converter function that takes a string and +// returns custom type +type CustomFromString func(string) (interface{}, error) + +// CustomToString is a custom converter function that takes a custom type and +// returns a string +type CustomToString func(interface{}) (string, error) + +// CustomFromBytes is a custom converter function that takes a byte slice and +// returns a custom type +type CustomFromBytes func([]byte) (interface{}, error) + +// CustomToBytes is a custom converter function that takes a custom type and +// returns a byte slice +type CustomToBytes func(interface{}) ([]byte, error) + +// CustomFromLink is a custom converter function that takes a cid.Cid and +// returns a custom type +type CustomFromLink func(cid.Cid) (interface{}, error) + +// CustomToLink is a custom converter function that takes a custom type and +// returns a cid.Cid +type CustomToLink func(interface{}) (cid.Cid, error) type converter struct { kind datamodel.Kind - customBool *CustomBool - customInt *CustomInt - customFloat *CustomFloat - customString *CustomString - customBytes *CustomBytes - customLink *CustomLink + customFromBool CustomFromBool + customToBool CustomToBool + + customFromInt CustomFromInt + customToInt CustomToInt + + customFromFloat CustomFromFloat + customToFloat CustomToFloat + + customFromString CustomFromString + customToString CustomToString + + customFromBytes CustomFromBytes + customToBytes CustomToBytes + + customFromLink CustomFromLink + customToLink CustomToLink } type config map[reflect.Type]converter @@ -108,64 +137,121 @@ type config map[reflect.Type]converter // Option is able to apply custom options to the bindnode API type Option func(config) -// AddCustomTypeConverter adds custom converter functions for a particular +// AddCustomTypeBoolConverter adds custom converter functions for a particular // type as identified by a pointer in the first argument. -// The fromFunc is of the form: func(kind) (interface{}, error) -// and toFunc is of the form: func(interface{}) (kind, error) -// where interface{} is a pointer form of the type we are converting and "kind" -// is a Go form of the kind being converted (bool, int64, float64, string, -// []byte, cid.Cid). +// 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. // -// AddCustomTypeConverter is an EXPERIMENTAL API and may be removed or +// AddCustomTypeBoolConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func AddCustomTypeConverter(ptrValue interface{}, customConverter interface{}) Option { - customType := reflect.ValueOf(ptrValue).Elem().Type() - - switch typedCustomConverter := customConverter.(type) { - case CustomBool: - return func(cfg config) { - cfg[customType] = converter{ - kind: datamodel.Kind_Bool, - customBool: &typedCustomConverter, - } +func AddCustomTypeBoolConverter(ptrVal interface{}, from CustomFromBool, to CustomToBool) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_Bool, + customFromBool: from, + customToBool: to, } - case CustomInt: - return func(cfg config) { - cfg[customType] = converter{ - kind: datamodel.Kind_Int, - customInt: &typedCustomConverter, - } + } +} + +// AddCustomTypeIntConverter 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. +// +// AddCustomTypeIntConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func AddCustomTypeIntConverter(ptrVal interface{}, from CustomFromInt, to CustomToInt) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_Int, + customFromInt: from, + customToInt: to, } - case CustomFloat: - return func(cfg config) { - cfg[customType] = converter{ - kind: datamodel.Kind_Float, - customFloat: &typedCustomConverter, - } + } +} + +// AddCustomTypeFloatConverter 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. +// +// AddCustomTypeFloatConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func AddCustomTypeFloatConverter(ptrVal interface{}, from CustomFromFloat, to CustomToFloat) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_Float, + customFromFloat: from, + customToFloat: to, } - case CustomString: - return func(cfg config) { - cfg[customType] = converter{ - kind: datamodel.Kind_String, - customString: &typedCustomConverter, - } + } +} + +// AddCustomTypeStringConverter 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. +// +// AddCustomTypeStringConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func AddCustomTypeStringConverter(ptrVal interface{}, from CustomFromString, to CustomToString) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_String, + customFromString: from, + customToString: to, } - case CustomBytes: - return func(cfg config) { - cfg[customType] = converter{ - kind: datamodel.Kind_Bytes, - customBytes: &typedCustomConverter, - } + } +} + +// AddCustomTypeBytesConverter 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. +// +// AddCustomTypeBytesConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func AddCustomTypeBytesConverter(ptrVal interface{}, from CustomFromBytes, to CustomToBytes) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_Bytes, + customFromBytes: from, + customToBytes: to, } - case CustomLink: - return func(cfg config) { - cfg[customType] = converter{ - kind: datamodel.Kind_Link, - customLink: &typedCustomConverter, - } + } +} + +// AddCustomTypeLinkConverter 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. +// +// AddCustomTypeLinkConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func AddCustomTypeLinkConverter(ptrVal interface{}, from CustomFromLink, to CustomToLink) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + return func(cfg config) { + cfg[customType] = converter{ + kind: datamodel.Kind_Link, + customFromLink: from, + customToLink: to, } - default: - panic("bindnode: fromFunc for Link must match one of the CustomFromX types") } } diff --git a/node/bindnode/custom_test.go b/node/bindnode/custom_test.go index 6facd9fe..015782e2 100644 --- a/node/bindnode/custom_test.go +++ b/node/bindnode/custom_test.go @@ -2,18 +2,119 @@ 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/dagjson" "github.com/ipld/go-ipld-prime/node/bindnode" + "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 } @@ -67,60 +168,119 @@ func (b *Frop) Bytes() []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"}},"Bptr":{"/":{"bytes":"dGhlc2UgYXJlIHBvaW50ZXIgYnl0ZXM"}},"F":{"/":{"bytes":"AAH3fubjrGlwOMpClAkh/ro13L5Uls4/CtI"}},"I":10101,"S":"a string here"}` +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{ - S: "a string here", 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, -} - -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") + 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.AddCustomTypeConverter(&Boop{}, bindnode.CustomBytes{From: BoopFromBytes, To: BoopToBytes}), - bindnode.AddCustomTypeConverter(&Frop{}, bindnode.CustomBytes{From: FropFromBytes, To: FropToBytes}), + bindnode.AddCustomTypeBytesConverter(&Boop{}, BoopFromBytes, BoopToBytes), + bindnode.AddCustomTypeBytesConverter(&Frop{}, FropFromBytes, FropToBytes), + bindnode.AddCustomTypeBoolConverter(BoolSubst(0), BoolSubstFromBool, BoolToBoolSubst), + bindnode.AddCustomTypeIntConverter(IntSubst(""), IntSubstFromInt, IntToIntSubst), + bindnode.AddCustomTypeFloatConverter(&BigFloat{}, BigFloatFromFloat, FloatFromBigFloat), + bindnode.AddCustomTypeStringConverter(&ByteArray{}, ByteArrayFromString, StringFromByteArray), + bindnode.AddCustomTypeLinkConverter(BtcId(""), FromCidToBtcId, FromBtcIdToCid), } typeSystem, err := ipld.LoadSchemaBytes([]byte(boomSchema)) @@ -139,6 +299,7 @@ func TestCustom(t *testing.T) { 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) diff --git a/node/bindnode/infer.go b/node/bindnode/infer.go index 20105cdb..c57e7537 100644 --- a/node/bindnode/infer.go +++ b/node/bindnode/infer.go @@ -66,38 +66,52 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } switch schemaType := schemaType.(type) { case *schema.TypeBool: - if goType.Kind() != reflect.Bool { + if customConverter, ok := cfg[goType]; ok { + if customConverter.kind != datamodel.Kind_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, ok := cfg[goType]; ok { + if customConverter.kind != datamodel.Kind_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, ok := cfg[goType]; ok { + if customConverter.kind != datamodel.Kind_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, ok := cfg[goType]; ok { + if customConverter.kind != datamodel.Kind_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? - customConverter, ok := cfg[goType] - if ok { + if customConverter, ok := cfg[goType]; ok { if customConverter.kind != datamodel.Kind_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 { - doPanic("kind mismatch; need slice of bytes") - } + } else if goType.Kind() != reflect.Slice { + doPanic("kind mismatch; need slice of bytes") + } else if goType.Elem().Kind() != reflect.Uint8 { + doPanic("kind mismatch; need slice of bytes") } case *schema.TypeEnum: if _, ok := schemaType.RepresentationStrategy().(schema.EnumRepresentation_Int); ok { @@ -216,7 +230,11 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ verifyCompatibility(cfg, seen, goType, schemaType) } case *schema.TypeLink: - if goType != goTypeLink && goType != goTypeCidLink && goType != goTypeCid { + if customConverter, ok := cfg[goType]; ok { + if customConverter.kind != datamodel.Kind_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: diff --git a/node/bindnode/node.go b/node/bindnode/node.go index a0bce495..64e50a54 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -141,6 +141,22 @@ func ptrVal(val reflect.Value) reflect.Value { return val.Addr() } +func nonPtrType(val reflect.Value) reflect.Type { + typ := val.Type() + if typ.Kind() == reflect.Ptr { + return typ.Elem() + } + return typ +} + +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: @@ -416,6 +432,9 @@ func (w *_node) AsBool() (bool, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Bool); err != nil { return false, err } + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + return customConverter.customToBool(ptrVal(w.val).Interface()) + } return nonPtrVal(w.val).Bool(), nil } @@ -423,6 +442,9 @@ func (w *_node) AsInt() (int64, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Int); err != nil { return 0, err } + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + return customConverter.customToInt(ptrVal(w.val).Interface()) + } val := nonPtrVal(w.val) if kindUint[val.Kind()] { // TODO: check for overflow @@ -435,6 +457,9 @@ func (w *_node) AsFloat() (float64, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Float); err != nil { return 0, err } + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + return customConverter.customToFloat(ptrVal(w.val).Interface()) + } return nonPtrVal(w.val).Float(), nil } @@ -442,6 +467,9 @@ func (w *_node) AsString() (string, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_String); err != nil { return "", err } + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + return customConverter.customToString(ptrVal(w.val).Interface()) + } return nonPtrVal(w.val).String(), nil } @@ -449,13 +477,8 @@ func (w *_node) AsBytes() ([]byte, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Bytes); err != nil { return nil, err } - - typ := w.val.Type() - if w.val.Kind() == reflect.Ptr { - typ = typ.Elem() - } - if customConverter, ok := w.cfg[typ]; ok { - return customConverter.customBytes.To(ptrVal(w.val).Interface()) + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + return customConverter.customToBytes(ptrVal(w.val).Interface()) } return nonPtrVal(w.val).Bytes(), nil } @@ -464,6 +487,13 @@ func (w *_node) AsLink() (datamodel.Link, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Link); err != nil { return nil, err } + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + 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 @@ -668,6 +698,12 @@ func (w *_assembler) AssignBool(b bool) error { } if _, ok := w.schemaType.(*schema.TypeAny); ok { w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewBool(b))) + } else if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + typ, err := customConverter.customFromBool(b) + if err != nil { + return err + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { w.createNonPtrVal().SetBool(b) } @@ -686,6 +722,12 @@ func (w *_assembler) AssignInt(i int64) error { // TODO: check for overflow if _, ok := w.schemaType.(*schema.TypeAny); ok { w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewInt(i))) + } else if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + typ, err := customConverter.customFromInt(i) + if err != nil { + return err + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else if kindUint[w.val.Kind()] { if i < 0 { // TODO: write a test @@ -709,6 +751,12 @@ func (w *_assembler) AssignFloat(f float64) error { } if _, ok := w.schemaType.(*schema.TypeAny); ok { w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewFloat(f))) + } else if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + typ, err := customConverter.customFromFloat(f) + if err != nil { + return err + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { w.createNonPtrVal().SetFloat(f) } @@ -727,7 +775,15 @@ func (w *_assembler) AssignString(s string) error { if _, ok := w.schemaType.(*schema.TypeAny); ok { w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewString(s))) } else { - w.createNonPtrVal().SetString(s) + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + typ, err := customConverter.customFromString(s) + if err != nil { + return err + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) + } else { + w.createNonPtrVal().SetString(s) + } } if w.finish != nil { if err := w.finish(); err != nil { @@ -744,22 +800,12 @@ func (w *_assembler) AssignBytes(p []byte) error { if _, ok := w.schemaType.(*schema.TypeAny); ok { w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewBytes(p))) } else { - typ := w.val.Type() - if w.val.Kind() == reflect.Ptr { - typ = typ.Elem() - } - if customConverter, ok := w.cfg[typ]; ok { - typ, err := customConverter.customBytes.From(p) + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + typ, err := customConverter.customFromBytes(p) if err != nil { return err } - rval := reflect.ValueOf(typ) - if w.val.Kind() == reflect.Ptr && rval.Kind() != reflect.Ptr { - rval = rval.Addr() - } else if w.val.Kind() != reflect.Ptr && rval.Kind() == reflect.Ptr { - rval = rval.Elem() - } - w.val.Set(rval) + w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { w.createNonPtrVal().SetBytes(p) } @@ -777,6 +823,16 @@ func (w *_assembler) AssignLink(link datamodel.Link) error { // TODO: newVal.Type() panics if link==nil; add a test and fix. if _, ok := w.schemaType.(*schema.TypeAny); ok { val.Set(reflect.ValueOf(basicnode.NewLink(link))) + } else if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if cl, ok := link.(cidlink.Link); ok { + 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) From aad67f80a868d0fc002c1e9af57a861491b6e82e Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 17 May 2022 18:09:07 +1000 Subject: [PATCH 05/13] feat(bindnode): add AddCustomTypeAnyConverter() to handle `Any` fields Can handle maps, lists and all scalar kinds; a receiving Go type that is registered as a CustomTypeAnyConverter will receive a datamodel.Node and is expected to return one when converting to and from that custom Go type. --- node/bindnode/api.go | 47 ++++++-- node/bindnode/custom_test.go | 192 +++++++++++++++++++++++++++++++++ node/bindnode/infer.go | 19 ++-- node/bindnode/node.go | 202 ++++++++++++++++++++++++++--------- 4 files changed, 394 insertions(+), 66 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index 8ca74df2..c2931f0c 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -110,8 +110,16 @@ type CustomFromLink func(cid.Cid) (interface{}, error) // returns a cid.Cid type CustomToLink func(interface{}) (cid.Cid, error) +// CustomFromAny is a custom converter function that takes a datamodel.Node and +// returns a custom type +type CustomFromAny func(datamodel.Node) (interface{}, error) + +// CustomToAny is a custom converter function that takes a custom type and +// returns a datamodel.Node +type CustomToAny func(interface{}) (datamodel.Node, error) + type converter struct { - kind datamodel.Kind + kind schema.TypeKind customFromBool CustomFromBool customToBool CustomToBool @@ -130,6 +138,9 @@ type converter struct { customFromLink CustomFromLink customToLink CustomToLink + + customFromAny CustomFromAny + customToAny CustomToAny } type config map[reflect.Type]converter @@ -149,7 +160,7 @@ func AddCustomTypeBoolConverter(ptrVal interface{}, from CustomFromBool, to Cust customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { cfg[customType] = converter{ - kind: datamodel.Kind_Bool, + kind: schema.TypeKind_Bool, customFromBool: from, customToBool: to, } @@ -168,7 +179,7 @@ func AddCustomTypeIntConverter(ptrVal interface{}, from CustomFromInt, to Custom customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { cfg[customType] = converter{ - kind: datamodel.Kind_Int, + kind: schema.TypeKind_Int, customFromInt: from, customToInt: to, } @@ -187,7 +198,7 @@ func AddCustomTypeFloatConverter(ptrVal interface{}, from CustomFromFloat, to Cu customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { cfg[customType] = converter{ - kind: datamodel.Kind_Float, + kind: schema.TypeKind_Float, customFromFloat: from, customToFloat: to, } @@ -206,7 +217,7 @@ func AddCustomTypeStringConverter(ptrVal interface{}, from CustomFromString, to customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { cfg[customType] = converter{ - kind: datamodel.Kind_String, + kind: schema.TypeKind_String, customFromString: from, customToString: to, } @@ -225,7 +236,7 @@ func AddCustomTypeBytesConverter(ptrVal interface{}, from CustomFromBytes, to Cu customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { cfg[customType] = converter{ - kind: datamodel.Kind_Bytes, + kind: schema.TypeKind_Bytes, customFromBytes: from, customToBytes: to, } @@ -248,13 +259,35 @@ func AddCustomTypeLinkConverter(ptrVal interface{}, from CustomFromLink, to Cust customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { cfg[customType] = converter{ - kind: datamodel.Kind_Link, + kind: schema.TypeKind_Link, customFromLink: from, customToLink: to, } } } +// AddCustomTypeAnyConverter 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. +// +// AddCustomTypeAnyConverter is an EXPERIMENTAL API and may be removed or +// changed in a future release. +func AddCustomTypeAnyConverter(ptrVal interface{}, from CustomFromAny, to CustomToAny) Option { + customType := nonPtrType(reflect.ValueOf(ptrVal)) + return func(cfg config) { + cfg[customType] = converter{ + kind: schema.TypeKind_Any, + customFromAny: from, + customToAny: to, + } + } +} + func applyOptions(opt ...Option) config { cfg := make(map[reflect.Type]converter) for _, o := range opt { diff --git a/node/bindnode/custom_test.go b/node/bindnode/custom_test.go index 015782e2..f0c78d59 100644 --- a/node/bindnode/custom_test.go +++ b/node/bindnode/custom_test.go @@ -11,8 +11,13 @@ import ( "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" @@ -310,3 +315,190 @@ func TestCustom(t *testing.T) { qt.Assert(t, buf.String(), qt.Equals, boomFixtureDagJson) } + +type AnyExtend struct { + Name string + Blob AnyExtendBlob + Count int + Null AnyCborEncoded + Bool AnyCborEncoded + Int AnyCborEncoded + Float AnyCborEncoded + String AnyCborEncoded + Bytes AnyCborEncoded + Link AnyCborEncoded + Map AnyCborEncoded + List AnyCborEncoded +} + +const anyExtendSchema = ` +type AnyExtend struct { + Name String + Blob Any + Count Int + Null nullable Any + Bool Any + Int Any + Float Any + String Any + Bytes Any + Link Any + Map Any + List 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 []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(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)) + if err != nil { + return nil, err + } + return na.Build(), nil +} + +const anyExtendDagJson = `{"Blob":{"baz":[2,3,4],"foo":"bar"},"Bool":false,"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,"String":"this is a string"}` + +var anyExtendFixtureInstance = AnyExtend{ + Name: "Any extend test", + Count: 101, + Blob: AnyExtendBlob{f: "bar", x: 2, y: 3, z: 4}, + Null: AnyCborEncoded(mustFromHex("f6")), // normally this field would be `nil`, but we now get to decide whether it should be something concrete + Bool: AnyCborEncoded(mustFromHex("f4")), + Int: AnyCborEncoded(mustFromHex("1a075bcd15")), // cbor encoded form of 123456789 + Float: AnyCborEncoded(mustFromHex("fb4002b851eb851eb8")), // cbor encoded form of 2.34 + String: AnyCborEncoded(mustFromHex("7074686973206973206120737472696e67")), // cbor encoded form of "this is a string" + Bytes: AnyCborEncoded(mustFromHex("4702030405060708")), // cbor encoded form of [2,3,4,5,6,7,8] + Link: AnyCborEncoded(mustFromHex("d82a58260001b0015620d13d09e8ae38120a5c4475fbd9ecc41cb1003f4f3b2bf86a0000000000000000")), // dag-cbor encoded CID bagyacvra2e6qt2fohajauxceox55t3gedsyqap2phmv7q2qaaaaaaaaaaaaa + Map: AnyCborEncoded(mustFromHex("a463666f6f63626172636f6e65016374776f0265746872656503")), // cbor encoded form of {"one":1,"two":2,"three":3,"foo":"bar"} + List: AnyCborEncoded(mustFromHex("88f6636f6e656374776f657468726565010203f5")), // cbor encoded form of [null,'one','two','three',1,2,3,true] +} + +func TestCustomAny(t *testing.T) { + opts := []bindnode.Option{ + bindnode.AddCustomTypeAnyConverter(&AnyExtendBlob{}, AnyExtendBlobFromNode, AnyExtendBlobToNode), + bindnode.AddCustomTypeAnyConverter(&AnyCborEncoded{}, AnyCborEncodedFromNode, AnyCborEncodedToNode), + } + + 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 + }), + ) + 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 c57e7537..4b3d00ea 100644 --- a/node/bindnode/infer.go +++ b/node/bindnode/infer.go @@ -67,7 +67,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ switch schemaType := schemaType.(type) { case *schema.TypeBool: if customConverter, ok := cfg[goType]; ok { - if customConverter.kind != datamodel.Kind_Bool { + if customConverter.kind != schema.TypeKind_Bool { doPanic("kind mismatch; custom converter for type is not for Bool") } } else if goType.Kind() != reflect.Bool { @@ -75,7 +75,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } case *schema.TypeInt: if customConverter, ok := cfg[goType]; ok { - if customConverter.kind != datamodel.Kind_Int { + 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] { @@ -83,7 +83,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } case *schema.TypeFloat: if customConverter, ok := cfg[goType]; ok { - if customConverter.kind != datamodel.Kind_Float { + if customConverter.kind != schema.TypeKind_Float { doPanic("kind mismatch; custom converter for type is not for Float") } } else { @@ -96,7 +96,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ case *schema.TypeString: // TODO: allow []byte? if customConverter, ok := cfg[goType]; ok { - if customConverter.kind != datamodel.Kind_String { + if customConverter.kind != schema.TypeKind_String { doPanic("kind mismatch; custom converter for type is not for String") } } else if goType.Kind() != reflect.String { @@ -105,7 +105,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ case *schema.TypeBytes: // TODO: allow string? if customConverter, ok := cfg[goType]; ok { - if customConverter.kind != datamodel.Kind_Bytes { + if customConverter.kind != schema.TypeKind_Bytes { doPanic("kind mismatch; custom converter for type is not for Bytes") } } else if goType.Kind() != reflect.Slice { @@ -231,15 +231,18 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } case *schema.TypeLink: if customConverter, ok := cfg[goType]; ok { - if customConverter.kind != datamodel.Kind_Link { + 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, ok := cfg[goType]; ok { + 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: diff --git a/node/bindnode/node.go b/node/bindnode/node.go index 64e50a54..0b4aacde 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -560,8 +560,9 @@ func (w *_assembler) Representation() datamodel.NodeAssembler { type basicMapAssembler struct { datamodel.MapAssembler - builder datamodel.NodeBuilder - parent *_assembler + builder datamodel.NodeBuilder + parent *_assembler + converter *converter } func (w *basicMapAssembler) Finish() error { @@ -569,7 +570,15 @@ func (w *basicMapAssembler) Finish() error { return err } basicNode := w.builder.Build() - w.parent.createNonPtrVal().Set(reflect.ValueOf(basicNode)) + if w.converter != nil { + 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 @@ -586,7 +595,11 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { if err != nil { return nil, err } - return &basicMapAssembler{MapAssembler: mapAsm, builder: basicBuilder, parent: w}, nil + var conv *converter = nil + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + conv = &customConverter + } + return &basicMapAssembler{MapAssembler: mapAsm, builder: basicBuilder, parent: w, converter: conv}, nil case *schema.TypeStruct: val := w.createNonPtrVal() doneFields := make([]bool, val.NumField()) @@ -631,8 +644,9 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { type basicListAssembler struct { datamodel.ListAssembler - builder datamodel.NodeBuilder - parent *_assembler + builder datamodel.NodeBuilder + parent *_assembler + converter *converter } func (w *basicListAssembler) Finish() error { @@ -640,7 +654,15 @@ func (w *basicListAssembler) Finish() error { return err } basicNode := w.builder.Build() - w.parent.createNonPtrVal().Set(reflect.ValueOf(basicNode)) + if w.converter != nil { + 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 @@ -657,7 +679,11 @@ func (w *_assembler) BeginList(sizeHint int64) (datamodel.ListAssembler, error) if err != nil { return nil, err } - return &basicListAssembler{ListAssembler: listAsm, builder: basicBuilder, parent: w}, nil + var conv *converter = nil + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + conv = &customConverter + } + return &basicListAssembler{ListAssembler: listAsm, builder: basicBuilder, parent: w, converter: conv}, nil case *schema.TypeList: val := w.createNonPtrVal() return &_listAssembler{ @@ -683,7 +709,15 @@ func (w *_assembler) AssignNull() error { // TODO } } - w.val.Set(reflect.Zero(w.val.Type())) + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + typ, err := customConverter.customFromAny(datamodel.Null) + if err != nil { + return err + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) + } else { + w.val.Set(reflect.Zero(w.val.Type())) + } if w.finish != nil { if err := w.finish(); err != nil { return err @@ -696,16 +730,27 @@ 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))) - } else if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { - typ, err := customConverter.customFromBool(b) - if err != nil { - return err + customConverter, hasCustomConverter := w.cfg[nonPtrType(w.val)] + _, isAny := w.schemaType.(*schema.TypeAny) + if hasCustomConverter { + var typ interface{} + var err error + if isAny { + if typ, err = customConverter.customFromAny(basicnode.NewBool(b)); err != nil { + return err + } + } else { + if typ, err = customConverter.customFromBool(b); err != nil { + return err + } } w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { - w.createNonPtrVal().SetBool(b) + if isAny { + w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewBool(b))) + } else { + w.createNonPtrVal().SetBool(b) + } } if w.finish != nil { if err := w.finish(); err != nil { @@ -720,22 +765,33 @@ 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 customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { - typ, err := customConverter.customFromInt(i) - if err != nil { - return err + customConverter, hasCustomConverter := w.cfg[nonPtrType(w.val)] + _, isAny := w.schemaType.(*schema.TypeAny) + if hasCustomConverter { + var typ interface{} + var err error + if isAny { + if typ, err = customConverter.customFromAny(basicnode.NewInt(i)); err != nil { + return err + } + } else { + if typ, err = customConverter.customFromInt(i); err != nil { + return err + } } w.createNonPtrVal().Set(matchSettable(typ, w.val)) - } 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 isAny { + 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 { @@ -749,16 +805,27 @@ 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))) - } else if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { - typ, err := customConverter.customFromFloat(f) - if err != nil { - return err + customConverter, hasCustomConverter := w.cfg[nonPtrType(w.val)] + _, isAny := w.schemaType.(*schema.TypeAny) + if hasCustomConverter { + var typ interface{} + var err error + if isAny { + if typ, err = customConverter.customFromAny(basicnode.NewFloat(f)); err != nil { + return err + } + } else { + if typ, err = customConverter.customFromFloat(f); err != nil { + return err + } } w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { - w.createNonPtrVal().SetFloat(f) + if isAny { + w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewFloat(f))) + } else { + w.createNonPtrVal().SetFloat(f) + } } if w.finish != nil { if err := w.finish(); err != nil { @@ -772,15 +839,24 @@ 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))) - } else { - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { - typ, err := customConverter.customFromString(s) - if err != nil { + customConverter, hasCustomConverter := w.cfg[nonPtrType(w.val)] + _, isAny := w.schemaType.(*schema.TypeAny) + if hasCustomConverter { + var typ interface{} + var err error + if isAny { + if typ, err = customConverter.customFromAny(basicnode.NewString(s)); err != nil { return err } - w.createNonPtrVal().Set(matchSettable(typ, w.val)) + } else { + if typ, err = customConverter.customFromString(s); err != nil { + return err + } + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) + } else { + if isAny { + w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewString(s))) } else { w.createNonPtrVal().SetString(s) } @@ -797,15 +873,24 @@ 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))) - } else { - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { - typ, err := customConverter.customFromBytes(p) - if err != nil { + customConverter, hasCustomConverter := w.cfg[nonPtrType(w.val)] + _, isAny := w.schemaType.(*schema.TypeAny) + if hasCustomConverter { + var typ interface{} + var err error + if isAny { + if typ, err = customConverter.customFromAny(basicnode.NewBytes(p)); err != nil { return err } - w.createNonPtrVal().Set(matchSettable(typ, w.val)) + } else { + if typ, err = customConverter.customFromBytes(p); err != nil { + return err + } + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) + } else { + if isAny { + w.createNonPtrVal().Set(reflect.ValueOf(basicnode.NewBytes(p))) } else { w.createNonPtrVal().SetBytes(p) } @@ -822,7 +907,15 @@ func (w *_assembler) AssignLink(link datamodel.Link) error { val := w.createNonPtrVal() // TODO: newVal.Type() panics if link==nil; add a test and fix. if _, ok := w.schemaType.(*schema.TypeAny); ok { - val.Set(reflect.ValueOf(basicnode.NewLink(link))) + if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + typ, err := customConverter.customFromAny(basicnode.NewLink(link)) + if err != nil { + return err + } + w.createNonPtrVal().Set(matchSettable(typ, w.val)) + } else { + val.Set(reflect.ValueOf(basicnode.NewLink(link))) + } } else if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { if cl, ok := link.(cidlink.Link); ok { typ, err := customConverter.customFromLink(cl.Cid) @@ -1224,6 +1317,13 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { } } if _, ok := field.Type().(*schema.TypeAny); ok { + if customConverter, ok := w.cfg[nonPtrType(val)]; ok { + v, err := customConverter.customToAny(ptrVal(val).Interface()) + if err != nil { + return nil, nil, err + } + return key, v, nil + } return key, nonPtrVal(val).Interface().(datamodel.Node), nil } node := &_node{ From 6d6215eebfab7006cabf897e3854cf0b95bc159c Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 17 May 2022 21:37:19 +1000 Subject: [PATCH 06/13] chore(bindnode): config helper refactor w/ short-circuit --- node/bindnode/api.go | 8 ++++++++ node/bindnode/node.go | 34 +++++++++++++++++----------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index c2931f0c..895cf456 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -145,6 +145,14 @@ type converter struct { type config map[reflect.Type]converter +func (c config) converterFor(val reflect.Value) (converter, bool) { + if len(c) == 0 { + return converter{}, false + } + conv, ok := c[nonPtrType(val)] + return conv, ok +} + // Option is able to apply custom options to the bindnode API type Option func(config) diff --git a/node/bindnode/node.go b/node/bindnode/node.go index 0b4aacde..509e330a 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -432,7 +432,7 @@ func (w *_node) AsBool() (bool, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Bool); err != nil { return false, err } - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if customConverter, ok := w.cfg.converterFor(w.val); ok { return customConverter.customToBool(ptrVal(w.val).Interface()) } return nonPtrVal(w.val).Bool(), nil @@ -442,7 +442,7 @@ func (w *_node) AsInt() (int64, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Int); err != nil { return 0, err } - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if customConverter, ok := w.cfg.converterFor(w.val); ok { return customConverter.customToInt(ptrVal(w.val).Interface()) } val := nonPtrVal(w.val) @@ -457,7 +457,7 @@ func (w *_node) AsFloat() (float64, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Float); err != nil { return 0, err } - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if customConverter, ok := w.cfg.converterFor(w.val); ok { return customConverter.customToFloat(ptrVal(w.val).Interface()) } return nonPtrVal(w.val).Float(), nil @@ -467,7 +467,7 @@ func (w *_node) AsString() (string, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_String); err != nil { return "", err } - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if customConverter, ok := w.cfg.converterFor(w.val); ok { return customConverter.customToString(ptrVal(w.val).Interface()) } return nonPtrVal(w.val).String(), nil @@ -477,7 +477,7 @@ func (w *_node) AsBytes() ([]byte, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Bytes); err != nil { return nil, err } - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if customConverter, ok := w.cfg.converterFor(w.val); ok { return customConverter.customToBytes(ptrVal(w.val).Interface()) } return nonPtrVal(w.val).Bytes(), nil @@ -487,7 +487,7 @@ func (w *_node) AsLink() (datamodel.Link, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Link); err != nil { return nil, err } - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if customConverter, ok := w.cfg.converterFor(w.val); ok { cid, err := customConverter.customToLink(ptrVal(w.val).Interface()) if err != nil { return nil, err @@ -596,7 +596,7 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { return nil, err } var conv *converter = nil - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if customConverter, ok := w.cfg.converterFor(w.val); ok { conv = &customConverter } return &basicMapAssembler{MapAssembler: mapAsm, builder: basicBuilder, parent: w, converter: conv}, nil @@ -680,7 +680,7 @@ func (w *_assembler) BeginList(sizeHint int64) (datamodel.ListAssembler, error) return nil, err } var conv *converter = nil - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if customConverter, ok := w.cfg.converterFor(w.val); ok { conv = &customConverter } return &basicListAssembler{ListAssembler: listAsm, builder: basicBuilder, parent: w, converter: conv}, nil @@ -709,7 +709,7 @@ func (w *_assembler) AssignNull() error { // TODO } } - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if customConverter, ok := w.cfg.converterFor(w.val); ok { typ, err := customConverter.customFromAny(datamodel.Null) if err != nil { return err @@ -730,7 +730,7 @@ func (w *_assembler) AssignBool(b bool) error { if err := compatibleKind(w.schemaType, datamodel.Kind_Bool); err != nil { return err } - customConverter, hasCustomConverter := w.cfg[nonPtrType(w.val)] + customConverter, hasCustomConverter := w.cfg.converterFor(w.val) _, isAny := w.schemaType.(*schema.TypeAny) if hasCustomConverter { var typ interface{} @@ -765,7 +765,7 @@ func (w *_assembler) AssignInt(i int64) error { return err } // TODO: check for overflow - customConverter, hasCustomConverter := w.cfg[nonPtrType(w.val)] + customConverter, hasCustomConverter := w.cfg.converterFor(w.val) _, isAny := w.schemaType.(*schema.TypeAny) if hasCustomConverter { var typ interface{} @@ -805,7 +805,7 @@ func (w *_assembler) AssignFloat(f float64) error { if err := compatibleKind(w.schemaType, datamodel.Kind_Float); err != nil { return err } - customConverter, hasCustomConverter := w.cfg[nonPtrType(w.val)] + customConverter, hasCustomConverter := w.cfg.converterFor(w.val) _, isAny := w.schemaType.(*schema.TypeAny) if hasCustomConverter { var typ interface{} @@ -839,7 +839,7 @@ func (w *_assembler) AssignString(s string) error { if err := compatibleKind(w.schemaType, datamodel.Kind_String); err != nil { return err } - customConverter, hasCustomConverter := w.cfg[nonPtrType(w.val)] + customConverter, hasCustomConverter := w.cfg.converterFor(w.val) _, isAny := w.schemaType.(*schema.TypeAny) if hasCustomConverter { var typ interface{} @@ -873,7 +873,7 @@ func (w *_assembler) AssignBytes(p []byte) error { if err := compatibleKind(w.schemaType, datamodel.Kind_Bytes); err != nil { return err } - customConverter, hasCustomConverter := w.cfg[nonPtrType(w.val)] + customConverter, hasCustomConverter := w.cfg.converterFor(w.val) _, isAny := w.schemaType.(*schema.TypeAny) if hasCustomConverter { var typ interface{} @@ -907,7 +907,7 @@ func (w *_assembler) AssignLink(link datamodel.Link) error { val := w.createNonPtrVal() // TODO: newVal.Type() panics if link==nil; add a test and fix. if _, ok := w.schemaType.(*schema.TypeAny); ok { - if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + if customConverter, ok := w.cfg.converterFor(w.val); ok { typ, err := customConverter.customFromAny(basicnode.NewLink(link)) if err != nil { return err @@ -916,7 +916,7 @@ func (w *_assembler) AssignLink(link datamodel.Link) error { } else { val.Set(reflect.ValueOf(basicnode.NewLink(link))) } - } else if customConverter, ok := w.cfg[nonPtrType(w.val)]; ok { + } else if customConverter, ok := w.cfg.converterFor(w.val); ok { if cl, ok := link.(cidlink.Link); ok { typ, err := customConverter.customFromLink(cl.Cid) if err != nil { @@ -1317,7 +1317,7 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { } } if _, ok := field.Type().(*schema.TypeAny); ok { - if customConverter, ok := w.cfg[nonPtrType(val)]; ok { + if customConverter, ok := w.cfg.converterFor(val); ok { v, err := customConverter.customToAny(ptrVal(val).Interface()) if err != nil { return nil, nil, err From 6515ffdeaf3ed4c23de8d21cd0d53e3253fbc8a1 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Wed, 18 May 2022 14:57:32 +1000 Subject: [PATCH 07/13] feat(bindnode): pass Null on to nullable custom converters --- node/bindnode/api.go | 8 +++++ node/bindnode/custom_test.go | 65 ++++++++++++++++++++---------------- node/bindnode/infer.go | 19 ++++++----- node/bindnode/node.go | 33 +++++++++--------- 4 files changed, 74 insertions(+), 51 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index 895cf456..64391ad1 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -153,6 +153,14 @@ func (c config) converterFor(val reflect.Value) (converter, bool) { return conv, ok } +func (c config) converterForType(typ reflect.Type) (converter, bool) { + if len(c) == 0 { + return converter{}, false + } + conv, ok := c[typ] + return conv, ok +} + // Option is able to apply custom options to the bindnode API type Option func(config) diff --git a/node/bindnode/custom_test.go b/node/bindnode/custom_test.go index f0c78d59..5dcf3ba8 100644 --- a/node/bindnode/custom_test.go +++ b/node/bindnode/custom_test.go @@ -317,18 +317,20 @@ func TestCustom(t *testing.T) { } type AnyExtend struct { - Name string - Blob AnyExtendBlob - Count int - Null AnyCborEncoded - Bool AnyCborEncoded - Int AnyCborEncoded - Float AnyCborEncoded - String AnyCborEncoded - Bytes AnyCborEncoded - Link AnyCborEncoded - Map AnyCborEncoded - List AnyCborEncoded + 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 } const anyExtendSchema = ` @@ -337,6 +339,8 @@ type AnyExtend struct { Blob Any Count Int Null nullable Any + NullPtr nullable Any + NullableWith nullable Any Bool Any Int Any Float Any @@ -416,7 +420,7 @@ func AnyExtendBlobToNode(ptr interface{}) (datamodel.Node, error) { // take a datamodel.Node, dag-cbor encode it and store it here, do the reverse // to get the datamodel.Node back -type AnyCborEncoded []byte +type AnyCborEncoded struct{ str []byte } func AnyCborEncodedFromNode(node datamodel.Node) (interface{}, error) { if tn, ok := node.(schema.TypedNode); ok { @@ -427,7 +431,7 @@ func AnyCborEncodedFromNode(node datamodel.Node) (interface{}, error) { if err != nil { return nil, err } - acb := AnyCborEncoded(buf.Bytes()) + acb := AnyCborEncoded{str: buf.Bytes()} return &acb, nil } @@ -437,28 +441,30 @@ func AnyCborEncodedToNode(ptr interface{}) (datamodel.Node, error) { return nil, fmt.Errorf("expected *AnyCborEncoded type") } na := basicnode.Prototype.Any.NewBuilder() - err := dagcbor.Decode(na, bytes.NewReader(*acb)) + 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,"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,"String":"this is a string"}` +const anyExtendDagJson = `{"Blob":{"baz":[2,3,4],"foo":"bar"},"Bool":false,"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"}` var anyExtendFixtureInstance = AnyExtend{ - Name: "Any extend test", - Count: 101, - Blob: AnyExtendBlob{f: "bar", x: 2, y: 3, z: 4}, - Null: AnyCborEncoded(mustFromHex("f6")), // normally this field would be `nil`, but we now get to decide whether it should be something concrete - Bool: AnyCborEncoded(mustFromHex("f4")), - Int: AnyCborEncoded(mustFromHex("1a075bcd15")), // cbor encoded form of 123456789 - Float: AnyCborEncoded(mustFromHex("fb4002b851eb851eb8")), // cbor encoded form of 2.34 - String: AnyCborEncoded(mustFromHex("7074686973206973206120737472696e67")), // cbor encoded form of "this is a string" - Bytes: AnyCborEncoded(mustFromHex("4702030405060708")), // cbor encoded form of [2,3,4,5,6,7,8] - Link: AnyCborEncoded(mustFromHex("d82a58260001b0015620d13d09e8ae38120a5c4475fbd9ecc41cb1003f4f3b2bf86a0000000000000000")), // dag-cbor encoded CID bagyacvra2e6qt2fohajauxceox55t3gedsyqap2phmv7q2qaaaaaaaaaaaaa - Map: AnyCborEncoded(mustFromHex("a463666f6f63626172636f6e65016374776f0265746872656503")), // cbor encoded form of {"one":1,"two":2,"three":3,"foo":"bar"} - List: AnyCborEncoded(mustFromHex("88f6636f6e656374776f657468726565010203f5")), // cbor encoded form of [null,'one','two','three',1,2,3,true] + 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")}, // cbor encoded form of 123456789 + Float: AnyCborEncoded{mustFromHex("fb4002b851eb851eb8")}, // cbor encoded form of 2.34 + String: AnyCborEncoded{mustFromHex("7074686973206973206120737472696e67")}, // cbor encoded form of "this is a string" + Bytes: AnyCborEncoded{mustFromHex("4702030405060708")}, // cbor encoded form of [2,3,4,5,6,7,8] + Link: AnyCborEncoded{mustFromHex("d82a58260001b0015620d13d09e8ae38120a5c4475fbd9ecc41cb1003f4f3b2bf86a0000000000000000")}, // dag-cbor encoded CID bagyacvra2e6qt2fohajauxceox55t3gedsyqap2phmv7q2qaaaaaaaaaaaaa + Map: AnyCborEncoded{mustFromHex("a463666f6f63626172636f6e65016374776f0265746872656503")}, // cbor encoded form of {"one":1,"two":2,"three":3,"foo":"bar"} + List: AnyCborEncoded{mustFromHex("88f6636f6e656374776f657468726565010203f5")}, // cbor encoded form of [null,'one','two','three',1,2,3,true] } func TestCustomAny(t *testing.T) { @@ -484,6 +490,9 @@ func TestCustomAny(t *testing.T) { 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) diff --git a/node/bindnode/infer.go b/node/bindnode/infer.go index 4b3d00ea..c6624207 100644 --- a/node/bindnode/infer.go +++ b/node/bindnode/infer.go @@ -66,7 +66,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } switch schemaType := schemaType.(type) { case *schema.TypeBool: - if customConverter, ok := cfg[goType]; ok { + if customConverter, ok := cfg.converterForType(goType); ok { if customConverter.kind != schema.TypeKind_Bool { doPanic("kind mismatch; custom converter for type is not for Bool") } @@ -74,7 +74,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ doPanic("kind mismatch; need boolean") } case *schema.TypeInt: - if customConverter, ok := cfg[goType]; ok { + if customConverter, ok := cfg.converterForType(goType); ok { if customConverter.kind != schema.TypeKind_Int { doPanic("kind mismatch; custom converter for type is not for Int") } @@ -82,7 +82,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ doPanic("kind mismatch; need integer") } case *schema.TypeFloat: - if customConverter, ok := cfg[goType]; ok { + if customConverter, ok := cfg.converterForType(goType); ok { if customConverter.kind != schema.TypeKind_Float { doPanic("kind mismatch; custom converter for type is not for Float") } @@ -95,7 +95,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } case *schema.TypeString: // TODO: allow []byte? - if customConverter, ok := cfg[goType]; ok { + if customConverter, ok := cfg.converterForType(goType); ok { if customConverter.kind != schema.TypeKind_String { doPanic("kind mismatch; custom converter for type is not for String") } @@ -104,7 +104,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } case *schema.TypeBytes: // TODO: allow string? - if customConverter, ok := cfg[goType]; ok { + if customConverter, ok := cfg.converterForType(goType); ok { if customConverter.kind != schema.TypeKind_Bytes { doPanic("kind mismatch; custom converter for type is not for Bytes") } @@ -187,6 +187,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ // 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 (**)") } @@ -203,7 +204,9 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } case schemaField.IsNullable(): if ptr, nilable := ptrOrNilable(goType.Kind()); !nilable { - doPanic("nullable fields must be nilable") + if _, hasConverter := cfg.converterForType(goType); !hasConverter { + doPanic("nullable fields must be nilable") + } } else if ptr { goType = goType.Elem() } @@ -230,7 +233,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ verifyCompatibility(cfg, seen, goType, schemaType) } case *schema.TypeLink: - if customConverter, ok := cfg[goType]; ok { + if customConverter, ok := cfg.converterForType(goType); ok { if customConverter.kind != schema.TypeKind_Link { doPanic("kind mismatch; custom converter for type is not for Link") } @@ -238,7 +241,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ doPanic("links in Go must be datamodel.Link, cidlink.Link, or cid.Cid") } case *schema.TypeAny: - if customConverter, ok := cfg[goType]; ok { + if customConverter, ok := cfg.converterForType(goType); ok { if customConverter.kind != schema.TypeKind_Any { doPanic("kind mismatch; custom converter for type is not for Any") } diff --git a/node/bindnode/node.go b/node/bindnode/node.go index 509e330a..1d387afa 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -702,13 +702,6 @@ 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 - } - } if customConverter, ok := w.cfg.converterFor(w.val); ok { typ, err := customConverter.customFromAny(datamodel.Null) if err != nil { @@ -716,6 +709,13 @@ func (w *_assembler) AssignNull() error { } w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { + if !w.nullable { + return datamodel.ErrWrongKind{ + TypeName: w.schemaType.Name(), + MethodName: "AssignNull", + // TODO + } + } w.val.Set(reflect.Zero(w.val.Type())) } if w.finish != nil { @@ -1308,6 +1308,16 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { val = val.Elem() } } + _, isAny := field.Type().(*schema.TypeAny) + if isAny { + if customConverter, ok := w.cfg.converterFor(val); ok { + 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 @@ -1316,14 +1326,7 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { val = val.Elem() } } - if _, ok := field.Type().(*schema.TypeAny); ok { - if customConverter, ok := w.cfg.converterFor(val); ok { - v, err := customConverter.customToAny(ptrVal(val).Interface()) - if err != nil { - return nil, nil, err - } - return key, v, nil - } + if isAny { return key, nonPtrVal(val).Interface().(datamodel.Node), nil } node := &_node{ From ee4d05afe9a1219447468d04476b8d6cb3629485 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Wed, 18 May 2022 15:52:37 +1000 Subject: [PATCH 08/13] fix(bindnode): only custom convert AssignNull for Any converter --- node/bindnode/custom_test.go | 6 +++++- node/bindnode/node.go | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/node/bindnode/custom_test.go b/node/bindnode/custom_test.go index 5dcf3ba8..677c00f4 100644 --- a/node/bindnode/custom_test.go +++ b/node/bindnode/custom_test.go @@ -331,6 +331,7 @@ type AnyExtend struct { Link AnyCborEncoded Map AnyCborEncoded List AnyCborEncoded + BoolPtr *BoolSubst // included to test that a null entry won't call a non-Any converter } const anyExtendSchema = ` @@ -349,6 +350,7 @@ type AnyExtend struct { Link Any Map Any List Any + BoolPtr nullable Bool } ` @@ -448,7 +450,7 @@ func AnyCborEncodedToNode(ptr interface{}) (datamodel.Node, error) { return na.Build(), nil } -const anyExtendDagJson = `{"Blob":{"baz":[2,3,4],"foo":"bar"},"Bool":false,"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"}` +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"}` var anyExtendFixtureInstance = AnyExtend{ Name: "Any extend test", @@ -465,12 +467,14 @@ var anyExtendFixtureInstance = AnyExtend{ Link: AnyCborEncoded{mustFromHex("d82a58260001b0015620d13d09e8ae38120a5c4475fbd9ecc41cb1003f4f3b2bf86a0000000000000000")}, // dag-cbor encoded CID bagyacvra2e6qt2fohajauxceox55t3gedsyqap2phmv7q2qaaaaaaaaaaaaa Map: AnyCborEncoded{mustFromHex("a463666f6f63626172636f6e65016374776f0265746872656503")}, // cbor encoded form of {"one":1,"two":2,"three":3,"foo":"bar"} List: AnyCborEncoded{mustFromHex("88f6636f6e656374776f657468726565010203f5")}, // cbor encoded form of [null,'one','two','three',1,2,3,true] + BoolPtr: nil, } func TestCustomAny(t *testing.T) { opts := []bindnode.Option{ bindnode.AddCustomTypeAnyConverter(&AnyExtendBlob{}, AnyExtendBlobFromNode, AnyExtendBlobToNode), bindnode.AddCustomTypeAnyConverter(&AnyCborEncoded{}, AnyCborEncodedFromNode, AnyCborEncodedToNode), + bindnode.AddCustomTypeBoolConverter(BoolSubst(0), BoolSubstFromBool, BoolToBoolSubst), } typeSystem, err := ipld.LoadSchemaBytes([]byte(anyExtendSchema)) diff --git a/node/bindnode/node.go b/node/bindnode/node.go index 1d387afa..e4955b41 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -702,7 +702,8 @@ func (w *_assembler) BeginList(sizeHint int64) (datamodel.ListAssembler, error) } func (w *_assembler) AssignNull() error { - if customConverter, ok := w.cfg.converterFor(w.val); ok { + _, isAny := w.schemaType.(*schema.TypeAny) + if customConverter, ok := w.cfg.converterFor(w.val); ok && isAny { typ, err := customConverter.customFromAny(datamodel.Null) if err != nil { return err From b1ca66840fcade2ea566e189c44284736b6a251b Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Thu, 19 May 2022 15:50:50 +1000 Subject: [PATCH 09/13] fix(bindnode): shorten converter option names, minor perf improvements --- node/bindnode/api.go | 76 ++++++++++++++++++------------------ node/bindnode/custom_test.go | 20 +++++----- node/bindnode/infer.go | 16 ++++---- node/bindnode/node.go | 54 ++++++++++++------------- 4 files changed, 80 insertions(+), 86 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index 64391ad1..d141a00e 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -143,39 +143,39 @@ type converter struct { customToAny CustomToAny } -type config map[reflect.Type]converter +type config map[reflect.Type]*converter -func (c config) converterFor(val reflect.Value) (converter, bool) { +func (c config) converterFor(val reflect.Value) *converter { if len(c) == 0 { - return converter{}, false + return nil } - conv, ok := c[nonPtrType(val)] - return conv, ok + conv, _ := c[nonPtrType(val)] + return conv } -func (c config) converterForType(typ reflect.Type) (converter, bool) { +func (c config) converterForType(typ reflect.Type) *converter { if len(c) == 0 { - return converter{}, false + return nil } - conv, ok := c[typ] - return conv, ok + conv, _ := c[typ] + return conv } // Option is able to apply custom options to the bindnode API type Option func(config) -// AddCustomTypeBoolConverter adds custom converter functions for a particular +// 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. // -// AddCustomTypeBoolConverter is an EXPERIMENTAL API and may be removed or +// TypedBoolConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func AddCustomTypeBoolConverter(ptrVal interface{}, from CustomFromBool, to CustomToBool) Option { +func TypedBoolConverter(ptrVal interface{}, from CustomFromBool, to CustomToBool) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { - cfg[customType] = converter{ + cfg[customType] = &converter{ kind: schema.TypeKind_Bool, customFromBool: from, customToBool: to, @@ -183,18 +183,18 @@ func AddCustomTypeBoolConverter(ptrVal interface{}, from CustomFromBool, to Cust } } -// AddCustomTypeIntConverter adds custom converter functions for a particular +// 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. // -// AddCustomTypeIntConverter is an EXPERIMENTAL API and may be removed or +// TypedIntConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func AddCustomTypeIntConverter(ptrVal interface{}, from CustomFromInt, to CustomToInt) Option { +func TypedIntConverter(ptrVal interface{}, from CustomFromInt, to CustomToInt) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { - cfg[customType] = converter{ + cfg[customType] = &converter{ kind: schema.TypeKind_Int, customFromInt: from, customToInt: to, @@ -202,18 +202,18 @@ func AddCustomTypeIntConverter(ptrVal interface{}, from CustomFromInt, to Custom } } -// AddCustomTypeFloatConverter adds custom converter functions for a particular +// 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. // -// AddCustomTypeFloatConverter is an EXPERIMENTAL API and may be removed or +// TypedFloatConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func AddCustomTypeFloatConverter(ptrVal interface{}, from CustomFromFloat, to CustomToFloat) Option { +func TypedFloatConverter(ptrVal interface{}, from CustomFromFloat, to CustomToFloat) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { - cfg[customType] = converter{ + cfg[customType] = &converter{ kind: schema.TypeKind_Float, customFromFloat: from, customToFloat: to, @@ -221,18 +221,18 @@ func AddCustomTypeFloatConverter(ptrVal interface{}, from CustomFromFloat, to Cu } } -// AddCustomTypeStringConverter adds custom converter functions for a particular +// 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. // -// AddCustomTypeStringConverter is an EXPERIMENTAL API and may be removed or +// TypedStringConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func AddCustomTypeStringConverter(ptrVal interface{}, from CustomFromString, to CustomToString) Option { +func TypedStringConverter(ptrVal interface{}, from CustomFromString, to CustomToString) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { - cfg[customType] = converter{ + cfg[customType] = &converter{ kind: schema.TypeKind_String, customFromString: from, customToString: to, @@ -240,18 +240,18 @@ func AddCustomTypeStringConverter(ptrVal interface{}, from CustomFromString, to } } -// AddCustomTypeBytesConverter adds custom converter functions for a particular +// 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. // -// AddCustomTypeBytesConverter is an EXPERIMENTAL API and may be removed or +// TypedBytesConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func AddCustomTypeBytesConverter(ptrVal interface{}, from CustomFromBytes, to CustomToBytes) Option { +func TypedBytesConverter(ptrVal interface{}, from CustomFromBytes, to CustomToBytes) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { - cfg[customType] = converter{ + cfg[customType] = &converter{ kind: schema.TypeKind_Bytes, customFromBytes: from, customToBytes: to, @@ -259,7 +259,7 @@ func AddCustomTypeBytesConverter(ptrVal interface{}, from CustomFromBytes, to Cu } } -// AddCustomTypeLinkConverter adds custom converter functions for a particular +// 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) @@ -269,12 +269,12 @@ func AddCustomTypeBytesConverter(ptrVal interface{}, from CustomFromBytes, to Cu // model and may result in errors if attempting to convert from other // datamodel.Link types. // -// AddCustomTypeLinkConverter is an EXPERIMENTAL API and may be removed or +// TypedLinkConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func AddCustomTypeLinkConverter(ptrVal interface{}, from CustomFromLink, to CustomToLink) Option { +func TypedLinkConverter(ptrVal interface{}, from CustomFromLink, to CustomToLink) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { - cfg[customType] = converter{ + cfg[customType] = &converter{ kind: schema.TypeKind_Link, customFromLink: from, customToLink: to, @@ -282,7 +282,7 @@ func AddCustomTypeLinkConverter(ptrVal interface{}, from CustomFromLink, to Cust } } -// AddCustomTypeAnyConverter adds custom converter functions for a particular +// 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) @@ -291,12 +291,12 @@ func AddCustomTypeLinkConverter(ptrVal interface{}, from CustomFromLink, to Cust // 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. // -// AddCustomTypeAnyConverter is an EXPERIMENTAL API and may be removed or +// TypedAnyConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func AddCustomTypeAnyConverter(ptrVal interface{}, from CustomFromAny, to CustomToAny) Option { +func TypedAnyConverter(ptrVal interface{}, from CustomFromAny, to CustomToAny) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) return func(cfg config) { - cfg[customType] = converter{ + cfg[customType] = &converter{ kind: schema.TypeKind_Any, customFromAny: from, customToAny: to, @@ -305,7 +305,7 @@ func AddCustomTypeAnyConverter(ptrVal interface{}, from CustomFromAny, to Custom } func applyOptions(opt ...Option) config { - cfg := make(map[reflect.Type]converter) + cfg := make(map[reflect.Type]*converter) for _, o := range opt { o(cfg) } diff --git a/node/bindnode/custom_test.go b/node/bindnode/custom_test.go index 677c00f4..1db29e83 100644 --- a/node/bindnode/custom_test.go +++ b/node/bindnode/custom_test.go @@ -279,13 +279,13 @@ var boomFixtureInstance = Boom{ func TestCustom(t *testing.T) { opts := []bindnode.Option{ - bindnode.AddCustomTypeBytesConverter(&Boop{}, BoopFromBytes, BoopToBytes), - bindnode.AddCustomTypeBytesConverter(&Frop{}, FropFromBytes, FropToBytes), - bindnode.AddCustomTypeBoolConverter(BoolSubst(0), BoolSubstFromBool, BoolToBoolSubst), - bindnode.AddCustomTypeIntConverter(IntSubst(""), IntSubstFromInt, IntToIntSubst), - bindnode.AddCustomTypeFloatConverter(&BigFloat{}, BigFloatFromFloat, FloatFromBigFloat), - bindnode.AddCustomTypeStringConverter(&ByteArray{}, ByteArrayFromString, StringFromByteArray), - bindnode.AddCustomTypeLinkConverter(BtcId(""), FromCidToBtcId, FromBtcIdToCid), + 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)) @@ -472,9 +472,9 @@ var anyExtendFixtureInstance = AnyExtend{ func TestCustomAny(t *testing.T) { opts := []bindnode.Option{ - bindnode.AddCustomTypeAnyConverter(&AnyExtendBlob{}, AnyExtendBlobFromNode, AnyExtendBlobToNode), - bindnode.AddCustomTypeAnyConverter(&AnyCborEncoded{}, AnyCborEncodedFromNode, AnyCborEncodedToNode), - bindnode.AddCustomTypeBoolConverter(BoolSubst(0), BoolSubstFromBool, BoolToBoolSubst), + bindnode.TypedAnyConverter(&AnyExtendBlob{}, AnyExtendBlobFromNode, AnyExtendBlobToNode), + bindnode.TypedAnyConverter(&AnyCborEncoded{}, AnyCborEncodedFromNode, AnyCborEncodedToNode), + bindnode.TypedBoolConverter(BoolSubst(0), BoolSubstFromBool, BoolToBoolSubst), } typeSystem, err := ipld.LoadSchemaBytes([]byte(anyExtendSchema)) diff --git a/node/bindnode/infer.go b/node/bindnode/infer.go index c6624207..6758bfc4 100644 --- a/node/bindnode/infer.go +++ b/node/bindnode/infer.go @@ -66,7 +66,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } switch schemaType := schemaType.(type) { case *schema.TypeBool: - if customConverter, ok := cfg.converterForType(goType); ok { + if customConverter := cfg.converterForType(goType); customConverter != nil { if customConverter.kind != schema.TypeKind_Bool { doPanic("kind mismatch; custom converter for type is not for Bool") } @@ -74,7 +74,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ doPanic("kind mismatch; need boolean") } case *schema.TypeInt: - if customConverter, ok := cfg.converterForType(goType); ok { + if customConverter := cfg.converterForType(goType); customConverter != nil { if customConverter.kind != schema.TypeKind_Int { doPanic("kind mismatch; custom converter for type is not for Int") } @@ -82,7 +82,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ doPanic("kind mismatch; need integer") } case *schema.TypeFloat: - if customConverter, ok := cfg.converterForType(goType); ok { + if customConverter := cfg.converterForType(goType); customConverter != nil { if customConverter.kind != schema.TypeKind_Float { doPanic("kind mismatch; custom converter for type is not for Float") } @@ -95,7 +95,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } case *schema.TypeString: // TODO: allow []byte? - if customConverter, ok := cfg.converterForType(goType); ok { + if customConverter := cfg.converterForType(goType); customConverter != nil { if customConverter.kind != schema.TypeKind_String { doPanic("kind mismatch; custom converter for type is not for String") } @@ -104,7 +104,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } case *schema.TypeBytes: // TODO: allow string? - if customConverter, ok := cfg.converterForType(goType); ok { + if customConverter := cfg.converterForType(goType); customConverter != nil { if customConverter.kind != schema.TypeKind_Bytes { doPanic("kind mismatch; custom converter for type is not for Bytes") } @@ -204,7 +204,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ } case schemaField.IsNullable(): if ptr, nilable := ptrOrNilable(goType.Kind()); !nilable { - if _, hasConverter := cfg.converterForType(goType); !hasConverter { + if customConverter := cfg.converterForType(goType); customConverter == nil { doPanic("nullable fields must be nilable") } } else if ptr { @@ -233,7 +233,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ verifyCompatibility(cfg, seen, goType, schemaType) } case *schema.TypeLink: - if customConverter, ok := cfg.converterForType(goType); ok { + if customConverter := cfg.converterForType(goType); customConverter != nil { if customConverter.kind != schema.TypeKind_Link { doPanic("kind mismatch; custom converter for type is not for Link") } @@ -241,7 +241,7 @@ func verifyCompatibility(cfg config, seen map[seenEntry]bool, goType reflect.Typ doPanic("links in Go must be datamodel.Link, cidlink.Link, or cid.Cid") } case *schema.TypeAny: - if customConverter, ok := cfg.converterForType(goType); ok { + if customConverter := cfg.converterForType(goType); customConverter != nil { if customConverter.kind != schema.TypeKind_Any { doPanic("kind mismatch; custom converter for type is not for Any") } diff --git a/node/bindnode/node.go b/node/bindnode/node.go index e4955b41..41d61d56 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -432,7 +432,7 @@ func (w *_node) AsBool() (bool, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Bool); err != nil { return false, err } - if customConverter, ok := w.cfg.converterFor(w.val); ok { + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { return customConverter.customToBool(ptrVal(w.val).Interface()) } return nonPtrVal(w.val).Bool(), nil @@ -442,7 +442,7 @@ func (w *_node) AsInt() (int64, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Int); err != nil { return 0, err } - if customConverter, ok := w.cfg.converterFor(w.val); ok { + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { return customConverter.customToInt(ptrVal(w.val).Interface()) } val := nonPtrVal(w.val) @@ -457,7 +457,7 @@ func (w *_node) AsFloat() (float64, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Float); err != nil { return 0, err } - if customConverter, ok := w.cfg.converterFor(w.val); ok { + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { return customConverter.customToFloat(ptrVal(w.val).Interface()) } return nonPtrVal(w.val).Float(), nil @@ -467,7 +467,7 @@ func (w *_node) AsString() (string, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_String); err != nil { return "", err } - if customConverter, ok := w.cfg.converterFor(w.val); ok { + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { return customConverter.customToString(ptrVal(w.val).Interface()) } return nonPtrVal(w.val).String(), nil @@ -477,7 +477,7 @@ func (w *_node) AsBytes() ([]byte, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Bytes); err != nil { return nil, err } - if customConverter, ok := w.cfg.converterFor(w.val); ok { + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { return customConverter.customToBytes(ptrVal(w.val).Interface()) } return nonPtrVal(w.val).Bytes(), nil @@ -487,7 +487,7 @@ func (w *_node) AsLink() (datamodel.Link, error) { if err := compatibleKind(w.schemaType, datamodel.Kind_Link); err != nil { return nil, err } - if customConverter, ok := w.cfg.converterFor(w.val); ok { + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { cid, err := customConverter.customToLink(ptrVal(w.val).Interface()) if err != nil { return nil, err @@ -595,11 +595,8 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { if err != nil { return nil, err } - var conv *converter = nil - if customConverter, ok := w.cfg.converterFor(w.val); ok { - conv = &customConverter - } - return &basicMapAssembler{MapAssembler: mapAsm, builder: basicBuilder, parent: w, converter: conv}, nil + converter := w.cfg.converterFor(w.val) + return &basicMapAssembler{MapAssembler: mapAsm, builder: basicBuilder, parent: w, converter: converter}, nil case *schema.TypeStruct: val := w.createNonPtrVal() doneFields := make([]bool, val.NumField()) @@ -679,11 +676,8 @@ func (w *_assembler) BeginList(sizeHint int64) (datamodel.ListAssembler, error) if err != nil { return nil, err } - var conv *converter = nil - if customConverter, ok := w.cfg.converterFor(w.val); ok { - conv = &customConverter - } - return &basicListAssembler{ListAssembler: listAsm, builder: basicBuilder, parent: w, converter: conv}, nil + converter := w.cfg.converterFor(w.val) + return &basicListAssembler{ListAssembler: listAsm, builder: basicBuilder, parent: w, converter: converter}, nil case *schema.TypeList: val := w.createNonPtrVal() return &_listAssembler{ @@ -703,7 +697,7 @@ func (w *_assembler) BeginList(sizeHint int64) (datamodel.ListAssembler, error) func (w *_assembler) AssignNull() error { _, isAny := w.schemaType.(*schema.TypeAny) - if customConverter, ok := w.cfg.converterFor(w.val); ok && isAny { + if customConverter := w.cfg.converterFor(w.val); customConverter != nil && isAny { typ, err := customConverter.customFromAny(datamodel.Null) if err != nil { return err @@ -731,9 +725,9 @@ func (w *_assembler) AssignBool(b bool) error { if err := compatibleKind(w.schemaType, datamodel.Kind_Bool); err != nil { return err } - customConverter, hasCustomConverter := w.cfg.converterFor(w.val) + customConverter := w.cfg.converterFor(w.val) _, isAny := w.schemaType.(*schema.TypeAny) - if hasCustomConverter { + if customConverter != nil { var typ interface{} var err error if isAny { @@ -766,9 +760,9 @@ func (w *_assembler) AssignInt(i int64) error { return err } // TODO: check for overflow - customConverter, hasCustomConverter := w.cfg.converterFor(w.val) + customConverter := w.cfg.converterFor(w.val) _, isAny := w.schemaType.(*schema.TypeAny) - if hasCustomConverter { + if customConverter != nil { var typ interface{} var err error if isAny { @@ -806,9 +800,9 @@ func (w *_assembler) AssignFloat(f float64) error { if err := compatibleKind(w.schemaType, datamodel.Kind_Float); err != nil { return err } - customConverter, hasCustomConverter := w.cfg.converterFor(w.val) + customConverter := w.cfg.converterFor(w.val) _, isAny := w.schemaType.(*schema.TypeAny) - if hasCustomConverter { + if customConverter != nil { var typ interface{} var err error if isAny { @@ -840,9 +834,9 @@ func (w *_assembler) AssignString(s string) error { if err := compatibleKind(w.schemaType, datamodel.Kind_String); err != nil { return err } - customConverter, hasCustomConverter := w.cfg.converterFor(w.val) + customConverter := w.cfg.converterFor(w.val) _, isAny := w.schemaType.(*schema.TypeAny) - if hasCustomConverter { + if customConverter != nil { var typ interface{} var err error if isAny { @@ -874,9 +868,9 @@ func (w *_assembler) AssignBytes(p []byte) error { if err := compatibleKind(w.schemaType, datamodel.Kind_Bytes); err != nil { return err } - customConverter, hasCustomConverter := w.cfg.converterFor(w.val) + customConverter := w.cfg.converterFor(w.val) _, isAny := w.schemaType.(*schema.TypeAny) - if hasCustomConverter { + if customConverter != nil { var typ interface{} var err error if isAny { @@ -908,7 +902,7 @@ func (w *_assembler) AssignLink(link datamodel.Link) error { val := w.createNonPtrVal() // TODO: newVal.Type() panics if link==nil; add a test and fix. if _, ok := w.schemaType.(*schema.TypeAny); ok { - if customConverter, ok := w.cfg.converterFor(w.val); ok { + if customConverter := w.cfg.converterFor(w.val); customConverter != nil { typ, err := customConverter.customFromAny(basicnode.NewLink(link)) if err != nil { return err @@ -917,7 +911,7 @@ func (w *_assembler) AssignLink(link datamodel.Link) error { } else { val.Set(reflect.ValueOf(basicnode.NewLink(link))) } - } else if customConverter, ok := w.cfg.converterFor(w.val); ok { + } else if customConverter := w.cfg.converterFor(w.val); customConverter != nil { if cl, ok := link.(cidlink.Link); ok { typ, err := customConverter.customFromLink(cl.Cid) if err != nil { @@ -1311,7 +1305,7 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { } _, isAny := field.Type().(*schema.TypeAny) if isAny { - if customConverter, ok := w.cfg.converterFor(val); ok { + if customConverter := w.cfg.converterFor(val); customConverter != nil { v, err := customConverter.customToAny(ptrVal(val).Interface()) if err != nil { return nil, nil, err From 8da60b1f88debc012e8989ab20402a26da09c7e6 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Thu, 19 May 2022 16:34:37 +1000 Subject: [PATCH 10/13] feat(bindnode): make Any converters work for List and Map values --- node/bindnode/api.go | 8 ++++---- node/bindnode/custom_test.go | 34 ++++++++++++++++++++++++++-------- node/bindnode/node.go | 26 +++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index d141a00e..3af936bf 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -145,20 +145,20 @@ type converter struct { 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 } - conv, _ := c[nonPtrType(val)] - return conv + return c[nonPtrType(val)] } func (c config) converterForType(typ reflect.Type) *converter { if len(c) == 0 { return nil } - conv, _ := c[typ] - return conv + return c[typ] } // Option is able to apply custom options to the bindnode API diff --git a/node/bindnode/custom_test.go b/node/bindnode/custom_test.go index 1db29e83..adfc84b1 100644 --- a/node/bindnode/custom_test.go +++ b/node/bindnode/custom_test.go @@ -332,6 +332,13 @@ type AnyExtend struct { 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 = ` @@ -351,6 +358,8 @@ type AnyExtend struct { Map Any List Any BoolPtr nullable Bool + XListAny [Any] + XMapAny {String:Any} } ` @@ -450,7 +459,7 @@ func AnyCborEncodedToNode(ptr interface{}) (datamodel.Node, error) { 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"}` +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", @@ -460,14 +469,23 @@ var anyExtendFixtureInstance = AnyExtend{ NullPtr: &AnyCborEncoded{mustFromHex("f6")}, NullableWith: &AnyCborEncoded{mustFromHex("1a075bcd15")}, Bool: AnyCborEncoded{mustFromHex("f4")}, - Int: AnyCborEncoded{mustFromHex("1a075bcd15")}, // cbor encoded form of 123456789 - Float: AnyCborEncoded{mustFromHex("fb4002b851eb851eb8")}, // cbor encoded form of 2.34 - String: AnyCborEncoded{mustFromHex("7074686973206973206120737472696e67")}, // cbor encoded form of "this is a string" - Bytes: AnyCborEncoded{mustFromHex("4702030405060708")}, // cbor encoded form of [2,3,4,5,6,7,8] - Link: AnyCborEncoded{mustFromHex("d82a58260001b0015620d13d09e8ae38120a5c4475fbd9ecc41cb1003f4f3b2bf86a0000000000000000")}, // dag-cbor encoded CID bagyacvra2e6qt2fohajauxceox55t3gedsyqap2phmv7q2qaaaaaaaaaaaaa - Map: AnyCborEncoded{mustFromHex("a463666f6f63626172636f6e65016374776f0265746872656503")}, // cbor encoded form of {"one":1,"two":2,"three":3,"foo":"bar"} - List: AnyCborEncoded{mustFromHex("88f6636f6e656374776f657468726565010203f5")}, // cbor encoded form of [null,'one','two','three',1,2,3,true] + 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) { diff --git a/node/bindnode/node.go b/node/bindnode/node.go index 41d61d56..d0f91428 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -188,6 +188,9 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { } } if _, ok := field.Type().(*schema.TypeAny); ok { + if customConverter := w.cfg.converterFor(fval); customConverter != nil { + return customConverter.customToAny(ptrVal(fval).Interface()) + } return nonPtrVal(fval).Interface().(datamodel.Node), nil } node := &_node{ @@ -228,6 +231,9 @@ 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 { + return customConverter.customToAny(ptrVal(fval).Interface()) + } return nonPtrVal(fval).Interface().(datamodel.Node), nil } node := &_node{ @@ -310,6 +316,9 @@ func (w *_node) LookupByIndex(idx int64) (datamodel.Node, error) { val = val.Elem() } if _, ok := typ.ValueType().(*schema.TypeAny); ok { + if customConverter := w.cfg.converterFor(val); customConverter != nil { + return customConverter.customToAny(ptrVal(val).Interface()) + } return nonPtrVal(val).Interface().(datamodel.Node), nil } return &_node{cfg: w.cfg, schemaType: typ.ValueType(), val: val}, nil @@ -1306,6 +1315,7 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { _, isAny := field.Type().(*schema.TypeAny) if isAny { if customConverter := w.cfg.converterFor(val); customConverter != nil { + fmt.Printf("ptrVal of %v : %v : %v\n", val, val.Kind(), val.Type()) v, err := customConverter.customToAny(ptrVal(val).Interface()) if err != nil { return nil, nil, err @@ -1357,13 +1367,23 @@ func (w *_mapIterator) Next() (key, value datamodel.Node, _ error) { schemaType: w.schemaType.KeyType(), val: goKey, } + _, isAny := w.schemaType.ValueType().(*schema.TypeAny) + if isAny { + if customConverter := w.cfg.converterFor(val); customConverter != nil { + // 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() } - if _, ok := w.schemaType.ValueType().(*schema.TypeAny); ok { + if isAny { return key, nonPtrVal(val).Interface().(datamodel.Node), nil } node := &_node{ @@ -1399,6 +1419,10 @@ func (w *_listIterator) Next() (index int64, value datamodel.Node, _ error) { val = val.Elem() } if _, ok := w.schemaType.ValueType().(*schema.TypeAny); ok { + if customConverter := w.cfg.converterFor(val); customConverter != nil { + val, err := customConverter.customToAny(ptrVal(val).Interface()) + return idx, val, err + } return idx, nonPtrVal(val).Interface().(datamodel.Node), nil } return idx, &_node{cfg: w.cfg, schemaType: w.schemaType.ValueType(), val: val}, nil From dc50167fa352d3429792ad156ccad73c76a75cdd Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Thu, 19 May 2022 20:42:12 +1000 Subject: [PATCH 11/13] chore(bindnode): docs and minor tweaks --- node/bindnode/api.go | 87 +++++++++++++---------- node/bindnode/infer.go | 7 ++ node/bindnode/node.go | 153 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 195 insertions(+), 52 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index 3af936bf..454cbd5d 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -174,12 +174,13 @@ type Option func(config) // changed in a future release. func TypedBoolConverter(ptrVal interface{}, from CustomFromBool, to CustomToBool) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Bool, + customFromBool: from, + customToBool: to, + } return func(cfg config) { - cfg[customType] = &converter{ - kind: schema.TypeKind_Bool, - customFromBool: from, - customToBool: to, - } + cfg[customType] = converter } } @@ -193,12 +194,13 @@ func TypedBoolConverter(ptrVal interface{}, from CustomFromBool, to CustomToBool // changed in a future release. func TypedIntConverter(ptrVal interface{}, from CustomFromInt, to CustomToInt) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Int, + customFromInt: from, + customToInt: to, + } return func(cfg config) { - cfg[customType] = &converter{ - kind: schema.TypeKind_Int, - customFromInt: from, - customToInt: to, - } + cfg[customType] = converter } } @@ -212,12 +214,13 @@ func TypedIntConverter(ptrVal interface{}, from CustomFromInt, to CustomToInt) O // changed in a future release. func TypedFloatConverter(ptrVal interface{}, from CustomFromFloat, to CustomToFloat) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Float, + customFromFloat: from, + customToFloat: to, + } return func(cfg config) { - cfg[customType] = &converter{ - kind: schema.TypeKind_Float, - customFromFloat: from, - customToFloat: to, - } + cfg[customType] = converter } } @@ -231,12 +234,13 @@ func TypedFloatConverter(ptrVal interface{}, from CustomFromFloat, to CustomToFl // changed in a future release. func TypedStringConverter(ptrVal interface{}, from CustomFromString, to CustomToString) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_String, + customFromString: from, + customToString: to, + } return func(cfg config) { - cfg[customType] = &converter{ - kind: schema.TypeKind_String, - customFromString: from, - customToString: to, - } + cfg[customType] = converter } } @@ -250,12 +254,13 @@ func TypedStringConverter(ptrVal interface{}, from CustomFromString, to CustomTo // changed in a future release. func TypedBytesConverter(ptrVal interface{}, from CustomFromBytes, to CustomToBytes) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Bytes, + customFromBytes: from, + customToBytes: to, + } return func(cfg config) { - cfg[customType] = &converter{ - kind: schema.TypeKind_Bytes, - customFromBytes: from, - customToBytes: to, - } + cfg[customType] = converter } } @@ -273,12 +278,13 @@ func TypedBytesConverter(ptrVal interface{}, from CustomFromBytes, to CustomToBy // changed in a future release. func TypedLinkConverter(ptrVal interface{}, from CustomFromLink, to CustomToLink) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Link, + customFromLink: from, + customToLink: to, + } return func(cfg config) { - cfg[customType] = &converter{ - kind: schema.TypeKind_Link, - customFromLink: from, - customToLink: to, - } + cfg[customType] = converter } } @@ -295,16 +301,22 @@ func TypedLinkConverter(ptrVal interface{}, from CustomFromLink, to CustomToLink // changed in a future release. func TypedAnyConverter(ptrVal interface{}, from CustomFromAny, to CustomToAny) Option { customType := nonPtrType(reflect.ValueOf(ptrVal)) + converter := &converter{ + kind: schema.TypeKind_Any, + customFromAny: from, + customToAny: to, + } return func(cfg config) { - cfg[customType] = &converter{ - kind: schema.TypeKind_Any, - customFromAny: from, - customToAny: to, - } + 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) @@ -340,6 +352,11 @@ func Wrap(ptrVal interface{}, schemaType schema.Type, options ...Option) schema. if schemaType == nil { schemaType = inferSchema(goVal.Type(), 0) } else { + // 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{cfg: cfg, val: goVal, schemaType: schemaType} diff --git a/node/bindnode/infer.go b/node/bindnode/infer.go index 6758bfc4..6b1c4849 100644 --- a/node/bindnode/infer.go +++ b/node/bindnode/infer.go @@ -39,6 +39,11 @@ type seenEntry struct { 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 { @@ -278,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)) @@ -407,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 d0f91428..ff92504c 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -2,6 +2,7 @@ package bindnode import ( "fmt" + "math" "reflect" "runtime" "strings" @@ -91,15 +92,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 { @@ -108,7 +113,6 @@ func compatibleKind(schemaType schema.Type, kind datamodel.Kind) error { methodName = methodName[strings.LastIndexByte(methodName, '.')+1:] } } - return datamodel.ErrWrongKind{ TypeName: schemaType.Name(), MethodName: methodName, @@ -149,6 +153,8 @@ func nonPtrType(val reflect.Value) reflect.Type { 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()) { @@ -189,8 +195,10 @@ 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{ @@ -200,12 +208,17 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { } return node, 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, @@ -232,8 +245,10 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { } 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{ @@ -243,6 +258,8 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { } return node, 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() { @@ -305,20 +322,28 @@ 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 customConverter := w.cfg.converterFor(val); customConverter != nil { - return customConverter.customToAny(ptrVal(val).Interface()) - } + if isAny { + // Any always yields a plain datamodel.Node return nonPtrVal(val).Interface().(datamodel.Node), nil } return &_node{cfg: w.cfg, schemaType: typ.ValueType(), val: val}, nil @@ -375,6 +400,9 @@ 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{ @@ -391,6 +419,7 @@ func (w *_node) MapIterator() datamodel.MapIterator { val: val, } case *schema.TypeMap: + // we can assume a: struct{Keys []string, Values map[x]y} return &_mapIterator{ cfg: w.cfg, schemaType: typ, @@ -437,11 +466,17 @@ 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 @@ -452,12 +487,16 @@ func (w *_node) AsInt() (int64, error) { 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 } @@ -467,6 +506,7 @@ func (w *_node) AsFloat() (float64, error) { 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 @@ -477,6 +517,7 @@ func (w *_node) AsString() (string, error) { 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 @@ -487,6 +528,7 @@ func (w *_node) AsBytes() ([]byte, error) { 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 @@ -497,6 +539,7 @@ func (w *_node) AsLink() (datamodel.Link, error) { 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 @@ -544,6 +587,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. @@ -566,6 +610,8 @@ 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 @@ -580,6 +626,9 @@ func (w *basicMapAssembler) Finish() error { } basicNode := w.builder.Build() 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 @@ -608,6 +657,10 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { 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, @@ -617,6 +670,8 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { 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") @@ -631,6 +686,8 @@ func (w *_assembler) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) { 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, @@ -647,6 +704,8 @@ 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 @@ -661,6 +720,9 @@ func (w *basicListAssembler) Finish() error { } basicNode := w.builder.Build() 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 @@ -688,6 +750,8 @@ func (w *_assembler) BeginList(sizeHint int64) (datamodel.ListAssembler, error) 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, @@ -707,6 +771,8 @@ func (w *_assembler) BeginList(sizeHint int64) (datamodel.ListAssembler, error) func (w *_assembler) AssignNull() error { _, 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 @@ -720,6 +786,7 @@ func (w *_assembler) AssignNull() error { // TODO } } + // set the zero value for the underlying type as a stand-in for Null w.val.Set(reflect.Zero(w.val.Type())) } if w.finish != nil { @@ -740,10 +807,14 @@ func (w *_assembler) AssignBool(b bool) error { 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 } @@ -751,6 +822,7 @@ func (w *_assembler) AssignBool(b bool) error { w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { 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) @@ -775,10 +847,14 @@ func (w *_assembler) AssignInt(i int64) error { 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 } @@ -786,6 +862,7 @@ func (w *_assembler) AssignInt(i int64) error { w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { 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 { @@ -815,10 +892,14 @@ func (w *_assembler) AssignFloat(f float64) error { 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 } @@ -826,6 +907,7 @@ func (w *_assembler) AssignFloat(f float64) error { w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { 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) @@ -849,10 +931,14 @@ func (w *_assembler) AssignString(s string) error { 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 } @@ -860,6 +946,7 @@ func (w *_assembler) AssignString(s string) error { w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { 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) @@ -883,10 +970,14 @@ func (w *_assembler) AssignBytes(p []byte) error { 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 } @@ -894,6 +985,7 @@ func (w *_assembler) AssignBytes(p []byte) error { w.createNonPtrVal().Set(matchSettable(typ, w.val)) } else { 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) @@ -910,18 +1002,24 @@ 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 { - if customConverter := w.cfg.converterFor(w.val); customConverter != nil { + 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 := w.cfg.converterFor(w.val); customConverter != nil { + } 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 @@ -948,7 +1046,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()) } @@ -974,6 +1071,7 @@ func (w *_assembler) Prototype() datamodel.NodePrototype { 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? @@ -1101,6 +1199,8 @@ 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 @@ -1126,8 +1226,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)) @@ -1168,6 +1266,7 @@ func (w *_mapAssembler) ValuePrototype(k string) datamodel.NodePrototype { 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 @@ -1200,6 +1299,8 @@ func (w *_listAssembler) ValuePrototype(idx int64) datamodel.NodePrototype { 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 @@ -1242,7 +1343,6 @@ 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 } @@ -1263,6 +1363,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"} @@ -1283,6 +1385,9 @@ 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 @@ -1315,7 +1420,8 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { _, isAny := field.Type().(*schema.TypeAny) if isAny { if customConverter := w.cfg.converterFor(val); customConverter != nil { - fmt.Printf("ptrVal of %v : %v : %v\n", val, val.Kind(), val.Type()) + // 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 @@ -1332,6 +1438,7 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { } } if isAny { + // field holds a datamodel.Node return key, nonPtrVal(val).Interface().(datamodel.Node), nil } node := &_node{ @@ -1346,6 +1453,8 @@ 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 @@ -1370,6 +1479,9 @@ func (w *_mapIterator) Next() (key, value datamodel.Node, _ error) { _, 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? @@ -1381,9 +1493,10 @@ func (w *_mapIterator) Next() (key, value datamodel.Node, _ error) { if val.IsNil() { return key, datamodel.Null, nil } - val = val.Elem() + val = val.Elem() // nullable entries are pointers } if isAny { + // Values holds datamodel.Nodes return key, nonPtrVal(val).Interface().(datamodel.Node), nil } node := &_node{ @@ -1398,6 +1511,7 @@ 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 @@ -1416,13 +1530,16 @@ 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{cfg: w.cfg, schemaType: w.schemaType.ValueType(), val: val}, nil @@ -1443,6 +1560,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{} } From 19479443bca9c68304834f8afa5ce2496145c25b Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Thu, 19 May 2022 20:49:45 +1000 Subject: [PATCH 12/13] chore(bindnode): remove typed functions for options API docs are too verbose --- node/bindnode/api.go | 100 +++++++++---------------------------------- 1 file changed, 21 insertions(+), 79 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index 454cbd5d..18a9a2d0 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -60,87 +60,29 @@ func Prototype(ptrType interface{}, schemaType schema.Type, options ...Option) s return &_prototype{cfg: cfg, schemaType: schemaType, goType: goType} } -// scalar kinds excluding Null - -// CustomFromBool is a custom converter function that takes a bool and returns a -// custom type -type CustomFromBool func(bool) (interface{}, error) - -// CustomToBool is a custom converter function that takes a custom type and -// returns a bool -type CustomToBool func(interface{}) (bool, error) - -// CustomFromInt is a custom converter function that takes an int and returns a -// custom type -type CustomFromInt func(int64) (interface{}, error) - -// CustomToInt is a custom converter function that takes a custom type and -// returns an int -type CustomToInt func(interface{}) (int64, error) - -// CustomFromFloat is a custom converter function that takes a float and returns -// a custom type -type CustomFromFloat func(float64) (interface{}, error) - -// CustomToFloat is a custom converter function that takes a custom type and -// returns a float -type CustomToFloat func(interface{}) (float64, error) - -// CustomFromString is a custom converter function that takes a string and -// returns custom type -type CustomFromString func(string) (interface{}, error) - -// CustomToString is a custom converter function that takes a custom type and -// returns a string -type CustomToString func(interface{}) (string, error) - -// CustomFromBytes is a custom converter function that takes a byte slice and -// returns a custom type -type CustomFromBytes func([]byte) (interface{}, error) - -// CustomToBytes is a custom converter function that takes a custom type and -// returns a byte slice -type CustomToBytes func(interface{}) ([]byte, error) - -// CustomFromLink is a custom converter function that takes a cid.Cid and -// returns a custom type -type CustomFromLink func(cid.Cid) (interface{}, error) - -// CustomToLink is a custom converter function that takes a custom type and -// returns a cid.Cid -type CustomToLink func(interface{}) (cid.Cid, error) - -// CustomFromAny is a custom converter function that takes a datamodel.Node and -// returns a custom type -type CustomFromAny func(datamodel.Node) (interface{}, error) - -// CustomToAny is a custom converter function that takes a custom type and -// returns a datamodel.Node -type CustomToAny func(interface{}) (datamodel.Node, error) - type converter struct { kind schema.TypeKind - customFromBool CustomFromBool - customToBool CustomToBool + customFromBool func(bool) (interface{}, error) + customToBool func(interface{}) (bool, error) - customFromInt CustomFromInt - customToInt CustomToInt + customFromInt func(int64) (interface{}, error) + customToInt func(interface{}) (int64, error) - customFromFloat CustomFromFloat - customToFloat CustomToFloat + customFromFloat func(float64) (interface{}, error) + customToFloat func(interface{}) (float64, error) - customFromString CustomFromString - customToString CustomToString + customFromString func(string) (interface{}, error) + customToString func(interface{}) (string, error) - customFromBytes CustomFromBytes - customToBytes CustomToBytes + customFromBytes func([]byte) (interface{}, error) + customToBytes func(interface{}) ([]byte, error) - customFromLink CustomFromLink - customToLink CustomToLink + customFromLink func(cid.Cid) (interface{}, error) + customToLink func(interface{}) (cid.Cid, error) - customFromAny CustomFromAny - customToAny CustomToAny + customFromAny func(datamodel.Node) (interface{}, error) + customToAny func(interface{}) (datamodel.Node, error) } type config map[reflect.Type]*converter @@ -172,7 +114,7 @@ type Option func(config) // // TypedBoolConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func TypedBoolConverter(ptrVal interface{}, from CustomFromBool, to CustomToBool) Option { +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, @@ -192,7 +134,7 @@ func TypedBoolConverter(ptrVal interface{}, from CustomFromBool, to CustomToBool // // TypedIntConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func TypedIntConverter(ptrVal interface{}, from CustomFromInt, to CustomToInt) Option { +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, @@ -212,7 +154,7 @@ func TypedIntConverter(ptrVal interface{}, from CustomFromInt, to CustomToInt) O // // TypedFloatConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func TypedFloatConverter(ptrVal interface{}, from CustomFromFloat, to CustomToFloat) Option { +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, @@ -232,7 +174,7 @@ func TypedFloatConverter(ptrVal interface{}, from CustomFromFloat, to CustomToFl // // TypedStringConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func TypedStringConverter(ptrVal interface{}, from CustomFromString, to CustomToString) Option { +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, @@ -252,7 +194,7 @@ func TypedStringConverter(ptrVal interface{}, from CustomFromString, to CustomTo // // TypedBytesConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func TypedBytesConverter(ptrVal interface{}, from CustomFromBytes, to CustomToBytes) Option { +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, @@ -276,7 +218,7 @@ func TypedBytesConverter(ptrVal interface{}, from CustomFromBytes, to CustomToBy // // TypedLinkConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func TypedLinkConverter(ptrVal interface{}, from CustomFromLink, to CustomToLink) Option { +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, @@ -299,7 +241,7 @@ func TypedLinkConverter(ptrVal interface{}, from CustomFromLink, to CustomToLink // // TypedAnyConverter is an EXPERIMENTAL API and may be removed or // changed in a future release. -func TypedAnyConverter(ptrVal interface{}, from CustomFromAny, to CustomToAny) Option { +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, From 87211682cb963ef1c98fa63909f67a8b02d1108c Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 24 May 2022 11:02:09 +1000 Subject: [PATCH 13/13] feat(bindnode): support full uint64 range --- node/bindnode/api.go | 2 +- node/bindnode/api_test.go | 75 +++++++++++ node/bindnode/node.go | 253 +++++++++++++++++++++++++++++++------- node/bindnode/repr.go | 18 +++ 4 files changed, 304 insertions(+), 44 deletions(-) diff --git a/node/bindnode/api.go b/node/bindnode/api.go index 18a9a2d0..062aec4a 100644 --- a/node/bindnode/api.go +++ b/node/bindnode/api.go @@ -301,7 +301,7 @@ func Wrap(ptrVal interface{}, schemaType schema.Type, options ...Option) schema. // inferred. verifyCompatibility(cfg, make(map[seenEntry]bool), goVal.Type(), schemaType) } - return &_node{cfg: cfg, 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/node.go b/node/bindnode/node.go index ff92504c..7930fa10 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -11,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" ) @@ -25,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) @@ -80,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 } @@ -201,12 +222,7 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { // field is an Any, safely assume a Node in fval return nonPtrVal(fval).Interface().(datamodel.Node), nil } - node := &_node{ - cfg: w.cfg, - 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 @@ -251,12 +267,7 @@ func (w *_node) LookupByString(key string) (datamodel.Node, error) { // value is an Any, safely assume a Node in fval return nonPtrVal(fval).Interface().(datamodel.Node), nil } - node := &_node{ - cfg: w.cfg, - 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' @@ -277,12 +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{ - cfg: w.cfg, - schemaType: mtyp, - val: mval, - } - return node, nil + return newNode(w.cfg, mtyp, mval), nil } return nil, datamodel.ErrWrongKind{ TypeName: w.schemaType.Name(), @@ -346,7 +352,7 @@ func (w *_node) LookupByIndex(idx int64) (datamodel.Node, error) { // Any always yields a plain datamodel.Node return nonPtrVal(val).Interface().(datamodel.Node), nil } - return &_node{cfg: w.cfg, schemaType: typ.ValueType(), val: val}, nil + return newNode(w.cfg, typ.ValueType(), val), nil } return nil, datamodel.ErrWrongKind{ TypeName: w.schemaType.Name(), @@ -566,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{cfg: w.cfg, schemaType: w.schemaType, val: w.val} + return newNode(w.cfg, w.schemaType, w.val) } func (w *_builder) Reset() { @@ -836,6 +842,35 @@ func (w *_assembler) AssignBool(b bool) error { 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 { + return err + } + } + return nil +} + func (w *_assembler) AssignInt(i int64) error { if err := compatibleKind(w.schemaType, datamodel.Kind_Int); err != nil { return err @@ -1064,6 +1099,9 @@ 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) } @@ -1441,12 +1479,7 @@ func (w *_structIterator) Next() (key, value datamodel.Node, _ error) { // field holds a datamodel.Node return key, nonPtrVal(val).Interface().(datamodel.Node), nil } - node := &_node{ - cfg: w.cfg, - schemaType: field.Type(), - val: val, - } - return key, node, nil + return key, newNode(w.cfg, field.Type(), val), nil } func (w *_structIterator) Done() bool { @@ -1471,11 +1504,7 @@ func (w *_mapIterator) Next() (key, value datamodel.Node, _ error) { val := w.valuesVal.MapIndex(goKey) w.nextIndex++ - key = &_node{ - cfg: w.cfg, - 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 { @@ -1499,12 +1528,7 @@ func (w *_mapIterator) Next() (key, value datamodel.Node, _ error) { // Values holds datamodel.Nodes return key, nonPtrVal(val).Interface().(datamodel.Node), nil } - node := &_node{ - cfg: w.cfg, - 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 { @@ -1542,7 +1566,7 @@ func (w *_listIterator) Next() (index int64, value datamodel.Node, _ error) { // values are Any, assume that they are datamodel.Nodes return idx, nonPtrVal(val).Interface().(datamodel.Node), nil } - return idx, &_node{cfg: w.cfg, schemaType: w.schemaType.ValueType(), val: val}, nil + return idx, newNode(w.cfg, w.schemaType.ValueType(), val), nil } func (w *_listIterator) Done() bool { @@ -1573,11 +1597,7 @@ func (w *_unionIterator) Next() (key, value datamodel.Node, _ error) { } mtyp := w.members[haveIdx] - node := &_node{ - cfg: w.cfg, - schemaType: mtyp, - val: mval, - } + node := newNode(w.cfg, mtyp, mval) key = basicnode.NewString(mtyp.Name()) return key, node, nil } @@ -1585,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/repr.go b/node/bindnode/repr.go index 22eef924..993defbd 100644 --- a/node/bindnode/repr.go +++ b/node/bindnode/repr.go @@ -656,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: @@ -820,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) }