Commit 14da24c4 authored by protolambda's avatar protolambda

op-service: jsonutil sorted-map

parent 6295ff59
package jsonutil
import (
"encoding/json"
"fmt"
"sort"
)
// LazySortedJsonMap provides sorted encoding order for JSON maps.
// The sorting is lazy: in-memory it's just a map, until it sorts just-in-time when the map is encoded to JSON.
// Warning: the just-in-time sorting requires a full allocation of the map structure and keys slice during encoding.
// Sorting order is not enforced when decoding from JSON.
type LazySortedJsonMap[K comparable, V any] map[K]V
func (m LazySortedJsonMap[K, V]) MarshalJSON() ([]byte, error) {
keys := make([]string, 0, len(m))
values := make(map[string]V)
for k, v := range m {
s := fmt.Sprintf("%q", any(k)) // format as quoted string
keys = append(keys, s)
values[s] = v
}
sort.Strings(keys)
var out []byte
out = append(out, '{')
for i, k := range keys {
out = append(out, k...) // quotes are already included
out = append(out, ':')
v, err := json.Marshal(values[k])
if err != nil {
return nil, fmt.Errorf("failed to encode value of %s: %w", k, err)
}
out = append(out, v...)
if i != len(keys)-1 {
out = append(out, ',')
}
}
out = append(out, '}')
return out, nil
}
func (m *LazySortedJsonMap[K, V]) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, (*map[K]V)(m))
}
package jsonutil
import (
"encoding/json"
"fmt"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
type LazySortedJsonMapTestCase[K comparable, V any] struct {
Object LazySortedJsonMap[K, V]
Json string
}
func (tc *LazySortedJsonMapTestCase[K, V]) Run(t *testing.T) {
t.Run("Marshal", func(t *testing.T) {
got, err := json.Marshal(tc.Object)
require.NoError(t, err)
require.Equal(t, tc.Json, string(got), "json output must match")
})
t.Run("Unmarshal", func(t *testing.T) {
var dest LazySortedJsonMap[K, V]
err := json.Unmarshal([]byte(tc.Json), &dest)
require.NoError(t, err)
require.Equal(t, len(tc.Object), len(dest), "lengths match")
for k, v := range tc.Object {
require.Equal(t, v, dest[k], "values of %q match", k)
}
})
}
func TestLazySortedJsonMap(t *testing.T) {
testCases := []interface{ Run(t *testing.T) }{
&LazySortedJsonMapTestCase[string, int]{Object: LazySortedJsonMap[string, int]{}, Json: `{}`},
&LazySortedJsonMapTestCase[string, int]{Object: LazySortedJsonMap[string, int]{"a": 1, "c": 2, "b": 3}, Json: `{"a":1,"b":3,"c":2}`},
&LazySortedJsonMapTestCase[common.Address, int]{Object: LazySortedJsonMap[common.Address, int]{
common.HexToAddress("0x4100000000000000000000000000000000000000"): 123,
common.HexToAddress("0x4200000000000000000000000000000000000000"): 100,
common.HexToAddress("0x4200000000000000000000000000000000000001"): 100,
},
Json: `{"0x4100000000000000000000000000000000000000":123,` +
`"0x4200000000000000000000000000000000000000":100,` +
`"0x4200000000000000000000000000000000000001":100}`},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("case %d", i), tc.Run)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment