convert/conv.go
// Package convert provides functions for converting data and functions between Go and Starlark.
package convert
import (
"errors"
"fmt"
"reflect"
"time"
startime "go.starlark.net/lib/time"
"go.starlark.net/resolve"
"go.starlark.net/starlark"
)
func init() {
resolve.AllowSet = true // allow the 'set' built-in
}
// ToValue attempts to convert the given value to a starlark.Value.
// It supports all int, uint, and float numeric types, plus strings and booleans.
// It supports structs, maps, slices, and functions that use the aforementioned.
// Any starlark.Value is passed through as-is.
func ToValue(v interface{}) (starlark.Value, error) {
if val, ok := v.(starlark.Value); ok {
return val, nil
}
return toValue(reflect.ValueOf(v), emptyStr)
}
// ToValueWithTag attempts to convert the given value to a starlark.Value.
// It works like ToValue, but also accepts a tag name to use for all nested struct fields.
func ToValueWithTag(v interface{}, tagName string) (starlark.Value, error) {
if val, ok := v.(starlark.Value); ok {
return val, nil
}
return toValue(reflect.ValueOf(v), tagName)
}
func hasMethods(val reflect.Value) bool {
if val.NumMethod() > 0 {
return true
}
if val.Kind() == reflect.Ptr && val.Elem().IsValid() && val.Elem().NumMethod() > 0 {
return true
}
return false
}
func toValue(val reflect.Value, tagName string) (result starlark.Value, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
if val.IsValid() {
if _, ok := val.Interface().(starlark.Value); ok {
// let Starlark values pass through, no conversion needed
return val.Interface().(starlark.Value), nil
}
if hasMethods(val) {
// this handles all basic types with methods (numbers, strings, booleans)
ifc, ok := makeGoInterface(val)
if ok {
return ifc, nil
}
// TODO: maps, functions, and slices with methods
}
}
kind := val.Kind()
if kind == reflect.Ptr {
if val.Elem().IsValid() {
kind = val.Elem().Kind()
// for pointers to basic types, dereference them
switch kind {
case reflect.Bool,
reflect.String,
reflect.Float32, reflect.Float64,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Slice, reflect.Array, reflect.Map:
val = val.Elem()
}
} else {
// If the pointer is nil and points to a struct, make a GoInterface for it
if val.Type().Elem().Kind() == reflect.Struct {
return &GoInterface{v: val}, nil
}
}
}
switch kind {
case reflect.Bool:
return starlark.Bool(val.Bool()), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return starlark.MakeInt64(val.Int()), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return starlark.MakeUint64(val.Uint()), nil
case reflect.Float32, reflect.Float64:
return starlark.Float(val.Float()), nil
case reflect.Func:
return makeStarFn("fn", val, tagName), nil
case reflect.Map:
return &GoMap{v: val, tag: tagName}, nil
case reflect.String:
return starlark.String(val.String()), nil
case reflect.Slice, reflect.Array:
return &GoSlice{v: val, tag: tagName}, nil
case reflect.Struct:
// handle special case from standard starlark lib
switch val.Type() {
case reflect.TypeOf(time.Time{}):
return startime.Time(val.Interface().(time.Time)), nil
}
return &GoStruct{v: val, tag: tagName}, nil
case reflect.Interface:
return &GoInterface{v: val, tag: tagName}, nil
//if innerVal, ok := val.Interface().(interface{}); ok {
// return ToValueWithTag(innerVal, tagName)
//} else {
// return &GoInterface{v: val, tag: tagName}, nil
//}
case reflect.Invalid:
return starlark.None, nil
}
return nil, fmt.Errorf("type %T is not a supported starlark type", val.Interface())
}
// FromValue converts a starlark value to a go value.
func FromValue(v starlark.Value) interface{} {
switch v := v.(type) {
case starlark.Bool:
return bool(v)
case starlark.Int:
// starlark ints can be signed or unsigned
if i, ok := v.Int64(); ok {
return i
}
if i, ok := v.Uint64(); ok {
return i
}
return v.BigInt()
// buh... maybe > maxint64? Dunno
// panic(fmt.Errorf("can't convert starlark.Int %v to int", v))
case starlark.Float:
return float64(v)
case starlark.String:
return string(v)
case starlark.Bytes:
return []byte(v)
case *starlark.List:
return FromList(v)
case starlark.Tuple:
return FromTuple(v)
case *starlark.Dict:
return FromDict(v)
case *starlark.Set:
return FromSet(v)
case starlark.NoneType:
return nil
case startime.Time:
return time.Time(v)
case *GoStruct:
return v.v.Interface()
case *GoInterface:
return v.v.Interface()
case *GoMap:
return v.v.Interface()
case *GoSlice:
return v.v.Interface()
default:
// dunno, hope it's a custom type that the receiver knows how to deal with.
// This can happen with custom-written go types that implement starlark.Value.
// Or maybe it's a Starlark function, module, or struct.
return v
}
}
// MakeStringDict makes a StringDict from the given arg. The types supported are the same as ToValue.
// It returns an empty dict for nil input.
func MakeStringDict(m map[string]interface{}) (starlark.StringDict, error) {
return makeStringDictTag(m, emptyStr)
}
// MakeStringDictWithTag makes a StringDict from the given arg with custom tag. The types supported are the same as ToValueWithTag.
// It returns an empty dict for nil input.
func MakeStringDictWithTag(m map[string]interface{}, tagName string) (starlark.StringDict, error) {
return makeStringDictTag(m, tagName)
}
func makeStringDictTag(m map[string]interface{}, tagName string) (starlark.StringDict, error) {
dict := make(starlark.StringDict, len(m))
for k, v := range m {
val, err := ToValueWithTag(v, tagName)
if err != nil {
return nil, err
}
dict[k] = val
}
return dict, nil
}
// FromStringDict makes a map[string]interface{} from the given arg. Any inconvertible values are ignored.
func FromStringDict(m starlark.StringDict) map[string]interface{} {
ret := make(map[string]interface{}, len(m))
for k, v := range m {
ret[k] = FromValue(v)
}
return ret
}
// MakeTuple makes a Starlark Tuple from the given Go slice. The types supported are the same as ToValue.
// It returns an empty tuple for nil input.
func MakeTuple(v []interface{}) (starlark.Tuple, error) {
tuple := make(starlark.Tuple, len(v))
for i, val := range v {
item, err := ToValue(val)
if err != nil {
return nil, err
}
tuple[i] = item
}
return tuple, nil
}
// FromTuple converts a starlark.Tuple into a []interface{}.
func FromTuple(v starlark.Tuple) []interface{} {
ret := make([]interface{}, len(v))
for i := range v {
ret[i] = FromValue(v[i])
}
return ret
}
// MakeList makes a Starlark List from the given Go slice. The types supported are the same as ToValue.
// It returns an empty list for nil input.
func MakeList(v []interface{}) (*starlark.List, error) {
values := make([]starlark.Value, len(v))
for i := range v {
item, err := ToValue(v[i])
if err != nil {
return nil, err
}
values[i] = item
}
return starlark.NewList(values), nil
}
// FromList creates a go slice from the given starlark list.
func FromList(l *starlark.List) []interface{} {
// return nil to avoid infinite recursion
if rd.hasVisited(l) {
return nil
}
rd.setVisited(l)
defer rd.clearVisited(l)
ret := make([]interface{}, 0, l.Len())
var v starlark.Value
i := l.Iterate()
defer i.Done()
for i.Next(&v) {
val := FromValue(v)
ret = append(ret, val)
}
return ret
}
// MakeDict makes a Dict from the given map. The acceptable keys and values are the same as ToValue.
// For nil input, it returns an empty Dict. It panics if the input is not a map.
func MakeDict(v interface{}) (starlark.Value, error) {
return makeDictTag(reflect.ValueOf(v), emptyStr)
}
// MakeDictWithTag makes a Dict from the given map with custom tag. The acceptable keys and values are the same as ToValueWithTag.
// For nil input, it returns an empty Dict. It panics if the input is not a map.
func MakeDictWithTag(v interface{}, tagName string) (starlark.Value, error) {
return makeDictTag(reflect.ValueOf(v), tagName)
}
func makeDictTag(val reflect.Value, tagName string) (starlark.Value, error) {
dict := starlark.NewDict(1)
// check if the value is not nil and is a map
if valid := val.IsValid(); valid && val.Kind() != reflect.Map {
// panic if not a map
panic(fmt.Errorf("can't make map of %T", val.Interface()))
} else if valid {
// iterate over the map and convert each key and value
for _, k := range val.MapKeys() {
vk, err := adjustedToValue(k, tagName)
if err != nil {
return nil, err
}
vv, err := adjustedToValue(val.MapIndex(k), tagName)
if err != nil {
return nil, err
}
dict.SetKey(vk, vv)
}
}
return dict, nil
}
// Helper method that checks the input value for interface{} and adjusts the conversion accordingly.
func adjustedToValue(val reflect.Value, tagName string) (starlark.Value, error) {
if val.Kind() == reflect.Interface && val.NumMethod() == 0 && val.Elem().IsValid() {
val = val.Elem()
}
return toValue(val, tagName)
}
// FromDict converts a starlark.Dict to a map[interface{}]interface{}
func FromDict(m *starlark.Dict) map[interface{}]interface{} {
// return nil to avoid infinite recursion
if rd.hasVisited(m) {
return nil
}
rd.setVisited(m)
defer rd.clearVisited(m)
ret := make(map[interface{}]interface{}, m.Len())
for _, k := range m.Keys() {
key := FromValue(k)
// should never be not found or unhashable, so ignore err and found.
val, _, _ := m.Get(k)
//ret[key] = val
ret[key] = FromValue(val)
}
return ret
}
// MakeSet makes a Set from the given map. The acceptable keys the same as ToValue.
// For nil input, it returns an empty Set.
func MakeSet(s map[interface{}]bool) (*starlark.Set, error) {
set := starlark.Set{}
for k := range s {
key, err := ToValue(k)
if err != nil {
return nil, err
}
if err = set.Insert(key); err != nil {
return nil, err
}
}
return &set, nil
}
// MakeSetFromSlice makes a Set from the given slice. The acceptable keys the same as ToValue.
// For nil input, it returns an empty Set.
func MakeSetFromSlice(s []interface{}) (*starlark.Set, error) {
set := starlark.Set{}
for i := range s {
key, err := ToValue(s[i])
if err != nil {
return nil, err
}
if err = set.Insert(key); err != nil {
return nil, err
}
}
return &set, nil
}
// FromSet converts a starlark.Set to a map[interface{}]bool
func FromSet(s *starlark.Set) map[interface{}]bool {
ret := make(map[interface{}]bool, s.Len())
var v starlark.Value
i := s.Iterate()
defer i.Done()
for i.Next(&v) {
val := FromValue(v)
ret[val] = true
}
return ret
}
// Kwarg is a single instance of a python foo=bar style named argument.
type Kwarg struct {
Name string
Value interface{}
}
// FromKwargs converts a Python style name=val, name2=val2 list of tuples into a
// []Kwarg. It is an error if any tuple is not exactly 2 values,
// or if the first one is not a string.
func FromKwargs(kwargs []starlark.Tuple) ([]Kwarg, error) {
args := make([]Kwarg, 0, len(kwargs))
for _, t := range kwargs {
tup := FromTuple(t)
if len(tup) != 2 {
return nil, fmt.Errorf("kwarg tuple should have 2 vals, has %v", len(tup))
}
s, ok := tup[0].(string)
if !ok {
return nil, fmt.Errorf("expected name of kwarg to be string, but was %T (%#v)", tup[0], tup[0])
}
args = append(args, Kwarg{Name: s, Value: tup[1]})
}
return args, nil
}
// MakeStarFn creates a wrapper around the given function that can be called from a starlark script. Argument support is the same as ToValue.
// If the last value the function returns is an error, it will cause an error to be returned from the starlark function.
// If there are no other errors, the function will return None.
// If there's exactly one other value, the function will return the starlark equivalent of that value.
// If there is more than one return value, they'll be returned as a tuple.
// MakeStarFn will panic if you pass it something other than a function, like nil or a non-function.
func MakeStarFn(name string, gofn interface{}) *starlark.Builtin {
v := reflect.ValueOf(gofn)
if v.Kind() != reflect.Func {
panic(errors.New("fn is not a function"))
}
return makeStarFn(name, v, emptyStr)
}
func makeStarFn(name string, gofn reflect.Value, tagName string) *starlark.Builtin {
if gofn.Type().IsVariadic() {
return makeVariadicStarFn(name, gofn, tagName)
}
return starlark.NewBuiltin(name, func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (sv starlark.Value, ef error) {
defer func() {
if r := recover(); r != nil {
sv = starlark.None
ef = fmt.Errorf("panic in func %s: %v", name, r)
}
}()
if len(args) != gofn.Type().NumIn() {
return starlark.None, fmt.Errorf("expected %d args but got %d", gofn.Type().NumIn(), len(args))
}
// convert all the args, but kwargs are ignored
vals := FromTuple(args)
rvs := make([]reflect.Value, 0, len(vals))
for i, v := range vals {
val := reflect.ValueOf(v)
argT := gofn.Type().In(i)
var err error
val, err = convertReflectValue(val, argT)
if err != nil {
return starlark.None, fmt.Errorf("arg %d: %v", i, err)
}
rvs = append(rvs, val)
}
out := gofn.Call(rvs)
return makeOut(out, tagName)
})
}
func makeVariadicStarFn(name string, gofn reflect.Value, tagName string) *starlark.Builtin {
return starlark.NewBuiltin(name, func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (sv starlark.Value, ef error) {
defer func() {
if r := recover(); r != nil {
sv = starlark.None
ef = fmt.Errorf("panic in func %s: %v", name, r)
}
}()
minArgs := gofn.Type().NumIn() - 1
if len(args) < minArgs {
return starlark.None, fmt.Errorf("expected at least %d args but got %d", minArgs, len(args))
}
// convert all the args, but kwargs are ignored
vals := FromTuple(args)
rvs := make([]reflect.Value, 0, len(args))
// grab all the non-variadics first
for i := 0; i < minArgs; i++ {
val := reflect.ValueOf(vals[i])
argT := gofn.Type().In(i)
var err error
val, err = convertReflectValue(val, argT)
if err != nil {
return starlark.None, fmt.Errorf("arg %d: %v", i, err)
}
rvs = append(rvs, val)
}
// last "in" type by definition must be a slice of something. We need to
// know what something, so we can convert things as needed.
vtype := gofn.Type().In(gofn.Type().NumIn() - 1).Elem()
// the rest of the args need to be batched into a slice for the variadic
for i := minArgs; i < len(vals); i++ {
val := reflect.ValueOf(vals[i])
var err error
val, err = convertReflectValue(val, vtype)
if err != nil {
return starlark.None, fmt.Errorf("arg %d: %v", i, err)
}
rvs = append(rvs, val)
}
out := gofn.Call(rvs)
return makeOut(out, tagName)
})
}
func makeOut(out []reflect.Value, tagName string) (starlark.Value, error) {
if len(out) == 0 {
return starlark.None, nil
}
last := out[len(out)-1]
var err error
if last.Type() == errType {
if v := last.Interface(); v != nil {
err = v.(error)
}
out = out[:len(out)-1]
}
if len(out) == 0 {
return starlark.None, err
}
if len(out) == 1 {
v, err2 := toValue(out[0], tagName)
if err2 != nil {
return starlark.None, err2
}
return v, err
}
// tuple-up multiple values
res := make([]starlark.Value, 0, len(out))
for i := range out {
val, err3 := toValue(out[i], tagName)
if err3 != nil {
return starlark.None, err3
}
res = append(res, val)
}
return starlark.Tuple(res), err
}
// convertReflectValue converts a reflect.Value to a given type.
func convertReflectValue(val reflect.Value, argT reflect.Type) (reflect.Value, error) {
if !val.IsValid() {
return reflect.Zero(argT), nil
}
if val.Type().AssignableTo(argT) {
return val, nil
}
if val.Type().ConvertibleTo(argT) {
return val.Convert(argT), nil
}
if val.Kind() == reflect.Slice && argT.Kind() == reflect.Slice {
return convertSlice(val, argT)
}
if val.Kind() == reflect.Map && argT.Kind() == reflect.Map {
return convertMap(val, argT)
}
return reflect.Value{}, fmt.Errorf("expected type %v got %v", argT, val.Type())
}
func convertSlice(val reflect.Value, argT reflect.Type) (reflect.Value, error) {
argElem := argT.Elem()
valLen := val.Len()
newSlice := reflect.MakeSlice(argT, valLen, valLen)
for i := 0; i < valLen; i++ {
elem := val.Index(i)
if elem.Type().AssignableTo(argElem) {
newSlice.Index(i).Set(elem)
} else if elem.Type().ConvertibleTo(argElem) {
newSlice.Index(i).Set(elem.Convert(argElem))
} else if elem.Elem().Type().ConvertibleTo(argElem) {
newSlice.Index(i).Set(elem.Elem().Convert(argElem))
} else {
return reflect.Value{}, fmt.Errorf("expected slice element type %v got %v", argElem, elem.Type())
}
}
return newSlice, nil
}
func convertMap(val reflect.Value, argT reflect.Type) (reflect.Value, error) {
argKey := argT.Key()
argElem := argT.Elem()
newMap := reflect.MakeMapWithSize(argT, val.Len())
for _, key := range val.MapKeys() {
newKey, err := convertElemValue(key, argKey)
if err != nil {
return reflect.Value{}, fmt.Errorf("map key conversion failed: %v", err)
}
valElem := val.MapIndex(key)
newElem, err := convertElemValue(valElem, argElem)
if err != nil {
return reflect.Value{}, fmt.Errorf("map value conversion failed: %v", err)
}
newMap.SetMapIndex(newKey, newElem)
}
return newMap, nil
}
func convertElemValue(val reflect.Value, targetType reflect.Type) (reflect.Value, error) {
if val.Type().AssignableTo(targetType) || val.Type().ConvertibleTo(targetType) {
return val.Convert(targetType), nil
} else if val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
if val.IsNil() {
return reflect.Value{}, fmt.Errorf("nil value cannot be converted to type %v", targetType)
}
if val.Elem().Type().ConvertibleTo(targetType) {
return val.Elem().Convert(targetType), nil
} else if val.Type().Kind() == reflect.Interface {
unwrapped := val.Elem()
if unwrapped.Type().ConvertibleTo(targetType) {
return unwrapped.Convert(targetType), nil
} else if sv, ok := unwrapped.Interface().(starlark.Value); ok {
// TODO: this path is not reachable in the current test, maybe we can remove it?
goVal := FromValue(sv)
goVal = convertNumericTypes(goVal, targetType)
if reflect.TypeOf(goVal) != targetType {
return reflect.Value{}, fmt.Errorf("expected type %v got %v", targetType, reflect.TypeOf(goVal))
}
return reflect.ValueOf(goVal), nil
}
}
}
return reflect.Value{}, fmt.Errorf("expected type %v got %v", targetType, val.Type())
}
func convertNumericTypes(value interface{}, targetType reflect.Type) interface{} {
// If the value is an integer, convert it to the appropriate integer type.
switch st := value.(type) {
case int64:
switch targetType.Kind() {
case reflect.Int:
return int(st)
case reflect.Int32:
return int32(st)
case reflect.Int16:
return int16(st)
case reflect.Int8:
return int8(st)
case reflect.Uint:
return uint(st)
case reflect.Uint64:
return uint64(st)
case reflect.Uint32:
return uint32(st)
case reflect.Uint16:
return uint16(st)
case reflect.Uint8:
return uint8(st)
}
// If the value is a float, convert it to the appropriate float type.
case float64:
if targetType.Kind() == reflect.Float32 {
return float32(st)
}
}
return value
}
// tryConv tries to convert starlark.Value v to Go t if v is not assignable to t.
func tryConv(v starlark.Value, t reflect.Type) (reflect.Value, error) {
if v == starlark.None {
switch t.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Interface, reflect.Func:
return reflect.Zero(t), nil
default:
return reflect.Value{}, fmt.Errorf("value of type None cannot be converted to non-nullable type %s", t)
}
}
out := reflect.ValueOf(FromValue(v))
if !out.Type().AssignableTo(t) {
if out.Type().ConvertibleTo(t) {
return out.Convert(t), nil
}
return reflect.Value{}, fmt.Errorf("value of type %s cannot be converted to type %s", out.Type(), t)
}
return out, nil
}