Skip to content

Commit 63909f0

Browse files
brendesppelletier
authored andcommitted
Option to keep fields ordered when marshal struct (#266)
Adds a new `Order()` option to preserve order of struct fields when marshaling.
1 parent f9070d3 commit 63909f0

File tree

5 files changed

+329
-137
lines changed

5 files changed

+329
-137
lines changed

marshal.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,21 @@ var annotationDefault = annotation{
5454
defaultValue: tagDefault,
5555
}
5656

57+
type marshalOrder int
58+
59+
// Orders the Encoder can write the fields to the output stream.
60+
const (
61+
// Sort fields alphabetically.
62+
OrderAlphabetical marshalOrder = iota + 1
63+
// Preserve the order the fields are encountered. For example, the order of fields in
64+
// a struct.
65+
OrderPreserve
66+
)
67+
5768
var timeType = reflect.TypeOf(time.Time{})
5869
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()
5970

60-
// Check if the given marshall type maps to a Tree primitive
71+
// Check if the given marshal type maps to a Tree primitive
6172
func isPrimitive(mtype reflect.Type) bool {
6273
switch mtype.Kind() {
6374
case reflect.Ptr:
@@ -79,7 +90,7 @@ func isPrimitive(mtype reflect.Type) bool {
7990
}
8091
}
8192

82-
// Check if the given marshall type maps to a Tree slice
93+
// Check if the given marshal type maps to a Tree slice
8394
func isTreeSlice(mtype reflect.Type) bool {
8495
switch mtype.Kind() {
8596
case reflect.Slice:
@@ -89,7 +100,7 @@ func isTreeSlice(mtype reflect.Type) bool {
89100
}
90101
}
91102

92-
// Check if the given marshall type maps to a non-Tree slice
103+
// Check if the given marshal type maps to a non-Tree slice
93104
func isOtherSlice(mtype reflect.Type) bool {
94105
switch mtype.Kind() {
95106
case reflect.Ptr:
@@ -101,7 +112,7 @@ func isOtherSlice(mtype reflect.Type) bool {
101112
}
102113
}
103114

104-
// Check if the given marshall type maps to a Tree
115+
// Check if the given marshal type maps to a Tree
105116
func isTree(mtype reflect.Type) bool {
106117
switch mtype.Kind() {
107118
case reflect.Map:
@@ -159,6 +170,8 @@ Tree primitive types and corresponding marshal types:
159170
string string, pointers to same
160171
bool bool, pointers to same
161172
time.Time time.Time{}, pointers to same
173+
174+
For additional flexibility, use the Encoder API.
162175
*/
163176
func Marshal(v interface{}) ([]byte, error) {
164177
return NewEncoder(nil).marshal(v)
@@ -169,6 +182,9 @@ type Encoder struct {
169182
w io.Writer
170183
encOpts
171184
annotation
185+
line int
186+
col int
187+
order marshalOrder
172188
}
173189

174190
// NewEncoder returns a new encoder that writes to w.
@@ -177,6 +193,9 @@ func NewEncoder(w io.Writer) *Encoder {
177193
w: w,
178194
encOpts: encOptsDefaults,
179195
annotation: annotationDefault,
196+
line: 0,
197+
col: 1,
198+
order: OrderAlphabetical,
180199
}
181200
}
182201

@@ -222,6 +241,12 @@ func (e *Encoder) ArraysWithOneElementPerLine(v bool) *Encoder {
222241
return e
223242
}
224243

244+
// Order allows to change in which order fields will be written to the output stream.
245+
func (e *Encoder) Order(ord marshalOrder) *Encoder {
246+
e.order = ord
247+
return e
248+
}
249+
225250
// SetTagName allows changing default tag "toml"
226251
func (e *Encoder) SetTagName(v string) *Encoder {
227252
e.tag = v
@@ -269,17 +294,22 @@ func (e *Encoder) marshal(v interface{}) ([]byte, error) {
269294
}
270295

271296
var buf bytes.Buffer
272-
_, err = t.writeTo(&buf, "", "", 0, e.arraysOneElementPerLine)
297+
_, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order)
273298

274299
return buf.Bytes(), err
275300
}
276301

302+
// Create next tree with a position based on Encoder.line
303+
func (e *Encoder) nextTree() *Tree {
304+
return newTreeWithPosition(Position{Line: e.line, Col: 1})
305+
}
306+
277307
// Convert given marshal struct or map value to toml tree
278308
func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, error) {
279309
if mtype.Kind() == reflect.Ptr {
280310
return e.valueToTree(mtype.Elem(), mval.Elem())
281311
}
282-
tval := newTree()
312+
tval := e.nextTree()
283313
switch mtype.Kind() {
284314
case reflect.Struct:
285315
for i := 0; i < mtype.NumField(); i++ {
@@ -347,6 +377,7 @@ func (e *Encoder) valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (int
347377

348378
// Convert given marshal value to toml value
349379
func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
380+
e.line++
350381
if mtype.Kind() == reflect.Ptr {
351382
return e.valueToToml(mtype.Elem(), mval.Elem())
352383
}

marshal_OrderPreserve_test.toml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
title = "TOML Marshal Testing"
2+
3+
[basic_lists]
4+
floats = [12.3,45.6,78.9]
5+
bools = [true,false,true]
6+
dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z]
7+
ints = [8001,8001,8002]
8+
uints = [5002,5003]
9+
strings = ["One","Two","Three"]
10+
11+
[[subdocptrs]]
12+
name = "Second"
13+
14+
[basic_map]
15+
one = "one"
16+
two = "two"
17+
18+
[subdoc]
19+
20+
[subdoc.second]
21+
name = "Second"
22+
23+
[subdoc.first]
24+
name = "First"
25+
26+
[basic]
27+
uint = 5001
28+
bool = true
29+
float = 123.4
30+
int = 5000
31+
string = "Bite me"
32+
date = 1979-05-27T07:32:00Z
33+
34+
[[subdoclist]]
35+
name = "List.First"
36+
37+
[[subdoclist]]
38+
name = "List.Second"

marshal_test.go

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import (
1212
)
1313

1414
type basicMarshalTestStruct struct {
15-
String string `toml:"string"`
16-
StringList []string `toml:"strlist"`
17-
Sub basicMarshalTestSubStruct `toml:"subdoc"`
18-
SubList []basicMarshalTestSubStruct `toml:"sublist"`
15+
String string `toml:"Zstring"`
16+
StringList []string `toml:"Ystrlist"`
17+
Sub basicMarshalTestSubStruct `toml:"Xsubdoc"`
18+
SubList []basicMarshalTestSubStruct `toml:"Wsublist"`
1919
}
2020

2121
type basicMarshalTestSubStruct struct {
@@ -29,16 +29,29 @@ var basicTestData = basicMarshalTestStruct{
2929
SubList: []basicMarshalTestSubStruct{{"Two"}, {"Three"}},
3030
}
3131

32-
var basicTestToml = []byte(`string = "Hello"
33-
strlist = ["Howdy","Hey There"]
32+
var basicTestToml = []byte(`Ystrlist = ["Howdy","Hey There"]
33+
Zstring = "Hello"
3434
35-
[subdoc]
35+
[[Wsublist]]
36+
String2 = "Two"
37+
38+
[[Wsublist]]
39+
String2 = "Three"
40+
41+
[Xsubdoc]
3642
String2 = "One"
43+
`)
3744

38-
[[sublist]]
45+
var basicTestTomlOrdered = []byte(`Zstring = "Hello"
46+
Ystrlist = ["Howdy","Hey There"]
47+
48+
[Xsubdoc]
49+
String2 = "One"
50+
51+
[[Wsublist]]
3952
String2 = "Two"
4053
41-
[[sublist]]
54+
[[Wsublist]]
4255
String2 = "Three"
4356
`)
4457

@@ -53,6 +66,18 @@ func TestBasicMarshal(t *testing.T) {
5366
}
5467
}
5568

69+
func TestBasicMarshalOrdered(t *testing.T) {
70+
var result bytes.Buffer
71+
err := NewEncoder(&result).Order(OrderPreserve).Encode(basicTestData)
72+
if err != nil {
73+
t.Fatal(err)
74+
}
75+
expected := basicTestTomlOrdered
76+
if !bytes.Equal(result.Bytes(), expected) {
77+
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result.Bytes())
78+
}
79+
}
80+
5681
func TestBasicMarshalWithPointer(t *testing.T) {
5782
result, err := Marshal(&basicTestData)
5883
if err != nil {
@@ -64,6 +89,18 @@ func TestBasicMarshalWithPointer(t *testing.T) {
6489
}
6590
}
6691

92+
func TestBasicMarshalOrderedWithPointer(t *testing.T) {
93+
var result bytes.Buffer
94+
err := NewEncoder(&result).Order(OrderPreserve).Encode(&basicTestData)
95+
if err != nil {
96+
t.Fatal(err)
97+
}
98+
expected := basicTestTomlOrdered
99+
if !bytes.Equal(result.Bytes(), expected) {
100+
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result.Bytes())
101+
}
102+
}
103+
67104
func TestBasicUnmarshal(t *testing.T) {
68105
result := basicMarshalTestStruct{}
69106
err := Unmarshal(basicTestToml, &result)
@@ -78,39 +115,39 @@ func TestBasicUnmarshal(t *testing.T) {
78115

79116
type testDoc struct {
80117
Title string `toml:"title"`
81-
Basics testDocBasics `toml:"basic"`
82118
BasicLists testDocBasicLists `toml:"basic_lists"`
119+
SubDocPtrs []*testSubDoc `toml:"subdocptrs"`
83120
BasicMap map[string]string `toml:"basic_map"`
84121
Subdocs testDocSubs `toml:"subdoc"`
122+
Basics testDocBasics `toml:"basic"`
85123
SubDocList []testSubDoc `toml:"subdoclist"`
86-
SubDocPtrs []*testSubDoc `toml:"subdocptrs"`
87124
err int `toml:"shouldntBeHere"`
88125
unexported int `toml:"shouldntBeHere"`
89126
Unexported2 int `toml:"-"`
90127
}
91128

92129
type testDocBasics struct {
130+
Uint uint `toml:"uint"`
93131
Bool bool `toml:"bool"`
94-
Date time.Time `toml:"date"`
95132
Float float32 `toml:"float"`
96133
Int int `toml:"int"`
97-
Uint uint `toml:"uint"`
98134
String *string `toml:"string"`
135+
Date time.Time `toml:"date"`
99136
unexported int `toml:"shouldntBeHere"`
100137
}
101138

102139
type testDocBasicLists struct {
140+
Floats []*float32 `toml:"floats"`
103141
Bools []bool `toml:"bools"`
104142
Dates []time.Time `toml:"dates"`
105-
Floats []*float32 `toml:"floats"`
106143
Ints []int `toml:"ints"`
107-
Strings []string `toml:"strings"`
108144
UInts []uint `toml:"uints"`
145+
Strings []string `toml:"strings"`
109146
}
110147

111148
type testDocSubs struct {
112-
First testSubDoc `toml:"first"`
113149
Second *testSubDoc `toml:"second"`
150+
First testSubDoc `toml:"first"`
114151
}
115152

116153
type testSubDoc struct {
@@ -174,6 +211,18 @@ func TestDocMarshal(t *testing.T) {
174211
}
175212
}
176213

214+
func TestDocMarshalOrdered(t *testing.T) {
215+
var result bytes.Buffer
216+
err := NewEncoder(&result).Order(OrderPreserve).Encode(docData)
217+
if err != nil {
218+
t.Fatal(err)
219+
}
220+
expected, _ := ioutil.ReadFile("marshal_OrderPreserve_test.toml")
221+
if !bytes.Equal(result.Bytes(), expected) {
222+
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result.Bytes())
223+
}
224+
}
225+
177226
func TestDocMarshalPointer(t *testing.T) {
178227
result, err := Marshal(&docData)
179228
if err != nil {

0 commit comments

Comments
 (0)