From d1e0fc37ce141a2a968bde16e03f6d85f0cb9d14 Mon Sep 17 00:00:00 2001 From: Oncilla Date: Sat, 25 Apr 2020 17:25:56 +0200 Subject: [PATCH] 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. --- doc_test.go | 64 ++++++++++++++++++++++++++++++++++ marshal.go | 51 ++++++++++++++++++++++----- marshal_test.go | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc_test.go b/doc_test.go index d64414c..7aaddab 100644 --- a/doc_test.go +++ b/doc_test.go @@ -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" +} diff --git a/marshal.go b/marshal.go index db05c58..0832630 100644 --- a/marshal.go +++ b/marshal.go @@ -22,6 +22,7 @@ const ( type tomlOpts struct { name string + nameFromTag bool comment string commented bool multiline bool @@ -190,9 +191,10 @@ type Encoder struct { w io.Writer encOpts annotation - line int - col int - order marshalOrder + 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,12 +353,15 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er if err != nil { return nil, err } - - tval.SetWithOptions(opts.name, SetOptions{ - Comment: opts.comment, - Commented: opts.commented, - Multiline: opts.multiline, - }, val) + 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, + Multiline: opts.multiline, + }, val) + } } } } @@ -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 != "" { diff --git a/marshal_test.go b/marshal_test.go index 7ac4522..04c90ef 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -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"`