From 7ae560edf298b3aa198afc5118fef8e1e193d9de Mon Sep 17 00:00:00 2001 From: Pavlos Karakalidis Date: Sun, 31 Dec 2023 21:07:34 +0200 Subject: [PATCH 1/4] feat(unmarshaler): add support for fields to unmarshal themselves --- unmarshaler.go | 12 +++++++ unmarshaler_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/unmarshaler.go b/unmarshaler.go index 857f3cf5..d3672485 100644 --- a/unmarshaler.go +++ b/unmarshaler.go @@ -17,6 +17,12 @@ import ( "github.com/pelletier/go-toml/v2/unstable" ) +// The Unmarshaler interface may be implemented by types to customize their +// behavior when being unmarshaled from a TOML document. +type Unmarshaler interface { + UnmarshalTOML(value *unstable.Node) error +} + // Unmarshal deserializes a TOML document into a Go value. // // It is a shortcut for Decoder.Decode() with the default options. @@ -648,6 +654,12 @@ func (d *decoder) handleValue(value *unstable.Node, v reflect.Value) error { v = initAndDereferencePointer(v) } + if v.CanAddr() && v.Addr().CanInterface() { + if outi, ok := v.Addr().Interface().(Unmarshaler); ok { + return outi.UnmarshalTOML(value) + } + } + ok, err := d.tryTextUnmarshaler(value, v) if ok || err != nil { return err diff --git a/unmarshaler_test.go b/unmarshaler_test.go index 78e06895..3b053004 100644 --- a/unmarshaler_test.go +++ b/unmarshaler_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/pelletier/go-toml/v2/unstable" "math" "strconv" "strings" @@ -3772,3 +3773,82 @@ func TestUnmarshal_Nil(t *testing.T) { }) } } + +type CustomUnmarshalerKey struct { + A int64 +} + +func (k *CustomUnmarshalerKey) UnmarshalTOML(value *unstable.Node) error { + item, err := strconv.ParseInt(string(value.Data), 10, 64) + if err != nil { + return fmt.Errorf("error converting to int64, %v", err) + } + k.A = item + return nil + +} + +func TestUnmarshal_CustomUnmarshaler(t *testing.T) { + type MyConfig struct { + Unmarshalers []CustomUnmarshalerKey `toml:"unmarshalers"` + Foo *string `toml:"foo,omitempty"` + } + + examples := []struct { + desc string + input string + expected MyConfig + err bool + }{ + { + desc: "empty", + input: ``, + expected: MyConfig{Unmarshalers: []CustomUnmarshalerKey{}, Foo: nil}, + }, + { + desc: "simple", + input: `unmarshalers = [1,2,3]`, + expected: MyConfig{ + Unmarshalers: []CustomUnmarshalerKey{ + {A: 1}, + {A: 2}, + {A: 3}, + }, + Foo: nil, + }, + }, + { + desc: "unmarshal string and custom unmarshaler", + input: `unmarshalers = [1,2,3] +foo = "bar"`, + expected: MyConfig{ + Unmarshalers: []CustomUnmarshalerKey{ + {A: 1}, + {A: 2}, + {A: 3}, + }, + Foo: func(v string) *string { + return &v + }("bar"), + }, + }, + } + + for _, ex := range examples { + e := ex + t.Run(e.desc, func(t *testing.T) { + foo := MyConfig{} + err := toml.Unmarshal([]byte(e.input), &foo) + if e.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, len(foo.Unmarshalers), len(e.expected.Unmarshalers)) + for i := 0; i < len(foo.Unmarshalers); i++ { + require.Equal(t, foo.Unmarshalers[i], e.expected.Unmarshalers[i]) + } + require.Equal(t, foo.Foo, e.expected.Foo) + } + }) + } +} From d7cb2b70a266cda59146f3e4e11b15b0fc3fff10 Mon Sep 17 00:00:00 2001 From: rszyma Date: Thu, 14 Mar 2024 22:51:25 +0100 Subject: [PATCH 2/4] feat: add `EnableUnmarshalerInterface` decoder method --- unmarshaler.go | 25 ++++++++++++++++++++++--- unmarshaler_test.go | 25 +++++++++++++++++++------ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/unmarshaler.go b/unmarshaler.go index d3672485..025202da 100644 --- a/unmarshaler.go +++ b/unmarshaler.go @@ -41,6 +41,9 @@ type Decoder struct { // global settings strict bool + + // toggles unmarshaler interface + unmarshalerInterface bool } // NewDecoder creates a new Decoder that will read from r. @@ -60,6 +63,16 @@ func (d *Decoder) DisallowUnknownFields() *Decoder { return d } +// EnableUnmarshalerInterface allows to enable unmarshaler interface. +// +// *Unstable:* This method does not follow the compatibility guarantees of +// semver. It can be changed or removed without a new major version being +// issued. +func (d *Decoder) EnableUnmarshalerInterface() *Decoder { + d.unmarshalerInterface = true + return d +} + // Decode the whole content of r into v. // // By default, values in the document that don't exist in the target Go value @@ -114,6 +127,7 @@ func (d *Decoder) Decode(v interface{}) error { strict: strict{ Enabled: d.strict, }, + unmarshalerInterface: d.unmarshalerInterface, } return dec.FromParser(v) @@ -149,6 +163,9 @@ type decoder struct { // Strict mode strict strict + // Flag that enables/disables unmarshaler interface. + unmarshalerInterface bool + // Current context for the error. errorContext *errorContext } @@ -654,9 +671,11 @@ func (d *decoder) handleValue(value *unstable.Node, v reflect.Value) error { v = initAndDereferencePointer(v) } - if v.CanAddr() && v.Addr().CanInterface() { - if outi, ok := v.Addr().Interface().(Unmarshaler); ok { - return outi.UnmarshalTOML(value) + if d.unmarshalerInterface { + if v.CanAddr() && v.Addr().CanInterface() { + if outi, ok := v.Addr().Interface().(Unmarshaler); ok { + return outi.UnmarshalTOML(value) + } } } diff --git a/unmarshaler_test.go b/unmarshaler_test.go index 3b053004..c1833fb0 100644 --- a/unmarshaler_test.go +++ b/unmarshaler_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/pelletier/go-toml/v2/unstable" "math" "strconv" "strings" @@ -13,6 +12,7 @@ import ( "time" "github.com/pelletier/go-toml/v2" + "github.com/pelletier/go-toml/v2/unstable" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -3795,10 +3795,11 @@ func TestUnmarshal_CustomUnmarshaler(t *testing.T) { } examples := []struct { - desc string - input string - expected MyConfig - err bool + desc string + disableUnmarshalerInterface bool + input string + expected MyConfig + err bool }{ { desc: "empty", @@ -3832,13 +3833,25 @@ foo = "bar"`, }("bar"), }, }, + { + desc: "simple example, but unmarshaler interface disabled", + disableUnmarshalerInterface: true, + input: `unmarshalers = [1,2,3]`, + err: true, + }, } for _, ex := range examples { e := ex t.Run(e.desc, func(t *testing.T) { foo := MyConfig{} - err := toml.Unmarshal([]byte(e.input), &foo) + + decoder := toml.NewDecoder(bytes.NewReader([]byte(e.input))) + if !ex.disableUnmarshalerInterface { + decoder.EnableUnmarshalerInterface() + } + err := decoder.Decode(&foo) + if e.err { require.Error(t, err) } else { From de8d3e9ecc5f2add0be31b8b9e7b39c1d18e310a Mon Sep 17 00:00:00 2001 From: rszyma Date: Thu, 14 Mar 2024 23:17:04 +0100 Subject: [PATCH 3/4] move Unmarshaler interface to unastable --- unmarshaler.go | 8 +------- unstable/unmarshaler.go | 7 +++++++ 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 unstable/unmarshaler.go diff --git a/unmarshaler.go b/unmarshaler.go index 025202da..a1c9f22d 100644 --- a/unmarshaler.go +++ b/unmarshaler.go @@ -17,12 +17,6 @@ import ( "github.com/pelletier/go-toml/v2/unstable" ) -// The Unmarshaler interface may be implemented by types to customize their -// behavior when being unmarshaled from a TOML document. -type Unmarshaler interface { - UnmarshalTOML(value *unstable.Node) error -} - // Unmarshal deserializes a TOML document into a Go value. // // It is a shortcut for Decoder.Decode() with the default options. @@ -673,7 +667,7 @@ func (d *decoder) handleValue(value *unstable.Node, v reflect.Value) error { if d.unmarshalerInterface { if v.CanAddr() && v.Addr().CanInterface() { - if outi, ok := v.Addr().Interface().(Unmarshaler); ok { + if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok { return outi.UnmarshalTOML(value) } } diff --git a/unstable/unmarshaler.go b/unstable/unmarshaler.go new file mode 100644 index 00000000..00cfd6de --- /dev/null +++ b/unstable/unmarshaler.go @@ -0,0 +1,7 @@ +package unstable + +// The Unmarshaler interface may be implemented by types to customize their +// behavior when being unmarshaled from a TOML document. +type Unmarshaler interface { + UnmarshalTOML(value *Node) error +} From 3cd69bafd15b90584e0d93b57c5566427be9a720 Mon Sep 17 00:00:00 2001 From: Thomas Pelletier Date: Tue, 19 Mar 2024 12:24:34 -0400 Subject: [PATCH 4/4] Add more warnings on EnableUnmarshalInterface --- unmarshaler.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/unmarshaler.go b/unmarshaler.go index a1c9f22d..98231bae 100644 --- a/unmarshaler.go +++ b/unmarshaler.go @@ -59,6 +59,14 @@ func (d *Decoder) DisallowUnknownFields() *Decoder { // EnableUnmarshalerInterface allows to enable unmarshaler interface. // +// With this feature enabled, types implementing the unstable/Unmarshaler +// interface can be decoded from any structure of the document. It allows types +// that don't have a straightfoward TOML representation to provide their own +// decoding logic. +// +// Currently, types can only decode from a single value. Tables and array tables +// are not supported. +// // *Unstable:* This method does not follow the compatibility guarantees of // semver. It can be changed or removed without a new major version being // issued.