Skip to content

Commit 1b4cab5

Browse files
committed
feat: ✨ unmarshal into ordered maps
this change will attempt to unmarshal `interface{}`/`any` values into an ordered map before falling back to default behavior resolves #54
1 parent 46d6868 commit 1b4cab5

File tree

5 files changed

+112
-7
lines changed

5 files changed

+112
-7
lines changed

json.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ var (
2020
// MarshalJSON implements the json.Marshaler interface.
2121
func (om *OrderedMap[K, V]) MarshalJSON() ([]byte, error) { //nolint:funlen
2222
if om == nil || om.list == nil {
23-
return []byte("null"), nil
23+
return JSONNullBytes(), nil
2424
}
2525

2626
writer := jwriter.Writer{
@@ -129,7 +129,6 @@ func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
129129
}
130130

131131
var key K
132-
var value V
133132

134133
switch typedKey := any(&key).(type) {
135134
case *string:
@@ -168,6 +167,29 @@ func (om *OrderedMap[K, V]) UnmarshalJSON(data []byte) error {
168167
}
169168
}
170169

170+
tValue := reflect.TypeFor[V]()
171+
tValueKind := tValue.Kind()
172+
173+
var value V
174+
175+
// in the case of an `any`
176+
// the value may be nil, we should represent it as such instead of an empty map
177+
// otherwise
178+
// try to unmarshall into an ordered map before falling back to
179+
// what json.Unmarshal would regularly do, which, when the value is a map, it would use map[string]any
180+
if tValueKind == reflect.Interface {
181+
if bytes.Equal(valueData, JSONNullBytes()) {
182+
om.Set(key, value)
183+
return nil
184+
}
185+
om1 := New[string, any]()
186+
if err := json.Unmarshal(valueData, &om1); err == nil {
187+
om.Set(key, any(om1).(V))
188+
return nil
189+
}
190+
}
191+
192+
// fallback to json.Unmarshal of V
171193
if err := json.Unmarshal(valueData, &value); err != nil {
172194
return err
173195
}

json_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,21 @@ func TestUnmarshallJSON(t *testing.T) {
202202

203203
assertLenEqual(t, om, 0)
204204
})
205+
206+
t.Run("nested maps become ordered", func(t *testing.T) {
207+
data := `{"foo":{"zebra":"z","apple":"a"}}`
208+
209+
om := New[string, any]()
210+
require.NoError(t, json.Unmarshal([]byte(data), &om))
211+
212+
expected := New[string, any]()
213+
expected.Set("zebra", "z")
214+
expected.Set("apple", "a")
215+
216+
got, ok := om.Get("foo")
217+
require.True(t, ok)
218+
assert.Equal(t, expected, got)
219+
})
205220
}
206221

207222
// const specialCharacters = "\\\\/\"\b\f\n\r\t\x00\uffff\ufffd世界\u007f\u00ff\U0010FFFF"

null.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package orderedmap
2+
3+
func JSONNullBytes() []byte {
4+
return []byte("null")
5+
}

yaml.go

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package orderedmap
22

33
import (
4+
"bytes"
5+
"errors"
46
"fmt"
7+
"reflect"
58

69
"gopkg.in/yaml.v3"
710
)
@@ -14,7 +17,7 @@ var (
1417
// MarshalYAML implements the yaml.Marshaler interface.
1518
func (om *OrderedMap[K, V]) MarshalYAML() (interface{}, error) {
1619
if om == nil {
17-
return []byte("null"), nil
20+
return JSONNullBytes(), nil
1821
}
1922

2023
node := yaml.Node{
@@ -55,11 +58,30 @@ func (om *OrderedMap[K, V]) UnmarshalYAML(value *yaml.Node) error {
5558

5659
for index := 0; index < len(value.Content); index += 2 {
5760
var key K
58-
var val V
59-
6061
if err := value.Content[index].Decode(&key); err != nil {
6162
return err
6263
}
64+
65+
tValue := reflect.TypeFor[V]()
66+
tValueKind := tValue.Kind()
67+
68+
fmt.Printf("tValueKind: %s\n", tValueKind)
69+
var val V
70+
71+
if tValueKind == reflect.Interface {
72+
valueNode := value.Content[index+1]
73+
if bytes.Equal([]byte(valueNode.Value), JSONNullBytes()) {
74+
om.Set(key, val)
75+
continue
76+
}
77+
78+
om1, err := tryDecodeToOrderedMap(value.Content[index+1].Decode)
79+
if err == nil {
80+
om.Set(key, om1.(V))
81+
continue
82+
}
83+
}
84+
6385
if err := value.Content[index+1].Decode(&val); err != nil {
6486
return err
6587
}
@@ -69,3 +91,27 @@ func (om *OrderedMap[K, V]) UnmarshalYAML(value *yaml.Node) error {
6991

7092
return nil
7193
}
94+
95+
// since yaml supports integers and strings as map keys, we need to try both
96+
func tryDecodeToOrderedMap(decoderFunc func(v any) (err error)) (any, error) {
97+
type newFunc func() any
98+
99+
possibilities := []newFunc{
100+
func() any { return New[int, any]() },
101+
func() any { return New[int32, any]() },
102+
func() any { return New[int64, any]() },
103+
func() any { return New[uint, any]() },
104+
func() any { return New[uint32, any]() },
105+
func() any { return New[uint64, any]() },
106+
func() any { return New[string, any]() },
107+
}
108+
109+
for _, possibilityFunc := range possibilities {
110+
possibility := possibilityFunc()
111+
if err := decoderFunc(possibility); err == nil {
112+
return possibility, nil
113+
}
114+
}
115+
116+
return nil, errors.New("could not decode to an ordered map")
117+
}

yaml_test.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package orderedmap
22

33
import (
4+
"testing"
5+
46
"github.com/stretchr/testify/assert"
57
"github.com/stretchr/testify/require"
68
"gopkg.in/yaml.v3"
7-
"testing"
89
)
910

1011
func TestMarshalYAML(t *testing.T) {
@@ -202,6 +203,21 @@ func TestUnmarshallYAML(t *testing.T) {
202203

203204
assertLenEqual(t, om, 0)
204205
})
206+
207+
t.Run("unmarshals nests maps as ordered maps", func(t *testing.T) {
208+
data := `{"foo":{"zebra":"z","apple":"a"}}`
209+
210+
om := New[string, any]()
211+
require.NoError(t, yaml.Unmarshal([]byte(data), &om))
212+
213+
expected := New[string, any]()
214+
expected.Set("zebra", "z")
215+
expected.Set("apple", "a")
216+
217+
actual, ok := om.Get("foo")
218+
require.True(t, ok)
219+
require.Equal(t, expected, actual)
220+
})
205221
}
206222

207223
func TestYAMLSpecialCharacters(t *testing.T) {
@@ -264,6 +280,7 @@ m:
264280
foo: bar
265281
foo:
266282
- 12:
283+
z: null
267284
b: true
268285
i: 12
269286
m:
@@ -277,8 +294,8 @@ m:
277294
- 2
278295
- 3
279296
- 3:
280-
c: null
281297
d: 87
298+
c: null
282299
4:
283300
e: true
284301
5:

0 commit comments

Comments
 (0)