There's a lot of repetitive boilerplate for marshalling to/from input types. This tends to be error prone and the error messages we give aren't very informative or consistent. Here's a demo example that marshals to/from starlark types, but it should be trivially convertible to the input type case:
package main
import (
"encoding/json"
"fmt"
"log"
"reflect"
"github.com/pkg/errors"
"github.com/weberc2/builder/core"
sl "go.starlark.net/starlark"
)
type TypeError struct {
Wanted string
Got string
}
func (te TypeError) Error() string {
return fmt.Sprintf("TypeError: Wanted '%s', got '%s'", te.Wanted, te.Got)
}
type Converter func(ptr reflect.Value, slv sl.Value) error
func StringConverter(ptr reflect.Value, slv sl.Value) error {
if s, ok := slv.(sl.String); ok {
ptr.Elem().Set(reflect.ValueOf(string(s)))
return nil
}
return TypeError{Wanted: "str", Got: slv.Type()}
}
func IntConverter(ptr reflect.Value, slv sl.Value) error {
if i, ok := slv.(sl.Int); ok {
i, _ := i.Int64()
ptr.Elem().Set(reflect.ValueOf(int(i)))
return nil
}
return TypeError{Wanted: "int", Got: slv.Type()}
}
func Convert(x interface{}, slv sl.Value) error {
v := reflect.ValueOf(x)
vt := v.Type()
if v.Type().Kind() != reflect.Ptr {
return fmt.Errorf(
"Can only convert pointer-type arguments, found %T",
x,
)
}
converter, err := MakeConverter(vt.Elem())
if err != nil {
return err
}
return converter(v, slv)
}
func MakeStructConverter(t reflect.Type) (Converter, error) {
type fieldConverter struct {
field reflect.StructField
converter Converter
}
fields := make([]fieldConverter, t.NumField())
for i := range fields {
field := t.Field(i)
conv, err := MakeConverter(field.Type)
if err != nil {
return nil, errors.Wrapf(
err,
"Making struct field '%s'",
field.Name,
)
}
fields[i] = fieldConverter{field: field, converter: conv}
}
return func(ptr reflect.Value, slv sl.Value) error {
dict, ok := slv.(*sl.Dict)
if !ok {
return TypeError{Wanted: "Dict", Got: slv.Type()}
}
v := ptr.Elem()
for i, field := range fields {
fieldValue, found, err := dict.Get(sl.String(field.field.Name))
// err should always be nil--sl only returns errors if the key is
// unhashable or an "excessively nested tuple"; however, we'll
// propagate it anyway out of an abundance of caution.
if err != nil {
return err
}
// If we didn't find a key for the field, use the default value.
// If necessary, we can backtrack on this (before 1.0) and make it
// an error condition.
if !found {
continue
}
if err := field.converter(
v.Field(i).Addr(),
fieldValue,
); err != nil {
return err
}
}
return nil
}, nil
}
func MakeSliceConverter(t reflect.Type) (Converter, error) {
elemConverter, err := MakeConverter(t.Elem())
if err != nil {
return nil, err
}
return func(ptr reflect.Value, slv sl.Value) error {
list, ok := slv.(*sl.List)
if !ok {
return TypeError{Wanted: "List", Got: slv.Type()}
}
v := ptr.Elem()
vt := v.Type()
slice := reflect.MakeSlice(vt, list.Len(), list.Len())
for i := 0; i < list.Len(); i++ {
if err := elemConverter(
// Take a pointer to the element at `i` in the new slice and
// pass it (along with the `i`th value in the starlark list)
// into the element converter.
slice.Index(i).Addr(),
list.Index(i),
); err != nil {
return err
}
}
// Set `v` to the newly created slice
v.Set(slice)
return nil
}, err
}
func MakePtrConverter(t reflect.Type) (Converter, error) {
vt := t.Elem()
vconverter, err := MakeConverter(vt)
if err != nil {
return nil, err
}
// In a converter, `ptr` always refers to a pointer to the value. Since
// MakePtrConverter's values are pointers, `ptr` is a pointer to a pointer.
return func(ptr reflect.Value, slv sl.Value) error {
// v is the pointer whose type is `t`. The converter of its pointee is
// vconverter.
v := ptr.Elem()
// Check to see if `v`'s pointee is settable (i.e., v is not nil). If
// not, we'll need to allocate a new pointee which can be hydrated by
// vconverter.
if !v.Elem().CanSet() {
v.Set(reflect.New(vt))
}
return vconverter(v, slv)
}, nil
}
func MakeInterfaceConverter(t reflect.Type) (Converter, error) {
// vt := t.Elem()
// vtconverter, err := MakeConverter(vt)
// if err != nil {
// return nil, err
// }
// `ptr` is the pointer to our interface.
return func(ptr reflect.Value, slv sl.Value) error {
// v is the interface (ptr is a pointer to the interface)
v := ptr.Elem()
vprime := v.Elem() // vprime is the value beneath the interface, if any
if vprime.IsNil() {
panic("TODO: Support nil interface and other unaddressable values")
}
if slv2, ok := vprime.Interface().(sl.Value); ok {
}
converter, err := MakeConverter(vprime.Type())
if err != nil {
return err
}
return converter(v, slv)
}, nil
}
func MakeConverter(t reflect.Type) (Converter, error) {
var x core.ArtifactID
if t == reflect.TypeOf(x) {
return func(ptr reflect.Value, slv sl.Value) error {
if aid, ok := slv.(core.ArtifactID); ok {
ptr.Elem().Set(reflect.ValueOf(aid))
}
return TypeError{Wanted: "Artifact", Got: slv.Type()}
}, nil
}
switch t.Kind() {
case reflect.Struct:
return MakeStructConverter(t)
case reflect.Slice:
return MakeSliceConverter(t)
case reflect.String:
return StringConverter, nil
case reflect.Int:
return IntConverter, nil
case reflect.Ptr:
return MakePtrConverter(t)
case reflect.Interface:
return MakeInterfaceConverter(t)
default:
return nil, fmt.Errorf("slconv: Unsupported type: %s", t)
}
}
func jsonPrettyPrint(v interface{}) string {
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
panic(err)
}
return string(data)
}
func slRun(program string) (sl.Value, error) {
globals, err := sl.ExecFile(&sl.Thread{}, "", program, nil)
if err != nil {
return nil, err
}
value, found := globals["value"]
if !found {
return nil, fmt.Errorf("Failed to find global 'value'")
}
return value, nil
}
type PythonLibrary struct {
Name string
Dependencies []string
}
func main() {
v, err := slRun("value = {'hello': 'world'}")
if err != nil {
log.Fatal(err)
}
var s struct{ Hello string }
var output interface{} = &s
if err := Convert(&output, v); err != nil {
log.Fatal(err)
}
fmt.Println(jsonPrettyPrint(output))
}
There's a lot of repetitive boilerplate for marshalling to/from input types. This tends to be error prone and the error messages we give aren't very informative or consistent. Here's a demo example that marshals to/from starlark types, but it should be trivially convertible to the input type case: