snapshot of https://github.com/ent/ent.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
99
dialect/gremlin/encoding/graphson/bench_test.go
Normal file
99
dialect/gremlin/encoding/graphson/bench_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
54
dialect/gremlin/encoding/graphson/common_test.go
Normal file
54
dialect/gremlin/encoding/graphson/common_test.go
Normal 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)
|
||||
}
|
||||
109
dialect/gremlin/encoding/graphson/decode.go
Normal file
109
dialect/gremlin/encoding/graphson/decode.go
Normal 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())
|
||||
}
|
||||
}
|
||||
5
dialect/gremlin/encoding/graphson/decode_test.go
Normal file
5
dialect/gremlin/encoding/graphson/decode_test.go
Normal 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
|
||||
98
dialect/gremlin/encoding/graphson/encode.go
Normal file
98
dialect/gremlin/encoding/graphson/encode.go
Normal 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())
|
||||
}
|
||||
}
|
||||
16
dialect/gremlin/encoding/graphson/encode_test.go
Normal file
16
dialect/gremlin/encoding/graphson/encode_test.go
Normal 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)
|
||||
}
|
||||
45
dialect/gremlin/encoding/graphson/error.go
Normal file
45
dialect/gremlin/encoding/graphson/error.go
Normal 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
|
||||
}
|
||||
}
|
||||
30
dialect/gremlin/encoding/graphson/error_test.go
Normal file
30
dialect/gremlin/encoding/graphson/error_test.go
Normal 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())
|
||||
}
|
||||
88
dialect/gremlin/encoding/graphson/extension.go
Normal file
88
dialect/gremlin/encoding/graphson/extension.go
Normal 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
|
||||
}
|
||||
16
dialect/gremlin/encoding/graphson/init.go
Normal file
16
dialect/gremlin/encoding/graphson/init.go
Normal 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{})
|
||||
}
|
||||
153
dialect/gremlin/encoding/graphson/interface.go
Normal file
153
dialect/gremlin/encoding/graphson/interface.go
Normal 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
|
||||
}
|
||||
352
dialect/gremlin/encoding/graphson/interface_test.go
Normal file
352
dialect/gremlin/encoding/graphson/interface_test.go
Normal 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"])
|
||||
}
|
||||
74
dialect/gremlin/encoding/graphson/lazy.go
Normal file
74
dialect/gremlin/encoding/graphson/lazy.go
Normal 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)
|
||||
}
|
||||
46
dialect/gremlin/encoding/graphson/lazy_test.go
Normal file
46
dialect/gremlin/encoding/graphson/lazy_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
116
dialect/gremlin/encoding/graphson/map.go
Normal file
116
dialect/gremlin/encoding/graphson/map.go
Normal 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")
|
||||
}
|
||||
}
|
||||
355
dialect/gremlin/encoding/graphson/map_test.go
Normal file
355
dialect/gremlin/encoding/graphson/map_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
133
dialect/gremlin/encoding/graphson/marshaler.go
Normal file
133
dialect/gremlin/encoding/graphson/marshaler.go
Normal 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)
|
||||
}
|
||||
101
dialect/gremlin/encoding/graphson/marshaler_test.go
Normal file
101
dialect/gremlin/encoding/graphson/marshaler_test.go
Normal 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)
|
||||
}
|
||||
137
dialect/gremlin/encoding/graphson/native.go
Normal file
137
dialect/gremlin/encoding/graphson/native.go
Normal 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))
|
||||
}
|
||||
}
|
||||
299
dialect/gremlin/encoding/graphson/native_test.go
Normal file
299
dialect/gremlin/encoding/graphson/native_test.go
Normal 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)
|
||||
}
|
||||
35
dialect/gremlin/encoding/graphson/raw.go
Normal file
35
dialect/gremlin/encoding/graphson/raw.go
Normal 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
|
||||
}
|
||||
32
dialect/gremlin/encoding/graphson/raw_test.go
Normal file
32
dialect/gremlin/encoding/graphson/raw_test.go
Normal 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))
|
||||
}
|
||||
142
dialect/gremlin/encoding/graphson/slice.go
Normal file
142
dialect/gremlin/encoding/graphson/slice.go
Normal 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
|
||||
})
|
||||
}
|
||||
219
dialect/gremlin/encoding/graphson/slice_test.go
Normal file
219
dialect/gremlin/encoding/graphson/slice_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
39
dialect/gremlin/encoding/graphson/struct.go
Normal file
39
dialect/gremlin/encoding/graphson/struct.go
Normal 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
|
||||
}
|
||||
205
dialect/gremlin/encoding/graphson/struct_test.go
Normal file
205
dialect/gremlin/encoding/graphson/struct_test.go
Normal 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)
|
||||
}
|
||||
39
dialect/gremlin/encoding/graphson/tags.go
Normal file
39
dialect/gremlin/encoding/graphson/tags.go
Normal 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
|
||||
}
|
||||
60
dialect/gremlin/encoding/graphson/tags_test.go
Normal file
60
dialect/gremlin/encoding/graphson/tags_test.go
Normal 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(""))
|
||||
}
|
||||
34
dialect/gremlin/encoding/graphson/time.go
Normal file
34
dialect/gremlin/encoding/graphson/time.go
Normal 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)
|
||||
}
|
||||
36
dialect/gremlin/encoding/graphson/time_test.go
Normal file
36
dialect/gremlin/encoding/graphson/time_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
155
dialect/gremlin/encoding/graphson/type.go
Normal file
155
dialect/gremlin/encoding/graphson/type.go
Normal 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)
|
||||
}
|
||||
95
dialect/gremlin/encoding/graphson/type_test.go
Normal file
95
dialect/gremlin/encoding/graphson/type_test.go
Normal 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)
|
||||
}
|
||||
114
dialect/gremlin/encoding/graphson/util.go
Normal file
114
dialect/gremlin/encoding/graphson/util.go
Normal 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)
|
||||
}
|
||||
129
dialect/gremlin/encoding/graphson/util_test.go
Normal file
129
dialect/gremlin/encoding/graphson/util_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user