snapshot of https://github.com/ent/ent.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Atlas CI Public / lint (push) Failing after 13s
Atlas CI Public / sync (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 22:12:36 +05:00
commit 4b3ad0b611
2680 changed files with 719472 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"testing"
jsoniter "github.com/json-iterator/go"
)
type book struct {
ID string `json:"id" graphson:"g:UUID"`
Title string `json:"title"`
Author string `json:"author"`
Pages int `json:"num_pages"`
Chapters []string `json:"chapters"`
}
func generateObject() *book {
return &book{
ID: "21d5dcbf-1fd4-493e-9b74-d6c429f9e4a5",
Title: "The Art of Computer Programming, Vol. 2",
Author: "Donald E. Knuth",
Pages: 784,
Chapters: []string{"Random numbers", "Arithmetic"},
}
}
func BenchmarkMarshalObject(b *testing.B) {
obj := generateObject()
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
_, err := Marshal(obj)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkUnmarshalObject(b *testing.B) {
b.ReportAllocs()
out, err := Marshal(generateObject())
if err != nil {
b.Fatal(err)
}
obj := &book{}
b.ResetTimer()
for n := 0; n < b.N; n++ {
err = Unmarshal(out, obj)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkMarshalInterface(b *testing.B) {
b.ReportAllocs()
data, err := jsoniter.Marshal(generateObject())
if err != nil {
b.Fatal(err)
}
var obj any
if err = jsoniter.Unmarshal(data, &obj); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for n := 0; n < b.N; n++ {
_, err = Marshal(obj)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkUnmarshalInterface(b *testing.B) {
b.ReportAllocs()
data, err := Marshal(generateObject())
if err != nil {
b.Fatal(err)
}
var obj any
b.ResetTimer()
for n := 0; n < b.N; n++ {
err = Unmarshal(data, &obj)
if err != nil {
b.Fatal(err)
}
}
}

View File

@@ -0,0 +1,54 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"unsafe"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/mock"
)
type mocker struct {
mock.Mock
}
// Encode belongs to jsoniter.ValEncoder interface.
func (m *mocker) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
m.Called(ptr, stream)
}
// IsEmpty belongs to jsoniter.ValEncoder interface.
func (m *mocker) IsEmpty(ptr unsafe.Pointer) bool {
args := m.Called(ptr)
return args.Bool(0)
}
// Decode implements jsoniter.ValDecoder interface.
func (m *mocker) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
m.Called(ptr, iter)
}
// CheckType implements typeChecker interface.
func (m *mocker) CheckType(typ Type) error {
args := m.Called(typ)
return args.Error(0)
}
// MarshalGraphson implements Marshaler interface.
func (m *mocker) MarshalGraphson() ([]byte, error) {
args := m.Called()
data, err := args.Get(0), args.Error(1)
if data == nil {
return nil, err
}
return data.([]byte), err
}
// UnmarshalGraphson implements Unmarshaler interface.
func (m *mocker) UnmarshalGraphson(data []byte) error {
args := m.Called(data)
return args.Error(0)
}

View File

@@ -0,0 +1,109 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"io"
"reflect"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
type decodeExtension struct {
jsoniter.DummyExtension
}
// Unmarshal parses the graphson encoded data and stores the result
// in the value pointed to by v.
func Unmarshal(data []byte, v any) error {
return config.Unmarshal(data, v)
}
// UnmarshalFromString parses the graphson encoded str and stores the result
// in the value pointed to by v.
func UnmarshalFromString(str string, v any) error {
return config.UnmarshalFromString(str, v)
}
// Decoder defines a graphson decoder.
type Decoder interface {
Decode(any) error
}
// NewDecoder create a graphson decoder.
func NewDecoder(r io.Reader) Decoder {
return config.NewDecoder(r)
}
// Unmarshaler is the interface implemented by types
// that can unmarshal a graphson description of themselves.
type Unmarshaler interface {
UnmarshalGraphson([]byte) error
}
// UpdateStructDescriptor decorates struct field encoders for graphson tags.
func (ext decodeExtension) UpdateStructDescriptor(desc *jsoniter.StructDescriptor) {
for _, binding := range desc.Fields {
if tag, ok := binding.Field.Tag().Lookup("graphson"); ok && tag != "-" {
if dec := ext.DecoratorOfStructField(binding.Decoder, tag); dec != nil {
binding.Decoder = dec
}
}
}
}
// CreateDecoder returns a value decoder for type.
func (ext decodeExtension) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
if dec := ext.DecoderOfRegistered(typ); dec != nil {
return dec
}
if dec := ext.DecoderOfUnmarshaler(typ); dec != nil {
return dec
}
if dec := ext.DecoderOfNative(typ); dec != nil {
return dec
}
switch typ.Kind() {
case reflect.Array:
return ext.DecoderOfArray(typ)
case reflect.Slice:
return ext.DecoderOfSlice(typ)
case reflect.Map:
return ext.DecoderOfMap(typ)
default:
return nil
}
}
// DecorateDecoder decorates an passed in value decoder for type.
func (ext decodeExtension) DecorateDecoder(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
if dec := ext.DecoratorOfRegistered(dec); dec != nil {
return dec
}
if dec := ext.DecoratorOfUnmarshaler(typ, dec); dec != nil {
return dec
}
if dec := ext.DecoratorOfTyper(typ, dec); dec != nil {
return dec
}
if dec := ext.DecoratorOfNative(typ, dec); dec != nil {
return dec
}
switch typ.Kind() {
case reflect.Ptr, reflect.Struct:
return dec
case reflect.Interface:
return ext.DecoratorOfInterface(typ, dec)
case reflect.Slice:
return ext.DecoratorOfSlice(typ, dec)
case reflect.Array:
return ext.DecoratorOfArray(dec)
case reflect.Map:
return ext.DecoratorOfMap(dec)
default:
return ext.DecoderOfError("graphson: unsupported type: %s", typ.String())
}
}

View File

@@ -0,0 +1,5 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson

View File

@@ -0,0 +1,98 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"io"
"reflect"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
type encodeExtension struct {
jsoniter.DummyExtension
}
// Marshal returns the graphson encoding of v.
func Marshal(v any) ([]byte, error) {
return config.Marshal(v)
}
// MarshalToString returns the graphson encoding of v as string.
func MarshalToString(v any) (string, error) {
return config.MarshalToString(v)
}
// Encoder defines a graphson encoder.
type Encoder interface {
Encode(any) error
}
// NewEncoder create a graphson encoder.
func NewEncoder(w io.Writer) Encoder {
return config.NewEncoder(w)
}
// Marshaler is the interface implemented by types that
// can marshal themselves as graphson.
type Marshaler interface {
MarshalGraphson() ([]byte, error)
}
// UpdateStructDescriptor decorates struct field encoders for graphson tags.
func (ext encodeExtension) UpdateStructDescriptor(desc *jsoniter.StructDescriptor) {
for _, binding := range desc.Fields {
if tag, ok := binding.Field.Tag().Lookup("graphson"); ok && tag != "-" {
if enc := ext.DecoratorOfStructField(binding.Encoder, tag); enc != nil {
binding.Encoder = enc
}
}
}
}
// CreateEncoder returns a value encoder for type.
func (ext encodeExtension) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if enc := ext.EncoderOfRegistered(typ); enc != nil {
return enc
}
if enc := ext.EncoderOfNative(typ); enc != nil {
return enc
}
switch typ.Kind() {
case reflect.Map:
return ext.EncoderOfMap(typ)
default:
return nil
}
}
// DecorateEncoder decorates an passed in value encoder for type.
func (ext encodeExtension) DecorateEncoder(typ reflect2.Type, enc jsoniter.ValEncoder) jsoniter.ValEncoder {
if enc := ext.DecoratorOfRegistered(enc); enc != nil {
return enc
}
if enc := ext.DecoratorOfMarshaler(typ, enc); enc != nil {
return enc
}
if enc := ext.DecoratorOfTyper(typ, enc); enc != nil {
return enc
}
if enc := ext.DecoratorOfNative(typ, enc); enc != nil {
return enc
}
switch typ.Kind() {
case reflect.Ptr, reflect.Interface, reflect.Struct:
return enc
case reflect.Array:
return ext.DecoratorOfArray(enc)
case reflect.Slice:
return ext.DecoratorOfSlice(typ, enc)
case reflect.Map:
return ext.DecoratorOfMap(enc)
default:
return ext.EncoderOfError("graphson: unsupported type: %s", typ.String())
}
}

View File

@@ -0,0 +1,16 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEncodeUnsupportedType(t *testing.T) {
_, err := Marshal(func() {})
assert.Error(t, err)
}

View File

