encode: respect stdlib rules for embedded structs (#747)

This commit is contained in:
Thomas Pelletier
2022-04-07 19:51:09 -04:00
committed by GitHub
parent b9edbeb611
commit 068279f13b
3 changed files with 97 additions and 39 deletions
+3 -31
View File
@@ -527,42 +527,14 @@ The new name is `Encoder.SetArraysMultiline`. The behavior should be the same.
The new name is `Encoder.SetIndentSymbol`. The behavior should be the same. The new name is `Encoder.SetIndentSymbol`. The behavior should be the same.
#### Embedded structs are tables #### Embedded structs behave like stdlib
V1 defaults to merging embedded struct fields into the embedding struct. This V1 defaults to merging embedded struct fields into the embedding struct. This
behavior was unexpected because it does not follow the standard library. To behavior was unexpected because it does not follow the standard library. To
avoid breaking backward compatibility, the `Encoder.PromoteAnonymous` method was avoid breaking backward compatibility, the `Encoder.PromoteAnonymous` method was
added to make the encoder behave correctly. Given backward compatibility is not added to make the encoder behave correctly. Given backward compatibility is not
a problem anymore, v2 does the right thing by default. There is no way to revert a problem anymore, v2 does the right thing by default: it follows the behavior
to the old behavior, and `Encoder.PromoteAnonymous` has been removed. of `encoding/json`. `Encoder.PromoteAnonymous` has been removed.
```go
type Embedded struct {
Value string `toml:"value"`
}
type Doc struct {
Embedded
}
d := Doc{}
fmt.Println("v1:")
b, err := v1.Marshal(d)
fmt.Println(string(b))
fmt.Println("v2:")
b, err = v2.Marshal(d)
fmt.Println(string(b))
// Output:
// v1:
// value = ""
//
// v2:
// [Embedded]
// value = ''
```
[nodoc]: https://github.com/pelletier/go-toml/discussions/506#discussioncomment-1526038 [nodoc]: https://github.com/pelletier/go-toml/discussions/506#discussioncomment-1526038
+30 -8
View File
@@ -555,16 +555,25 @@ type table struct {
} }
func (t *table) pushKV(k string, v reflect.Value, options valueOptions) { func (t *table) pushKV(k string, v reflect.Value, options valueOptions) {
for _, e := range t.kvs {
if e.Key == k {
return
}
}
t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options}) t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options})
} }
func (t *table) pushTable(k string, v reflect.Value, options valueOptions) { func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
for _, e := range t.tables {
if e.Key == k {
return
}
}
t.tables = append(t.tables, entry{Key: k, Value: v, Options: options}) t.tables = append(t.tables, entry{Key: k, Value: v, Options: options})
} }
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
var t table
// TODO: cache this // TODO: cache this
typ := v.Type() typ := v.Type()
for i := 0; i < typ.NumField(); i++ { for i := 0; i < typ.NumField(); i++ {
@@ -575,8 +584,6 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
continue continue
} }
k := fieldType.Name
tag := fieldType.Tag.Get("toml") tag := fieldType.Tag.Get("toml")
// special field name to skip field // special field name to skip field
@@ -584,13 +591,22 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
continue continue
} }
name, opts := parseTag(tag) k, opts := parseTag(tag)
if isValidName(name) { if !isValidName(k) {
k = name k = ""
} }
f := v.Field(i) f := v.Field(i)
if k == "" {
if fieldType.Anonymous {
walkStruct(ctx, t, f)
continue
} else {
k = fieldType.Name
}
}
if isNil(f) { if isNil(f) {
continue continue
} }
@@ -607,6 +623,12 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
t.pushTable(k, f, options) t.pushTable(k, f, options)
} }
} }
}
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
var t table
walkStruct(ctx, &t, v)
return enc.encodeTable(b, ctx, t) return enc.encodeTable(b, ctx, t)
} }
+64
View File
@@ -947,6 +947,70 @@ func TestIssue678(t *testing.T) {
require.Equal(t, cfg, cfg2) require.Equal(t, cfg, cfg2)
} }
func TestMarshalNestedAnonymousStructs(t *testing.T) {
type Embedded struct {
Value string `toml:"value" json:"value"`
Top struct {
Value string `toml:"value" json:"value"`
} `toml:"top" json:"top"`
}
type Named struct {
Value string `toml:"value" json:"value"`
}
var doc struct {
Embedded
Named `toml:"named" json:"named"`
Anonymous struct {
Value string `toml:"value" json:"value"`
} `toml:"anonymous" json:"anonymous"`
}
expected := `value = ''
[top]
value = ''
[named]
value = ''
[anonymous]
value = ''
`
result, err := toml.Marshal(doc)
require.NoError(t, err)
require.Equal(t, expected, string(result))
}
func TestMarshalNestedAnonymousStructs_DuplicateField(t *testing.T) {
type Embedded struct {
Value string `toml:"value" json:"value"`
Top struct {
Value string `toml:"value" json:"value"`
} `toml:"top" json:"top"`
}
var doc struct {
Value string `toml:"value" json:"value"`
Embedded
}
doc.Embedded.Value = "shadowed"
doc.Value = "shadows"
expected := `value = 'shadows'
[top]
value = ''
`
result, err := toml.Marshal(doc)
require.NoError(t, err)
require.NoError(t, err)
require.Equal(t, expected, string(result))
}
func ExampleMarshal() { func ExampleMarshal() {
type MyConfig struct { type MyConfig struct {
Version int Version int