Skip to content

Commit 8ed6d13

Browse files
rszymapkarakalpelletier
authored
Decode: unstable/Unmarshal interface (#940)
Co-authored-by: Pavlos Karakalidis <[email protected]> Co-authored-by: Thomas Pelletier <[email protected]>
1 parent 7dad877 commit 8ed6d13

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

unmarshaler.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ type Decoder struct {
3535

3636
// global settings
3737
strict bool
38+
39+
// toggles unmarshaler interface
40+
unmarshalerInterface bool
3841
}
3942

4043
// NewDecoder creates a new Decoder that will read from r.
@@ -54,6 +57,24 @@ func (d *Decoder) DisallowUnknownFields() *Decoder {
5457
return d
5558
}
5659

60+
// EnableUnmarshalerInterface allows to enable unmarshaler interface.
61+
//
62+
// With this feature enabled, types implementing the unstable/Unmarshaler
63+
// interface can be decoded from any structure of the document. It allows types
64+
// that don't have a straightfoward TOML representation to provide their own
65+
// decoding logic.
66+
//
67+
// Currently, types can only decode from a single value. Tables and array tables
68+
// are not supported.
69+
//
70+
// *Unstable:* This method does not follow the compatibility guarantees of
71+
// semver. It can be changed or removed without a new major version being
72+
// issued.
73+
func (d *Decoder) EnableUnmarshalerInterface() *Decoder {
74+
d.unmarshalerInterface = true
75+
return d
76+
}
77+
5778
// Decode the whole content of r into v.
5879
//
5980
// By default, values in the document that don't exist in the target Go value
@@ -108,6 +129,7 @@ func (d *Decoder) Decode(v interface{}) error {
108129
strict: strict{
109130
Enabled: d.strict,
110131
},
132+
unmarshalerInterface: d.unmarshalerInterface,
111133
}
112134

113135
return dec.FromParser(v)
@@ -143,6 +165,9 @@ type decoder struct {
143165
// Strict mode
144166
strict strict
145167

168+
// Flag that enables/disables unmarshaler interface.
169+
unmarshalerInterface bool
170+
146171
// Current context for the error.
147172
errorContext *errorContext
148173
}
@@ -648,6 +673,14 @@ func (d *decoder) handleValue(value *unstable.Node, v reflect.Value) error {
648673
v = initAndDereferencePointer(v)
649674
}
650675

676+
if d.unmarshalerInterface {
677+
if v.CanAddr() && v.Addr().CanInterface() {
678+
if outi, ok := v.Addr().Interface().(unstable.Unmarshaler); ok {
679+
return outi.UnmarshalTOML(value)
680+
}
681+
}
682+
}
683+
651684
ok, err := d.tryTextUnmarshaler(value, v)
652685
if ok || err != nil {
653686
return err

unmarshaler_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
"github.com/pelletier/go-toml/v2"
15+
"github.com/pelletier/go-toml/v2/unstable"
1516
"github.com/stretchr/testify/assert"
1617
"github.com/stretchr/testify/require"
1718
)
@@ -3772,3 +3773,95 @@ func TestUnmarshal_Nil(t *testing.T) {
37723773
})
37733774
}
37743775
}
3776+
3777+
type CustomUnmarshalerKey struct {
3778+
A int64
3779+
}
3780+
3781+
func (k *CustomUnmarshalerKey) UnmarshalTOML(value *unstable.Node) error {
3782+
item, err := strconv.ParseInt(string(value.Data), 10, 64)
3783+
if err != nil {
3784+
return fmt.Errorf("error converting to int64, %v", err)
3785+
}
3786+
k.A = item
3787+
return nil
3788+
3789+
}
3790+
3791+
func TestUnmarshal_CustomUnmarshaler(t *testing.T) {
3792+
type MyConfig struct {
3793+
Unmarshalers []CustomUnmarshalerKey `toml:"unmarshalers"`
3794+
Foo *string `toml:"foo,omitempty"`
3795+
}
3796+
3797+
examples := []struct {
3798+
desc string
3799+
disableUnmarshalerInterface bool
3800+
input string
3801+
expected MyConfig
3802+
err bool
3803+
}{
3804+
{
3805+
desc: "empty",
3806+
input: ``,
3807+
expected: MyConfig{Unmarshalers: []CustomUnmarshalerKey{}, Foo: nil},
3808+
},
3809+
{
3810+
desc: "simple",
3811+
input: `unmarshalers = [1,2,3]`,
3812+
expected: MyConfig{
3813+
Unmarshalers: []CustomUnmarshalerKey{
3814+
{A: 1},
3815+
{A: 2},
3816+
{A: 3},
3817+
},
3818+
Foo: nil,
3819+
},
3820+
},
3821+
{
3822+
desc: "unmarshal string and custom unmarshaler",
3823+
input: `unmarshalers = [1,2,3]
3824+
foo = "bar"`,
3825+
expected: MyConfig{
3826+
Unmarshalers: []CustomUnmarshalerKey{
3827+
{A: 1},
3828+
{A: 2},
3829+
{A: 3},
3830+
},
3831+
Foo: func(v string) *string {
3832+
return &v
3833+
}("bar"),
3834+
},
3835+
},
3836+
{
3837+
desc: "simple example, but unmarshaler interface disabled",
3838+
disableUnmarshalerInterface: true,
3839+
input: `unmarshalers = [1,2,3]`,
3840+
err: true,
3841+
},
3842+
}
3843+
3844+
for _, ex := range examples {
3845+
e := ex
3846+
t.Run(e.desc, func(t *testing.T) {
3847+
foo := MyConfig{}
3848+
3849+
decoder := toml.NewDecoder(bytes.NewReader([]byte(e.input)))
3850+
if !ex.disableUnmarshalerInterface {
3851+
decoder.EnableUnmarshalerInterface()
3852+
}
3853+
err := decoder.Decode(&foo)
3854+
3855+
if e.err {
3856+
require.Error(t, err)
3857+
} else {
3858+
require.NoError(t, err)
3859+
require.Equal(t, len(foo.Unmarshalers), len(e.expected.Unmarshalers))
3860+
for i := 0; i < len(foo.Unmarshalers); i++ {
3861+
require.Equal(t, foo.Unmarshalers[i], e.expected.Unmarshalers[i])
3862+
}
3863+
require.Equal(t, foo.Foo, e.expected.Foo)
3864+
}
3865+
})
3866+
}
3867+
}

unstable/unmarshaler.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package unstable
2+
3+
// The Unmarshaler interface may be implemented by types to customize their
4+
// behavior when being unmarshaled from a TOML document.
5+
type Unmarshaler interface {
6+
UnmarshalTOML(value *Node) error
7+
}

0 commit comments

Comments
 (0)