@@ -0,0 +1,45 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"fmt"
"unsafe"
jsoniter "github.com/json-iterator/go"
)
// EncoderOfError returns a value encoder which always fails to encode.
func (encodeExtension) EncoderOfError(format string, args ...any) jsoniter.ValEncoder {
return decoratorOfError(format, args...)
}
// DecoderOfError returns a value decoder which always fails to decode.
func (decodeExtension) DecoderOfError(format string, args ...any) jsoniter.ValDecoder {
return decoratorOfError(format, args...)
}
func decoratorOfError(format string, args ...any) errorCodec {
err := fmt.Errorf(format, args...)
return errorCodec{err}
}
type errorCodec struct{ error }
func (ec errorCodec) Encode(_ unsafe.Pointer, stream *jsoniter.Stream) {
if stream.Error == nil {
stream.Error = ec.error
}
}
func (errorCodec) IsEmpty(unsafe.Pointer) bool {
return false
}
func (ec errorCodec) Decode(_ unsafe.Pointer, iter *jsoniter.Iterator) {
if iter.Error == nil {
iter.Error = ec.error
}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"bytes"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestErrorCodec(t *testing.T) {
codec := errorCodec{errors.New("codec error")}
assert.False(t, codec.IsEmpty(nil))
var buf bytes.Buffer
stream := config.BorrowStream(&buf)
defer config.ReturnStream(stream)
codec.Encode(nil, stream)
assert.Empty(t, buf.Bytes())
assert.EqualError(t, stream.Error, codec.Error())
iter := config.BorrowIterator([]byte{})
defer config.ReturnIterator(iter)
codec.Decode(nil, iter)
assert.EqualError(t, iter.Error, codec.Error())
}

View File

@@ -0,0 +1,88 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"reflect"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
var (
typeEncoders = map[string]jsoniter.ValEncoder{}
typeDecoders = map[string]jsoniter.ValDecoder{}
)
// RegisterTypeEncoder register type encoder for typ.
func RegisterTypeEncoder(typ string, enc jsoniter.ValEncoder) {
typeEncoders[typ] = enc
}
// RegisterTypeDecoder register type decoder for typ.
func RegisterTypeDecoder(typ string, dec jsoniter.ValDecoder) {
typeDecoders[typ] = dec
}
type registeredEncoder struct{ jsoniter.ValEncoder }
// EncoderOfRegistered returns a value encoder of a registered type.
func (encodeExtension) EncoderOfRegistered(typ reflect2.Type) jsoniter.ValEncoder {
enc := typeEncoders[typ.String()]
if enc != nil {
return registeredEncoder{enc}
}
if typ.Kind() == reflect.Ptr {
ptrType := typ.(reflect2.PtrType)
enc := typeEncoders[ptrType.Elem().String()]
if enc != nil {
return registeredEncoder{
ValEncoder: &jsoniter.OptionalEncoder{
ValueEncoder: enc,
},
}
}
}
return nil
}
// DecoratorOfRegistered decorates a value encoder of a registered type.
func (encodeExtension) DecoratorOfRegistered(enc jsoniter.ValEncoder) jsoniter.ValEncoder {
if _, ok := enc.(registeredEncoder); ok {
return enc
}
return nil
}
type registeredDecoder struct{ jsoniter.ValDecoder }
// DecoderOfRegistered returns a value decoder of a registered type.
func (decodeExtension) DecoderOfRegistered(typ reflect2.Type) jsoniter.ValDecoder {
dec := typeDecoders[typ.String()]
if dec != nil {
return registeredDecoder{dec}
}
if typ.Kind() == reflect.Ptr {
ptrType := typ.(reflect2.PtrType)
dec := typeDecoders[ptrType.Elem().String()]
if dec != nil {
return registeredDecoder{
ValDecoder: &jsoniter.OptionalDecoder{
ValueType: ptrType.Elem(),
ValueDecoder: dec,
},
}
}
}
return nil
}
// DecoratorOfRegistered decorates a value decoder of a registered type.
func (decodeExtension) DecoratorOfRegistered(dec jsoniter.ValDecoder) jsoniter.ValDecoder {
if _, ok := dec.(registeredDecoder); ok {
return dec
}
return nil
}

View File

@@ -0,0 +1,16 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
jsoniter "github.com/json-iterator/go"
)
var config = jsoniter.Config{}.Froze()
func init() {
config.RegisterExtension(&encodeExtension{})
config.RegisterExtension(&decodeExtension{})
}

View File

@@ -0,0 +1,153 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"bytes"
"errors"
"fmt"
"io"
"reflect"
"unsafe"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
// DecoratorOfInterface decorates a value decoder of an interface type.
func (decodeExtension) DecoratorOfInterface(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
if _, ok := typ.(*reflect2.UnsafeEFaceType); ok {
return efaceDecoder{typ, dec}
}
return dec
}
type efaceDecoder struct {
typ reflect2.Type
jsoniter.ValDecoder
}
func (dec efaceDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
switch next := iter.WhatIsNext(); next {
case jsoniter.StringValue, jsoniter.BoolValue, jsoniter.NilValue:
dec.ValDecoder.Decode(ptr, iter)
case jsoniter.ObjectValue:
dec.decode(ptr, iter)
default:
iter.ReportError("decode empty interface", fmt.Sprintf("unexpected value type: %d", next))
}
}
func (dec efaceDecoder) decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
data := iter.SkipAndReturnBytes()
if iter.Error != nil && iter.Error != io.EOF {
return
}
rtype, err := dec.reflectBytes(data)
if err != nil {
iter.ReportError("decode empty interface", err.Error())
return
}
it := config.BorrowIterator(data)
defer config.ReturnIterator(it)
var val any
if rtype != nil {
val = rtype.New()
it.ReadVal(val)
val = rtype.Indirect(val)
} else {
if jsoniter.Get(data, TypeKey).LastError() == nil {
vk := jsoniter.Get(data, ValueKey)
if vk.LastError() == nil {
val = vk.GetInterface()
}
}
if val == nil {
val = it.Read()
}
}
if it.Error != nil && it.Error != io.EOF {
iter.ReportError("decode empty interface", it.Error.Error())
return
}
// nolint: gas
dec.typ.UnsafeSet(ptr, unsafe.Pointer(&val))
}
func (dec efaceDecoder) reflectBytes(data []byte) (reflect2.Type, error) {
typ := Type(jsoniter.Get(data, TypeKey).ToString())
rtype := dec.reflectType(typ)
if rtype != nil {
return rtype, nil
}
switch typ {
case listType:
return dec.reflectSlice(data)
case mapType:
return dec.reflectMap(data)
default:
return nil, nil
}
}
func (efaceDecoder) reflectType(typ Type) reflect2.Type {
switch typ {
case doubleType:
return reflect2.TypeOf(float64(0))
case floatType:
return reflect2.TypeOf(float32(0))
case byteType:
return reflect2.TypeOf(uint8(0))
case int16Type:
return reflect2.TypeOf(int16(0))
case int32Type:
return reflect2.TypeOf(int32(0))
case int64Type, bigIntegerType:
return reflect2.TypeOf(int64(0))
case byteBufferType:
return reflect2.TypeOf([]byte{})
default:
return nil
}
}
func (efaceDecoder) reflectSlice(data []byte) (reflect2.Type, error) {
var elem any
if err := Unmarshal(data, &[...]*any{&elem}); err != nil {
return nil, fmt.Errorf("cannot read first list element: %w", err)
}
if elem == nil {
return reflect2.TypeOf([]any{}), nil
}
sliceType := reflect.SliceOf(reflect.TypeOf(elem))
return reflect2.Type2(sliceType), nil
}
func (efaceDecoder) reflectMap(data []byte) (reflect2.Type, error) {
var key, elem any
if err := Unmarshal(
bytes.Replace(data, []byte(mapType), []byte(listType), 1),
&[...]*any{&key, &elem},
); err != nil {
return nil, fmt.Errorf("cannot unmarshal first map item: %w", err)
}
if key == nil {
return reflect2.TypeOf(map[any]any{}), nil
} else if elem == nil {
return nil, errors.New("expect map element, but found only key")
}
mapType := reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(elem))
return reflect2.Type2(mapType), nil
}

View File

