weberc2 / builder

Prototype build tool
0 stars 0 forks source link

Standardize marshalling to/from `Input` types #23

Closed weberc2 closed 4 years ago

weberc2 commented 4 years ago

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))
}