forked from TrueCloudLab/neoneo-go
stackitem: implement marshaling to JSON with types
There are 2 kinds of JSON marshaling: 1. Lossy raw marshaling, when type information is lost and map keys are expected to be valid utf-8 strings. 2. Almost lossless marshaling, which can handle any non-recursive item. Interop value preserves only type. This commit implements the second. Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
parent
b53f0257f5
commit
c16bb466a0
2 changed files with 143 additions and 0 deletions
|
@ -200,3 +200,67 @@ func (d *decoder) decodeMap() (*Map, error) {
|
||||||
m.Add(NewByteArray([]byte(k)), val)
|
m.Add(NewByteArray([]byte(k)), val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToJSONWithTypes serializes any stackitem to JSON in a lossless way.
|
||||||
|
func ToJSONWithTypes(item Item) ([]byte, error) {
|
||||||
|
result, err := toJSONWithTypes(item, make(map[Item]bool))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return json.Marshal(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toJSONWithTypes(item Item, seen map[Item]bool) (interface{}, error) {
|
||||||
|
typ := item.Type()
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"type": typ.String(),
|
||||||
|
}
|
||||||
|
var value interface{}
|
||||||
|
switch it := item.(type) {
|
||||||
|
case *Array, *Struct:
|
||||||
|
if seen[item] {
|
||||||
|
return "", errors.New("recursive structures can't be serialized to json")
|
||||||
|
}
|
||||||
|
seen[item] = true
|
||||||
|
arr := []interface{}{}
|
||||||
|
for _, elem := range it.Value().([]Item) {
|
||||||
|
s, err := toJSONWithTypes(elem, seen)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
arr = append(arr, s)
|
||||||
|
}
|
||||||
|
value = arr
|
||||||
|
case *Bool:
|
||||||
|
value = it.value
|
||||||
|
case *Buffer, *ByteArray:
|
||||||
|
value = base64.StdEncoding.EncodeToString(it.Value().([]byte))
|
||||||
|
case *BigInteger:
|
||||||
|
value = it.value.String()
|
||||||
|
case *Map:
|
||||||
|
if seen[item] {
|
||||||
|
return "", errors.New("recursive structures can't be serialized to json")
|
||||||
|
}
|
||||||
|
seen[item] = true
|
||||||
|
arr := []interface{}{}
|
||||||
|
for i := range it.value {
|
||||||
|
// map keys are primitive types and can always be converted to json
|
||||||
|
key, _ := toJSONWithTypes(it.value[i].Key, seen)
|
||||||
|
val, err := toJSONWithTypes(it.value[i].Value, seen)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
arr = append(arr, map[string]interface{}{
|
||||||
|
"key": key,
|
||||||
|
"value": val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value = arr
|
||||||
|
case *Pointer:
|
||||||
|
value = it.pos
|
||||||
|
}
|
||||||
|
if value != nil {
|
||||||
|
result["value"] = value
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -103,3 +104,81 @@ func TestFromToJSON(t *testing.T) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This test is taken from the C# code
|
||||||
|
// https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/VM/UT_Helper.cs#L30
|
||||||
|
func TestToJSONWithTypeCompat(t *testing.T) {
|
||||||
|
items := []Item{
|
||||||
|
Make(5), Make("hello world"),
|
||||||
|
Make([]byte{1, 2, 3}), Make(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we use `Equal` and not `JSONEq` because there are no spaces and maps so the order is well-defined.
|
||||||
|
s, err := ToJSONWithTypes(items[0])
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `{"type":"Integer","value":"5"}`, string(s))
|
||||||
|
|
||||||
|
s, err = ToJSONWithTypes(items[1])
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `{"type":"ByteString","value":"aGVsbG8gd29ybGQ="}`, string(s))
|
||||||
|
|
||||||
|
s, err = ToJSONWithTypes(items[2])
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `{"type":"ByteString","value":"AQID"}`, string(s))
|
||||||
|
|
||||||
|
s, err = ToJSONWithTypes(items[3])
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `{"type":"Boolean","value":true}`, string(s))
|
||||||
|
|
||||||
|
s, err = ToJSONWithTypes(NewArray(items))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `{"type":"Array","value":[{"type":"Integer","value":"5"},{"type":"ByteString","value":"aGVsbG8gd29ybGQ="},{"type":"ByteString","value":"AQID"},{"type":"Boolean","value":true}]}`, string(s))
|
||||||
|
|
||||||
|
item := NewMap()
|
||||||
|
item.Add(Make(1), NewPointer(0, []byte{0}))
|
||||||
|
s, err = ToJSONWithTypes(item)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `{"type":"Map","value":[{"key":{"type":"Integer","value":"1"},"value":{"type":"Pointer","value":0}}]}`, string(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToJSONWithTypes(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
item Item
|
||||||
|
result string
|
||||||
|
}{
|
||||||
|
{"Null", Null{}, `{"type":"Any"}`},
|
||||||
|
{"Integer", NewBigInteger(big.NewInt(42)), `{"type":"Integer","value":"42"}`},
|
||||||
|
{"ByteString", NewByteArray([]byte{1, 2, 3}), `{"type":"ByteString","value":"AQID"}`},
|
||||||
|
{"Buffer", NewBuffer([]byte{1, 2, 3}), `{"type":"Buffer","value":"AQID"}`},
|
||||||
|
{"BoolTrue", NewBool(true), `{"type":"Boolean","value":true}`},
|
||||||
|
{"BoolFalse", NewBool(false), `{"type":"Boolean","value":false}`},
|
||||||
|
{"Struct", NewStruct([]Item{Make(11)}),
|
||||||
|
`{"type":"Struct","value":[{"type":"Integer","value":"11"}]}`},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
s, err := ToJSONWithTypes(tc.item)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.result, string(s))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Invalid", func(t *testing.T) {
|
||||||
|
t.Run("RecursiveArray", func(t *testing.T) {
|
||||||
|
arr := NewArray(nil)
|
||||||
|
arr.value = []Item{Make(5), arr, Make(true)}
|
||||||
|
|
||||||
|
_, err := ToJSONWithTypes(arr)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("RecursiveMap", func(t *testing.T) {
|
||||||
|
m := NewMap()
|
||||||
|
m.Add(Make(3), Make(true))
|
||||||
|
m.Add(Make(5), m)
|
||||||
|
|
||||||
|
_, err := ToJSONWithTypes(m)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue