marshal: do not encode embedded structs as sub-table (#368)

Currently, the marshalling code encodes the embedded structs as sub-tables.
This is a bit unexpected, as it differs from what encoding/json does in
that case: https://play.golang.org/p/KDPaGtrijV1

Unmarshalling code handles this scenario gracefully.

This PR adapts the encoder to behave like encoding/json.
Fields in an embedded struct are promoted to the top level table.
In case the embedded struct is named in the tag, it will still
encode as a sub-table.

The added PromoteAnonymous option on the Encoder allows configuring
the old behavior, where anonymous structs are encoded as sub-tables.

On duplicate keys, the behavior of encoding/json is mimicked:
Fields from anonymous structs are shadowed by regular fields.

An example is added to show the affects of setting PromoteAnonymous.
This commit is contained in:
Oncilla
2020-04-25 17:25:56 +02:00
committed by GitHub
parent 947ab3f90a
commit d1e0fc37ce
3 changed files with 199 additions and 9 deletions
+64
View File
@@ -5,6 +5,7 @@ package toml_test
import (
"fmt"
"log"
"os"
toml "github.com/pelletier/go-toml"
)
@@ -104,3 +105,66 @@ func ExampleUnmarshal() {
// Output:
// user= pelletier
}
func ExampleEncoder_anonymous() {
type Credentials struct {
User string `toml:"user"`
Password string `toml:"password"`
}
type Protocol struct {
Name string `toml:"name"`
}
type Config struct {
Version int `toml:"version"`
Credentials
Protocol `toml:"Protocol"`
}
config := Config{
Version: 2,
Credentials: Credentials{
User: "pelletier",
Password: "mypassword",
},
Protocol: Protocol{
Name: "tcp",
},
}
fmt.Println("Default:")
fmt.Println("---------------")
def := toml.NewEncoder(os.Stdout)
if err := def.Encode(config); err != nil {
log.Fatal(err)
}
fmt.Println("---------------")
fmt.Println("With promotion:")
fmt.Println("---------------")
prom := toml.NewEncoder(os.Stdout).PromoteAnonymous(true)
if err := prom.Encode(config); err != nil {
log.Fatal(err)
}
// Output:
// Default:
// ---------------
// password = "mypassword"
// user = "pelletier"
// version = 2
//
// [Protocol]
// name = "tcp"
// ---------------
// With promotion:
// ---------------
// version = 2
//
// [Credentials]
// password = "mypassword"
// user = "pelletier"
//
// [Protocol]
// name = "tcp"
}
+34 -1
View File
@@ -22,6 +22,7 @@ const (
type tomlOpts struct {
name string
nameFromTag bool
comment string
commented bool
multiline bool
@@ -193,6 +194,7 @@ type Encoder struct {
line int
col int
order marshalOrder
promoteAnon bool
}
// NewEncoder returns a new encoder that writes to w.
@@ -279,6 +281,19 @@ func (e *Encoder) SetTagMultiline(v string) *Encoder {
return e
}
// PromoteAnonymous allows to change how anonymous struct fields are marshaled.
// Usually, they are marshaled as if the inner exported fields were fields in
// the outer struct. However, if an anonymous struct field is given a name in
// its TOML tag, it is treated like a regular struct field with that name.
// rather than being anonymous.
//
// In case anonymous promotion is enabled, all anonymous structs are promoted
// and treated like regular struct fields.
func (e *Encoder) PromoteAnonymous(promote bool) *Encoder {
e.promoteAnon = promote
return e
}
func (e *Encoder) marshal(v interface{}) ([]byte, error) {
mtype := reflect.TypeOf(v)
if mtype == nil {
@@ -338,7 +353,9 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
if err != nil {
return nil, err
}
if tree, ok := val.(*Tree); ok && mtypef.Anonymous && !opts.nameFromTag && !e.promoteAnon {
e.appendTree(tval, tree)
} else {
tval.SetWithOptions(opts.name, SetOptions{
Comment: opts.comment,
Commented: opts.commented,
@@ -347,6 +364,7 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
}
}
}
}
case reflect.Map:
keys := mval.MapKeys()
if e.order == OrderPreserve && len(keys) > 0 {
@@ -460,6 +478,19 @@ func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface
}
}
func (e *Encoder) appendTree(t, o *Tree) error {
for key, value := range o.values {
if _, ok := t.values[key]; ok {
continue
}
if tomlValue, ok := value.(*tomlValue); ok {
tomlValue.position.Col = t.position.Col
}
t.values[key] = value
}
return nil
}
// Unmarshal attempts to unmarshal the Tree into a Go struct pointed by v.
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
// sub-structs, and only definite types can be unmarshaled.
@@ -913,6 +944,7 @@ func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
defaultValue := vf.Tag.Get(tagDefault)
result := tomlOpts{
name: vf.Name,
nameFromTag: false,
comment: comment,
commented: commented,
multiline: multiline,
@@ -925,6 +957,7 @@ func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
result.include = false
} else {
result.name = strings.Trim(parse[0], " ")
result.nameFromTag = true
}
}
if vf.PkgPath != "" {
+93
View File
@@ -1974,6 +1974,99 @@ func TestUnmarshalDefaultFailureUnsupported(t *testing.T) {
}
}
func TestMarshalNestedAnonymousStructs(t *testing.T) {
type Embedded struct {
Value string `toml:"value"`
Top struct {
Value string `toml:"value"`
} `toml:"top"`
}
type Named struct {
Value string `toml:"value"`
}
var doc struct {
Embedded
Named `toml:"named"`
Anonymous struct {
Value string `toml:"value"`
} `toml:"anonymous"`
}
expected := `value = ""
[anonymous]
value = ""
[named]
value = ""
[top]
value = ""
`
result, err := Marshal(doc)
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}
if !bytes.Equal(result, []byte(expected)) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, string(result))
}
}
func TestEncoderPromoteNestedAnonymousStructs(t *testing.T) {
type Embedded struct {
Value string `toml:"value"`
}
var doc struct {
Embedded
}
expected := `
[Embedded]
value = ""
`
var buf bytes.Buffer
if err := NewEncoder(&buf).PromoteAnonymous(true).Encode(doc); err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}
if !bytes.Equal(buf.Bytes(), []byte(expected)) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, buf.String())
}
}
func TestMarshalNestedAnonymousStructs_DuplicateField(t *testing.T) {
type Embedded struct {
Value string `toml:"value"`
Top struct {
Value string `toml:"value"`
} `toml:"top"`
}
var doc struct {
Value string `toml:"value"`
Embedded
}
doc.Embedded.Value = "shadowed"
doc.Value = "shadows"
expected := `value = "shadows"
[top]
value = ""
`
result, err := Marshal(doc)
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}
if !bytes.Equal(result, []byte(expected)) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, string(result))
}
}
func TestUnmarshalNestedAnonymousStructs(t *testing.T) {
type Nested struct {
Value string `toml:"nested_field"`