@@ -0,0 +1,352 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDecodeInterface(t *testing.T) {
tests := []struct {
name string
in string
want any
wantErr bool
}{
{
name: "Boolean",
in: "false",
want: false,
},
{
name: "String",
in: `"str"`,
want: "str",
},
{
name: "Double",
in: `{
"@type": "g:Double",
"@value": 3.14
}`,
want: float64(3.14),
},
{
name: "Float",
in: `{
"@type": "g:Float",
"@value": -22.567
}`,
want: float32(-22.567),
},
{
name: "Int32",
in: `{
"@type": "g:Int32",
"@value": 9000
}`,
want: int32(9000),
},
{
name: "Int64",
in: `{
"@type": "g:Int64",
"@value": 188786
}`,
want: int64(188786),
},
{
name: "BigInteger",
in: `{
"@type": "gx:BigInteger",
"@value": 352353463712
}`,
want: int64(352353463712),
},
{
name: "Byte",
in: `{
"@type": "gx:Byte",
"@value": 100
}`,
want: uint8(100),
},
{
name: "Int16",
in: `{
"@type": "gx:Int16",
"@value": 2000
}`,
want: int16(2000),
},
{
name: "UnknownType",
in: `{
"@type": "g:T",
"@value": "label"
}`,
want: "label",
},
{
name: "UntypedArray",
in: "[]",
wantErr: true,
},
{
name: "NoType",
in: `{
"@typ": "g:Int32",
"@value": 345
}`,
wantErr: true,
},
{
name: "BadObject",
in: `{
"@type": "g:Int32",
"@value": 345
`,
wantErr: true,
},
{
name: "BadList",
in: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Int64",
"@val": 123457990
}
]
}`,
wantErr: true,
},
{
name: "BadMap",
in: `{
"@type": "g:Map",
"@value": [
{
"@type": "g:Int64",
"@val": 123457990
},
"First"
]
}`,
wantErr: true,
},
{
name: "KeyOnlyMap",
in: `{
"@type": "g:Map",
"@value": ["Key"]
}`,
wantErr: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var got any
err := UnmarshalFromString(tc.in, &got)
if !tc.wantErr {
require.NoError(t, err)
assert.Equal(t, tc.want, got)
} else {
assert.Error(t, err)
}
})
}
}
func TestDecodeInterfaceSlice(t *testing.T) {
tests := []struct {
in string
want any
}{
{
in: `{
"@type": "g:List",
"@value": []
}`,
want: []any{},
},
{
in: `{
"@type": "g:List",
"@value": ["x", "y", "z"]
}`,
want: []string{"x", "y", "z"},
},
{
in: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Int64",
"@value": 123457990
},
{
"@type": "g:Int64",
"@value": 23456111
},
{
"@type": "g:Int64",
"@value": -687450
}
]
}`,
want: []int64{123457990, 23456111, -687450},
},
{
in: `{
"@type": "gx:ByteBuffer",
"@value": "AQIDBAU="
}`,
want: []byte{1, 2, 3, 4, 5},
},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%T", tc.want), func(t *testing.T) {
t.Parallel()
var got any
err := UnmarshalFromString(tc.in, &got)
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestDecodeInterfaceMap(t *testing.T) {
tests := []struct {
in string
want any
}{
{
in: `{
"@type": "g:Map",
"@value": []
}`,
want: map[any]any{},
},
{
in: `{
"@type": "g:Map",
"@value": [
"Sep",
{
"@type": "g:Int32",
"@value": 9
},
"Oct",
{
"@type": "g:Int32",
"@value": 10
},
"Nov",
{
"@type": "g:Int32",
"@value": 11
}
]
}`,
want: map[string]int32{
"Sep": int32(9),
"Oct": int32(10),
"Nov": int32(11),
},
},
{
in: `{
"@type": "g:Map",
"@value": [
"One",
{
"@type": "g:List",
"@value": [
{
"@type": "g:Int32",
"@value": 1
}
]
},
"Two",
{
"@type": "g:List",
"@value": [
{
"@type": "g:Int32",
"@value": 2
}
]
},
"Three",
{
"@type": "g:List",
"@value": [
{
"@type": "g:Int32",
"@value": 3
}
]
}
]
}`,
want: map[string][]int32{
"One": {1},
"Two": {2},
"Three": {3},
},
},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%T", tc.want), func(t *testing.T) {
t.Parallel()
var got any
err := UnmarshalFromString(tc.in, &got)
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestDecodeInterfaceObject(t *testing.T) {
book := struct {
ID string `json:"id" graphson:"g:UUID"`
Title string `json:"title"`
Author string `json:"author"`
Pages int `json:"num_pages"`
Chapters []string `json:"chapters"`
}{
ID: "21d5dcbf-1fd4-493e-9b74-d6c429f9e4a5",
Title: "The Art of Computer Programming, Vol. 2",
Author: "Donald E. Knuth",
Pages: 784,
Chapters: []string{"Random numbers", "Arithmetic"},
}
data, err := Marshal(book)
require.NoError(t, err)
var v any
err = Unmarshal(data, &v)
require.NoError(t, err)
obj := v.(map[string]any)
assert.Equal(t, book.ID, obj["id"])
assert.Equal(t, book.Title, obj["title"])
assert.Equal(t, book.Author, obj["author"])
assert.EqualValues(t, book.Pages, obj["num_pages"])
assert.ElementsMatch(t, book.Chapters, obj["chapters"])
}

View File

@@ -0,0 +1,74 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"fmt"
"sync"
"unsafe"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
// LazyEncoderOf returns a lazy encoder for type.
func (encodeExtension) LazyEncoderOf(typ reflect2.Type) jsoniter.ValEncoder {
return &lazyEncoder{resolve: func() jsoniter.ValEncoder {
return config.EncoderOf(typ)
}}
}
// LazyDecoderOf returns a lazy unique decoder for type.
func (decodeExtension) LazyDecoderOf(typ reflect2.Type) jsoniter.ValDecoder {
return &lazyDecoder{resolve: func() jsoniter.ValDecoder {
dec := config.DecoderOf(reflect2.PtrTo(typ))
if td, ok := dec.(typeDecoder); ok {
td.typeChecker = &uniqueType{elemChecker: td.typeChecker}
dec = td
}
return dec
}}
}
type lazyEncoder struct {
jsoniter.ValEncoder
resolve func() jsoniter.ValEncoder
once sync.Once
}
func (enc *lazyEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
enc.once.Do(func() { enc.ValEncoder = enc.resolve() })
enc.ValEncoder.Encode(ptr, stream)
}
func (enc *lazyEncoder) IsEmpty(ptr unsafe.Pointer) bool {
enc.once.Do(func() { enc.ValEncoder = enc.resolve() })
return enc.ValEncoder.IsEmpty(ptr)
}
type lazyDecoder struct {
jsoniter.ValDecoder
resolve func() jsoniter.ValDecoder
once sync.Once
}
func (dec *lazyDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
dec.once.Do(func() { dec.ValDecoder = dec.resolve() })
dec.ValDecoder.Decode(ptr, iter)
}
type uniqueType struct {
typ Type
once sync.Once
elemChecker typeChecker
}
func (u *uniqueType) CheckType(other Type) error {
u.once.Do(func() { u.typ = other })
if u.typ != other {
return fmt.Errorf("expect type %s, but found %s", u.typ, other)
}
return u.elemChecker.CheckType(u.typ)
}

View File

@@ -0,0 +1,46 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"sync/atomic"
"testing"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestLazyEncode(t *testing.T) {
var m mocker
m.On("IsEmpty", mock.Anything).Return(false).Once()
m.On("Encode", mock.Anything, mock.Anything).Once()
defer m.AssertExpectations(t)
var cnt uint32
var enc jsoniter.ValEncoder = &lazyEncoder{resolve: func() jsoniter.ValEncoder {
assert.Equal(t, uint32(1), atomic.AddUint32(&cnt, 1))
return &m
}}
enc.IsEmpty(nil)
enc.Encode(nil, nil)
}
func TestLazyDecode(t *testing.T) {
var m mocker
m.On("Decode", mock.Anything, mock.Anything).Times(3)
defer m.AssertExpectations(t)
var cnt uint32
var dec jsoniter.ValDecoder = &lazyDecoder{resolve: func() jsoniter.ValDecoder {
assert.Equal(t, uint32(1), atomic.AddUint32(&cnt, 1))
return &m
}}
for i := 0; i < 3; i++ {
dec.Decode(nil, nil)
}
}

View File

@@ -0,0 +1,116 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"unsafe"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
// EncoderOfMap returns a value encoder of a map type.
func (ext encodeExtension) EncoderOfMap(typ reflect2.Type) jsoniter.ValEncoder {
mapType := typ.(reflect2.MapType)
return &mapEncoder{
mapType: mapType,
keyEnc: ext.LazyEncoderOf(mapType.Key()),
elemEnc: ext.LazyEncoderOf(mapType.Elem()),
}
}
// DecoratorOfMap decorates a value encoder of a map type.
func (encodeExtension) DecoratorOfMap(enc jsoniter.ValEncoder) jsoniter.ValEncoder {
return typeEncoder{enc, mapType}
}
type mapEncoder struct {
mapType reflect2.MapType
keyEnc jsoniter.ValEncoder
elemEnc jsoniter.ValEncoder
}
func (enc *mapEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
iter := enc.mapType.UnsafeIterate(ptr)
if !iter.HasNext() {
stream.WriteEmptyArray()
return
}
stream.WriteArrayStart()
for {
key, elem := iter.UnsafeNext()
enc.keyEnc.Encode(key, stream)
stream.WriteMore()
enc.elemEnc.Encode(elem, stream)
if !iter.HasNext() {
break
}
stream.WriteMore()
}
stream.WriteArrayEnd()
}
func (enc *mapEncoder) IsEmpty(ptr unsafe.Pointer) bool {
return !enc.mapType.UnsafeIterate(ptr).HasNext()
}
// DecoderOfMap returns a value decoder of a map type.
func (ext decodeExtension) DecoderOfMap(typ reflect2.Type) jsoniter.ValDecoder {
mapType := typ.(reflect2.MapType)
keyType, elemType := mapType.Key(), mapType.Elem()
return &mapDecoder{
mapType: mapType,
keyType: keyType,
elemType: elemType,
keyDec: ext.LazyDecoderOf(keyType),
elemDec: ext.LazyDecoderOf(elemType),
}
}
// DecoratorOfMap decorates a value decoder of a map type.
func (decodeExtension) DecoratorOfMap(dec jsoniter.ValDecoder) jsoniter.ValDecoder {
return typeDecoder{dec, mapType}
}
type mapDecoder struct {
mapType reflect2.MapType
keyType reflect2.Type
elemType reflect2.Type
keyDec jsoniter.ValDecoder
elemDec jsoniter.ValDecoder
}
func (dec *mapDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
mapType := dec.mapType
if mapType.UnsafeIsNil(ptr) {
mapType.UnsafeSet(ptr, mapType.UnsafeMakeMap(0))
}
var key unsafe.Pointer
if !iter.ReadArrayCB(func(iter *jsoniter.Iterator) bool {
if key == nil {
key = dec.keyType.UnsafeNew()
dec.keyDec.Decode(key, iter)
return iter.Error == nil
}
elem := dec.elemType.UnsafeNew()
dec.elemDec.Decode(elem, iter)
if iter.Error != nil {
return false
}
mapType.UnsafeSetIndex(ptr, key, elem)
key = nil
return true
}) {
return
}
if key != nil {
iter.ReportError("decode map", "odd number of map items")
}
}

View File

@@ -0,0 +1,355 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"strings"
"testing"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEncodeMap(t *testing.T) {
tests := []struct {
name string
in any
want string
}{
{
name: "simple",
in: map[int32]string{
3: "Mar",
1: "Jan",
2: "Feb",
},
want: `[
{
"@type": "g:Int32",
"@value": 1
},
"Jan",
{
"@type": "g:Int32",
"@value": 2
},
"Feb",
{
"@type": "g:Int32",
"@value": 3
},
"Mar"
]`,
},
{
name: "mixed",
in: map[string]any{
"byte": byte('a'),
"string": "str",
"slice": []int{1, 2, 3},
"map": map[string]int{},
},
want: `[
"byte",
{
"@type": "gx:Byte",
"@value": 97
},
"string",
"str",
"slice",
{
"@type": "g:List",
"@value": [
{
"@type": "g:Int64",
"@value": 1
},
{
"@type": "g:Int64",
"@value": 2
},
{
"@type": "g:Int64",
"@value": 3
}
]
},
"map",
{
"@type": "g:Map",
"@value": []
}
]`,
},
{
name: "struct-key",
in: map[struct {
K string `json:"key"`
}]int32{
{"result"}: 42,
},
want: `[
{
"key": "result"
},
{
"@type": "g:Int32",
"@value": 42
}
]`,
},
{
name: "nil",
in: map[string]uint8(nil),
want: "null",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
data, err := Marshal(tc.in)
require.NoError(t, err)
assert.Equal(t, "g:Map", jsoniter.Get(data, "@type").ToString())
var want []any
err = jsoniter.UnmarshalFromString(tc.want, &want)
require.NoError(t, err)
got, ok := jsoniter.Get(data, "@value").GetInterface().([]any)
require.True(t, ok)
assert.ElementsMatch(t, want, got)
})
}
}
func TestDecodeMap(t *testing.T) {
tests := []struct {
name string
in string
want any
}{
{
name: "empty",
in: `{
"@type": "g:Map",
"@value": []
}`,
want: map[int]int{},
},
{
name: "simple",
in: `{
"@type": "g:Map",
"@value": [
{
"@type": "g:Int32",
"@value": 6
},
"Jun",
{
"@type": "g:Int32",
"@value": 7
},
"Jul",
{
"@type": "g:Int32",
"@value": 8
},
"Aug"
]
}`,
want: map[int]string{
6: "Jun",
7: "Jul",
8: "Aug",
},
},
{
name: "duplicate",
in: `{
"@type": "g:Map",
"@value": [
"Sep",
{
"@type": "g:Int32",
"@value": 9
},
"Oct",
{
"@type": "g:Int32",
"@value": 65
},
"Oct",
{
"@type": "g:Int32",
"@value": 10
},
"Nov",
null
]
}`,
want: map[string]*int{
"Sep": func() *int { v := 9; return &v }(),
"Oct": func() *int { v := 10; return &v }(),
"Nov": nil,
},
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
typ := reflect2.TypeOf(tc.want).(reflect2.MapType)
got := typ.MakeMap(0)
err := UnmarshalFromString(tc.in, got)
require.NoError(t, err)
assert.Equal(t, tc.want, typ.Indirect(got))
})
}
}
func TestDecodeMapIntoNil(t *testing.T) {
var got map[int64]int32
err := UnmarshalFromString(`{
"@type": "g:Map",
"@value": [
{
"@type": "g:Int64",
"@value": 9
},
{
"@type": "g:Int32",
"@value": -9
},
{
"@type": "g:Int64",
"@value": 99
},
{
"@type": "g:Int32",
"@value": -99
},
{
"@type": "g:Int64",
"@value": 999
},
{
"@type": "g:Int32",
"@value": -999
}
]
}`, &got)
require.NoError(t, err)
assert.Equal(t, map[int64]int32{9: -9, 99: -99, 999: -999}, got)
}
func TestDecodeBadMap(t *testing.T) {
tests := []struct {
name string
in string
}{
{
name: "BadValue",
in: `{
"@type": "g:Map",
"@value": [
{
"@type": "g:Int64",
"@value": 9
},
{
"@type": "g:Int32",
"@value": "55"
}
]
}`,
},
{
name: "NoValue",
in: `{
"@type": "g:Map",
"@value": [
{
"@type": "g:Int64",
"@value": 9
},
{
"@type": "g:Int32",
"@value": 9
},
{
"@type": "g:Int64",
"@value": 42
}
]
}`,
},
{
name: "AlterKeyType",
in: `{
"@type": "g:Map",
"@value": [
{
"@type": "g:Int64",
"@value": 9
},
{
"@type": "g:Int32",
"@value": 9
},
{
"@type": "g:Int32",
"@value": 42
},
{
"@type": "g:Int32",
"@value": 42
}
]
}`,
},
{
name: "AlterValType",
in: `{
"@type": "g:Map",
"@value": [
{
"@type": "g:Int64",
"@value": 9
},
{
"@type": "g:Int32",
"@value": 9
},
{
"@type": "g:Int64",
"@value": 42
},
{
"@type": "g:Int64",
"@value": 42
}
]
}`,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var v map[int]int
err := NewDecoder(strings.NewReader(tc.in)).Decode(&v)
assert.Error(t, err)
})
}
}

