diff --git a/node/bindnode/api.go b/node/bindnode/api.go new file mode 100644 index 0000000000000000000000000000000000000000..239c45771b3a34b3adf8571c1ac0d664925d4bd7 --- /dev/null +++ b/node/bindnode/api.go @@ -0,0 +1,95 @@ +// Package bindnode provides an ipld.Node implementation via Go reflection. +package bindnode + +import ( + "reflect" + + ipld "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/schema" +) + +// Prototype implements a TypedPrototype given a Go pointer type and an IPLD +// schema type. Note that the result is also an ipld.NodePrototype. +// +// If both the Go type and schema type are supplied, it is assumed that they are +// compatible with one another. +// +// If either the Go type or schema type are nil, we infer the missing type from +// the other provided type. For example, we can infer an unnamed Go struct type +// for a schema struct tyep, and we can infer a schema Int type for a Go int64 +// type. The inferring logic is still a work in progress and subject to change. +// +// When supplying a non-nil ptrType, Prototype only obtains the Go pointer type +// 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) TypedPrototype { + if ptrType == nil && schemaType == nil { + panic("either ptrType or schemaType must not be nil") + } + + // TODO: if both are supplied, verify that they are compatible + + var goType reflect.Type + if ptrType == nil { + goType = inferGoType(schemaType) + } else { + goPtrType := reflect.TypeOf(ptrType) + if goPtrType.Kind() != reflect.Ptr { + panic("ptrType must be a pointer") + } + goType = goPtrType.Elem() + } + + if schemaType == nil { + schemaType = inferSchema(goType) + } + + return &_prototype{schemaType: schemaType, goType: goType} +} + +// Wrap implements a schema.TypedNode given a non-nil pointer to a Go value and an +// IPLD schema type. Note that the result is also an ipld.Node. +// +// Wrap is meant to be used when one already has a Go value with data. +// As such, ptrVal must not be nil. +// +// 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 { + if ptrVal == nil { + panic("ptrVal must not be nil") + } + goPtrVal := reflect.ValueOf(ptrVal) + if goPtrVal.Kind() != reflect.Ptr { + panic("ptrVal must be a pointer") + } + if goPtrVal.IsNil() { + panic("ptrVal must not be nil") + } + goVal := goPtrVal.Elem() + if schemaType == nil { + schemaType = inferSchema(goVal.Type()) + } + return &_node{val: goVal, schemaType: schemaType} +} + +// Unwrap takes an ipld.Node implemented by Prototype or Wrap, +// and returns a pointer to the inner Go value. +// +// Unwrap returns nil if the node isn't implemented by this package. +func Unwrap(node ipld.Node) (ptr interface{}) { + var val reflect.Value + switch node := node.(type) { + case *_node: + val = node.val + case *_nodeRepr: + val = node.val + default: + return nil + } + if val.Kind() == reflect.Ptr { + panic("didn't expect val to be a pointer") + } + return val.Addr().Interface() +} diff --git a/node/bindnode/example_test.go b/node/bindnode/example_test.go index 17db56a111b3b2312af5480ac86d3cb90ea48657..199d6cf9c23461c725727f28face13dbfc8669ee 100644 --- a/node/bindnode/example_test.go +++ b/node/bindnode/example_test.go @@ -8,10 +8,10 @@ import ( "github.com/ipld/go-ipld-prime/fluent/qp" "github.com/ipld/go-ipld-prime/node/bindnode" "github.com/ipld/go-ipld-prime/schema" - "github.com/polydawn/refmt/json" + refmtjson "github.com/polydawn/refmt/json" ) -func ExamplePrototypeOnlySchema() { +func ExampleWrap_withSchema() { ts := schema.TypeSystem{} ts.Init() ts.Accumulate(schema.SpawnString("String")) @@ -27,9 +27,44 @@ func ExamplePrototypeOnlySchema() { ts.Accumulate(schema.SpawnList("List_String", "String", false)) schemaType := ts.TypeByName("Person") - proto := bindnode.PrototypeOnlySchema(schemaType) - n, err := qp.BuildMap(proto, -1, func(ma ipld.MapAssembler) { + type Person struct { + Name string + Age *int64 // optional + Friends []string + } + person := &Person{ + Name: "Michael", + Friends: []string{"Sarah", "Alex"}, + } + node := bindnode.Wrap(person, schemaType) + + nodeRepr := node.Representation() + dagjson.Marshal(nodeRepr, refmtjson.NewEncoder(os.Stdout, refmtjson.EncodeOptions{}), true) + + // Output: + // {"Name":"Michael","Friends":["Sarah","Alex"]} +} + +func ExamplePrototype_onlySchema() { + ts := schema.TypeSystem{} + ts.Init() + ts.Accumulate(schema.SpawnString("String")) + ts.Accumulate(schema.SpawnInt("Int")) + ts.Accumulate(schema.SpawnStruct("Person", + []schema.StructField{ + schema.SpawnStructField("Name", "String", false, false), + schema.SpawnStructField("Age", "Int", true, false), + schema.SpawnStructField("Friends", "List_String", false, false), + }, + schema.SpawnStructRepresentationMap(nil), + )) + ts.Accumulate(schema.SpawnList("List_String", "String", false)) + + schemaType := ts.TypeByName("Person") + proto := bindnode.Prototype(nil, schemaType) + + node, err := qp.BuildMap(proto, -1, func(ma ipld.MapAssembler) { qp.MapEntry(ma, "Name", qp.String("Michael")) qp.MapEntry(ma, "Friends", qp.List(-1, func(la ipld.ListAssembler) { qp.ListEntry(la, qp.String("Sarah")) @@ -39,8 +74,9 @@ func ExamplePrototypeOnlySchema() { if err != nil { panic(err) } - nr := n.(schema.TypedNode).Representation() - dagjson.Marshal(nr, json.NewEncoder(os.Stdout, json.EncodeOptions{}), true) + + nodeRepr := node.(schema.TypedNode).Representation() + dagjson.Marshal(nodeRepr, refmtjson.NewEncoder(os.Stdout, refmtjson.EncodeOptions{}), true) // Output: // {"Name":"Michael","Friends":["Sarah","Alex"]} diff --git a/node/bindnode/infer.go b/node/bindnode/infer.go new file mode 100644 index 0000000000000000000000000000000000000000..a8506e06ba904130e5076c867e53f52a9510bc8b --- /dev/null +++ b/node/bindnode/infer.go @@ -0,0 +1,92 @@ +package bindnode + +import ( + "fmt" + "reflect" + "strings" + + "github.com/ipld/go-ipld-prime/schema" +) + +// Consider exposing these APIs later, if they might be useful. + +func inferGoType(typ schema.Type) reflect.Type { + switch typ := typ.(type) { + case *schema.TypeBool: + return goTypeBool + case *schema.TypeInt: + return goTypeInt + case *schema.TypeFloat: + return goTypeFloat + case *schema.TypeString: + return goTypeString + case *schema.TypeBytes: + return goTypeBytes + case *schema.TypeStruct: + fields := typ.Fields() + goFields := make([]reflect.StructField, len(fields)) + for i, field := range fields { + ftyp := inferGoType(field.Type()) + if field.IsNullable() { + ftyp = reflect.PtrTo(ftyp) + } + if field.IsOptional() { + ftyp = reflect.PtrTo(ftyp) + } + goFields[i] = reflect.StructField{ + Name: fieldNameFromSchema(field.Name()), + Type: ftyp, + } + } + return reflect.StructOf(goFields) + case *schema.TypeMap: + ktyp := inferGoType(typ.KeyType()) + vtyp := inferGoType(typ.ValueType()) + if typ.ValueIsNullable() { + vtyp = reflect.PtrTo(vtyp) + } + // We need an extra field to keep the map ordered, + // since IPLD maps must have stable iteration order. + // We could sort when iterating, but that's expensive. + // Keeping the insertion order is easy and intuitive. + // + // struct { + // Keys []K + // Values map[K]V + // } + goFields := []reflect.StructField{ + { + Name: "Keys", + Type: reflect.SliceOf(ktyp), + }, + { + Name: "Values", + Type: reflect.MapOf(ktyp, vtyp), + }, + } + return reflect.StructOf(goFields) + case *schema.TypeList: + etyp := inferGoType(typ.ValueType()) + if typ.ValueIsNullable() { + etyp = reflect.PtrTo(etyp) + } + return reflect.SliceOf(etyp) + case *schema.TypeUnion: + // We need an extra field to record what member we stored. + type goUnion struct { + Index int // 0..len(typ.Members)-1 + Value interface{} + } + return reflect.TypeOf(goUnion{}) + } + panic(fmt.Sprintf("%T\n", typ)) +} + +// from IPLD Schema field names like "foo" to Go field names like "Foo". +func fieldNameFromSchema(name string) string { + return strings.Title(name) +} + +func inferSchema(typ reflect.Type) schema.Type { + panic("TODO") +} diff --git a/node/bindnode/node.go b/node/bindnode/node.go index b0ba3bc1b42f68f111f70d56c61e50ad76d63ce3..3099d8f4eebd4e9b59034acd05d04bbf260d5109 100644 --- a/node/bindnode/node.go +++ b/node/bindnode/node.go @@ -3,178 +3,12 @@ package bindnode import ( "fmt" "reflect" - "strings" ipld "github.com/ipld/go-ipld-prime" basicnode "github.com/ipld/go-ipld-prime/node/basic" "github.com/ipld/go-ipld-prime/schema" ) -// WrapNoSchema implements an ipld.Node given a pointer to a Go value. -// -// Same rules as PrototypeNoSchema apply. -func WrapNoSchema(ptr interface{}) ipld.Node { - panic("TODO") - // ptrVal := reflect.ValueOf(ptr) - // if ptrVal.Kind() != reflect.Ptr { - // panic("must be a pointer") - // } - // return &_node{val: ptrVal.Elem()} -} - -// Unwrap takes an ipld.Node implemented by one of the Wrap* or Prototype* APIs, -// and returns a pointer to the inner Go value. -// -// Unwrap returns the input node if the node isn't implemented by this package. -func Unwrap(node ipld.Node) (ptr interface{}) { - var val reflect.Value - switch node := node.(type) { - case *_node: - val = node.val - case *_nodeRepr: - val = node.val - default: - return node - } - if val.Kind() == reflect.Ptr { - panic("didn't expect val to be a pointer") - } - if !val.CanAddr() { - // Not addressable? Just return the interface as-is. - // TODO: This happens in some tests, figure out why. - return val.Interface() - } - return val.Addr().Interface() -} - -// PrototypeNoSchema implements an ipld.NodePrototype given a Go pointer type. -// -// In this form, no IPLD schema is used; it is entirely inferred from the Go -// type. -// -// Go types map to schema types in simple ways: Go string to schema String, Go -// []byte to schema Bytes, Go struct to schema Map, and so on. -// -// A Go struct field is optional when its type is a pointer. Nullable fields are -// not supported in this mode. -func PrototypeNoSchema(ptrType interface{}) ipld.NodePrototype { - panic("TODO") - // typ := reflect.TypeOf(ptrType) - // if typ.Kind() != reflect.Ptr { - // panic("must be a pointer") - // } - // return &_prototype{goType: typ.Elem()} -} - -// PrototypeOnlySchema implements an ipld.NodePrototype given an IPLD schema type. -// -// In this form, Go values are constructed with types inferred from the IPLD -// schema, like a reverse of PrototypeNoSchema. -func PrototypeOnlySchema(schemaType schema.Type) ipld.NodePrototype { - goType := inferGoType(schemaType) - return prototype(goType, schemaType) -} - -// from IPLD Schema field names like "foo" to Go field names like "Foo". -func fieldNameFromSchema(name string) string { - return strings.Title(name) -} - -func inferGoType(typ schema.Type) reflect.Type { - switch typ := typ.(type) { - case *schema.TypeBool: - return goTypeBool - case *schema.TypeInt: - return goTypeInt - case *schema.TypeFloat: - return goTypeFloat - case *schema.TypeString: - return goTypeString - case *schema.TypeBytes: - return goTypeBytes - case *schema.TypeStruct: - fields := typ.Fields() - goFields := make([]reflect.StructField, len(fields)) - for i, field := range fields { - ftyp := inferGoType(field.Type()) - if field.IsNullable() { - ftyp = reflect.PtrTo(ftyp) - } - if field.IsOptional() { - ftyp = reflect.PtrTo(ftyp) - } - goFields[i] = reflect.StructField{ - Name: fieldNameFromSchema(field.Name()), - Type: ftyp, - } - } - return reflect.StructOf(goFields) - case *schema.TypeMap: - ktyp := inferGoType(typ.KeyType()) - vtyp := inferGoType(typ.ValueType()) - if typ.ValueIsNullable() { - vtyp = reflect.PtrTo(vtyp) - } - // We need an extra field to keep the map ordered, - // since IPLD maps must have stable iteration order. - // We could sort when iterating, but that's expensive. - // Keeping the insertion order is easy and intuitive. - // - // struct { - // Keys []K - // Values map[K]V - // } - goFields := []reflect.StructField{ - { - Name: "Keys", - Type: reflect.SliceOf(ktyp), - }, - { - Name: "Values", - Type: reflect.MapOf(ktyp, vtyp), - }, - } - return reflect.StructOf(goFields) - case *schema.TypeList: - etyp := inferGoType(typ.ValueType()) - if typ.ValueIsNullable() { - etyp = reflect.PtrTo(etyp) - } - return reflect.SliceOf(etyp) - case *schema.TypeUnion: - // We need an extra field to record what member we stored. - type goUnion struct { - Index int // 0..len(typ.Members)-1 - Value interface{} - } - return reflect.TypeOf(goUnion{}) - } - panic(fmt.Sprintf("%T\n", typ)) -} - -// Prototype implements an ipld.NodePrototype given a Go pointer type and an -// IPLD schema type. -// -// In this form, it is assumed that the Go type and IPLD schema type are -// compatible. TODO: check upfront and panic otherwise -func Prototype(ptrType interface{}, schemaType schema.Type) ipld.NodePrototype { - goPtrType := reflect.TypeOf(ptrType) - if goPtrType.Kind() != reflect.Ptr { - panic("ptrType must be a pointer") - } - return prototype(goPtrType.Elem(), schemaType) -} - -func prototype(goType reflect.Type, schemaType schema.Type) ipld.NodePrototype { - if goType.Kind() == reflect.Invalid { - panic("goType must be valid") - } - if schemaType == nil { - panic("schemaType must not be nil") - } - return &_prototype{schemaType: schemaType, goType: goType} -} - // Assert that we implement all the interfaces as expected. // Grouped by the interfaces to implement, roughly. var ( diff --git a/node/bindnode/schema_test.go b/node/bindnode/schema_test.go index 69ce096ae721c940f505e486eab6be1f8678576f..fdecd760678f3c7b23fec2d88dea8b65b8baf988 100644 --- a/node/bindnode/schema_test.go +++ b/node/bindnode/schema_test.go @@ -10,7 +10,7 @@ import ( "github.com/ipld/go-ipld-prime/schema" ) -// For now, we simply run all schema tests with PrototypeOnlySchema. +// For now, we simply run all schema tests with Prototype. // In the future, forSchemaTest might return multiple engines. func forSchemaTest(name string) []tests.EngineSubtest { @@ -41,12 +41,8 @@ func (e *bindEngine) PrototypeByName(name string) ipld.NodePrototype { name = strings.TrimSuffix(name, ".Repr") } schemaType := e.ts.TypeByName(name) - if schemaType == nil { - return nil - } - proto := bindnode.PrototypeOnlySchema(schemaType) if wantRepr { - proto = proto.(bindnode.TypedPrototype).Representation() + return bindnode.Prototype(nil, schemaType).Representation() } - return proto + return bindnode.Prototype(nil, schemaType) }