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 }