1set/starlight

View on GitHub
convert/interface.go

Summary

Maintainability
A
2 hrs
Test Coverage
package convert

import (
    "errors"
    "fmt"
    "reflect"

    "go.starlark.net/starlark"
)

// MakeGoInterface converts the given value into a GoInterface.
// This will panic if the value is nil or the type is not a bool, string, float kind, int kind, or uint kind.
func MakeGoInterface(v interface{}) *GoInterface {
    val := reflect.ValueOf(v)
    ifc, ok := makeGoInterface(val)
    if !ok {
        panic(fmt.Errorf("value of type %T is not supported by GoInterface", val.Interface()))
    }
    return ifc
}

func makeGoInterface(val reflect.Value) (*GoInterface, bool) {
    // we accept pointers to anything except structs, which should go through GoStruct.
    if val.Kind() == reflect.Ptr && val.Elem().Kind() == reflect.Struct {
        return nil, false
    }
    switch val.Kind() {
    case reflect.Ptr,
        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:
        return &GoInterface{v: val}, true
    }
    return nil, false
}

// GoInterface wraps a go value to expose its methods to starlark scripts. Basic
// types will not behave as their base type (you can't add 2 to an ID, even if
// it is an int underneath).
type GoInterface struct {
    v   reflect.Value
    tag string
}

// Attr returns a starlark value that wraps the method or field with the given name.
func (g *GoInterface) Attr(name string) (starlark.Value, error) {
    switch name {
    case "toInt":
        return MakeStarFn(name, g.ToInt), nil
    case "toString":
        return MakeStarFn(name, g.ToString), nil
    case "toFloat":
        return MakeStarFn(name, g.ToFloat), nil
    case "toUint":
        return MakeStarFn(name, g.ToUint), nil
    case "toBool":
        return MakeStarFn(name, g.ToBool), nil
    }

    method := g.v.MethodByName(name)
    if method.Kind() != reflect.Invalid {
        return makeStarFn(name, method, g.tag), nil
    }
    return nil, nil
}

// AttrNames returns the list of all fields and methods on this struct.
func (g *GoInterface) AttrNames() []string {
    if !g.v.IsValid() {
        return nil
    }

    count := g.v.NumMethod()
    if g.v.Kind() == reflect.Ptr && !g.v.IsNil() {
        count += g.v.Elem().NumMethod()
    }

    names := make([]string, 0, count)
    for i := 0; i < g.v.NumMethod(); i++ {
        names = append(names, g.v.Type().Method(i).Name)
    }
    if g.v.Kind() == reflect.Ptr && !g.v.IsNil() {
        t := g.v.Elem().Type()
        for i := 0; i < t.NumMethod(); i++ {
            names = append(names, t.Method(i).Name)
        }
    }
    return names
}

// String returns the string representation of the value.
// Starlark string values are quoted as if by Python's repr.
func (g *GoInterface) String() string {
    return fmt.Sprint(g.v.Interface())
}

// Type returns a short string describing the value's type.
func (g *GoInterface) Type() string {
    return fmt.Sprintf("starlight_interface<%T>", g.v.Interface())
}

// Value returns reflect.Value of the underlying value.
func (g *GoInterface) Value() reflect.Value {
    return g.v
}

// Freeze causes the value, and all values transitively
// reachable from it through collections and closures, to be
// marked as frozen.  All subsequent mutations to the data
// structure through this API will fail dynamically, making the
// data structure immutable and safe for publishing to other
// Starlark interpreters running concurrently.
func (g *GoInterface) Freeze() {}

// Truth returns the truth value of an object.
func (g *GoInterface) Truth() starlark.Bool {
    switch g.v.Kind() {
    case reflect.Ptr:
        return starlark.Bool(!g.v.IsNil())
    case reflect.Bool:
        return starlark.Bool(g.v.Bool())
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        return g.v.Int() != 0
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
        return g.v.Uint() > 0
    case reflect.Float32, reflect.Float64:
        return g.v.Float() != 0
    case reflect.String:
        return g.v.String() != ""
    }
    // otherwise... I dunno man, sure.
    return true
}

// Hash returns a function of x such that Equals(x, y) => Hash(x) == Hash(y).
// Hash may fail if the value's type is not hashable, or if the value
// contains a non-hashable value.
func (g *GoInterface) Hash() (uint32, error) {
    return 0, errors.New("starlight_interface is not hashable")
}

// Below are conversion functions, they only work on the appropriate underlying type.
// Note that there is no ToBool because Truth() already serves that purpose.

// ToInt converts the interface value into a starlark int.  This will fail if
// the underlying type is not an int type or pointer to an int type.
func (g *GoInterface) ToInt() (int64, error) {
    v := g.v
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }
    switch v.Kind() {
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        return v.Int(), nil
    }
    return 0, fmt.Errorf("can't convert type %T to int64", v.Interface())
}

// ToBool converts the interface value into a starlark bool.  This will fail if
// the underlying type is not a bool type or pointer to a bool type.
func (g *GoInterface) ToBool() (bool, error) {
    v := g.v
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }
    switch v.Kind() {
    case reflect.Bool:
        return v.Bool(), nil
    }
    return false, fmt.Errorf("can't convert type %T to bool", v.Interface())
}

// ToUint converts the interface value into a starlark int.  This will fail if
// the underlying type is not an uint type or pointer to an uint type.
func (g *GoInterface) ToUint() (uint64, error) {
    v := g.v
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }
    switch v.Kind() {
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
        return v.Uint(), nil
    }
    return 0, fmt.Errorf("can't convert type %T to uint64", v.Interface())
}

// ToString converts the interface value into a starlark string.  This will fail if
// the underlying type is not a string (including if the underlying type is a
// pointer to a string).
func (g *GoInterface) ToString() (string, error) {
    switch g.v.Kind() {
    case reflect.String:
        return g.v.String(), nil
    }
    return "", fmt.Errorf("can't convert type %T to string", g.v)
}

// ToFloat converts the interface value into a starlark float.  This will fail
// if the underlying type is not a float type (including if the underlying type
// is a pointer to a float).
func (g *GoInterface) ToFloat() (float64, error) {
    switch g.v.Kind() {
    case reflect.Float32, reflect.Float64:
        return g.v.Float(), nil
    }
    return 0, fmt.Errorf("can't convert type %T to float64", g.v)
}