Encoder: omitempty flag (#692)

Fixes #597
This commit is contained in:
Thomas Pelletier
2021-11-30 21:32:28 -05:00
committed by GitHub
parent 3990899d7e
commit 0d20a84523
3 changed files with 191 additions and 67 deletions
@@ -457,35 +457,6 @@ func TestEmptytomlUnmarshal(t *testing.T) {
assert.Equal(t, emptyTestData, result) assert.Equal(t, emptyTestData, result)
} }
func TestEmptyUnmarshalOmit(t *testing.T) {
t.Skipf("Have not figured yet if omitempty is a good idea")
type emptyMarshalTestStruct2 struct {
Title string `toml:"title"`
Bool bool `toml:"bool,omitempty"`
Int int `toml:"int, omitempty"`
String string `toml:"string,omitempty "`
StringList []string `toml:"stringlist,omitempty"`
Ptr *basicMarshalTestStruct `toml:"ptr,omitempty"`
Map map[string]string `toml:"map,omitempty"`
}
emptyTestData2 := emptyMarshalTestStruct2{
Title: "Placeholder",
Bool: false,
Int: 0,
String: "",
StringList: []string{},
Ptr: nil,
Map: map[string]string{},
}
result := emptyMarshalTestStruct2{}
err := toml.Unmarshal(emptyTestToml, &result)
require.NoError(t, err)
assert.Equal(t, emptyTestData2, result)
}
type pointerMarshalTestStruct struct { type pointerMarshalTestStruct struct {
Str *string Str *string
List *[]string List *[]string
+103 -29
View File
@@ -11,6 +11,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode"
) )
// Marshal serializes a Go value as a TOML document. // Marshal serializes a Go value as a TOML document.
@@ -111,21 +112,22 @@ func (enc *Encoder) SetIndentTables(indent bool) *Encoder {
// //
// Struct tags // Struct tags
// //
// The following struct tags are available to tweak encoding on a per-field // The encoding of each public struct field can be customized by the
// basis: // format string in the "toml" key of the struct field's tag. This
// follows encoding/json's convention. The format string starts with
// the name of the field, optionally followed by a comma-separated
// list of options. The name may be empty in order to provide options
// without overriding the default name.
// //
// toml:"foo" // The "multiline" option emits strings as quoted multi-line TOML
// Changes the name of the key to use for the field to foo. By default, all // strings. It has no effect on fields that would not be encoded as
// public fields are encoded. If you want to prevent a public field from // strings.
// being exported, you can use the special field name "-".
// //
// multiline:"true" // The "inline" option turns fields that would be emitted as tables
// When the field contains a string, it will be emitted as a quoted // into inline tables instead. It has no effect on other fields.
// multi-line TOML string.
// //
// inline:"true" // The "omitempty" option prevents empty values or groups from being
// When the field would normally be encoded as a table, it is instead // emitted.
// encoded as an inline table.
func (enc *Encoder) Encode(v interface{}) error { func (enc *Encoder) Encode(v interface{}) error {
var ( var (
b []byte b []byte
@@ -153,6 +155,7 @@ func (enc *Encoder) Encode(v interface{}) error {
type valueOptions struct { type valueOptions struct {
multiline bool multiline bool
omitempty bool
} }
type encoderCtx struct { type encoderCtx struct {
@@ -202,7 +205,6 @@ func (ctx *encoderCtx) isRoot() bool {
return len(ctx.parentKey) == 0 && !ctx.hasKey return len(ctx.parentKey) == 0 && !ctx.hasKey
} }
//nolint:cyclop,funlen
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if !v.IsZero() { if !v.IsZero() {
i, ok := v.Interface().(time.Time) i, ok := v.Interface().(time.Time)
@@ -299,6 +301,11 @@ func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v r
if !ctx.hasKey { if !ctx.hasKey {
panic("caller of encodeKv should have set the key in the context") panic("caller of encodeKv should have set the key in the context")
} }
if (ctx.options.omitempty || options.omitempty) && isEmptyValue(v) {
return b, nil
}
b = enc.indent(ctx.indent, b) b = enc.indent(ctx.indent, b)
b, err = enc.encodeKey(b, ctx.key) b, err = enc.encodeKey(b, ctx.key)
@@ -323,6 +330,24 @@ func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v r
return b, nil return b, nil
} }
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}
const literalQuote = '\'' const literalQuote = '\''
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte { func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte {
@@ -532,8 +557,7 @@ func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
var t table var t table
//nolint:godox // 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++ {
fieldType := typ.Field(i) fieldType := typ.Field(i)
@@ -543,16 +567,20 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
continue continue
} }
k, ok := fieldType.Tag.Lookup("toml") k := fieldType.Name
if !ok {
k = fieldType.Name tag := fieldType.Tag.Get("toml")
}
// special field name to skip field // special field name to skip field
if k == "-" { if tag == "-" {
continue continue
} }
name, opts := parseTag(tag)
if isValidName(name) {
k = name
}
f := v.Field(i) f := v.Field(i)
if isNil(f) { if isNil(f) {
@@ -560,12 +588,11 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
} }
options := valueOptions{ options := valueOptions{
multiline: fieldBoolTag(fieldType, "multiline"), multiline: opts.multiline,
omitempty: opts.omitempty,
} }
inline := fieldBoolTag(fieldType, "inline") if opts.inline || !willConvertToTableOrArrayTable(ctx, f) {
if inline || !willConvertToTableOrArrayTable(ctx, f) {
t.pushKV(k, f, options) t.pushKV(k, f, options)
} else { } else {
t.pushTable(k, f, options) t.pushTable(k, f, options)
@@ -575,13 +602,60 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
return enc.encodeTable(b, ctx, t) return enc.encodeTable(b, ctx, t)
} }
func fieldBoolTag(field reflect.StructField, tag string) bool { func isValidName(s string) bool {
x, ok := field.Tag.Lookup(tag) if s == "" {
return false
return ok && x == "true" }
for _, c := range s {
switch {
case strings.ContainsRune("!#$%&()*+-./:;<=>?@[]^_{|}~ ", c):
// Backslash and quote chars are reserved, but
// otherwise any punctuation chars are allowed
// in a tag name.
case !unicode.IsLetter(c) && !unicode.IsDigit(c):
return false
}
}
return true
}
type tagOptions struct {
multiline bool
inline bool
omitempty bool
}
func parseTag(tag string) (string, tagOptions) {
opts := tagOptions{}
idx := strings.Index(tag, ",")
if idx == -1 {
return tag, opts
}
raw := tag[idx+1:]
tag = string(tag[:idx])
for raw != "" {
var o string
i := strings.Index(raw, ",")
if i >= 0 {
o, raw = raw[:i], raw[i+1:]
} else {
o, raw = raw, ""
}
switch o {
case "multiline":
opts.multiline = true
case "inline":
opts.inline = true
case "omitempty":
opts.omitempty = true
}
}
return tag, opts
} }
//nolint:cyclop
func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) { func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) {
var err error var err error
+88 -9
View File
@@ -7,18 +7,18 @@ import (
"math/big" "math/big"
"strings" "strings"
"testing" "testing"
"time"
"github.com/pelletier/go-toml/v2" "github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
//nolint:funlen
func TestMarshal(t *testing.T) { func TestMarshal(t *testing.T) {
someInt := 42 someInt := 42
type structInline struct { type structInline struct {
A interface{} `inline:"true"` A interface{} `toml:",inline"`
} }
examples := []struct { examples := []struct {
@@ -194,9 +194,9 @@ name = 'Alice'
{ {
desc: "string escapes", desc: "string escapes",
v: map[string]interface{}{ v: map[string]interface{}{
"a": `'"\`, "a": "'\b\f\r\t\"\\",
}, },
expected: `a = "'\"\\"`, expected: `a = "'\b\f\r\t\"\\"`,
}, },
{ {
desc: "string utf8 low", desc: "string utf8 low",
@@ -243,7 +243,7 @@ name = 'Alice'
{ {
desc: "multi-line forced", desc: "multi-line forced",
v: struct { v: struct {
A string `multiline:"true"` A string `toml:",multiline"`
}{ }{
A: "hello\nworld", A: "hello\nworld",
}, },
@@ -254,7 +254,7 @@ world"""`,
{ {
desc: "inline field", desc: "inline field",
v: struct { v: struct {
A map[string]string `inline:"true"` A map[string]string `toml:",inline"`
B map[string]string B map[string]string
}{ }{
A: map[string]string{ A: map[string]string{
@@ -273,7 +273,7 @@ isinline = 'no'
{ {
desc: "mutiline array int", desc: "mutiline array int",
v: struct { v: struct {
A []int `multiline:"true"` A []int `toml:",multiline"`
B []int B []int
}{ }{
A: []int{1, 2, 3, 4}, A: []int{1, 2, 3, 4},
@@ -292,7 +292,7 @@ B = [1, 2, 3, 4]
{ {
desc: "mutiline array in array", desc: "mutiline array in array",
v: struct { v: struct {
A [][]int `multiline:"true"` A [][]int `toml:",multiline"`
}{ }{
A: [][]int{{1, 2}, {3, 4}}, A: [][]int{{1, 2}, {3, 4}},
}, },
@@ -470,6 +470,28 @@ hello = 'world'`,
}, },
err: true, err: true,
}, },
{
desc: "time",
v: struct {
T time.Time
}{
T: time.Time{},
},
expected: `T = '0001-01-01T00:00:00Z'`,
},
{
desc: "bool",
v: struct {
A bool
B bool
}{
A: false,
B: true,
},
expected: `
A = false
B = true`,
},
{ {
desc: "numbers", desc: "numbers",
v: struct { v: struct {
@@ -484,6 +506,7 @@ hello = 'world'`,
I int16 I int16
J int8 J int8
K int K int
L float64
}{ }{
A: 1.1, A: 1.1,
B: 42, B: 42,
@@ -496,6 +519,7 @@ hello = 'world'`,
I: 42, I: 42,
J: 42, J: 42,
K: 42, K: 42,
L: 2.2,
}, },
expected: ` expected: `
A = 1.1 A = 1.1
@@ -508,7 +532,8 @@ G = 42
H = 42 H = 42
I = 42 I = 42
J = 42 J = 42
K = 42`, K = 42
L = 2.2`,
}, },
} }
@@ -735,6 +760,60 @@ func TestEncoderSetIndentSymbol(t *testing.T) {
equalStringsIgnoreNewlines(t, expected, w.String()) equalStringsIgnoreNewlines(t, expected, w.String())
} }
func TestEncoderOmitempty(t *testing.T) {
type doc struct {
String string `toml:",omitempty,multiline"`
Bool bool `toml:",omitempty,multiline"`
Int int `toml:",omitempty,multiline"`
Int8 int8 `toml:",omitempty,multiline"`
Int16 int16 `toml:",omitempty,multiline"`
Int32 int32 `toml:",omitempty,multiline"`
Int64 int64 `toml:",omitempty,multiline"`
Uint uint `toml:",omitempty,multiline"`
Uint8 uint8 `toml:",omitempty,multiline"`
Uint16 uint16 `toml:",omitempty,multiline"`
Uint32 uint32 `toml:",omitempty,multiline"`
Uint64 uint64 `toml:",omitempty,multiline"`
Float32 float32 `toml:",omitempty,multiline"`
Float64 float64 `toml:",omitempty,multiline"`
MapNil map[string]string `toml:",omitempty,multiline"`
Slice []string `toml:",omitempty,multiline"`
Ptr *string `toml:",omitempty,multiline"`
Iface interface{} `toml:",omitempty,multiline"`
Struct struct{} `toml:",omitempty,multiline"`
}
d := doc{}
b, err := toml.Marshal(d)
require.NoError(t, err)
expected := `[Struct]`
equalStringsIgnoreNewlines(t, expected, string(b))
}
func TestEncoderTagFieldName(t *testing.T) {
type doc struct {
String string `toml:"hello"`
OkSym string `toml:"#"`
Bad string `toml:"\"`
}
d := doc{String: "world"}
b, err := toml.Marshal(d)
require.NoError(t, err)
expected := `
hello = 'world'
'#' = ''
Bad = ''
`
equalStringsIgnoreNewlines(t, expected, string(b))
}
func TestIssue436(t *testing.T) { func TestIssue436(t *testing.T) {
data := []byte(`{"a": [ { "b": { "c": "d" } } ]}`) data := []byte(`{"a": [ { "b": { "c": "d" } } ]}`)