Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.18 tracking #444

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 221 additions & 6 deletions node/bindnode/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -27,11 +28,13 @@ import (
// from it, so its underlying value will typically be nil. For example:
//
// proto := bindnode.Prototype((*goType)(nil), schemaType)
func Prototype(ptrType interface{}, schemaType schema.Type) schema.TypedPrototype {
func Prototype(ptrType interface{}, schemaType schema.Type, options ...Option) schema.TypedPrototype {
if ptrType == nil && schemaType == nil {
panic("bindnode: either ptrType or schemaType must not be nil")
}

cfg := applyOptions(options...)

// TODO: if both are supplied, verify that they are compatible

var goType reflect.Type
Expand All @@ -50,11 +53,217 @@ func Prototype(ptrType interface{}, schemaType schema.Type) schema.TypedPrototyp
if schemaType == nil {
schemaType = inferSchema(goType, 0)
} else {
verifyCompatibility(make(map[seenEntry]bool), goType, schemaType)
verifyCompatibility(cfg, make(map[seenEntry]bool), goType, schemaType)
}
}

return &_prototype{schemaType: schemaType, goType: goType}
return &_prototype{cfg: cfg, schemaType: schemaType, goType: goType}
}

type converter struct {
kind schema.TypeKind

customFromBool func(bool) (interface{}, error)
customToBool func(interface{}) (bool, error)

customFromInt func(int64) (interface{}, error)
customToInt func(interface{}) (int64, error)

customFromFloat func(float64) (interface{}, error)
customToFloat func(interface{}) (float64, error)

customFromString func(string) (interface{}, error)
customToString func(interface{}) (string, error)

customFromBytes func([]byte) (interface{}, error)
customToBytes func(interface{}) ([]byte, error)

customFromLink func(cid.Cid) (interface{}, error)
customToLink func(interface{}) (cid.Cid, error)

customFromAny func(datamodel.Node) (interface{}, error)
customToAny func(interface{}) (datamodel.Node, error)
}

type config map[reflect.Type]*converter

// this mainly exists to short-circuit the nonPtrType() call; the `Type()` variant
// exists for completeness
func (c config) converterFor(val reflect.Value) *converter {
if len(c) == 0 {
return nil
}
return c[nonPtrType(val)]
}

func (c config) converterForType(typ reflect.Type) *converter {
if len(c) == 0 {
return nil
}
return c[typ]
}

// Option is able to apply custom options to the bindnode API
type Option func(config)

// TypedBoolConverter adds custom converter functions for a particular
// type as identified by a pointer in the first argument.
// The fromFunc is of the form: func(bool) (interface{}, error)
// and toFunc is of the form: func(interface{}) (bool, error)
// where interface{} is a pointer form of the type we are converting.
//
// TypedBoolConverter is an EXPERIMENTAL API and may be removed or
// changed in a future release.
func TypedBoolConverter(ptrVal interface{}, from func(bool) (interface{}, error), to func(interface{}) (bool, error)) Option {
customType := nonPtrType(reflect.ValueOf(ptrVal))
converter := &converter{
kind: schema.TypeKind_Bool,
customFromBool: from,
customToBool: to,
}
return func(cfg config) {
cfg[customType] = converter
}
}

// TypedIntConverter adds custom converter functions for a particular
// type as identified by a pointer in the first argument.
// The fromFunc is of the form: func(int64) (interface{}, error)
// and toFunc is of the form: func(interface{}) (int64, error)
// where interface{} is a pointer form of the type we are converting.
//
// TypedIntConverter is an EXPERIMENTAL API and may be removed or
// changed in a future release.
func TypedIntConverter(ptrVal interface{}, from func(int64) (interface{}, error), to func(interface{}) (int64, error)) Option {
customType := nonPtrType(reflect.ValueOf(ptrVal))
converter := &converter{
kind: schema.TypeKind_Int,
customFromInt: from,
customToInt: to,
}
return func(cfg config) {
cfg[customType] = converter
}
}

// TypedFloatConverter adds custom converter functions for a particular
// type as identified by a pointer in the first argument.
// The fromFunc is of the form: func(float64) (interface{}, error)
// and toFunc is of the form: func(interface{}) (float64, error)
// where interface{} is a pointer form of the type we are converting.
//
// TypedFloatConverter is an EXPERIMENTAL API and may be removed or
// changed in a future release.
func TypedFloatConverter(ptrVal interface{}, from func(float64) (interface{}, error), to func(interface{}) (float64, error)) Option {
customType := nonPtrType(reflect.ValueOf(ptrVal))
converter := &converter{
kind: schema.TypeKind_Float,
customFromFloat: from,
customToFloat: to,
}
return func(cfg config) {
cfg[customType] = converter
}
}

// TypedStringConverter adds custom converter functions for a particular
// type as identified by a pointer in the first argument.
// The fromFunc is of the form: func(string) (interface{}, error)
// and toFunc is of the form: func(interface{}) (string, error)
// where interface{} is a pointer form of the type we are converting.
//
// TypedStringConverter is an EXPERIMENTAL API and may be removed or
// changed in a future release.
func TypedStringConverter(ptrVal interface{}, from func(string) (interface{}, error), to func(interface{}) (string, error)) Option {
customType := nonPtrType(reflect.ValueOf(ptrVal))
converter := &converter{
kind: schema.TypeKind_String,
customFromString: from,
customToString: to,
}
return func(cfg config) {
cfg[customType] = converter
}
}

// TypedBytesConverter adds custom converter functions for a particular
// type as identified by a pointer in the first argument.
// The fromFunc is of the form: func([]byte) (interface{}, error)
// and toFunc is of the form: func(interface{}) ([]byte, error)
// where interface{} is a pointer form of the type we are converting.
//
// TypedBytesConverter is an EXPERIMENTAL API and may be removed or
// changed in a future release.
func TypedBytesConverter(ptrVal interface{}, from func([]byte) (interface{}, error), to func(interface{}) ([]byte, error)) Option {
customType := nonPtrType(reflect.ValueOf(ptrVal))
converter := &converter{
kind: schema.TypeKind_Bytes,
customFromBytes: from,
customToBytes: to,
}
return func(cfg config) {
cfg[customType] = converter
}
}

// TypedLinkConverter adds custom converter functions for a particular
// type as identified by a pointer in the first argument.
// The fromFunc is of the form: func([]byte) (interface{}, error)
// and toFunc is of the form: func(interface{}) ([]byte, error)
// where interface{} is a pointer form of the type we are converting.
//
// Beware that this API is only compatible with cidlink.Link types in the data
// model and may result in errors if attempting to convert from other
// datamodel.Link types.
//
// TypedLinkConverter is an EXPERIMENTAL API and may be removed or
// changed in a future release.
func TypedLinkConverter(ptrVal interface{}, from func(cid.Cid) (interface{}, error), to func(interface{}) (cid.Cid, error)) Option {
customType := nonPtrType(reflect.ValueOf(ptrVal))
converter := &converter{
kind: schema.TypeKind_Link,
customFromLink: from,
customToLink: to,
}
return func(cfg config) {
cfg[customType] = converter
}
}

// TypedAnyConverter adds custom converter functions for a particular
// type as identified by a pointer in the first argument.
// The fromFunc is of the form: func(datamodel.Node) (interface{}, error)
// and toFunc is of the form: func(interface{}) (datamodel.Node, error)
// where interface{} is a pointer form of the type we are converting.
//
// This method should be able to deal with all forms of Any and return an error
// if the expected data forms don't match the expected.
//
// TypedAnyConverter is an EXPERIMENTAL API and may be removed or
// changed in a future release.
func TypedAnyConverter(ptrVal interface{}, from func(datamodel.Node) (interface{}, error), to func(interface{}) (datamodel.Node, error)) Option {
customType := nonPtrType(reflect.ValueOf(ptrVal))
converter := &converter{
kind: schema.TypeKind_Any,
customFromAny: from,
customToAny: to,
}
return func(cfg config) {
cfg[customType] = converter
}
}

func applyOptions(opt ...Option) config {
if len(opt) == 0 {
// no need to allocate, we access it via converterFor and converterForType
// which are safe for nil maps
return nil
}
cfg := make(map[reflect.Type]*converter)
for _, o := range opt {
o(cfg)
}
return cfg
}

// Wrap implements a schema.TypedNode given a non-nil pointer to a Go value and an
Expand All @@ -65,7 +274,7 @@ func Prototype(ptrType interface{}, schemaType schema.Type) schema.TypedPrototyp
//
// Similar to Prototype, if schemaType is non-nil it is assumed to be compatible
// with the Go type, and otherwise it's inferred from the Go type.
func Wrap(ptrVal interface{}, schemaType schema.Type) schema.TypedNode {
func Wrap(ptrVal interface{}, schemaType schema.Type, options ...Option) schema.TypedNode {
if ptrVal == nil {
panic("bindnode: ptrVal must not be nil")
}
Expand All @@ -77,16 +286,22 @@ 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")
}
if schemaType == nil {
schemaType = inferSchema(goVal.Type(), 0)
} else {
verifyCompatibility(make(map[seenEntry]bool), goVal.Type(), schemaType)
// TODO(rvagg): explore ways to make this skippable by caching in the schema.Type
// passed in to this function; e.g. if you call Prototype(), then you've gone through
// this already, then calling .Type() on that could return a bindnode version of
// schema.Type that has the config cached and can be assumed to have been checked or
// inferred.
verifyCompatibility(cfg, make(map[seenEntry]bool), goVal.Type(), schemaType)
}
return &_node{val: goVal, schemaType: schemaType}
return newNode(cfg, schemaType, goVal)
}

// TODO: consider making our own Node interface, like:
Expand Down
75 changes: 75 additions & 0 deletions node/bindnode/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bindnode_test

import (
"encoding/hex"
"math"
"testing"

qt "github.com/frankban/quicktest"
Expand Down Expand Up @@ -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)
})
}
Loading