qri-io / starlib

qri's standard library for starlark
MIT License
112 stars 29 forks source link

(proposal) Marshal\Unmarshal custom type via starlarkstruct.Struct #48

Open gebv opened 4 years ago

gebv commented 4 years ago

It is proposal and dirty code to illustrate the concept

NOTE: factory object saved in to starlarakstruct.Struct in constructor for unmarshal (no need to global store custom types). Similar to database.Valuer

Implement custom type via starlarkstruct.Struct Added two interfaces

type Unmarshaler interface {
    UnmarshalStarlark(starlark.Value) error
}

type Marshaler interface {
    MarshalStarlark() (starlark.Value, error)
}

Added new cases for util.Marahsl and util.Unmarshal

// Unmarshal
case *starlarkstruct.Struct:
        if _var, ok := v.Constructor().(Unmarshaler); ok {
            err = _var.UnmarshalStarlark(x)
            if err != nil {
                err = errors.Wrapf(err, "failed marshal %q to Starlark object", v.Constructor().Type())
                return
            }
            val = _var
        } else {
            err = fmt.Errorf("constructor object from *starlarkstruct.Struct not supported Marshaler to starlark object: %s", v.Constructor().Type())
        }

// Marshal
case Marshaler:
        v, err = x.MarshalStarlark()

Example custom type


type customType struct {
    Foo int64
}

func (t *customType) UnmarshalStarlark(v starlark.Value) error {
    // asserts
    if v.Type() != "struct" {
        return fmt.Errorf("not expected top level type, want struct, got %q", v.Type())
    }
    if _, ok := v.(*starlarkstruct.Struct).Constructor().(*customType); !ok {
        return fmt.Errorf("not expected construct type got %T, want %T", v.(*starlarkstruct.Struct).Constructor(), t)
    }

    // TODO: refactoring transform data

    mustInt64 := func(sv starlark.Value) int64 {
        i, _ := sv.(starlark.Int).Int64()
        return i
    }

    data := starlark.StringDict{}
    v.(*starlarkstruct.Struct).ToStringDict(data)

    *t = customType{
        Foo: mustInt64(data["foo"]),
    }
    return nil
}

func (t *customType) MarshalStarlark() (starlark.Value, error) {
    v := starlarkstruct.FromStringDict(&customType{}, starlark.StringDict{
        "foo": starlark.MakeInt64(t.Foo),
    })
    return v, nil
}

func (c customType) String() string {
    return "customType"
}

func (c customType) Type() string { return "test.customType" }

func (customType) Freeze() {}

func (c customType) Truth() starlark.Bool {
    return starlark.True
}

func (c customType) Hash() (uint32, error) {
    return 0, fmt.Errorf("unhashable: %s", c.Type())
}

var _ Unmarshaler = (*customType)(nil)
var _ Marshaler = (*customType)(nil)
var _ starlark.Value = (*customType)(nil)

Tests

func TestLifeCycle(t *testing.T) {
    t.Run("once", func(t *testing.T) {
        // golang value
        goVal := &customType{42}
        // starlark value
        slVal, err := Marshal(goVal)
        assert.NoError(t, err)

        assert.IsType(t, &starlarkstruct.Struct{}, slVal)

        gotGoVal, err := Unmarshal(slVal)
        assert.NoError(t, err)
        log.Println(slVal.String())
        assert.EqualValues(t, goVal, gotGoVal)
    })

    t.Run("asDictValue", func(t *testing.T) {
        // golang value
        goVal := map[string]interface{}{
            "foo": &customType{42},
        }

        // starlark value
        slVal, err := Marshal(goVal)
        assert.NoError(t, err)

        wantSlVal := starlark.NewDict(1)
        assert.IsType(t, wantSlVal, slVal)

        wantSlVal.SetKey(starlark.String("foo"), func() starlark.Value { v, _ := Marshal(&customType{42}); return v }())
        assert.EqualValues(t, wantSlVal, slVal)

        gotGoVal, err := Unmarshal(slVal)
        assert.NoError(t, err)
        log.Println(slVal.String())
        assert.EqualValues(t, goVal, gotGoVal)
    })

    t.Run("asListValue", func(t *testing.T) {
        // golang value
        goVal := []interface{}{
            &customType{42},
            &customType{42},
        }

        // starlark value
        slVal, err := Marshal(goVal)
        assert.NoError(t, err)

        wantSlVal := starlark.NewList(nil)
        wantSlVal.Append(func() starlark.Value { v, _ := Marshal(&customType{42}); return v }())
        wantSlVal.Append(func() starlark.Value { v, _ := Marshal(&customType{42}); return v }())
        assert.IsType(t, wantSlVal, slVal)

        assert.EqualValues(t, wantSlVal, slVal)

        gotGoVal, err := Unmarshal(slVal)
        assert.NoError(t, err)
        log.Println(slVal.String())
        assert.EqualValues(t, goVal, gotGoVal)
    })

}
gebv commented 4 years ago

https://github.com/qri-io/starlib/pull/34 ref

hundredwatt commented 4 years ago

@gebv @b5 FYI Starlark recently merged the Unpacker interface: https://github.com/google/starlark-go/pull/272

I need to solve the Custom Type problem in my application soon, so planning to look into both UnpackArgs for things other than Starlark function arguments and/or continue using util.Unmarshal. Will report back if I learn anything that can be contributed here.

Strange that google/starlark-go has no generic interface for Marshal. Packer would make a lot of sense 😄. Any others addressed this problem?