View File

@@ -0,0 +1,133 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"fmt"
"io"
"unsafe"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
// DecoratorOfMarshaler decorates a value encoder of a Marshaler interface.
func (ext encodeExtension) DecoratorOfMarshaler(typ reflect2.Type, enc jsoniter.ValEncoder) jsoniter.ValEncoder {
if typ == marshalerType {
enc := marshalerEncoder{enc, typ}
return directMarshalerEncoder{enc}
}
if typ.Implements(marshalerType) {
return marshalerEncoder{enc, typ}
}
ptrType := reflect2.PtrTo(typ)
if ptrType.Implements(marshalerType) {
ptrEnc := ext.LazyEncoderOf(ptrType)
enc := marshalerEncoder{ptrEnc, ptrType}
return referenceEncoder{enc}
}
return nil
}
// DecoderOfUnmarshaler returns a value decoder of an Unmarshaler interface.
func (decodeExtension) DecoderOfUnmarshaler(typ reflect2.Type) jsoniter.ValDecoder {
ptrType := reflect2.PtrTo(typ)
if ptrType.Implements(unmarshalerType) {
return referenceDecoder{
unmarshalerDecoder{ptrType},
}
}
return nil
}
// DecoratorOfUnmarshaler decorates a value encoder of an Unmarshaler interface.
func (decodeExtension) DecoratorOfUnmarshaler(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
if reflect2.PtrTo(typ).Implements(unmarshalerType) {
return dec
}
return nil
}
var (
marshalerType = reflect2.TypeOfPtr((*Marshaler)(nil)).Elem()
unmarshalerType = reflect2.TypeOfPtr((*Unmarshaler)(nil)).Elem()
)
type marshalerEncoder struct {
jsoniter.ValEncoder
reflect2.Type
}
func (enc marshalerEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
marshaler := enc.Type.UnsafeIndirect(ptr).(Marshaler)
enc.encode(marshaler, stream)
}
func (enc marshalerEncoder) encode(marshaler Marshaler, stream *jsoniter.Stream) {
data, err := marshaler.MarshalGraphson()
if err != nil {
stream.Error = fmt.Errorf("graphson: error calling MarshalGraphson for type %s: %w", enc.Type, err)
return
}
if !config.Valid(data) {
stream.Error = fmt.Errorf("graphson: syntax error when marshaling type %s", enc.Type)
return
}
_, stream.Error = stream.Write(data)
}
type directMarshalerEncoder struct {
marshalerEncoder
}
func (enc directMarshalerEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
marshaler := *(*Marshaler)(ptr)
enc.encode(marshaler, stream)
}
type referenceEncoder struct {
jsoniter.ValEncoder
}
func (enc referenceEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
// nolint: gas
enc.ValEncoder.Encode(unsafe.Pointer(&ptr), stream)
}
func (enc referenceEncoder) IsEmpty(ptr unsafe.Pointer) bool {
// nolint: gas
return enc.ValEncoder.IsEmpty(unsafe.Pointer(&ptr))
}
type unmarshalerDecoder struct {
reflect2.Type
}
func (dec unmarshalerDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
bytes := iter.SkipAndReturnBytes()
if iter.Error != nil && iter.Error != io.EOF {
return
}
unmarshaler := dec.UnsafeIndirect(ptr).(Unmarshaler)
if err := unmarshaler.UnmarshalGraphson(bytes); err != nil {
iter.ReportError(
"unmarshal graphson",
fmt.Sprintf(
"graphson: error calling UnmarshalGraphson for type %s: %s",
dec.Type, err,
),
)
}
}
type referenceDecoder struct {
jsoniter.ValDecoder
}
func (dec referenceDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
// nolint: gas
dec.ValDecoder.Decode(unsafe.Pointer(&ptr), iter)
}

View File

@@ -0,0 +1,101 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"errors"
"fmt"
"reflect"
"testing"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestMarshalerEncode(t *testing.T) {
want := []byte(`{"@type": "g:Int32", "@value": 42}`)
m := &mocker{}
call := m.On("MarshalGraphson").Return(want, nil)
defer m.AssertExpectations(t)
tests := []any{m, &m, func() *Marshaler { marshaler := Marshaler(m); return &marshaler }(), Marshaler(nil)}
call.Times(len(tests) - 1)
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%T", tc), func(t *testing.T) {
got, err := Marshal(tc)
assert.NoError(t, err)
if !reflect2.IsNil(tc) {
assert.Equal(t, want, got)
} else {
assert.Equal(t, []byte("null"), got)
}
})
}
}
func TestMarshalerError(t *testing.T) {
errStr := "marshaler error"
m := &mocker{}
m.On("MarshalGraphson").Return(nil, errors.New(errStr)).Once()
defer m.AssertExpectations(t)
_, err := Marshal(m)
assert.Error(t, err)
assert.Contains(t, err.Error(), errStr)
}
func TestBadMarshaler(t *testing.T) {
m := &mocker{}
m.On("MarshalGraphson").Return([]byte(`{"@type": "g:Int32", "@value":`), nil).Once()
defer m.AssertExpectations(t)
_, err := Marshal(m)
assert.Error(t, err)
}
func TestUnmarshalerDecode(t *testing.T) {
data := `{"@type": "g:UUID", "@value": "cb682578-9d92-4499-9ebc-5c6aa73c5397"}`
var value string
m := &mocker{}
m.On("UnmarshalGraphson", mock.Anything).
Run(func(args mock.Arguments) {
data := args.Get(0).([]byte)
value = jsoniter.Get(data, "@value").ToString()
}).
Return(nil).
Once()
defer m.AssertExpectations(t)
err := UnmarshalFromString(data, m)
require.NoError(t, err)
assert.Equal(t, "cb682578-9d92-4499-9ebc-5c6aa73c5397", value)
}
func TestUnmarshalerError(t *testing.T) {
errStr := "unmarshaler error"
m := &mocker{}
m.On("UnmarshalGraphson", mock.Anything).Return(errors.New(errStr)).Once()
defer m.AssertExpectations(t)
err := Unmarshal([]byte(`{}`), m)
require.Error(t, err)
assert.Contains(t, err.Error(),
fmt.Sprintf("graphson: error calling UnmarshalGraphson for type %s: %s",
reflect.TypeOf(m), errStr,
),
)
}
func TestUnmarshalBadInput(t *testing.T) {
m := &mocker{}
defer m.AssertExpectations(t)
err := UnmarshalFromString(`{"@type"}`, m)
assert.Error(t, err)
}

View File

@@ -0,0 +1,137 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"fmt"
"io"
"math"
"reflect"
"unsafe"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
// EncoderOfNative returns a value encoder of a native type.
func (encodeExtension) EncoderOfNative(typ reflect2.Type) jsoniter.ValEncoder {
switch typ.Kind() {
case reflect.Float64:
return float64Encoder{typ}
default:
return nil
}
}
// DecoratorOfNative decorates a value encoder of a native type.
func (encodeExtension) DecoratorOfNative(typ reflect2.Type, enc jsoniter.ValEncoder) jsoniter.ValEncoder {
switch typ.Kind() {
case reflect.Bool, reflect.String:
return enc
case reflect.Int64, reflect.Int, reflect.Uint32:
return typeEncoder{enc, int64Type}
case reflect.Int32, reflect.Int8, reflect.Uint16:
return typeEncoder{enc, int32Type}
case reflect.Int16:
return typeEncoder{enc, int16Type}
case reflect.Uint64, reflect.Uint:
return typeEncoder{enc, bigIntegerType}
case reflect.Uint8:
return typeEncoder{enc, byteType}
case reflect.Float32:
return typeEncoder{enc, floatType}
case reflect.Float64:
return typeEncoder{enc, doubleType}
default:
return nil
}
}
// DecoderOfNative returns a value decoder of a native type.
func (decodeExtension) DecoderOfNative(typ reflect2.Type) jsoniter.ValDecoder {
switch typ.Kind() {
case reflect.Float64:
return float64Decoder{typ}
default:
return nil
}
}
// DecoratorOfNative decorates a value decoder of a native type.
func (decodeExtension) DecoratorOfNative(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
switch typ.Kind() {
case reflect.Bool:
return dec
case reflect.String:
return typeDecoder{dec, typeCheckerFunc(func(Type) error { return nil })}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return typeDecoder{dec, integerTypes}
case reflect.Float32:
return typeDecoder{dec, floatTypes}
case reflect.Float64:
return typeDecoder{dec, doubleTypes}
default:
return nil
}
}
type float64Encoder struct {
reflect2.Type
}
func (enc float64Encoder) IsEmpty(ptr unsafe.Pointer) bool {
return enc.UnsafeIndirect(ptr).(float64) == 0
}
func (enc float64Encoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
f := enc.UnsafeIndirect(ptr).(float64)
switch {
case math.IsNaN(f):
stream.WriteString("NaN")
case math.IsInf(f, 1):
stream.WriteString("Infinity")
case math.IsInf(f, -1):
stream.WriteString("-Infinity")
default:
stream.WriteFloat64(f)
}
}
type float64Decoder struct {
reflect2.Type
}
var (
integerTypes = Types{byteType, int16Type, int32Type, int64Type, bigIntegerType}
floatTypes = append(integerTypes, floatType, bigDecimal)
doubleTypes = append(floatTypes, doubleType)
)
func (dec float64Decoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
var val float64
switch next := iter.WhatIsNext(); next {
case jsoniter.NumberValue:
val = iter.ReadFloat64()
case jsoniter.StringValue:
switch str := iter.ReadString(); str {
case "NaN":
val = math.NaN()
case "Infinity":
val = math.Inf(1)
case "-Infinity":
val = math.Inf(-1)
default:
iter.ReportError("decode float64", "invalid value "+str)
}
default:
iter.ReportError("decode float64", fmt.Sprintf("unexpected value type: %d", next))
}
if iter.Error == nil || iter.Error == io.EOF {
// nolint: gas
dec.UnsafeSet(ptr, unsafe.Pointer(&val))
}
}

View File

@@ -0,0 +1,299 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"fmt"
"math"
"testing"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEncodeNative(t *testing.T) {
tests := []struct {
in any
want string
wantErr bool
}{
{
in: true,
want: "true",
},
{
in: "hello",
want: `"hello"`,
},
{
in: int8(120),
want: `{
"@type": "g:Int32",
"@value": 120
}`,
},
{
in: int16(-16),
want: `{
"@type": "gx:Int16",
"@value": -16
}`,
},
{
in: int32(3232),
want: `{
"@type": "g:Int32",
"@value": 3232
}`,
},
{
in: int64(646464),
want: `{
"@type": "g:Int64",
"@value": 646464
}`,
},
{
in: int(127001),
want: `{
"@type": "g:Int64",
"@value": 127001
}`,
},
{
in: uint8(81),
want: `{
"@type": "gx:Byte",
"@value": 81
}`,
},
{
in: uint16(12345),
want: `{
"@type": "g:Int32",
"@value": 12345
}`,
},
{
in: uint32(123454321),
want: `{
"@type": "g:Int64",
"@value": 123454321
}`,
},
{
in: uint64(1234567890),
want: `{
"@type": "gx:BigInteger",
"@value": 1234567890
}`,
},
{
in: uint(9876543210),
want: `{
"@type" :"gx:BigInteger",
"@value": 9876543210
}`,
},
{
in: float32(math.Pi),
want: `{
"@type": "g:Float",
"@value": 3.1415927
}`,
},
{
in: float64(math.E),
want: `{
"@type": "g:Double",
"@value": 2.718281828459045
}`,
},
{
in: math.NaN(),
want: `{
"@type": "g:Double",
"@value": "NaN"
}`,
},
{
in: math.Inf(1),
want: `{
"@type": "g:Double",
"@value": "Infinity"
}`,
},
{
in: math.Inf(-1),
want: `{
"@type": "g:Double",
"@value": "-Infinity"
}`,
},
{
in: func() *int { v := 7142; return &v }(),
want: `{
"@type": "g:Int64",
"@value": 7142
}`,
},
{
in: func() any { v := int16(6116); return &v }(),
want: `{
"@type": "gx:Int16",
"@value": 6116
}`,
},
{
in: nil,
want: "null",
},
{
in: make(chan int),
wantErr: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%T", tc.in), func(t *testing.T) {
t.Parallel()
got, err := MarshalToString(tc.in)
if !tc.wantErr {
assert.NoError(t, err)
assert.JSONEq(t, tc.want, got)
} else {
assert.Error(t, err)
assert.Empty(t, got)
}
})
}
}
func TestDecodeNative(t *testing.T) {
tests := []struct {
in string
want any
}{
{
in: `{"@type": "g:Float", "@value": 3.14}`,
want: float32(3.14),
},
{
in: `{"@type": "g:Float", "@value": "Float"}`,
},
{
in: `{"@type": "g:Double", "@value": 2.71}`,
want: float64(2.71),
},
{
in: `{"@type": "gx:BigDecimal", "@value": 3.142}`,
want: float32(3.142),
},
{
in: `{"@type": "gx:BigDecimal", "@value": 55512.5176}`,
want: float64(55512.5176),
},
{
in: `{"@type": "g:T", "@value": "world"}`,
want: "world",
},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%T", tc.want), func(t *testing.T) {
t.Parallel()
if tc.want != nil {
typ := reflect2.TypeOf(tc.want)
got := typ.New()
err := UnmarshalFromString(tc.in, got)
require.NoError(t, err)
assert.Equal(t, tc.want, typ.Indirect(got))
} else {
var msg jsoniter.RawMessage
err := UnmarshalFromString(tc.in, &msg)
assert.Error(t, err)
}
})
}
}
func TestDecodeTypeMismatch(t *testing.T) {
t.Run("FloatToInt", func(t *testing.T) {
var v int
err := UnmarshalFromString(`{"@type": "g:Float", "@value": 3.14}`, &v)
assert.Error(t, err)
})
t.Run("DoubleToFloat", func(t *testing.T) {
var v float32
err := UnmarshalFromString(`{"@type": "g:Double", "@value": 5.51}`, &v)
assert.Error(t, err)
})
t.Run("BigDecimalToUint64", func(t *testing.T) {
var v uint64
err := UnmarshalFromString(`{"@type": "gx:BigDecimal", "@value": 5645.51834}`, &v)
assert.Error(t, err)
})
}
func TestDecodeNaNInfinity(t *testing.T) {
tests := []struct {
data []byte
expect func(*testing.T, float64, error)
}{
{
data: []byte(`{"@type": "g:Double", "@value": "NaN"}`),
expect: func(t *testing.T, f float64, err error) {
assert.NoError(t, err)
assert.True(t, math.IsNaN(f))
},
},
{
data: []byte(`{"@type": "g:Double", "@value": "Infinity"}`),
expect: func(t *testing.T, f float64, err error) {
assert.NoError(t, err)
assert.True(t, math.IsInf(f, 1))
},
},
{
data: []byte(`{"@type": "g:Double", "@value": "-Infinity"}`),
expect: func(t *testing.T, f float64, err error) {
assert.NoError(t, err)
assert.True(t, math.IsInf(f, -1))
},
},
{
data: []byte(`{"@type": "g:Double", "@value": "Junk"}`),
expect: func(t *testing.T, _ float64, err error) {
assert.Error(t, err)
},
},
{
data: []byte(`{"@type": "g:Double", "@value": [42]}`),
expect: func(t *testing.T, _ float64, err error) {
assert.Error(t, err)
},
},
}
for _, tc := range tests {
var f float64
err := Unmarshal(tc.data, &f)
tc.expect(t, f, err)
}
}
func TestDecodeTypeDefinition(t *testing.T) {
type Status int
const StatusOk Status = 42
var status Status
err := UnmarshalFromString(`{"@type": "g:Int64", "@value": 42}`, &status)
assert.NoError(t, err)
assert.Equal(t, StatusOk, status)
}

View File

@@ -0,0 +1,35 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"errors"
)
// RawMessage is a raw encoded graphson value.
type RawMessage []byte
// RawMessage must implement Marshaler/Unmarshaler interfaces.
var (
_ Marshaler = (*RawMessage)(nil)
_ Unmarshaler = (*RawMessage)(nil)
)
// MarshalGraphson returns m as the graphson encoding of m.
func (m RawMessage) MarshalGraphson() ([]byte, error) {
if m == nil {
return []byte("null"), nil
}
return m, nil
}
// UnmarshalGraphson sets *m to a copy of data.
func (m *RawMessage) UnmarshalGraphson(data []byte) error {
if m == nil {
return errors.New("graphson.RawMessage: UnmarshalGraphson on nil pointer")
}
*m = append((*m)[0:0], data...)
return nil
}

View File

@@ -0,0 +1,32 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRawMessageEncoding(t *testing.T) {
var s struct{ M RawMessage }
got, err := MarshalToString(s)
require.NoError(t, err)
assert.Equal(t, `{"M":null}`, got)
s.M = []byte(`"155a"`)
got, err = MarshalToString(s)
require.NoError(t, err)
assert.JSONEq(t, `{"M": "155a"}`, got)
err = (*RawMessage)(nil).UnmarshalGraphson(s.M)
assert.Error(t, err)
s.M = nil
err = UnmarshalFromString(got, &s)
require.NoError(t, err)
assert.Equal(t, `"155a"`, string(s.M))
}

View File

@@ -0,0 +1,142 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"fmt"
"io"
"reflect"
"unsafe"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
// DecoratorOfSlice decorates a value encoder of a slice type.
func (encodeExtension) DecoratorOfSlice(typ reflect2.Type, enc jsoniter.ValEncoder) jsoniter.ValEncoder {
encoder := typeEncoder{ValEncoder: enc}
sliceType := typ.(reflect2.SliceType)
if sliceType.Elem().Kind() == reflect.Uint8 {
encoder.Type = byteBufferType
} else {
encoder.Type = listType
}
return sliceEncoder{sliceType, encoder}
}
// DecoratorOfArray decorates a value encoder of an array type.
func (encodeExtension) DecoratorOfArray(enc jsoniter.ValEncoder) jsoniter.ValEncoder {
return typeEncoder{enc, listType}
}
// DecoderOfSlice returns a value decoder of a slice type.
func (ext decodeExtension) DecoderOfSlice(typ reflect2.Type) jsoniter.ValDecoder {
sliceType := typ.(reflect2.SliceType)
elemType := sliceType.Elem()
if elemType.Kind() == reflect.Uint8 {
return nil
}
return sliceDecoder{
sliceType: sliceType,
elemDec: ext.LazyDecoderOf(elemType),
}
}
// DecoderOfArray returns a value decoder of an array type.
func (ext decodeExtension) DecoderOfArray(typ reflect2.Type) jsoniter.ValDecoder {
arrayType := typ.(reflect2.ArrayType)
return arrayDecoder{
arrayType: arrayType,
elemDec: ext.LazyDecoderOf(arrayType.Elem()),
}
}
// DecoratorOfSlice decorates a value decoder of a slice type.
func (ext decodeExtension) DecoratorOfSlice(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
if typ.(reflect2.SliceType).Elem().Kind() == reflect.Uint8 {
return typeDecoder{dec, byteBufferType}
}
return typeDecoder{dec, listType}
}
// DecoratorOfArray decorates a value decoder of an array type.
func (ext decodeExtension) DecoratorOfArray(dec jsoniter.ValDecoder) jsoniter.ValDecoder {
return typeDecoder{dec, listType}
}
type sliceEncoder struct {
sliceType reflect2.SliceType
jsoniter.ValEncoder
}
func (enc sliceEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
if enc.sliceType.UnsafeIsNil(ptr) {
stream.WriteNil()
} else {
enc.ValEncoder.Encode(ptr, stream)
}
}
type sliceDecoder struct {
sliceType reflect2.SliceType
elemDec jsoniter.ValDecoder
}
func (dec sliceDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
dec.decode(ptr, iter)
if iter.Error != nil && iter.Error != io.EOF {
iter.Error = fmt.Errorf("decoding slice %s: %w", dec.sliceType, iter.Error)
}
}
func (dec sliceDecoder) decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
sliceType := dec.sliceType
if iter.ReadNil() {
sliceType.UnsafeSetNil(ptr)
return
}
sliceType.UnsafeSet(ptr, sliceType.UnsafeMakeSlice(0, 0))
var length int
iter.ReadArrayCB(func(iter *jsoniter.Iterator) bool {
idx := length
length++
sliceType.UnsafeGrow(ptr, length)
elem := sliceType.UnsafeGetIndex(ptr, idx)
dec.elemDec.Decode(elem, iter)
return iter.Error == nil
})
}
type arrayDecoder struct {
arrayType reflect2.ArrayType
elemDec jsoniter.ValDecoder
}
func (dec arrayDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
dec.decode(ptr, iter)
if iter.Error != nil && iter.Error != io.EOF {
iter.Error = fmt.Errorf("decoding array %s: %w", dec.arrayType, iter.Error)
}
}
func (dec arrayDecoder) decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
var (
arrayType = dec.arrayType
length int
)
iter.ReadArrayCB(func(iter *jsoniter.Iterator) bool {
if length < arrayType.Len() {
idx := length
length++
elem := arrayType.UnsafeGetIndex(ptr, idx)
dec.elemDec.Decode(elem, iter)
} else {
iter.Skip()
}
return iter.Error == nil
})
}

View File

@@ -0,0 +1,219 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/modern-go/reflect2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEncodeArray(t *testing.T) {
t.Parallel()
got, err := MarshalToString([...]string{"a", "b", "c"})
require.NoError(t, err)
want := `{ "@type": "g:List", "@value": ["a", "b", "c"]}`
assert.JSONEq(t, want, got)
}
func TestEncodeSlice(t *testing.T) {
tests := []struct {
in any
want string
}{
{
in: []int32{5, 6, 7, 8},
want: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Int32",
"@value": 5
},
{
"@type": "g:Int32",
"@value": 6
},
{
"@type": "g:Int32",
"@value": 7
},
{
"@type": "g:Int32",
"@value": 8
}
]
}`,
},
{
in: []byte{1, 2, 3, 4, 5},
want: `{
"@type": "gx:ByteBuffer",
"@value": "AQIDBAU="
}`,
},
{
in: [...]byte{4, 5},
want: `{
"@type": "g:List",
"@value": [
{
"@type": "gx:Byte",
"@value": 4
},
{
"@type": "gx:Byte",
"@value": 5
}
]
}`,
},
{
in: []uint64(nil),
want: "null",
},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%T", tc.in), func(t *testing.T) {
t.Parallel()
var got bytes.Buffer
err := NewEncoder(&got).Encode(tc.in)
assert.NoError(t, err)
assert.JSONEq(t, tc.want, got.String())
})
}
}
func TestDecodeSlice(t *testing.T) {
tests := []struct {
in string
want any
}{
{
in: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Int32",
"@value": 3
},
{
"@type": "g:Int32",
"@value": -2
},
{
"@type": "g:Int32",
"@value": 1
}
]
}`,
want: []int32{3, -2, 1},
},
{
in: `{
"@type": "g:List",
"@value": ["a", "b", "c"]
}`,
want: []string{"a", "b", "c"},
},
{
in: `{
"@type": "gx:ByteBuffer",
"@value": "AQIDBAU="
}`,
want: []byte{1, 2, 3, 4, 5},
},
{
in: `{
"@type": "g:List",
"@value": [
{
"@type": "gx:Byte",
"@value": 42
},
{
"@type": "gx:Byte",
"@value": 55
},
{
"@type": "gx:Byte",
"@value": 94
}
]
}`,
want: [...]byte{42, 55},
},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf("%T", tc.want), func(t *testing.T) {
t.Parallel()
typ := reflect2.TypeOf(tc.want)
got := typ.New()
err := NewDecoder(strings.NewReader(tc.in)).Decode(got)
require.NoError(t, err)
assert.Equal(t, tc.want, typ.Indirect(got))
})
}
}
func TestDecodeBadSlice(t *testing.T) {
tests := []struct {
name string
in string
new func() any
}{
{
name: "TypeMismatch",
in: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Int32",
"@value": 3
},
{
"@type": "g:Int64",
"@value": 2
}
]
}`,
new: func() any { return &[]int{} },
},
{
name: "BadValue",
in: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Int32",
"@value": 3
},
{
"@type": "g:Int32",
"@value": "2"
}
]
}`,
new: func() any { return &[2]int{} },
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := NewDecoder(strings.NewReader(tc.in)).Decode(tc.new())
assert.Error(t, err)
})
}
}

View File

@@ -0,0 +1,39 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import jsoniter "github.com/json-iterator/go"
// DecoratorOfStructField decorates a struct field value encoder.
func (encodeExtension) DecoratorOfStructField(enc jsoniter.ValEncoder, tag string) jsoniter.ValEncoder {
typ, _ := parseTag(tag)
if typ == "" {
return nil
}
encoder, ok := enc.(typeEncoder)
if !ok {
encoder = typeEncoder{ValEncoder: enc}
}
encoder.Type = Type(typ)
return encoder
}
// DecoratorOfStructField decorates a struct field value decoder.
func (decodeExtension) DecoratorOfStructField(dec jsoniter.ValDecoder, tag string) jsoniter.ValDecoder {
typ, _ := parseTag(tag)
if typ == "" {
return nil
}
decoder, ok := dec.(typeDecoder)
if !ok {
decoder = typeDecoder{ValDecoder: dec}
}
decoder.typeChecker = Type(typ)
return decoder
}

View File

@@ -0,0 +1,205 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"testing"
"github.com/modern-go/reflect2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEncodeStruct(t *testing.T) {
tests := []struct {
name string
in any
want string
}{
{
name: "Simple",
in: struct {
S string
I int
}{
S: "string",
I: 1000,
},
want: `{
"S":"string",
"I": {
"@type": "g:Int64",
"@value": 1000
}
}`,
},
{
name: "Tagged",
in: struct {
ID string `json:"requestId" graphson:"g:UUID"`
Seq int `json:"seq" graphson:"g:Int32"`
Op string `json:"op" graphson:","`
Args map[string]string `json:"args"`
}{
ID: "cb682578-9d92-4499-9ebc-5c6aa73c5397",
Seq: 42,
Op: "authentication",
Args: map[string]string{
"sasl": "AHN0ZXBocGhlbgBwYXNzd29yZA==",
},
},
want: `{
"requestId": {
"@type": "g:UUID",
"@value": "cb682578-9d92-4499-9ebc-5c6aa73c5397"
},
"seq": {
"@type": "g:Int32",
"@value": 42
},
"op": "authentication",
"args": {
"@type": "g:Map",
"@value": ["sasl", "AHN0ZXBocGhlbgBwYXNzd29yZA=="]
}
}`,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
data, err := MarshalToString(tc.in)
require.NoError(t, err)
assert.JSONEq(t, tc.want, data)
})
}
}
func TestEncodeNestedStruct(t *testing.T) {
type S struct {
Parent *S `json:"parent,omitempty"`
ID int `json:"id" graphson:"g:Int32"`
}
v := S{Parent: &S{ID: 1}, ID: 2}
want := `{
"id": {
"@type": "g:Int32",
"@value": 2
},
"parent": {
"id": {
"@type": "g:Int32",
"@value": 1
}
}
}`
got, err := MarshalToString(&v)
require.NoError(t, err)
assert.JSONEq(t, want, got)
}
func TestDecodeStruct(t *testing.T) {
tests := []struct {
name string
in string
want any
}{
{
name: "Simple",
in: `{
"S":"str",
"I": {
"@type": "g:Int32",
"@value": 9999
}
}`,
want: struct {
S string
I int32
}{
S: "str",
I: 9999,
},
},
{
name: "Tagged",
in: `{
"requestId": {
"@type": "g:UUID",
"@value": "cb682578-9d92-4499-9ebc-5c6aa73c5397"
},
"seq": {
"@type": "g:Int32",
"@value": 42
},
"op": "authentication",
"args": {
"@type": "g:Map",
"@value": ["sasl", "AHN0ZXBocGhlbgBwYXNzd29yZA=="]
}
}`,
want: struct {
ID string `json:"requestId" graphson:"g:UUID"`
Seq int `json:"seq" graphson:"g:Int32"`
Op string `json:"op" graphson:","`
Args map[string]string `json:"args"`
}{
ID: "cb682578-9d92-4499-9ebc-5c6aa73c5397",
Seq: 42,
Op: "authentication",
Args: map[string]string{
"sasl": "AHN0ZXBocGhlbgBwYXNzd29yZA==",
},
},
},
{
name: "Empty",
in: `{}`,
want: struct{}{},
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
typ := reflect2.TypeOf(tc.want)
got := typ.New()
err := UnmarshalFromString(tc.in, got)
require.NoError(t, err)
assert.Equal(t, tc.want, typ.Indirect(got))
})
}
}
func TestDecodeNestedStruct(t *testing.T) {
type S struct {
Parent *S `json:"parent,omitempty"`
ID int `json:"id" graphson:"g:Int32"`
}
in := `{
"id": {
"@type": "g:Int32",
"@value": 37
},
"parent": {
"id": {
"@type": "g:Int32",
"@value": 65
}
}
}`
var got S
err := UnmarshalFromString(in, &got)
require.NoError(t, err)
want := S{Parent: &S{ID: 65}, ID: 37}
assert.Equal(t, want, got)
}

View File

@@ -0,0 +1,39 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"strings"
)
type tagOptions string
// parseTag splits a struct field's graphson tag into its type and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
if idx := strings.Index(tag, ","); idx != -1 {
return tag[:idx], tagOptions(tag[idx+1:])
}
return tag, ""
}
// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (opts tagOptions) Contains(opt string) bool {
s := string(opts)
for s != "" {
var next string
i := strings.Index(s, ",")
if i >= 0 {
s, next = s[:i], s[i+1:]
}
if s == opt {
return true
}
s = next
}
return false
}

View File

@@ -0,0 +1,60 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseTag(t *testing.T) {
tests := []struct {
name string
tag string
typ string
opts tagOptions
}{
{
name: "Empty",
},
{
name: "TypeOnly",
tag: "g:Int32",
typ: "g:Int32",
},
{
name: "OptsOnly",
tag: ",opt1,opt2",
opts: "opt1,opt2",
},
{
name: "TypeAndOpts",
tag: "g:UUID,opt3,opt4",
typ: "g:UUID",
opts: "opt3,opt4",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
typ, opts := parseTag(tc.tag)
assert.Equal(t, tc.typ, typ)
assert.Equal(t, tc.opts, opts)
})
}
}
func TestTagOptionsContains(t *testing.T) {
_, opts := parseTag(",opt1,opt2,opt3")
assert.True(t, opts.Contains("opt1"))
assert.True(t, opts.Contains("opt2"))
assert.True(t, opts.Contains("opt3"))
assert.False(t, opts.Contains("opt4"))
assert.False(t, opts.Contains("opt11"))
assert.False(t, opts.Contains(""))
}

View File

@@ -0,0 +1,34 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"time"
"unsafe"
jsoniter "github.com/json-iterator/go"
)
func init() {
RegisterTypeEncoder("time.Time", typeEncoder{timeCodec{}, Timestamp})
RegisterTypeDecoder("time.Time", typeDecoder{timeCodec{}, Types{Timestamp, Date}})
}
type timeCodec struct{}
func (timeCodec) IsEmpty(ptr unsafe.Pointer) bool {
ts := *((*time.Time)(ptr))
return ts.IsZero()
}
func (timeCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
ts := *((*time.Time)(ptr))
stream.WriteInt64(ts.UnixNano() / time.Millisecond.Nanoseconds())
}
func (timeCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
ns := iter.ReadInt64() * time.Millisecond.Nanoseconds()
*((*time.Time)(ptr)) = time.Unix(0, ns)
}

View File

@@ -0,0 +1,36 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTimeEncoding(t *testing.T) {
const ms = 1481750076295
ts := time.Unix(0, ms*time.Millisecond.Nanoseconds())
for _, v := range []any{ts, &ts} {
got, err := MarshalToString(v)
require.NoError(t, err)
assert.JSONEq(t, `{ "@type": "g:Timestamp", "@value": 1481750076295 }`, got)
}
strs := []string{
`{ "@type": "g:Timestamp", "@value": 1481750076295 }`,
`{ "@type": "g:Date", "@value": 1481750076295 }`,
}
for _, str := range strs {
var v time.Time
err := UnmarshalFromString(str, &v)
assert.NoError(t, err)
assert.True(t, ts.Equal(v))
}
}

View File

@@ -0,0 +1,155 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"fmt"
"reflect"
"strings"
"unsafe"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
// A Type is a graphson type.
type Type string
// graphson typed value types.
const (
// core
doubleType Type = "g:Double"
floatType Type = "g:Float"
int32Type Type = "g:Int32"
int64Type Type = "g:Int64"
listType Type = "g:List"
mapType Type = "g:Map"
Timestamp Type = "g:Timestamp"
Date Type = "g:Date"
// extended
bigIntegerType Type = "gx:BigInteger"
bigDecimal Type = "gx:BigDecimal"
byteType Type = "gx:Byte"
byteBufferType Type = "gx:ByteBuffer"
int16Type Type = "gx:Int16"
)
// String implements fmt.Stringer interface.
func (typ Type) String() string {
return string(typ)
}
// CheckType implements typeChecker interface.
func (typ Type) CheckType(other Type) error {
if typ != other {
return fmt.Errorf("expect type %s, but found %s", typ, other)
}
return nil
}
// Types is a slice of Type.
type Types []Type
// Contains reports whether a slice of types contains a particular type.
func (types Types) Contains(typ Type) bool {
for i := range types {
if types[i] == typ {
return true
}
}
return false
}
// String implements fmt.Stringer interface.
func (types Types) String() string {
var builder strings.Builder
builder.WriteByte('[')
for i := range types {
if i > 0 {
builder.WriteByte(',')
}
builder.WriteString(types[i].String())
}
builder.WriteByte(']')
return builder.String()
}
// CheckType implements typeChecker interface.
func (types Types) CheckType(typ Type) error {
if !types.Contains(typ) {
return fmt.Errorf("expect any of %s, but found %s", types, typ)
}
return nil
}
// Typer is the interface implemented by types that
// define an underlying graphson type.
type Typer interface {
GraphsonType() Type
}
var typerType = reflect2.TypeOfPtr((*Typer)(nil)).Elem()
// DecoratorOfTyper decorates a value encoder of a Typer interface.
func (ext encodeExtension) DecoratorOfTyper(typ reflect2.Type, enc jsoniter.ValEncoder) jsoniter.ValEncoder {
if typ.Kind() != reflect.Struct {
return nil
}
if typ.Implements(typerType) {
return typerEncoder{
typeEncoder: typeEncoder{ValEncoder: enc},
typerOf: func(ptr unsafe.Pointer) Typer {
return typ.UnsafeIndirect(ptr).(Typer)
},
}
}
ptrType := reflect2.PtrTo(typ)
if ptrType.Implements(typerType) {
return typerEncoder{
typeEncoder: typeEncoder{ValEncoder: enc},
typerOf: func(ptr unsafe.Pointer) Typer {
// nolint: gas
return ptrType.UnsafeIndirect(unsafe.Pointer(&ptr)).(Typer)
},
}
}
return nil
}
// DecoratorOfTyper decorates a value decoder of a Typer interface.
func (ext decodeExtension) DecoratorOfTyper(typ reflect2.Type, dec jsoniter.ValDecoder) jsoniter.ValDecoder {
ptrType := reflect2.PtrTo(typ)
if ptrType.Implements(typerType) {
return typerDecoder{
typeDecoder: typeDecoder{ValDecoder: dec},
typerOf: func(ptr unsafe.Pointer) Typer {
// nolint: gas
return ptrType.UnsafeIndirect(unsafe.Pointer(&ptr)).(Typer)
},
}
}
return nil
}
type typerEncoder struct {
typeEncoder
typerOf func(unsafe.Pointer) Typer
}
func (enc typerEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
enc.typeEncoder.Type = enc.typerOf(ptr).GraphsonType()
enc.typeEncoder.Encode(ptr, stream)
}
type typerDecoder struct {
typeDecoder
typerOf func(unsafe.Pointer) Typer
}
func (dec typerDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
dec.typeDecoder.typeChecker = dec.typerOf(ptr).GraphsonType()
dec.typeDecoder.Decode(ptr, iter)
}

