Skip to content

Commit 846f793

Browse files
committed
Add support for JSON serialization for ordered map
Signed-off-by: Dušan Mitrović <[email protected]>
1 parent 3e40878 commit 846f793

File tree

3 files changed

+219
-26
lines changed

3 files changed

+219
-26
lines changed

types/ordered_map.go

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,20 @@ import (
55
)
66

77
type (
8-
OrderedMap[T comparable, V any] struct {
9-
insertionOrder []T
10-
values map[T]V
11-
cmpFunc func(a, b T) int
8+
OrderedMap[K comparable, V any] struct {
9+
insertionOrder []K
10+
values map[K]V
11+
cmpFunc func(a, b K) int
1212
}
1313
)
1414

1515
// NewOrderedMap
1616
//
1717
// Initializes a new instance of the OrderedMap.
18-
func NewOrderedMap[T comparable, V any](length int) OrderedMap[T, V] {
19-
return OrderedMap[T, V]{
20-
insertionOrder: make([]T, 0, length),
21-
values: make(map[T]V, length),
18+
func NewOrderedMap[K comparable, V any](length int) OrderedMap[K, V] {
19+
return OrderedMap[K, V]{
20+
insertionOrder: make([]K, 0, length),
21+
values: make(map[K]V, length),
2222
cmpFunc: nil,
2323
}
2424
}
@@ -27,28 +27,28 @@ func NewOrderedMap[T comparable, V any](length int) OrderedMap[T, V] {
2727
//
2828
// Initializes a new instance of the OrderedMap with custom ordering
2929
// dictated by the cmpFunc comparison function.
30-
func NewOrderedMapWithCompareFunc[T comparable, V any](
30+
func NewOrderedMapWithCompareFunc[K comparable, V any](
3131
length int,
32-
cmpFunc func(a, b T) int,
33-
) OrderedMap[T, V] {
34-
return OrderedMap[T, V]{
35-
insertionOrder: make([]T, 0, length),
36-
values: make(map[T]V, length),
32+
cmpFunc func(a, b K) int,
33+
) OrderedMap[K, V] {
34+
return OrderedMap[K, V]{
35+
insertionOrder: make([]K, 0, length),
36+
values: make(map[K]V, length),
3737
cmpFunc: cmpFunc,
3838
}
3939
}
4040

4141
// SetCompareFunc
4242
//
4343
// Sets the comparison function to determine ordering.
44-
func (om *OrderedMap[T, V]) SetCompareFunc(cmpFunc func(a, b T) int) {
44+
func (om *OrderedMap[K, V]) SetCompareFunc(cmpFunc func(a, b K) int) {
4545
om.cmpFunc = cmpFunc
4646
}
4747

4848
// UnsetCompareFunc
4949
//
5050
// Unsets the comparison function.
51-
func (om *OrderedMap[T, V]) UnsetCompareFunc() {
51+
func (om *OrderedMap[K, V]) UnsetCompareFunc() {
5252
om.cmpFunc = nil
5353
}
5454

@@ -57,14 +57,14 @@ func (om *OrderedMap[T, V]) UnsetCompareFunc() {
5757
// Gets the value associated with a key.
5858
// If the key doesn't exist in the map the zero value
5959
// for the type is returned and the boolean is set to false.
60-
func (om OrderedMap[T, V]) Get(key T) (V, bool) {
60+
func (om OrderedMap[K, V]) Get(key K) (V, bool) {
6161
v, ok := om.values[key]
6262

6363
return v, ok
6464
}
6565

6666
// Inserts a new value into the map while tracking the order.
67-
func (om *OrderedMap[T, V]) Set(key T, value V) {
67+
func (om *OrderedMap[K, V]) Set(key K, value V) {
6868
_, exists := om.values[key]
6969
if !exists {
7070
om.insertionOrder = append(om.insertionOrder, key)
@@ -76,8 +76,8 @@ func (om *OrderedMap[T, V]) Set(key T, value V) {
7676
// Unset
7777
//
7878
// Deletes the key from the map.
79-
func (om *OrderedMap[T, V]) Unset(key T) {
80-
newInsertionOrder := make([]T, 0, len(om.insertionOrder)-1)
79+
func (om *OrderedMap[K, V]) Unset(key K) {
80+
newInsertionOrder := make([]K, 0, len(om.insertionOrder)-1)
8181
for _, k := range om.insertionOrder {
8282
if k != key {
8383
newInsertionOrder = append(newInsertionOrder, k)
@@ -92,22 +92,22 @@ func (om *OrderedMap[T, V]) Unset(key T) {
9292
// Reset
9393
//
9494
// Resets all state including the comparison function.
95-
func (om *OrderedMap[T, V]) Reset() {
96-
om.insertionOrder = make([]T, 0)
97-
om.values = make(map[T]V, 0)
95+
func (om *OrderedMap[K, V]) Reset() {
96+
om.insertionOrder = make([]K, 0)
97+
om.values = make(map[K]V, 0)
9898
om.cmpFunc = nil
9999
}
100100

101101
// Iter iterates through the map.
102102
// If the CompareFunc is unset, it iterates in insertion order.
103-
// Otherewise, it iterates in the order dictated by CompareFunc.
103+
// Otherwise, it iterates in the order dictated by CompareFunc.
104104
//
105105
// Either use NewOrderedMapWithCompareFunc or
106106
// call SetCompareFunc to set the comparison function.
107-
func (om OrderedMap[T, V]) Iter(yield func(key T, value V) bool) {
107+
func (om OrderedMap[K, V]) Iter(yield func(key K, value V) bool) {
108108
order := om.insertionOrder
109109
if om.cmpFunc != nil {
110-
order = make([]T, len(om.insertionOrder))
110+
order = make([]K, len(om.insertionOrder))
111111
copy(order, om.insertionOrder)
112112

113113
slices.SortFunc(order, om.cmpFunc)

types/ordered_map_serialize.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package types
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"slices"
8+
)
9+
10+
var (
11+
ErrInvalidJSON = errors.New("expected a JSON object")
12+
ErrInvalidJSONKey = errors.New("expected a string key in JSON")
13+
)
14+
15+
// UnmarshalJSON
16+
//
17+
// Unserializes JSON objects into an ordered map
18+
// preserving the order in which keys appear in
19+
// the input datastream.
20+
//
21+
// Doesn't support unmarshaling JSON arrays as those
22+
// are already ordered structures for which you
23+
// shouldn't use this ordered map.
24+
func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
25+
decoder := json.NewDecoder(bytes.NewReader(data))
26+
token, err := decoder.Token()
27+
if err != nil {
28+
return err
29+
}
30+
31+
delimiter, ok := token.(json.Delim)
32+
if !ok || delimiter != '{' {
33+
return ErrInvalidJSON
34+
}
35+
36+
if om.insertionOrder == nil {
37+
om.insertionOrder = make([]K, 0)
38+
}
39+
40+
if om.values == nil {
41+
om.values = make(map[K]V, 0)
42+
}
43+
44+
for decoder.More() {
45+
token, err = decoder.Token()
46+
if err != nil {
47+
return err
48+
}
49+
50+
key, ok := token.(string)
51+
if !ok {
52+
return ErrInvalidJSONKey
53+
}
54+
55+
kJson, err := json.Marshal(key)
56+
if err != nil {
57+
return err
58+
}
59+
60+
var k K
61+
if err := json.Unmarshal(kJson, &k); err != nil {
62+
return err
63+
}
64+
65+
var v V
66+
if err := decoder.Decode(&v); err != nil {
67+
return err
68+
}
69+
70+
om.Set(k, v)
71+
}
72+
73+
_, err = decoder.Token()
74+
return err
75+
}
76+
77+
// MarshalJSON
78+
//
79+
// Serializes the ordered map into a JSON stream.
80+
//
81+
// If the CompareFunc is unset, it serializes in insertion order.
82+
// Otherwise, it serializes in the order dictated by CompareFunc.
83+
//
84+
// Either use NewOrderedMapWithCompareFunc or
85+
// call SetCompareFunc to set the comparison function.
86+
func (om OrderedMap[K, V]) MarshalJSON() ([]byte, error) {
87+
if len(om.values) == 0 {
88+
return []byte("{}"), nil
89+
}
90+
91+
b := new(bytes.Buffer)
92+
order := om.insertionOrder
93+
if om.cmpFunc != nil {
94+
order = make([]K, len(om.insertionOrder))
95+
copy(order, om.insertionOrder)
96+
97+
slices.SortFunc(order, om.cmpFunc)
98+
}
99+
100+
b.WriteRune('{')
101+
for i, k := range order {
102+
key, err := json.Marshal(k)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
value, err := json.Marshal(om.values[k])
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
b.Write(key)
113+
b.WriteRune(':')
114+
b.Write(value)
115+
if i < len(om.values)-1 {
116+
b.WriteRune(',')
117+
}
118+
}
119+
b.WriteRune('}')
120+
121+
return b.Bytes(), nil
122+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package types
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestMarshalJSONPreservesOrder(t *testing.T) {
11+
t.Parallel()
12+
13+
om := NewOrderedMap[string, any](10)
14+
om.Set("id", 123)
15+
om.Set("name", "Anamarija")
16+
om.Set("city", "Belgrade")
17+
18+
data, err := json.Marshal(om)
19+
require.NoError(t, err)
20+
21+
require.JSONEq(t,
22+
`{"id":123,"name":"Anamarija","city":"Belgrade"}`,
23+
string(data),
24+
)
25+
}
26+
27+
func TestUnmarshalJSONPopulatesValuesAndOrder(t *testing.T) {
28+
t.Parallel()
29+
30+
input := []byte(`{"z":1,"a":2,"x":5}`)
31+
om := NewOrderedMap[string, int](10)
32+
err := json.Unmarshal(input, &om)
33+
require.NoError(t, err)
34+
35+
require.Len(t, om.values, 3)
36+
require.Contains(t, om.values, "z")
37+
require.Contains(t, om.values, "a")
38+
require.Contains(t, om.values, "x")
39+
40+
require.Equal(t, []string{"z", "a", "x"}, om.insertionOrder)
41+
}
42+
43+
func TestMarshalJSONWithEmptyMap(t *testing.T) {
44+
t.Parallel()
45+
46+
om := NewOrderedMap[string, int](0)
47+
data, err := json.Marshal(om)
48+
require.NoError(t, err)
49+
require.Equal(t, "{}", string(data))
50+
}
51+
52+
func TestMarshalJSONWithCustomSort(t *testing.T) {
53+
t.Parallel()
54+
om := NewOrderedMapWithCompareFunc[string, int](10, func(i, j string) int {
55+
if i < j {
56+
return -1
57+
}
58+
if i > j {
59+
return 1
60+
}
61+
return 0
62+
})
63+
64+
om.Set("b", 2)
65+
om.Set("a", 1)
66+
67+
data, err := json.Marshal(om)
68+
require.NoError(t, err)
69+
70+
require.JSONEq(t, `{"a":1,"b":2}`, string(data))
71+
}

0 commit comments

Comments
 (0)