From 65b27e6823c427415d156c92d7034dd57a154cf8 Mon Sep 17 00:00:00 2001 From: Brent DeSpain <35586981+brendesp@users.noreply.github.com> Date: Thu, 11 Apr 2019 06:52:29 -0600 Subject: [PATCH] Order map keys alphabetically (#270) This makes sure we have a stable output when marshaling maps. Fixes #268 --- marshal.go | 22 ++++++++++++- marshal_OrderPreserve_Map_test.toml | 17 ++++++++++ marshal_test.go | 49 +++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 marshal_OrderPreserve_Map_test.toml diff --git a/marshal.go b/marshal.go index d35ac85..2571448 100644 --- a/marshal.go +++ b/marshal.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "reflect" + "sort" "strconv" "strings" "time" @@ -329,7 +330,26 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er } } case reflect.Map: - for _, key := range mval.MapKeys() { + keys := mval.MapKeys() + if e.order == OrderPreserve && len(keys) > 0 { + // Sorting []reflect.Value is not straight forward. + // + // OrderPreserve will support deterministic results when string is used + // as the key to maps. + typ := keys[0].Type() + kind := keys[0].Kind() + if kind == reflect.String { + ikeys := make([]string, len(keys)) + for i := range keys { + ikeys[i] = keys[i].Interface().(string) + } + sort.Strings(ikeys) + for i := range ikeys { + keys[i] = reflect.ValueOf(ikeys[i]).Convert(typ) + } + } + } + for _, key := range keys { mvalf := mval.MapIndex(key) val, err := e.valueToToml(mtype.Elem(), mvalf) if err != nil { diff --git a/marshal_OrderPreserve_Map_test.toml b/marshal_OrderPreserve_Map_test.toml new file mode 100644 index 0000000..a3bd513 --- /dev/null +++ b/marshal_OrderPreserve_Map_test.toml @@ -0,0 +1,17 @@ +title = "TOML Marshal Testing" + +[basic_map] + one = "one" + two = "two" + +[long_map] + a7 = "1" + b3 = "2" + c8 = "3" + d4 = "4" + e6 = "5" + f5 = "6" + g10 = "7" + h1 = "8" + i2 = "9" + j9 = "10" diff --git a/marshal_test.go b/marshal_test.go index 1ba82fa..712433a 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -126,6 +126,12 @@ type testDoc struct { Unexported2 int `toml:"-"` } +type testMapDoc struct { + Title string `toml:"title"` + BasicMap map[string]string `toml:"basic_map"` + LongMap map[string]string `toml:"long_map"` +} + type testDocBasics struct { Uint uint `toml:"uint"` Bool bool `toml:"bool"` @@ -200,6 +206,26 @@ var docData = testDoc{ SubDocPtrs: []*testSubDoc{&subdoc}, } +var mapTestDoc = testMapDoc{ + Title: "TOML Marshal Testing", + BasicMap: map[string]string{ + "one": "one", + "two": "two", + }, + LongMap: map[string]string{ + "h1": "8", + "i2": "9", + "b3": "2", + "d4": "4", + "f5": "6", + "e6": "5", + "a7": "1", + "c8": "3", + "j9": "10", + "g10": "7", + }, +} + func TestDocMarshal(t *testing.T) { result, err := Marshal(docData) if err != nil { @@ -223,6 +249,29 @@ func TestDocMarshalOrdered(t *testing.T) { } } +func TestDocMarshalMaps(t *testing.T) { + result, err := Marshal(mapTestDoc) + if err != nil { + t.Fatal(err) + } + expected, _ := ioutil.ReadFile("marshal_OrderPreserve_Map_test.toml") + if !bytes.Equal(result, expected) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result) + } +} + +func TestDocMarshalOrderedMaps(t *testing.T) { + var result bytes.Buffer + err := NewEncoder(&result).Order(OrderPreserve).Encode(mapTestDoc) + if err != nil { + t.Fatal(err) + } + expected, _ := ioutil.ReadFile("marshal_OrderPreserve_Map_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 {