@@ -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
@@ -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
@@ -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" } } ]}`)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user