Skip to content

Commit

Permalink
Option to keep fields ordered when marshal struct (#266)
Browse files Browse the repository at this point in the history
Adds a new `Order()` option to preserve order of struct fields when
marshaling.
  • Loading branch information
brendesp authored and pelletier committed Apr 2, 2019
1 parent f9070d3 commit 63909f0
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 137 deletions.
43 changes: 37 additions & 6 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,21 @@ var annotationDefault = annotation{
defaultValue: tagDefault,
}

type marshalOrder int

// Orders the Encoder can write the fields to the output stream.
const (
// Sort fields alphabetically.
OrderAlphabetical marshalOrder = iota + 1
// Preserve the order the fields are encountered. For example, the order of fields in
// a struct.
OrderPreserve
)

var timeType = reflect.TypeOf(time.Time{})
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()

// Check if the given marshall type maps to a Tree primitive
// Check if the given marshal type maps to a Tree primitive
func isPrimitive(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
Expand All @@ -79,7 +90,7 @@ func isPrimitive(mtype reflect.Type) bool {
}
}

// Check if the given marshall type maps to a Tree slice
// Check if the given marshal type maps to a Tree slice
func isTreeSlice(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Slice:
Expand All @@ -89,7 +100,7 @@ func isTreeSlice(mtype reflect.Type) bool {
}
}

// Check if the given marshall type maps to a non-Tree slice
// Check if the given marshal type maps to a non-Tree slice
func isOtherSlice(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
Expand All @@ -101,7 +112,7 @@ func isOtherSlice(mtype reflect.Type) bool {
}
}

// Check if the given marshall type maps to a Tree
// Check if the given marshal type maps to a Tree
func isTree(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Map:
Expand Down Expand Up @@ -159,6 +170,8 @@ Tree primitive types and corresponding marshal types:
string string, pointers to same
bool bool, pointers to same
time.Time time.Time{}, pointers to same
For additional flexibility, use the Encoder API.
*/
func Marshal(v interface{}) ([]byte, error) {
return NewEncoder(nil).marshal(v)
Expand All @@ -169,6 +182,9 @@ type Encoder struct {
w io.Writer
encOpts
annotation
line int
col int
order marshalOrder
}

// NewEncoder returns a new encoder that writes to w.
Expand All @@ -177,6 +193,9 @@ func NewEncoder(w io.Writer) *Encoder {
w: w,
encOpts: encOptsDefaults,
annotation: annotationDefault,
line: 0,
col: 1,
order: OrderAlphabetical,
}
}

Expand Down Expand Up @@ -222,6 +241,12 @@ func (e *Encoder) ArraysWithOneElementPerLine(v bool) *Encoder {
return e
}

// Order allows to change in which order fields will be written to the output stream.
func (e *Encoder) Order(ord marshalOrder) *Encoder {
e.order = ord
return e
}

// SetTagName allows changing default tag "toml"
func (e *Encoder) SetTagName(v string) *Encoder {
e.tag = v
Expand Down Expand Up @@ -269,17 +294,22 @@ func (e *Encoder) marshal(v interface{}) ([]byte, error) {
}

var buf bytes.Buffer
_, err = t.writeTo(&buf, "", "", 0, e.arraysOneElementPerLine)
_, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order)

return buf.Bytes(), err
}

// Create next tree with a position based on Encoder.line
func (e *Encoder) nextTree() *Tree {
return newTreeWithPosition(Position{Line: e.line, Col: 1})
}

// Convert given marshal struct or map value to toml tree
func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, error) {
if mtype.Kind() == reflect.Ptr {
return e.valueToTree(mtype.Elem(), mval.Elem())
}
tval := newTree()
tval := e.nextTree()
switch mtype.Kind() {
case reflect.Struct:
for i := 0; i < mtype.NumField(); i++ {
Expand Down Expand Up @@ -347,6 +377,7 @@ func (e *Encoder) valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (int

// Convert given marshal value to toml value
func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
e.line++
if mtype.Kind() == reflect.Ptr {
return e.valueToToml(mtype.Elem(), mval.Elem())
}
Expand Down
38 changes: 38 additions & 0 deletions marshal_OrderPreserve_test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
title = "TOML Marshal Testing"

[basic_lists]
floats = [12.3,45.6,78.9]
bools = [true,false,true]
dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z]
ints = [8001,8001,8002]
uints = [5002,5003]
strings = ["One","Two","Three"]

[[subdocptrs]]
name = "Second"

[basic_map]
one = "one"
two = "two"

[subdoc]

[subdoc.second]
name = "Second"

[subdoc.first]
name = "First"

[basic]
uint = 5001
bool = true
float = 123.4
int = 5000
string = "Bite me"
date = 1979-05-27T07:32:00Z

[[subdoclist]]
name = "List.First"

[[subdoclist]]
name = "List.Second"
81 changes: 65 additions & 16 deletions marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import (
)

type basicMarshalTestStruct struct {
String string `toml:"string"`
StringList []string `toml:"strlist"`
Sub basicMarshalTestSubStruct `toml:"subdoc"`
SubList []basicMarshalTestSubStruct `toml:"sublist"`
String string `toml:"Zstring"`
StringList []string `toml:"Ystrlist"`
Sub basicMarshalTestSubStruct `toml:"Xsubdoc"`
SubList []basicMarshalTestSubStruct `toml:"Wsublist"`
}

type basicMarshalTestSubStruct struct {
Expand All @@ -29,16 +29,29 @@ var basicTestData = basicMarshalTestStruct{
SubList: []basicMarshalTestSubStruct{{"Two"}, {"Three"}},
}

var basicTestToml = []byte(`string = "Hello"
strlist = ["Howdy","Hey There"]
var basicTestToml = []byte(`Ystrlist = ["Howdy","Hey There"]
Zstring = "Hello"
[subdoc]
[[Wsublist]]
String2 = "Two"
[[Wsublist]]
String2 = "Three"
[Xsubdoc]
String2 = "One"
`)

[[sublist]]
var basicTestTomlOrdered = []byte(`Zstring = "Hello"
Ystrlist = ["Howdy","Hey There"]
[Xsubdoc]
String2 = "One"
[[Wsublist]]
String2 = "Two"
[[sublist]]
[[Wsublist]]
String2 = "Three"
`)

Expand All @@ -53,6 +66,18 @@ func TestBasicMarshal(t *testing.T) {
}
}

func TestBasicMarshalOrdered(t *testing.T) {
var result bytes.Buffer
err := NewEncoder(&result).Order(OrderPreserve).Encode(basicTestData)
if err != nil {
t.Fatal(err)
}
expected := basicTestTomlOrdered
if !bytes.Equal(result.Bytes(), expected) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result.Bytes())
}
}

func TestBasicMarshalWithPointer(t *testing.T) {
result, err := Marshal(&basicTestData)
if err != nil {
Expand All @@ -64,6 +89,18 @@ func TestBasicMarshalWithPointer(t *testing.T) {
}
}

func TestBasicMarshalOrderedWithPointer(t *testing.T) {
var result bytes.Buffer
err := NewEncoder(&result).Order(OrderPreserve).Encode(&basicTestData)
if err != nil {
t.Fatal(err)
}
expected := basicTestTomlOrdered
if !bytes.Equal(result.Bytes(), expected) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result.Bytes())
}
}

func TestBasicUnmarshal(t *testing.T) {
result := basicMarshalTestStruct{}
err := Unmarshal(basicTestToml, &result)
Expand All @@ -78,39 +115,39 @@ func TestBasicUnmarshal(t *testing.T) {

type testDoc struct {
Title string `toml:"title"`
Basics testDocBasics `toml:"basic"`
BasicLists testDocBasicLists `toml:"basic_lists"`
SubDocPtrs []*testSubDoc `toml:"subdocptrs"`
BasicMap map[string]string `toml:"basic_map"`
Subdocs testDocSubs `toml:"subdoc"`
Basics testDocBasics `toml:"basic"`
SubDocList []testSubDoc `toml:"subdoclist"`
SubDocPtrs []*testSubDoc `toml:"subdocptrs"`
err int `toml:"shouldntBeHere"`
unexported int `toml:"shouldntBeHere"`
Unexported2 int `toml:"-"`
}

type testDocBasics struct {
Uint uint `toml:"uint"`
Bool bool `toml:"bool"`
Date time.Time `toml:"date"`
Float float32 `toml:"float"`
Int int `toml:"int"`
Uint uint `toml:"uint"`
String *string `toml:"string"`
Date time.Time `toml:"date"`
unexported int `toml:"shouldntBeHere"`
}

type testDocBasicLists struct {
Floats []*float32 `toml:"floats"`
Bools []bool `toml:"bools"`
Dates []time.Time `toml:"dates"`
Floats []*float32 `toml:"floats"`
Ints []int `toml:"ints"`
Strings []string `toml:"strings"`
UInts []uint `toml:"uints"`
Strings []string `toml:"strings"`
}

type testDocSubs struct {
First testSubDoc `toml:"first"`
Second *testSubDoc `toml:"second"`
First testSubDoc `toml:"first"`
}

type testSubDoc struct {
Expand Down Expand Up @@ -174,6 +211,18 @@ func TestDocMarshal(t *testing.T) {
}
}

func TestDocMarshalOrdered(t *testing.T) {
var result bytes.Buffer
err := NewEncoder(&result).Order(OrderPreserve).Encode(docData)
if err != nil {
t.Fatal(err)
}
expected, _ := ioutil.ReadFile("marshal_OrderPreserve_test.toml")
if !bytes.Equal(result.Bytes(), expected) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result.Bytes())
}
}

func TestDocMarshalPointer(t *testing.T) {
result, err := Marshal(&docData)
if err != nil {
Expand Down
Loading

0 comments on commit 63909f0

Please sign in to comment.