Sort numerically-keyed maps by numeric value.
This matches the C++ output and the specification
(https://developers.google.com/protocol-buffers/docs/proto#maps).
Technically we don't have to do this for the wire format,
but it's faster and less code to make them the same.
diff --git a/proto/lib.go b/proto/lib.go
index 0b28b08..95f7975 100644
--- a/proto/lib.go
+++ b/proto/lib.go
@@ -211,6 +211,7 @@
"fmt"
"log"
"reflect"
+ "sort"
"strconv"
"sync"
)
@@ -787,12 +788,39 @@
// If this turns out to be inefficient we can always consider other options,
// such as doing a Schwartzian transform.
-type mapKeys []reflect.Value
+func mapKeys(vs []reflect.Value) sort.Interface {
+ s := mapKeySorter{
+ vs: vs,
+ // default Less function: textual comparison
+ less: func(a, b reflect.Value) bool {
+ return fmt.Sprint(a.Interface()) < fmt.Sprint(b.Interface())
+ },
+ }
-func (s mapKeys) Len() int { return len(s) }
-func (s mapKeys) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
-func (s mapKeys) Less(i, j int) bool {
- return fmt.Sprint(s[i].Interface()) < fmt.Sprint(s[j].Interface())
+ // Type specialization per https://developers.google.com/protocol-buffers/docs/proto#maps;
+ // numeric keys are sorted numerically.
+ if len(vs) == 0 {
+ return s
+ }
+ switch vs[0].Kind() {
+ case reflect.Int32, reflect.Int64:
+ s.less = func(a, b reflect.Value) bool { return a.Int() < b.Int() }
+ case reflect.Uint32, reflect.Uint64:
+ s.less = func(a, b reflect.Value) bool { return a.Uint() < b.Uint() }
+ }
+
+ return s
+}
+
+type mapKeySorter struct {
+ vs []reflect.Value
+ less func(a, b reflect.Value) bool
+}
+
+func (s mapKeySorter) Len() int { return len(s.vs) }
+func (s mapKeySorter) Swap(i, j int) { s.vs[i], s.vs[j] = s.vs[j], s.vs[i] }
+func (s mapKeySorter) Less(i, j int) bool {
+ return s.less(s.vs[i], s.vs[j])
}
// isProto3Zero reports whether v is a zero proto3 value.
diff --git a/proto/text.go b/proto/text.go
index f7dc58a..f3db2cf 100644
--- a/proto/text.go
+++ b/proto/text.go
@@ -245,7 +245,7 @@
}
if fv.Kind() == reflect.Map {
// Map fields are rendered as a repeated struct with key/value fields.
- keys := fv.MapKeys() // TODO: should we sort these for deterministic output?
+ keys := fv.MapKeys()
sort.Sort(mapKeys(keys))
for _, key := range keys {
val := fv.MapIndex(key)
diff --git a/proto/text_test.go b/proto/text_test.go
index 39861d1..64579e9 100644
--- a/proto/text_test.go
+++ b/proto/text_test.go
@@ -421,10 +421,19 @@
{&proto3pb.Message{Name: "Rob", HeightInCm: 175}, `name:"Rob" height_in_cm:175`},
// empty map
{&pb.MessageWithMap{}, ``},
- // non-empty map; current map format is the same as a repeated struct
+ // non-empty map; map format is the same as a repeated struct,
+ // and they are sorted by key (numerically for numeric keys).
{
- &pb.MessageWithMap{NameMapping: map[int32]string{1234: "Feist"}},
- `name_mapping:<key:1234 value:"Feist" >`,
+ &pb.MessageWithMap{NameMapping: map[int32]string{
+ -1: "Negatory",
+ 7: "Lucky",
+ 1234: "Feist",
+ 6345789: "Otis",
+ }},
+ `name_mapping:<key:-1 value:"Negatory" > ` +
+ `name_mapping:<key:7 value:"Lucky" > ` +
+ `name_mapping:<key:1234 value:"Feist" > ` +
+ `name_mapping:<key:6345789 value:"Otis" >`,
},
// map with nil value; not well-defined, but we shouldn't crash
{