View File

@@ -0,0 +1,95 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestTypeCheckType(t *testing.T) {
assert.NoError(t, int32Type.CheckType(int32Type))
assert.Error(t, int32Type.CheckType(int64Type))
}
func TestTypesCheckType(t *testing.T) {
assert.NoError(t, Types{int16Type, int32Type, int64Type}.CheckType(int32Type))
assert.Error(t, Types{floatType, doubleType}.CheckType(bigIntegerType))
}
func TestTypesString(t *testing.T) {
assert.Equal(t, "[]", Types{}.String())
assert.Equal(t, "[gx:Byte]", Types{byteType}.String())
assert.Equal(t, "[gx:Int16,g:Int32,g:Int64]", Types{int16Type, int32Type, int64Type}.String())
}
type vertex struct {
ID int `json:"id"`
Label string `json:"label"`
}
func (vertex) GraphsonType() Type {
return Type("g:Vertex")
}
type mockVertex struct {
mock.Mock `json:"-"`
ID int `json:"id"`
Label string `json:"label"`
}
func (m *mockVertex) GraphsonType() Type {
return m.Called().Get(0).(Type)
}
func TestEncodeTyper(t *testing.T) {
m := &mockVertex{ID: 42, Label: "person"}
m.On("GraphsonType").Return(Type("g:Vertex")).Twice()
defer m.AssertExpectations(t)
v := vertex{ID: m.ID, Label: m.Label}
var vv Typer = v
want := `{
"@type": "g:Vertex",
"@value": {
"id": {
"@type": "g:Int64",
"@value": 42
},
"label": "person"
}
}`
for _, tc := range []any{m, &m, v, vv, &vv} {
got, err := MarshalToString(tc)
assert.NoError(t, err)
assert.JSONEq(t, want, got)
}
}
func TestDecodeTyper(t *testing.T) {
var m mockVertex
m.On("GraphsonType").Return(Type("g:Vertex")).Once()
defer m.AssertExpectations(t)
in := `{
"@type": "g:Vertex",
"@value": {
"id": {
"@type": "g:Int64",
"@value": 55
},
"label": "user"
}
}`
err := UnmarshalFromString(in, &m)
assert.NoError(t, err)
assert.Equal(t, 55, m.ID)
assert.Equal(t, "user", m.Label)
}

View File

@@ -0,0 +1,114 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"errors"
"io"
"unsafe"
jsoniter "github.com/json-iterator/go"
)
// graphson encoding type / value keys
const (
TypeKey = "@type"
ValueKey = "@value"
)
// typeEncoder adds graphson type information to a value encoder.
type typeEncoder struct {
jsoniter.ValEncoder
Type Type
}
// Encode belongs to jsoniter.ValEncoder interface.
func (enc typeEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
stream.WriteObjectStart()
stream.WriteObjectField(TypeKey)
stream.WriteString(enc.Type.String())
stream.WriteMore()
stream.WriteObjectField(ValueKey)
enc.ValEncoder.Encode(ptr, stream)
stream.WriteObjectEnd()
}
type (
// typeDecoder decorates a value decoder and adds graphson type verification.
typeDecoder struct {
jsoniter.ValDecoder
typeChecker
}
// typeChecker defines an interface for graphson type verification.
typeChecker interface {
CheckType(Type) error
}
// typeCheckerFunc allows the use of functions as type checkers.
typeCheckerFunc func(Type) error
// typeValue defines a graphson type / value pair.
typeValue struct {
Type Type
Value jsoniter.RawMessage
}
)
// Decode belongs to jsoniter.ValDecoder interface.
func (dec typeDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
if iter.WhatIsNext() != jsoniter.ObjectValue {
dec.ValDecoder.Decode(ptr, iter)
return
}
data := iter.SkipAndReturnBytes()
if iter.Error != nil && iter.Error != io.EOF {
return
}
var tv typeValue
if err := jsoniter.Unmarshal(data, &tv); err != nil {
iter.ReportError("unmarshal type value", err.Error())
return
}
if err := dec.CheckType(tv.Type); err != nil {
iter.ReportError("check type", err.Error())
return
}
it := config.BorrowIterator(tv.Value)
defer config.ReturnIterator(it)
dec.ValDecoder.Decode(ptr, it)
if it.Error != nil && it.Error != io.EOF {
iter.ReportError("decode value", it.Error.Error())
}
}
// UnmarshalJSON implements json.Unmarshaler interface.
func (tv *typeValue) UnmarshalJSON(data []byte) error {
var v struct {
Type *Type `json:"@type"`
Value jsoniter.RawMessage `json:"@value"`
}
if err := jsoniter.Unmarshal(data, &v); err != nil {
return err
}
if v.Type == nil || v.Value == nil {
return errors.New("missing type or value")
}
tv.Type = *v.Type
tv.Value = v.Value
return nil
}
// CheckType implements typeChecker interface.
func (f typeCheckerFunc) CheckType(typ Type) error {
return f(typ)
}

View File

@@ -0,0 +1,129 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graphson
import (
"bytes"
"errors"
"fmt"
"testing"
"unsafe"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestTypeEncode(t *testing.T) {
var got bytes.Buffer
stream := config.BorrowStream(&got)
defer config.ReturnStream(stream)
typ, val := int32Type, 42
ptr := unsafe.Pointer(&val)
var m mocker
m.On("Encode", ptr, stream).
Run(func(args mock.Arguments) {
stream := args.Get(1).(*jsoniter.Stream)
stream.WriteInt(val)
}).
Once()
defer m.AssertExpectations(t)
typeEncoder{&m, typ}.Encode(ptr, stream)
require.NoError(t, stream.Flush())
want := fmt.Sprintf(`{"@type": "%s", "@value": %d}`, typ, val)
assert.JSONEq(t, want, got.String())
}
func TestTypeDecode(t *testing.T) {
typ, val := int64Type, 84
ptr := unsafe.Pointer(&val)
data := fmt.Sprintf(`{"@value": %d, "@type": "%s"}`, val, typ)
iter := config.BorrowIterator([]byte(data))
defer config.ReturnIterator(iter)
m := &mocker{}
m.On("CheckType", typ).
Return(nil).
Once()
m.On("Decode", ptr, mock.Anything).
Run(func(args mock.Arguments) {
iter := args.Get(1).(*jsoniter.Iterator)
assert.Equal(t, val, iter.ReadInt())
}).
Once()
defer m.AssertExpectations(t)
typeDecoder{m, m}.Decode(ptr, iter)
assert.NoError(t, iter.Error)
}
func TestTypeDecodeBadType(t *testing.T) {
typ, val := int64Type, 55
ptr := unsafe.Pointer(&val)
m := &mocker{}
m.On("CheckType", typ).Return(errors.New("bad type")).Once()
defer m.AssertExpectations(t)
data := fmt.Sprintf(`{"@type": "%s", "@value": %d}`, typ, val)
iter := config.BorrowIterator([]byte(data))
defer config.ReturnIterator(iter)
typeDecoder{m, m}.Decode(ptr, iter)
require.Error(t, iter.Error)
assert.Contains(t, iter.Error.Error(), "bad type")
}
func TestTypeDecodeDuplicateField(t *testing.T) {
data := `{"@type": "gx:Byte", "@value": 33, "@type": "g:Int32"}`
iter := config.BorrowIterator([]byte(data))
defer config.ReturnIterator(iter)
var ptr unsafe.Pointer
m := &mocker{}
m.On("CheckType", mock.MatchedBy(func(typ Type) bool { return typ == int32Type })).
Return(nil).
Once()
m.On("Decode", ptr, mock.Anything).
Run(func(args mock.Arguments) {
args.Get(1).(*jsoniter.Iterator).Skip()
require.NoError(t, iter.Error)
}).
Once()
defer m.AssertExpectations(t)
typeDecoder{m, m}.Decode(ptr, iter)
assert.NoError(t, iter.Error)
}
func TestTypeDecodeMissingField(t *testing.T) {
data := `{"@type": "g:Int32"}`
iter := config.BorrowIterator([]byte(data))
defer config.ReturnIterator(iter)
m := &mocker{}
defer m.AssertExpectations(t)
typeDecoder{m, m}.Decode(nil, iter)
require.Error(t, iter.Error)
assert.Contains(t, iter.Error.Error(), "missing type or value")
}
func TestTypeDecodeSyntaxError(t *testing.T) {
data := `{"@type": "gx:Int16", "@value", 65000}`
iter := config.BorrowIterator([]byte(data))
defer config.ReturnIterator(iter)
m := &mocker{}
defer m.AssertExpectations(t)
typeDecoder{m, m}.Decode(nil, iter)
assert.Error(t, iter.Error)
}