Skip to content

Commit fb6d1d6

Browse files
authored
Marshal: define and fix newlines behavior when using omitempty (#798)
Ref #786
1 parent d017a6d commit fb6d1d6

File tree

6 files changed

+244
-128
lines changed

6 files changed

+244
-128
lines changed

cmd/jsontoml/main_test.go

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ func TestConvert(t *testing.T) {
2626
}`,
2727
expected: `[mytoml]
2828
a = 42.0
29-
3029
`,
3130
},
3231
{

cmd/tomll/main_test.go

-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ mytoml.a = 42.0
2323
`,
2424
expected: `[mytoml]
2525
a = 42.0
26-
2726
`,
2827
},
2928
{

internal/imported_tests/marshal_imported_test.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ func TestDocMarshal(t *testing.T) {
6767
}
6868

6969
marshalTestToml := `title = 'TOML Marshal Testing'
70+
7071
[basic_lists]
7172
floats = [12.3, 45.6, 78.9]
7273
bools = [true, false, true]
@@ -89,7 +90,6 @@ name = 'Second'
8990
[subdoc.first]
9091
name = 'First'
9192
92-
9393
[basic]
9494
uint = 5001
9595
bool = true
@@ -101,9 +101,9 @@ date = 1979-05-27T07:32:00Z
101101
102102
[[subdoclist]]
103103
name = 'List.First'
104+
104105
[[subdoclist]]
105106
name = 'List.Second'
106-
107107
`
108108

109109
result, err := toml.Marshal(docData)
@@ -117,14 +117,15 @@ func TestBasicMarshalQuotedKey(t *testing.T) {
117117

118118
expected := `'Z.string-àéù' = 'Hello'
119119
'Yfloat-𝟘' = 3.5
120+
120121
['Xsubdoc-àéù']
121122
String2 = 'One'
122123
123124
[['W.sublist-𝟘']]
124125
String2 = 'Two'
126+
125127
[['W.sublist-𝟘']]
126128
String2 = 'Three'
127-
128129
`
129130

130131
require.Equal(t, string(expected), string(result))
@@ -159,8 +160,8 @@ bool = false
159160
int = 0
160161
string = ''
161162
stringlist = []
162-
[map]
163163
164+
[map]
164165
`
165166

166167
require.Equal(t, string(expected), string(result))

marshaler.go

+76-9
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func NewEncoder(w io.Writer) *Encoder {
5454
// This behavior can be controlled on an individual struct field basis with the
5555
// inline tag:
5656
//
57-
// MyField `inline:"true"`
57+
// MyField `toml:",inline"`
5858
func (enc *Encoder) SetTablesInline(inline bool) *Encoder {
5959
enc.tablesInline = inline
6060
return enc
@@ -117,6 +117,19 @@ func (enc *Encoder) SetIndentTables(indent bool) *Encoder {
117117
// When encoding structs, fields are encoded in order of definition, with their
118118
// exact name.
119119
//
120+
// Tables and array tables are separated by empty lines. However, consecutive
121+
// subtables definitions are not. For example:
122+
//
123+
// [top1]
124+
//
125+
// [top2]
126+
// [top2.child1]
127+
//
128+
// [[array]]
129+
//
130+
// [[array]]
131+
// [array.child2]
132+
//
120133
// Struct tags
121134
//
122135
// The encoding of each public struct field can be customized by the format
@@ -333,13 +346,13 @@ func isNil(v reflect.Value) bool {
333346
}
334347
}
335348

349+
func shouldOmitEmpty(ctx encoderCtx, options valueOptions, v reflect.Value) bool {
350+
return (ctx.options.omitempty || options.omitempty) && isEmptyValue(v)
351+
}
352+
336353
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
337354
var err error
338355

339-
if (ctx.options.omitempty || options.omitempty) && isEmptyValue(v) {
340-
return b, nil
341-
}
342-
343356
if !ctx.inline {
344357
b = enc.encodeComment(ctx.indent, options.comment, b)
345358
}
@@ -365,6 +378,8 @@ func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v r
365378

366379
func isEmptyValue(v reflect.Value) bool {
367380
switch v.Kind() {
381+
case reflect.Struct:
382+
return isEmptyStruct(v)
368383
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
369384
return v.Len() == 0
370385
case reflect.Bool:
@@ -381,6 +396,34 @@ func isEmptyValue(v reflect.Value) bool {
381396
return false
382397
}
383398

399+
func isEmptyStruct(v reflect.Value) bool {
400+
// TODO: merge with walkStruct and cache.
401+
typ := v.Type()
402+
for i := 0; i < typ.NumField(); i++ {
403+
fieldType := typ.Field(i)
404+
405+
// only consider exported fields
406+
if fieldType.PkgPath != "" {
407+
continue
408+
}
409+
410+
tag := fieldType.Tag.Get("toml")
411+
412+
// special field name to skip field
413+
if tag == "-" {
414+
continue
415+
}
416+
417+
f := v.Field(i)
418+
419+
if !isEmptyValue(f) {
420+
return false
421+
}
422+
}
423+
424+
return true
425+
}
426+
384427
const literalQuote = '\''
385428

386429
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte {
@@ -410,7 +453,6 @@ func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte {
410453
return b
411454
}
412455

413-
//nolint:cyclop
414456
func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte {
415457
stringQuote := `"`
416458

@@ -757,7 +799,13 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
757799
}
758800
ctx.skipTableHeader = false
759801

802+
hasNonEmptyKV := false
760803
for _, kv := range t.kvs {
804+
if shouldOmitEmpty(ctx, kv.Options, kv.Value) {
805+
continue
806+
}
807+
hasNonEmptyKV = true
808+
761809
ctx.setKey(kv.Key)
762810

763811
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
@@ -768,7 +816,20 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
768816
b = append(b, '\n')
769817
}
770818

819+
first := true
771820
for _, table := range t.tables {
821+
if shouldOmitEmpty(ctx, table.Options, table.Value) {
822+
continue
823+
}
824+
if first {
825+
first = false
826+
if hasNonEmptyKV {
827+
b = append(b, '\n')
828+
}
829+
} else {
830+
b = append(b, "\n"...)
831+
}
832+
772833
ctx.setKey(table.Key)
773834

774835
ctx.options = table.Options
@@ -777,8 +838,6 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
777838
if err != nil {
778839
return nil, err
779840
}
780-
781-
b = append(b, '\n')
782841
}
783842

784843
return b, nil
@@ -791,6 +850,10 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
791850

792851
first := true
793852
for _, kv := range t.kvs {
853+
if shouldOmitEmpty(ctx, kv.Options, kv.Value) {
854+
continue
855+
}
856+
794857
if first {
795858
first = false
796859
} else {
@@ -806,7 +869,7 @@ func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte
806869
}
807870

808871
if len(t.tables) > 0 {
809-
panic("inline table cannot contain nested tables, online key-values")
872+
panic("inline table cannot contain nested tables, only key-values")
810873
}
811874

812875
b = append(b, "}"...)
@@ -905,6 +968,10 @@ func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.
905968
b = enc.encodeComment(ctx.indent, ctx.options.comment, b)
906969

907970
for i := 0; i < v.Len(); i++ {
971+
if i != 0 {
972+
b = append(b, "\n"...)
973+
}
974+
908975
b = append(b, scratch...)
909976

910977
var err error

0 commit comments

Comments
 (0)