Commit 2fc002c2 authored by Daniel Martí's avatar Daniel Martí

node/bindnode: rethink and better document APIs

Before, we envisioned exposing the following APIs:

	Prototype
	PrototypeNoSchema
	PrototypeOnlySchema
	Wrap
	WrapNoSchema

The names were long, though, and perhaps a bit confusing too. Instead,
just expose Prototype and Wrap, and allow either of the parameters to be
nil to infer its value. This makes the API easier to navigate, and we
can concentrate documentation in fewer places.

Now there's two main entrypoints: Prototype if one wants a high-level
prototype to create nodes of a certain shape, and Wrap if one wants to
expose an existing Go value as an IPLD node. Whether one supplies only
an IPLD Schema, only a Go type/value, or both, is their choice. The only
exception is Wrap, which always requires a Go value by definition.

Wrap works now, too, and gains an example. It's the same idea as the
previous Prototype example, but it builds the value via a Go composite
literal rather than fluent/qp on the IPLD side.

Another noteworthy change is that we now return TypedPrototype and
TypedNode rather than the regular NodePrototype and Node interfaces,
since bindnode always works with a schema - even when it's inferred.
The big advantage here is that one can call the Representation method
directly on the result without having to type assert, like the example
does.
parent b832b762
// 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()
}
......@@ -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"]}
......
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")
}
......@@ -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 (
......
......@@ -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)
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment