Skip to content

Commit

Permalink
encode: respect stdlib rules for embedded structs (#747)
Browse files Browse the repository at this point in the history
  • Loading branch information
pelletier committed Apr 7, 2022
1 parent b9edbeb commit 068279f
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 39 deletions.
34 changes: 3 additions & 31 deletions README.md
Expand Up @@ -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.


#### Embedded structs are tables
#### Embedded structs behave like stdlib

V1 defaults to merging embedded struct fields into the embedding struct. This
behavior was unexpected because it does not follow the standard library. To
avoid breaking backward compatibility, the `Encoder.PromoteAnonymous` method was
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
to the old behavior, and `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 = ''
```
a problem anymore, v2 does the right thing by default: it follows the behavior
of `encoding/json`. `Encoder.PromoteAnonymous` has been removed.

[nodoc]: https://github.com/pelletier/go-toml/discussions/506#discussioncomment-1526038

Expand Down
38 changes: 30 additions & 8 deletions marshaler.go
Expand Up @@ -555,16 +555,25 @@ type table struct {
}

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})
}

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})
}

func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
var t table

func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
// TODO: cache this
typ := v.Type()
for i := 0; i < typ.NumField(); i++ {
Expand All @@ -575,22 +584,29 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
continue
}

k := fieldType.Name

tag := fieldType.Tag.Get("toml")

// special field name to skip field
if tag == "-" {
continue
}

name, opts := parseTag(tag)
if isValidName(name) {
k = name
k, opts := parseTag(tag)
if !isValidName(k) {
k = ""
}

f := v.Field(i)

if k == "" {
if fieldType.Anonymous {
walkStruct(ctx, t, f)
continue
} else {
k = fieldType.Name
}
}

if isNil(f) {
continue
}
Expand All @@ -607,6 +623,12 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
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)
}
Expand Down
64 changes: 64 additions & 0 deletions marshaler_test.go
Expand Up @@ -947,6 +947,70 @@ func TestIssue678(t *testing.T) {
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() {
type MyConfig struct {
Version int
Expand Down

0 comments on commit 068279f

Please sign in to comment.