diff --git a/encode.go b/encode.go index 2cc80e37..33534aef 100644 --- a/encode.go +++ b/encode.go @@ -89,6 +89,17 @@ type Marshaler interface { // // Go maps will be sorted alphabetically by key for deterministic output. // +// The toml struct tag can be used to provide the key name; if omitted the +// struct field name will be used. If the "omitempty" option is present the +// following value will be skipped: +// +// - arrays, slices, maps, and string with len of 0 +// - struct with all zero values +// - bool false +// +// If omitzero is given all int and float types with a value of 0 will be +// skipped. +// // Encoding Go values without a corresponding TOML representation will return an // error. Examples of this includes maps with non-string keys, slices with nil // elements, embedded non-struct types, and nested slices containing maps or @@ -655,6 +666,8 @@ func isEmpty(rv reflect.Value) bool { switch rv.Kind() { case reflect.Array, reflect.Slice, reflect.Map, reflect.String: return rv.Len() == 0 + case reflect.Struct: + return reflect.Zero(rv.Type()).Interface() == rv.Interface() case reflect.Bool: return !rv.Bool() } diff --git a/encode_test.go b/encode_test.go index 0cc73262..446979b5 100644 --- a/encode_test.go +++ b/encode_test.go @@ -116,6 +116,70 @@ func TestEncodeArrayHashWithNormalHashOrder(t *testing.T) { encodeExpected(t, "array hash with normal hash order", val, expected, nil) } +func TestEncodeOmitEmptyStruct(t *testing.T) { + type ( + T struct{ Int int } + Tpriv struct { + Int int + private int + } + Ttime struct { + Time time.Time + } + ) + + tests := []struct { + in interface{} + want string + }{ + {struct { + F T `toml:"f,omitempty"` + }{}, ""}, + {struct { + F T `toml:"f,omitempty"` + }{T{1}}, "[f]\n Int = 1"}, + + {struct { + F Tpriv `toml:"f,omitempty"` + }{}, ""}, + {struct { + F Tpriv `toml:"f,omitempty"` + }{Tpriv{1, 0}}, "[f]\n Int = 1"}, + + // Private field being set also counts as "not empty". + {struct { + F Tpriv `toml:"f,omitempty"` + }{Tpriv{0, 1}}, "[f]\n Int = 0"}, + + // time.Time is common use case, so test that explicitly. + {struct { + F Ttime `toml:"t,omitempty"` + }{}, ""}, + {struct { + F Ttime `toml:"t,omitempty"` + }{Ttime{time.Time{}.Add(1)}}, "[t]\n Time = 0001-01-01T00:00:00.000000001Z"}, + + // TODO: also test with MarshalText, MarshalTOML returning non-zero + // value. + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + buf := new(bytes.Buffer) + + err := NewEncoder(buf).Encode(tt.in) + if err != nil { + t.Fatal(err) + } + + have := strings.TrimSpace(buf.String()) + if have != tt.want { + t.Errorf("\nhave:\n%s\nwant:\n%s", have, tt.want) + } + }) + } +} + func TestEncodeWithOmitEmpty(t *testing.T) { type simple struct { Bool bool `toml:"bool,omitempty"` @@ -123,6 +187,7 @@ func TestEncodeWithOmitEmpty(t *testing.T) { Array [0]byte `toml:"array,omitempty"` Slice []int `toml:"slice,omitempty"` Map map[string]string `toml:"map,omitempty"` + Time time.Time `toml:"time,omitempty"` } var v simple @@ -132,10 +197,12 @@ func TestEncodeWithOmitEmpty(t *testing.T) { String: " ", Slice: []int{2, 3, 4}, Map: map[string]string{"foo": "bar"}, + Time: time.Date(1985, 6, 18, 15, 16, 17, 0, time.UTC), } expected := `bool = true string = " " slice = [2, 3, 4] +time = 1985-06-18T15:16:17Z [map] foo = "bar" diff --git a/internal/toml-test/json.go b/internal/toml-test/json.go index a5fa2830..4394f419 100644 --- a/internal/toml-test/json.go +++ b/internal/toml-test/json.go @@ -1,3 +1,6 @@ +//go:build go1.16 +// +build go1.16 + package tomltest import ( diff --git a/internal/toml-test/runner_test.go b/internal/toml-test/runner_test.go deleted file mode 100644 index 715c0516..00000000 --- a/internal/toml-test/runner_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package tomltest - -import ( - "os" - "testing" -) - -func notInList(t *testing.T, list []string, str string) { - t.Helper() - for _, item := range list { - if item == str { - t.Fatalf("error: %q in list", str) - } - } -} - -func TestVersion(t *testing.T) { - _, err := Runner{Version: "0.9", Files: os.DirFS("./tests")}.Run() - if err == nil { - t.Fatal("expected an error for version 0.9") - } - - r := Runner{Version: "1.0.0", Files: os.DirFS("./tests")} - ls, err := r.List() - if err != nil { - t.Fatal() - } - notInList(t, ls, "valid/string/escape-esc") - - r = Runner{Version: "0.4.0", Files: os.DirFS("./tests")} - ls, err = r.List() - if err != nil { - t.Fatal() - } - notInList(t, ls, "valid/string/escape-esc") // 1.0 - notInList(t, ls, "valid/array/mixed-int-string") // 0.5 - notInList(t, ls, "valid/key/dotted") // 0.4 -} diff --git a/internal/toml-test/toml.go b/internal/toml-test/toml.go index 346f0a96..934e6314 100644 --- a/internal/toml-test/toml.go +++ b/internal/toml-test/toml.go @@ -1,3 +1,6 @@ +//go:build go1.16 +// +build go1.16 + package tomltest import ( diff --git a/internal/toml-test/version.go b/internal/toml-test/version.go index 300282ad..8ebadb0c 100644 --- a/internal/toml-test/version.go +++ b/internal/toml-test/version.go @@ -1,3 +1,6 @@ +//go:build go1.16 +// +build go1.16 + package tomltest type versionSpec struct {