starlib icon indicating copy to clipboard operation
starlib copied to clipboard

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

Open gebv opened this issue 4 years ago • 2 comments

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 avatar Apr 08 '20 06:04 gebv

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

gebv avatar Apr 08 '20 06:04 gebv

@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?

hundredwatt avatar Jun 30 '20 23:06 hundredwatt