github.com/ipld/go-ipld-prime@v0.21.0/node/bindnode/api.go (about)

     1  // Package bindnode provides a datamodel.Node implementation via Go reflection.
     2  //
     3  // This package is EXPERIMENTAL; its behavior and API might change as it's still
     4  // in development.
     5  package bindnode
     6  
     7  import (
     8  	"reflect"
     9  
    10  	"github.com/ipfs/go-cid"
    11  	"github.com/ipld/go-ipld-prime/datamodel"
    12  	"github.com/ipld/go-ipld-prime/schema"
    13  )
    14  
    15  // Prototype implements a schema.TypedPrototype given a Go pointer type and an
    16  // IPLD schema type. Note that the result is also a datamodel.NodePrototype.
    17  //
    18  // If both the Go type and schema type are supplied, it is assumed that they are
    19  // compatible with one another.
    20  //
    21  // If either the Go type or schema type are nil, we infer the missing type from
    22  // the other provided type. For example, we can infer an unnamed Go struct type
    23  // for a schema struct type, and we can infer a schema Int type for a Go int64
    24  // type. The inferring logic is still a work in progress and subject to change.
    25  // At this time, inferring IPLD Unions and Enums from Go types is not supported.
    26  //
    27  // When supplying a non-nil ptrType, Prototype only obtains the Go pointer type
    28  // from it, so its underlying value will typically be nil. For example:
    29  //
    30  //	proto := bindnode.Prototype((*goType)(nil), schemaType)
    31  func Prototype(ptrType interface{}, schemaType schema.Type, options ...Option) schema.TypedPrototype {
    32  	if ptrType == nil && schemaType == nil {
    33  		panic("bindnode: either ptrType or schemaType must not be nil")
    34  	}
    35  
    36  	cfg := applyOptions(options...)
    37  
    38  	// TODO: if both are supplied, verify that they are compatible
    39  
    40  	var goType reflect.Type
    41  	if ptrType == nil {
    42  		goType = inferGoType(schemaType, make(map[schema.TypeName]inferredStatus), 0)
    43  	} else {
    44  		goPtrType := reflect.TypeOf(ptrType)
    45  		if goPtrType.Kind() != reflect.Ptr {
    46  			panic("bindnode: ptrType must be a pointer")
    47  		}
    48  		goType = goPtrType.Elem()
    49  		if goType.Kind() == reflect.Ptr {
    50  			panic("bindnode: ptrType must not be a pointer to a pointer")
    51  		}
    52  
    53  		if schemaType == nil {
    54  			schemaType = inferSchema(goType, 0)
    55  		} else {
    56  			verifyCompatibility(cfg, make(map[seenEntry]bool), goType, schemaType)
    57  		}
    58  	}
    59  
    60  	return &_prototype{cfg: cfg, schemaType: schemaType, goType: goType}
    61  }
    62  
    63  type converter struct {
    64  	kind schema.TypeKind
    65  
    66  	customFromBool func(bool) (interface{}, error)
    67  	customToBool   func(interface{}) (bool, error)
    68  
    69  	customFromInt func(int64) (interface{}, error)
    70  	customToInt   func(interface{}) (int64, error)
    71  
    72  	customFromFloat func(float64) (interface{}, error)
    73  	customToFloat   func(interface{}) (float64, error)
    74  
    75  	customFromString func(string) (interface{}, error)
    76  	customToString   func(interface{}) (string, error)
    77  
    78  	customFromBytes func([]byte) (interface{}, error)
    79  	customToBytes   func(interface{}) ([]byte, error)
    80  
    81  	customFromLink func(cid.Cid) (interface{}, error)
    82  	customToLink   func(interface{}) (cid.Cid, error)
    83  
    84  	customFromAny func(datamodel.Node) (interface{}, error)
    85  	customToAny   func(interface{}) (datamodel.Node, error)
    86  }
    87  
    88  type config map[reflect.Type]*converter
    89  
    90  // this mainly exists to short-circuit the nonPtrType() call; the `Type()` variant
    91  // exists for completeness
    92  func (c config) converterFor(val reflect.Value) *converter {
    93  	if len(c) == 0 {
    94  		return nil
    95  	}
    96  	return c[nonPtrType(val)]
    97  }
    98  
    99  func (c config) converterForType(typ reflect.Type) *converter {
   100  	if len(c) == 0 {
   101  		return nil
   102  	}
   103  	return c[typ]
   104  }
   105  
   106  // Option is able to apply custom options to the bindnode API
   107  type Option func(config)
   108  
   109  // TypedBoolConverter adds custom converter functions for a particular
   110  // type as identified by a pointer in the first argument.
   111  // The fromFunc is of the form: func(bool) (interface{}, error)
   112  // and toFunc is of the form: func(interface{}) (bool, error)
   113  // where interface{} is a pointer form of the type we are converting.
   114  //
   115  // TypedBoolConverter is an EXPERIMENTAL API and may be removed or
   116  // changed in a future release.
   117  func TypedBoolConverter(ptrVal interface{}, from func(bool) (interface{}, error), to func(interface{}) (bool, error)) Option {
   118  	customType := nonPtrType(reflect.ValueOf(ptrVal))
   119  	converter := &converter{
   120  		kind:           schema.TypeKind_Bool,
   121  		customFromBool: from,
   122  		customToBool:   to,
   123  	}
   124  	return func(cfg config) {
   125  		cfg[customType] = converter
   126  	}
   127  }
   128  
   129  // TypedIntConverter adds custom converter functions for a particular
   130  // type as identified by a pointer in the first argument.
   131  // The fromFunc is of the form: func(int64) (interface{}, error)
   132  // and toFunc is of the form: func(interface{}) (int64, error)
   133  // where interface{} is a pointer form of the type we are converting.
   134  //
   135  // TypedIntConverter is an EXPERIMENTAL API and may be removed or
   136  // changed in a future release.
   137  func TypedIntConverter(ptrVal interface{}, from func(int64) (interface{}, error), to func(interface{}) (int64, error)) Option {
   138  	customType := nonPtrType(reflect.ValueOf(ptrVal))
   139  	converter := &converter{
   140  		kind:          schema.TypeKind_Int,
   141  		customFromInt: from,
   142  		customToInt:   to,
   143  	}
   144  	return func(cfg config) {
   145  		cfg[customType] = converter
   146  	}
   147  }
   148  
   149  // TypedFloatConverter adds custom converter functions for a particular
   150  // type as identified by a pointer in the first argument.
   151  // The fromFunc is of the form: func(float64) (interface{}, error)
   152  // and toFunc is of the form: func(interface{}) (float64, error)
   153  // where interface{} is a pointer form of the type we are converting.
   154  //
   155  // TypedFloatConverter is an EXPERIMENTAL API and may be removed or
   156  // changed in a future release.
   157  func TypedFloatConverter(ptrVal interface{}, from func(float64) (interface{}, error), to func(interface{}) (float64, error)) Option {
   158  	customType := nonPtrType(reflect.ValueOf(ptrVal))
   159  	converter := &converter{
   160  		kind:            schema.TypeKind_Float,
   161  		customFromFloat: from,
   162  		customToFloat:   to,
   163  	}
   164  	return func(cfg config) {
   165  		cfg[customType] = converter
   166  	}
   167  }
   168  
   169  // TypedStringConverter adds custom converter functions for a particular
   170  // type as identified by a pointer in the first argument.
   171  // The fromFunc is of the form: func(string) (interface{}, error)
   172  // and toFunc is of the form: func(interface{}) (string, error)
   173  // where interface{} is a pointer form of the type we are converting.
   174  //
   175  // TypedStringConverter is an EXPERIMENTAL API and may be removed or
   176  // changed in a future release.
   177  func TypedStringConverter(ptrVal interface{}, from func(string) (interface{}, error), to func(interface{}) (string, error)) Option {
   178  	customType := nonPtrType(reflect.ValueOf(ptrVal))
   179  	converter := &converter{
   180  		kind:             schema.TypeKind_String,
   181  		customFromString: from,
   182  		customToString:   to,
   183  	}
   184  	return func(cfg config) {
   185  		cfg[customType] = converter
   186  	}
   187  }
   188  
   189  // TypedBytesConverter adds custom converter functions for a particular
   190  // type as identified by a pointer in the first argument.
   191  // The fromFunc is of the form: func([]byte) (interface{}, error)
   192  // and toFunc is of the form: func(interface{}) ([]byte, error)
   193  // where interface{} is a pointer form of the type we are converting.
   194  //
   195  // TypedBytesConverter is an EXPERIMENTAL API and may be removed or
   196  // changed in a future release.
   197  func TypedBytesConverter(ptrVal interface{}, from func([]byte) (interface{}, error), to func(interface{}) ([]byte, error)) Option {
   198  	customType := nonPtrType(reflect.ValueOf(ptrVal))
   199  	converter := &converter{
   200  		kind:            schema.TypeKind_Bytes,
   201  		customFromBytes: from,
   202  		customToBytes:   to,
   203  	}
   204  	return func(cfg config) {
   205  		cfg[customType] = converter
   206  	}
   207  }
   208  
   209  // TypedLinkConverter adds custom converter functions for a particular
   210  // type as identified by a pointer in the first argument.
   211  // The fromFunc is of the form: func([]byte) (interface{}, error)
   212  // and toFunc is of the form: func(interface{}) ([]byte, error)
   213  // where interface{} is a pointer form of the type we are converting.
   214  //
   215  // Beware that this API is only compatible with cidlink.Link types in the data
   216  // model and may result in errors if attempting to convert from other
   217  // datamodel.Link types.
   218  //
   219  // TypedLinkConverter is an EXPERIMENTAL API and may be removed or
   220  // changed in a future release.
   221  func TypedLinkConverter(ptrVal interface{}, from func(cid.Cid) (interface{}, error), to func(interface{}) (cid.Cid, error)) Option {
   222  	customType := nonPtrType(reflect.ValueOf(ptrVal))
   223  	converter := &converter{
   224  		kind:           schema.TypeKind_Link,
   225  		customFromLink: from,
   226  		customToLink:   to,
   227  	}
   228  	return func(cfg config) {
   229  		cfg[customType] = converter
   230  	}
   231  }
   232  
   233  // TypedAnyConverter adds custom converter functions for a particular
   234  // type as identified by a pointer in the first argument.
   235  // The fromFunc is of the form: func(datamodel.Node) (interface{}, error)
   236  // and toFunc is of the form: func(interface{}) (datamodel.Node, error)
   237  // where interface{} is a pointer form of the type we are converting.
   238  //
   239  // This method should be able to deal with all forms of Any and return an error
   240  // if the expected data forms don't match the expected.
   241  //
   242  // TypedAnyConverter is an EXPERIMENTAL API and may be removed or
   243  // changed in a future release.
   244  func TypedAnyConverter(ptrVal interface{}, from func(datamodel.Node) (interface{}, error), to func(interface{}) (datamodel.Node, error)) Option {
   245  	customType := nonPtrType(reflect.ValueOf(ptrVal))
   246  	converter := &converter{
   247  		kind:          schema.TypeKind_Any,
   248  		customFromAny: from,
   249  		customToAny:   to,
   250  	}
   251  	return func(cfg config) {
   252  		cfg[customType] = converter
   253  	}
   254  }
   255  
   256  func applyOptions(opt ...Option) config {
   257  	if len(opt) == 0 {
   258  		// no need to allocate, we access it via converterFor and converterForType
   259  		// which are safe for nil maps
   260  		return nil
   261  	}
   262  	cfg := make(map[reflect.Type]*converter)
   263  	for _, o := range opt {
   264  		o(cfg)
   265  	}
   266  	return cfg
   267  }
   268  
   269  // Wrap implements a schema.TypedNode given a non-nil pointer to a Go value and an
   270  // IPLD schema type. Note that the result is also a datamodel.Node.
   271  //
   272  // Wrap is meant to be used when one already has a Go value with data.
   273  // As such, ptrVal must not be nil.
   274  //
   275  // Similar to Prototype, if schemaType is non-nil it is assumed to be compatible
   276  // with the Go type, and otherwise it's inferred from the Go type.
   277  func Wrap(ptrVal interface{}, schemaType schema.Type, options ...Option) schema.TypedNode {
   278  	if ptrVal == nil {
   279  		panic("bindnode: ptrVal must not be nil")
   280  	}
   281  	goPtrVal := reflect.ValueOf(ptrVal)
   282  	if goPtrVal.Kind() != reflect.Ptr {
   283  		panic("bindnode: ptrVal must be a pointer")
   284  	}
   285  	if goPtrVal.IsNil() {
   286  		// Note that this can happen if ptrVal was a typed nil.
   287  		panic("bindnode: ptrVal must not be nil")
   288  	}
   289  	cfg := applyOptions(options...)
   290  	goVal := goPtrVal.Elem()
   291  	if goVal.Kind() == reflect.Ptr {
   292  		panic("bindnode: ptrVal must not be a pointer to a pointer")
   293  	}
   294  	if schemaType == nil {
   295  		schemaType = inferSchema(goVal.Type(), 0)
   296  	} else {
   297  		// TODO(rvagg): explore ways to make this skippable by caching in the schema.Type
   298  		// passed in to this function; e.g. if you call Prototype(), then you've gone through
   299  		// this already, then calling .Type() on that could return a bindnode version of
   300  		// schema.Type that has the config cached and can be assumed to have been checked or
   301  		// inferred.
   302  		verifyCompatibility(cfg, make(map[seenEntry]bool), goVal.Type(), schemaType)
   303  	}
   304  	return newNode(cfg, schemaType, goVal)
   305  }
   306  
   307  // TODO: consider making our own Node interface, like:
   308  //
   309  // type WrappedNode interface {
   310  //     datamodel.Node
   311  //     Unwrap() (ptrVal interface)
   312  // }
   313  //
   314  // Pros: API is easier to understand, harder to mix up with other datamodel.Nodes.
   315  // Cons: One usually only has a datamodel.Node, and type assertions can be weird.
   316  
   317  // Unwrap takes a datamodel.Node implemented by Prototype or Wrap,
   318  // and returns a pointer to the inner Go value.
   319  //
   320  // Unwrap returns nil if the node isn't implemented by this package.
   321  func Unwrap(node datamodel.Node) (ptrVal interface{}) {
   322  	var val reflect.Value
   323  	switch node := node.(type) {
   324  	case *_node:
   325  		val = node.val
   326  	case *_nodeRepr:
   327  		val = node.val
   328  	default:
   329  		return nil
   330  	}
   331  	if val.Kind() == reflect.Ptr {
   332  		panic("bindnode: didn't expect val to be a pointer")
   333  	}
   334  	return val.Addr().Interface()
   335  }