feat(x/tx): Sort JSON attributes for "inline_json" encoder (#20049)
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
@@ -92,10 +93,11 @@ func nullSliceAsEmptyEncoder(enc *Encoder, v protoreflect.Value, w io.Writer) er
|
||||
func cosmosInlineJSON(_ *Encoder, v protoreflect.Value, w io.Writer) error {
|
||||
switch bz := v.Interface().(type) {
|
||||
case []byte:
|
||||
if !json.Valid(bz) {
|
||||
return errors.New("invalid JSON bytes")
|
||||
json, err := sortedJsonStringify(bz)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not normalize JSON")
|
||||
}
|
||||
_, err := w.Write(bz)
|
||||
_, err = w.Write(json)
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("unsupported type %T", bz)
|
||||
@@ -189,3 +191,42 @@ func thresholdStringEncoder(enc *Encoder, msg protoreflect.Message, w io.Writer)
|
||||
_, err = io.WriteString(w, `}`)
|
||||
return err
|
||||
}
|
||||
|
||||
// sortedObject returns a new object that mirrors the structure of the original
|
||||
// but with all maps having their keys sorted.
|
||||
func sortedObject(obj interface{}) interface{} {
|
||||
switch v := obj.(type) {
|
||||
case map[string]interface{}:
|
||||
sortedKeys := make([]string, 0, len(v))
|
||||
for key := range v {
|
||||
sortedKeys = append(sortedKeys, key)
|
||||
}
|
||||
sort.Strings(sortedKeys)
|
||||
result := make(map[string]interface{})
|
||||
for _, key := range sortedKeys {
|
||||
result[key] = sortedObject(v[key])
|
||||
}
|
||||
return result
|
||||
case []interface{}:
|
||||
for i, val := range v {
|
||||
v[i] = sortedObject(val)
|
||||
}
|
||||
return v
|
||||
default:
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
// sortedJsonStringify returns a JSON with objects sorted by key.
|
||||
func sortedJsonStringify(jsonBytes []byte) ([]byte, error) {
|
||||
var obj interface{}
|
||||
if err := json.Unmarshal(jsonBytes, &obj); err != nil {
|
||||
return nil, errors.New("invalid JSON bytes")
|
||||
}
|
||||
sorted := sortedObject(obj)
|
||||
jsonData, err := json.Marshal(sorted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ func TestCosmosInlineJSON(t *testing.T) {
|
||||
wantErr: false,
|
||||
wantOutput: `[1,2,3]`,
|
||||
},
|
||||
"supported type - valid JSON is not normalized": {
|
||||
"supported type - valid JSON is normalized": {
|
||||
value: protoreflect.ValueOfBytes([]byte(`[1, 2, 3]`)),
|
||||
wantErr: false,
|
||||
wantOutput: `[1, 2, 3]`,
|
||||
wantOutput: `[1,2,3]`,
|
||||
},
|
||||
"supported type - valid JSON array (empty)": {
|
||||
value: protoreflect.ValueOfBytes([]byte(`[]`)),
|
||||
@@ -92,3 +92,83 @@ func TestCosmosInlineJSON(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortedJsonStringify(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input []byte
|
||||
wantOutput string
|
||||
}{
|
||||
"leaves true unchanged": {
|
||||
input: []byte(`true`),
|
||||
wantOutput: "true",
|
||||
},
|
||||
"leaves false unchanged": {
|
||||
input: []byte(`false`),
|
||||
wantOutput: "false",
|
||||
},
|
||||
"leaves string unchanged": {
|
||||
input: []byte(`"aabbccdd"`),
|
||||
wantOutput: `"aabbccdd"`,
|
||||
},
|
||||
"leaves number unchanged": {
|
||||
input: []byte(`75`),
|
||||
wantOutput: "75",
|
||||
},
|
||||
"leaves nil unchanged": {
|
||||
input: []byte(`null`),
|
||||
wantOutput: "null",
|
||||
},
|
||||
"leaves simple array unchanged": {
|
||||
input: []byte(`[5, 6, 7, 1]`),
|
||||
wantOutput: "[5,6,7,1]",
|
||||
},
|
||||
"leaves complex array unchanged": {
|
||||
input: []byte(`[5, ["a", "b"], true, null, 1]`),
|
||||
wantOutput: `[5,["a","b"],true,null,1]`,
|
||||
},
|
||||
"sorts empty object": {
|
||||
input: []byte(`{}`),
|
||||
wantOutput: `{}`,
|
||||
},
|
||||
"sorts single key object": {
|
||||
input: []byte(`{"a": 3}`),
|
||||
wantOutput: `{"a":3}`,
|
||||
},
|
||||
"sorts multiple keys object": {
|
||||
input: []byte(`{"a": 3, "b": 2, "c": 1}`),
|
||||
wantOutput: `{"a":3,"b":2,"c":1}`,
|
||||
},
|
||||
"sorts unsorted object": {
|
||||
input: []byte(`{"b": 2, "a": 3, "c": 1}`),
|
||||
wantOutput: `{"a":3,"b":2,"c":1}`,
|
||||
},
|
||||
"sorts unsorted complex object": {
|
||||
input: []byte(`{"aaa": true, "aa": true, "a": true}`),
|
||||
wantOutput: `{"a":true,"aa":true,"aaa":true}`,
|
||||
},
|
||||
"sorts nested objects": {
|
||||
input: []byte(`{"x": {"y": {"z": null}}}`),
|
||||
wantOutput: `{"x":{"y":{"z":null}}}`,
|
||||
},
|
||||
"sorts deeply nested unsorted objects": {
|
||||
input: []byte(`{"b": {"z": true, "x": true, "y": true}, "a": true, "c": true}`),
|
||||
wantOutput: `{"a":true,"b":{"x":true,"y":true,"z":true},"c":true}`,
|
||||
},
|
||||
"sorts objects in array sorted": {
|
||||
input: []byte(`[1, 2, {"x": {"y": {"z": null}}}, 4]`),
|
||||
wantOutput: `[1,2,{"x":{"y":{"z":null}}},4]`,
|
||||
},
|
||||
"sorts objects in array unsorted": {
|
||||
input: []byte(`[1, 2, {"b": {"z": true, "x": true, "y": true}, "a": true, "c": true}, 4]`),
|
||||
wantOutput: `[1,2,{"a":true,"b":{"x":true,"y":true,"z":true},"c":true},4]`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := sortedJsonStringify(tc.input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.wantOutput, string(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user