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:
+64
@@ -5,6 +5,7 @@ package toml_test
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
toml "github.com/pelletier/go-toml"
|
toml "github.com/pelletier/go-toml"
|
||||||
)
|
)
|
||||||
@@ -104,3 +105,66 @@ func ExampleUnmarshal() {
|
|||||||
// Output:
|
// Output:
|
||||||
// user= pelletier
|
// 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"
|
||||||
|
}
|
||||||
|
|||||||
+42
-9
@@ -22,6 +22,7 @@ const (
|
|||||||
|
|
||||||
type tomlOpts struct {
|
type tomlOpts struct {
|
||||||
name string
|
name string
|
||||||
|
nameFromTag bool
|
||||||
comment string
|
comment string
|
||||||
commented bool
|
commented bool
|
||||||
multiline bool
|
multiline bool
|
||||||
@@ -190,9 +191,10 @@ type Encoder struct {
|
|||||||
w io.Writer
|
w io.Writer
|
||||||
encOpts
|
encOpts
|
||||||
annotation
|
annotation
|
||||||
line int
|
line int
|
||||||
col int
|
col int
|
||||||
order marshalOrder
|
order marshalOrder
|
||||||
|
promoteAnon bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEncoder returns a new encoder that writes to w.
|
// NewEncoder returns a new encoder that writes to w.
|
||||||
@@ -279,6 +281,19 @@ func (e *Encoder) SetTagMultiline(v string) *Encoder {
|
|||||||
return e
|
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) {
|
func (e *Encoder) marshal(v interface{}) ([]byte, error) {
|
||||||
mtype := reflect.TypeOf(v)
|
mtype := reflect.TypeOf(v)
|
||||||
if mtype == nil {
|
if mtype == nil {
|
||||||
@@ -338,12 +353,15 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if tree, ok := val.(*Tree); ok && mtypef.Anonymous && !opts.nameFromTag && !e.promoteAnon {
|
||||||
tval.SetWithOptions(opts.name, SetOptions{
|
e.appendTree(tval, tree)
|
||||||
Comment: opts.comment,
|
} else {
|
||||||
Commented: opts.commented,
|
tval.SetWithOptions(opts.name, SetOptions{
|
||||||
Multiline: opts.multiline,
|
Comment: opts.comment,
|
||||||
}, val)
|
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.
|
// Unmarshal attempts to unmarshal the Tree into a Go struct pointed by v.
|
||||||
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
|
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
|
||||||
// sub-structs, and only definite types can be unmarshaled.
|
// 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)
|
defaultValue := vf.Tag.Get(tagDefault)
|
||||||
result := tomlOpts{
|
result := tomlOpts{
|
||||||
name: vf.Name,
|
name: vf.Name,
|
||||||
|
nameFromTag: false,
|
||||||
comment: comment,
|
comment: comment,
|
||||||
commented: commented,
|
commented: commented,
|
||||||
multiline: multiline,
|
multiline: multiline,
|
||||||
@@ -925,6 +957,7 @@ func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
|
|||||||
result.include = false
|
result.include = false
|
||||||
} else {
|
} else {
|
||||||
result.name = strings.Trim(parse[0], " ")
|
result.name = strings.Trim(parse[0], " ")
|
||||||
|
result.nameFromTag = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if vf.PkgPath != "" {
|
if vf.PkgPath != "" {
|
||||||
|
|||||||
@@ -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) {
|
func TestUnmarshalNestedAnonymousStructs(t *testing.T) {
|
||||||
type Nested struct {
|
type Nested struct {
|
||||||
Value string `toml:"nested_field"`
|
Value string `toml:"nested_field"`
|
||||||
|
|||||||
Reference in New Issue
Block a user