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

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

77
dialect/gremlin/client.go Normal file
View File

@@ -0,0 +1,77 @@
// 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 gremlin
import (
"context"
"fmt"
"net/http"
)
// RoundTripper is an interface representing the ability to execute a
// single gremlin transaction, obtaining the Response for a given Request.
type RoundTripper interface {
RoundTrip(context.Context, *Request) (*Response, error)
}
// The RoundTripperFunc type is an adapter to allow the use of ordinary functions as Gremlin RoundTripper.
type RoundTripperFunc func(context.Context, *Request) (*Response, error)
// RoundTrip calls f(ctx, r).
func (f RoundTripperFunc) RoundTrip(ctx context.Context, r *Request) (*Response, error) {
return f(ctx, r)
}
// Interceptor provides a hook to intercept the execution of a Gremlin Request.
type Interceptor func(RoundTripper) RoundTripper
// A Client is a gremlin client.
type Client struct {
// Transport specifies the mechanism by which individual
// Gremlin requests are made.
Transport RoundTripper
}
// MaxResponseSize defines the maximum response size allowed.
const MaxResponseSize = 2 << 20
// NewClient creates a gremlin client from config and options.
func NewClient(cfg Config, opt ...Option) (*Client, error) {
return cfg.Build(opt...)
}
// NewHTTPClient creates an http based gremlin client.
func NewHTTPClient(url string, client *http.Client) (*Client, error) {
transport, err := NewHTTPTransport(url, client)
if err != nil {
return nil, err
}
return &Client{transport}, nil
}
// Do sends a gremlin request and returns a gremlin response.
func (c Client) Do(ctx context.Context, req *Request) (*Response, error) {
rsp, err := c.Transport.RoundTrip(ctx, req)
if err == nil {
err = rsp.Err()
}
// If we got an error, and the context has been canceled,
// the context's error is probably more useful.
if err != nil && ctx.Err() != nil {
err = ctx.Err()
}
return rsp, err
}
// Query issues an eval request via the Do function.
func (c Client) Query(ctx context.Context, query string) (*Response, error) {
return c.Do(ctx, NewEvalRequest(query))
}
// Queryf formats a query string and invokes Query.
func (c Client) Queryf(ctx context.Context, format string, args ...any) (*Response, error) {
return c.Query(ctx, fmt.Sprintf(format, args...))
}

View File

@@ -0,0 +1,93 @@
// 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 gremlin
import (
"context"
"io"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestNewClient(t *testing.T) {
var cfg Config
cfg.Endpoint.URL, _ = url.Parse("http://gremlin-server/gremlin")
c, err := NewClient(cfg)
assert.NotNil(t, c)
assert.NoError(t, err)
}
type mockRoundTripper struct{ mock.Mock }
func (m *mockRoundTripper) RoundTrip(ctx context.Context, req *Request) (*Response, error) {
args := m.Called(ctx, req)
return args.Get(0).(*Response), args.Error(1)
}
func TestClientRequest(t *testing.T) {
ctx := context.Background()
req, rsp := &Request{}, &Response{}
var m mockRoundTripper
m.On("RoundTrip", ctx, req).
Run(func(mock.Arguments) { rsp.Status.Code = StatusSuccess }).
Return(rsp, nil).
Once()
defer m.AssertExpectations(t)
response, err := Client{&m}.Do(context.Background(), req)
assert.NoError(t, err)
assert.Equal(t, rsp, response)
}
func TestClientResponseError(t *testing.T) {
rsp := &Response{}
var m mockRoundTripper
m.On("RoundTrip", mock.Anything, mock.Anything).
Run(func(mock.Arguments) { rsp.Status.Code = StatusServerError }).
Return(rsp, nil).
Once()
defer m.AssertExpectations(t)
_, err := Client{&m}.Do(context.Background(), nil)
assert.Error(t, err)
}
func TestClientCanceledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
var m mockRoundTripper
m.On("RoundTrip", ctx, mock.Anything).
Run(func(mock.Arguments) { cancel() }).
Return(&Response{}, io.ErrUnexpectedEOF).
Once()
defer m.AssertExpectations(t)
_, err := Client{&m}.Query(ctx, "g.E()")
assert.EqualError(t, err, context.Canceled.Error())
}
func TestClientQuery(t *testing.T) {
rsp := &Response{}
rsp.Status.Code = StatusNoContent
var m mockRoundTripper
m.On("RoundTrip", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
req := args.Get(1).(*Request)
assert.Equal(t, "g.V(1)", req.Arguments[ArgsGremlin])
}).
Return(rsp, nil).
Once()
defer m.AssertExpectations(t)
rsp, err := Client{&m}.Queryf(context.Background(), "g.V(%d)", 1)
assert.NotNil(t, rsp)
assert.NoError(t, err)
}

82
dialect/gremlin/config.go Normal file
View File

@@ -0,0 +1,82 @@
// 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 gremlin
import (
"fmt"
"net/http"
"net/url"
)
type (
// Config offers a declarative way to construct a client.
Config struct {
Endpoint Endpoint `env:"ENDPOINT" long:"endpoint" default:"" description:"gremlin endpoint to connect to"`
DisableExpansion bool `env:"DISABLE_EXPANSION" long:"disable-expansion" description:"disable bindings expansion"`
}
// An Option configured client.
Option func(*options)
options struct {
interceptors []Interceptor
httpClient *http.Client
}
// Endpoint wraps a url to add flag unmarshalling.
Endpoint struct {
*url.URL
}
)
// WithInterceptor adds interceptors to the client's transport.
func WithInterceptor(interceptors ...Interceptor) Option {
return func(opts *options) {
opts.interceptors = append(opts.interceptors, interceptors...)
}
}
// WithHTTPClient assigns underlying http client to be used by http transport.
func WithHTTPClient(client *http.Client) Option {
return func(opts *options) {
opts.httpClient = client
}
}
// Build constructs a client from Config.
func (cfg Config) Build(opt ...Option) (c *Client, err error) {
opts := cfg.buildOptions(opt)
switch cfg.Endpoint.Scheme {
case "http", "https":
c, err = NewHTTPClient(cfg.Endpoint.String(), opts.httpClient)
default:
err = fmt.Errorf("unsupported endpoint scheme: %s", cfg.Endpoint.Scheme)
}
if err != nil {
return nil, err
}
for i := len(opts.interceptors) - 1; i >= 0; i-- {
c.Transport = opts.interceptors[i](c.Transport)
}
if !cfg.DisableExpansion {
c.Transport = ExpandBindings(c.Transport)
}
return c, nil
}
func (Config) buildOptions(opts []Option) options {
var o options
for _, opt := range opts {
opt(&o)
}
return o
}
// UnmarshalFlag implements flag.Unmarshaler interface.
func (ep *Endpoint) UnmarshalFlag(value string) (err error) {
ep.URL, err = url.Parse(value)
return
}

View File

@@ -0,0 +1,154 @@
// 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 gremlin
import (
"context"
"errors"
"net/http"
"net/url"
"testing"
"github.com/jessevdk/go-flags"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestConfigParsing(t *testing.T) {
var cfg Config
_, err := flags.ParseArgs(&cfg, []string{
"--disable-expansion",
"--endpoint", "http://localhost:8182/gremlin",
})
assert.NoError(t, err)
assert.True(t, cfg.DisableExpansion)
assert.Equal(t, "http", cfg.Endpoint.Scheme)
assert.Equal(t, "http://localhost:8182/gremlin", cfg.Endpoint.String())
cfg = Config{}
_, err = flags.ParseArgs(&cfg, nil)
assert.NoError(t, err)
assert.NotNil(t, cfg.Endpoint.URL)
}
func TestConfigBuild(t *testing.T) {
tests := []struct {
name string
cfg Config
opts []Option
wantErr bool
}{
{
name: "HTTP",
cfg: Config{
Endpoint: Endpoint{
URL: func() *url.URL {
u, _ := url.Parse("http://gremlin-server/gremlin")
return u
}(),
},
},
},
{
name: "NoScheme",
cfg: Config{
Endpoint: Endpoint{
URL: &url.URL{},
},
},
wantErr: true,
},
{
name: "BadScheme",
cfg: Config{
Endpoint: Endpoint{
URL: &url.URL{
Scheme: "bad",
},
},
},
wantErr: true,
},
{
name: "WithOptions",
cfg: Config{
Endpoint: Endpoint{
URL: func() *url.URL {
u, _ := url.Parse("http://gremlin-server/gremlin")
return u
}(),
},
DisableExpansion: true,
},
opts: []Option{WithHTTPClient(&http.Client{})},
},
{
name: "NoExpansion",
cfg: Config{
Endpoint: Endpoint{
URL: &url.URL{
Scheme: "bad",
},
},
DisableExpansion: true,
},
wantErr: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
client, err := tc.cfg.Build(tc.opts...)
if !tc.wantErr {
assert.NotNil(t, client)
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
})
}
}
type testRoundTripper struct{ mock.Mock }
func (rt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
args := rt.Called(req)
rsp, _ := args.Get(0).(*http.Response)
return rsp, args.Error(1)
}
func TestBuildWithHTTPClient(t *testing.T) {
var transport testRoundTripper
transport.On("RoundTrip", mock.Anything).
Return(nil, errors.New("noop")).
Once()
defer transport.AssertExpectations(t)
u, err := url.Parse("http://gremlin-server:8182/gremlin")
require.NoError(t, err)
client, err := Config{Endpoint: Endpoint{u}}.
Build(WithHTTPClient(&http.Client{Transport: &transport}))
require.NoError(t, err)
_, _ = client.Do(context.Background(), NewEvalRequest("g.V()"))
}
func TestExpandOrdering(t *testing.T) {
var cfg Config
cfg.Endpoint.URL, _ = url.Parse("http://gremlin-server/gremlin")
interceptor := func(RoundTripper) RoundTripper {
return RoundTripperFunc(func(ctx context.Context, req *Request) (*Response, error) {
assert.Equal(t, `g.V().hasLabel("user")`, req.Arguments[ArgsGremlin])
assert.Nil(t, req.Arguments[ArgsBindings])
return nil, errors.New("noop")
})
}
c, err := cfg.Build(WithInterceptor(interceptor))
require.NoError(t, err)
req := NewEvalRequest("g.V().hasLabel($1)", WithBindings(map[string]any{"$1": "user"}))
_, _ = c.Do(context.Background(), req)
}

58
dialect/gremlin/driver.go Normal file
View File

@@ -0,0 +1,58 @@
// 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 gremlin
import (
"context"
"fmt"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/gremlin/graph/dsl"
)
// Driver is a dialect.Driver implementation for TinkerPop gremlin.
type Driver struct {
*Client
}
// NewDriver returns a new dialect.Driver implementation for gremlin.
func NewDriver(c *Client) *Driver {
c.Transport = ExpandBindings(c.Transport)
return &Driver{c}
}
// Dialect implements the dialect.Dialect method.
func (Driver) Dialect() string { return dialect.Gremlin }
// Exec implements the dialect.Exec method.
func (c *Driver) Exec(ctx context.Context, query string, args, v any) error {
vr, ok := v.(*Response)
if !ok {
return fmt.Errorf("dialect/gremlin: invalid type %T. expect *gremlin.Response", v)
}
bindings, ok := args.(dsl.Bindings)
if !ok {
return fmt.Errorf("dialect/gremlin: invalid type %T. expect map[string]any for bindings", args)
}
res, err := c.Do(ctx, NewEvalRequest(query, WithBindings(bindings)))
if err != nil {
return err
}
*vr = *res
return nil
}
// Query implements the dialect.Query method.
func (c *Driver) Query(ctx context.Context, query string, args, v any) error {
return c.Exec(ctx, query, args, v)
}
// Close is a nop close call. It should close the connection in case of WS client.
func (Driver) Close() error { return nil }
// Tx returns a nop transaction.
func (c *Driver) Tx(context.Context) (dialect.Tx, error) { return dialect.NopTx(c), nil }
var _ dialect.Driver = (*Driver)(nil)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package encoding
import (
"bytes"
)
// Mime defines a gremlin mime type.
type Mime []byte
// GraphSON3Mime mime headers.
var (
GraphSON3Mime = NewMime("application/vnd.gremlin-v3.0+json")
)
// NewMime creates a wire format mime header.
func NewMime(s string) Mime {
var buf bytes.Buffer
buf.WriteByte(byte(len(s)))
buf.WriteString(s)
return buf.Bytes()
}
// String implements fmt.Stringer interface.
func (m Mime) String() string {
return string(m[1:])
}

View File

@@ -0,0 +1,21 @@
// 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 encoding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewMime(t *testing.T) {
str := "application/vnd.gremlin-v2.0+json"
mime := NewMime(str)
require.Len(t, mime, len(str)+1)
assert.EqualValues(t, len(str), mime[0])
assert.EqualValues(t, str, mime[1:])
assert.Equal(t, str, mime.String())
}

View File

@@ -0,0 +1,46 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package gremlin
import (
"context"
"flag"
"log"
"os"
"time"
)
func ExampleClient_Query() {
addr := flag.String("gremlin-server", os.Getenv("GREMLIN_SERVER"), "gremlin server address")
flag.Parse()
if *addr == "" {
log.Fatal("missing gremlin server address")
}
client, err := NewHTTPClient(*addr, nil)
if err != nil {
log.Fatalf("creating client: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
rsp, err := client.Query(ctx, "g.E()")
if err != nil {
log.Fatalf("executing query: %v", err)
}
edges, err := rsp.ReadEdges()
if err != nil {
log.Fatalf("unmashal edges")
}
defer cancel()
for _, e := range edges {
log.Println(e.String())
}
// - Output:
}

47
dialect/gremlin/expand.go Normal file
View File

@@ -0,0 +1,47 @@
// 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 gremlin
import (
"context"
"fmt"
"sort"
"strings"
jsoniter "github.com/json-iterator/go"
)
// ExpandBindings expands the given RoundTripper and expands the request bindings into the Gremlin traversal.
func ExpandBindings(rt RoundTripper) RoundTripper {
return RoundTripperFunc(func(ctx context.Context, r *Request) (*Response, error) {
bindings, ok := r.Arguments[ArgsBindings]
if !ok {
return rt.RoundTrip(ctx, r)
}
query, ok := r.Arguments[ArgsGremlin]
if !ok {
return rt.RoundTrip(ctx, r)
}
{
query, bindings := query.(string), bindings.(map[string]any)
keys := make(sort.StringSlice, 0, len(bindings))
for k := range bindings {
keys = append(keys, k)
}
sort.Sort(sort.Reverse(keys))
kv := make([]string, 0, len(bindings)*2)
for _, k := range keys {
s, err := jsoniter.MarshalToString(bindings[k])
if err != nil {
return nil, fmt.Errorf("marshal bindings value for key %s: %w", k, err)
}
kv = append(kv, k, s)
}
delete(r.Arguments, ArgsBindings)
r.Arguments[ArgsGremlin] = strings.NewReplacer(kv...).Replace(query)
}
return rt.RoundTrip(ctx, r)
})
}

View File

@@ -0,0 +1,71 @@
// 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 gremlin
import (
"context"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExpandBindings(t *testing.T) {
tests := []struct {
req *Request
wantErr bool
wantQuery string
}{
{
req: NewEvalRequest("no bindings"),
wantQuery: "no bindings",
},
{
req: NewEvalRequest("g.V($0)", WithBindings(map[string]any{"$0": 1})),
wantQuery: "g.V(1)",
},
{
req: NewEvalRequest("g.V().has($1, $2)", WithBindings(map[string]any{"$1": "name", "$2": "a8m"})),
wantQuery: "g.V().has(\"name\", \"a8m\")",
},
{
req: NewEvalRequest("g.V().limit(n)", WithBindings(map[string]any{"n": 10})),
wantQuery: "g.V().limit(10)",
},
{
req: NewEvalRequest("g.V()", WithBindings(map[string]any{"$0": func() {}})),
wantErr: true,
},
{
req: NewEvalRequest("g.V().has($0, $1)", WithBindings(map[string]any{"$0": "active", "$1": true})),
wantQuery: "g.V().has(\"active\", true)",
},
{
req: NewEvalRequest("g.V().has($1, $11)", WithBindings(map[string]any{"$1": "active", "$11": true})),
wantQuery: "g.V().has(\"active\", true)",
},
}
for i, tt := range tests {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
rt := ExpandBindings(RoundTripperFunc(func(ctx context.Context, r *Request) (*Response, error) {
assert.Equal(t, tt.wantQuery, r.Arguments[ArgsGremlin])
return nil, nil
}))
_, err := rt.RoundTrip(context.Background(), tt.req)
assert.Equal(t, tt.wantErr, err != nil)
})
}
}
func TestExpandBindingsNoQuery(t *testing.T) {
rt := ExpandBindings(RoundTripperFunc(func(ctx context.Context, r *Request) (*Response, error) {
return nil, nil
}))
_, err := rt.RoundTrip(context.Background(), &Request{Arguments: map[string]any{
ArgsBindings: map[string]any{},
}})
assert.NoError(t, err)
}

View File

@@ -0,0 +1,75 @@
// 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 __
import "entgo.io/ent/dialect/gremlin/graph/dsl"
// As is the api for calling __.As().
func As(args ...any) *dsl.Traversal { return New().As(args...) }
// Is is the api for calling __.Is().
func Is(args ...any) *dsl.Traversal { return New().Is(args...) }
// Not is the api for calling __.Not().
func Not(args ...any) *dsl.Traversal { return New().Not(args...) }
// Has is the api for calling __.Has().
func Has(args ...any) *dsl.Traversal { return New().Has(args...) }
// HasNot is the api for calling __.HasNot().
func HasNot(args ...any) *dsl.Traversal { return New().HasNot(args...) }
// Or is the api for calling __.Or().
func Or(args ...any) *dsl.Traversal { return New().Or(args...) }
// And is the api for calling __.And().
func And(args ...any) *dsl.Traversal { return New().And(args...) }
// In is the api for calling __.In().
func In(args ...any) *dsl.Traversal { return New().In(args...) }
// Out is the api for calling __.Out().
func Out(args ...any) *dsl.Traversal { return New().Out(args...) }
// OutE is the api for calling __.OutE().
func OutE(args ...any) *dsl.Traversal { return New().OutE(args...) }
// InE is the api for calling __.InE().
func InE(args ...any) *dsl.Traversal { return New().InE(args...) }
// InV is the api for calling __.InV().
func InV(args ...any) *dsl.Traversal { return New().InV(args...) }
// V is the api for calling __.V().
func V(args ...any) *dsl.Traversal { return New().V(args...) }
// OutV is the api for calling __.OutV().
func OutV(args ...any) *dsl.Traversal { return New().OutV(args...) }
// Values is the api for calling __.Values().
func Values(args ...string) *dsl.Traversal { return New().Values(args...) }
// Union is the api for calling __.Union().
func Union(args ...any) *dsl.Traversal { return New().Union(args...) }
// Constant is the api for calling __.Constant().
func Constant(args ...any) *dsl.Traversal { return New().Constant(args...) }
// Properties is the api for calling __.Properties().
func Properties(args ...any) *dsl.Traversal { return New().Properties(args...) }
// OtherV is the api for calling __.OtherV().
func OtherV() *dsl.Traversal { return New().OtherV() }
// Count is the api for calling __.Count().
func Count() *dsl.Traversal { return New().Count() }
// Drop is the api for calling __.Drop().
func Drop() *dsl.Traversal { return New().Drop() }
// Fold is the api for calling __.Fold().
func Fold() *dsl.Traversal { return New().Fold() }
func New() *dsl.Traversal { return new(dsl.Traversal).Add(dsl.Token("__")) }

View File

@@ -0,0 +1,214 @@
// 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 dsl provide an API for writing gremlin dsl queries almost as-is
// in Go without using strings in the code.
//
// Note that, the API is not type-safe and assume the provided query and
// its arguments are valid.
package dsl
import (
"fmt"
"strings"
"time"
)
// Node represents a DSL step in the traversal.
type Node interface {
// Code returns the code representation of the element and its bindings (if any).
Code() (string, []any)
}
type (
// Token holds a simple token, like assignment.
Token string
// List represents a list of elements.
List struct {
Elements []any
}
// Func represents a function call.
Func struct {
Name string
Args []any
}
// Block represents a block/group of nodes.
Block struct {
Nodes []any
}
// Var represents a variable assignment and usage.
Var struct {
Name string
Elem any
}
)
// Code stringified the token.
func (t Token) Code() (string, []any) { return string(t), nil }
// Code returns the code representation of a list.
func (l List) Code() (string, []any) {
c, args := codeList(", ", l.Elements...)
return fmt.Sprintf("[%s]", c), args
}
// Code returns the code representation of a function call.
func (f Func) Code() (string, []any) {
c, args := codeList(", ", f.Args...)
return fmt.Sprintf("%s(%s)", f.Name, c), args
}
// Code returns the code representation of group/block of nodes.
func (b Block) Code() (string, []any) {
return codeList("; ", b.Nodes...)
}
// Code returns the code representation of variable declaration or its identifier.
func (v Var) Code() (string, []any) {
c, args := code(v.Elem)
if v.Name == "" {
return c, args
}
return fmt.Sprintf("%s = %s", v.Name, c), args
}
// predefined nodes.
var (
G = Token("g")
Dot = Token(".")
)
// NewFunc returns a new function node.
func NewFunc(name string, args ...any) *Func {
return &Func{Name: name, Args: args}
}
// NewList returns a new list node.
func NewList(args ...any) *List {
return &List{Elements: args}
}
// Querier is the interface that wraps the Query method.
type Querier interface {
// Query returns the query-string (similar to the Gremlin byte-code) and its bindings.
Query() (string, Bindings)
}
// Bindings are used to associate a variable with a value.
type Bindings map[string]any
// Add adds new value to the bindings map, formats it if needed, and returns its generated name.
func (b Bindings) Add(v any) string {
k := fmt.Sprintf("$%x", len(b))
switch v := v.(type) {
case time.Time:
b[k] = v.UnixNano()
default:
b[k] = v
}
return k
}
// Cardinality of vertex properties.
type Cardinality string
// Cardinality options.
const (
Set Cardinality = "set"
Single Cardinality = "single"
)
// Code implements the Node interface.
func (c Cardinality) Code() (string, []any) { return string(c), nil }
// Keyword defines a Gremlin keyword.
type Keyword string
// Keyword options.
const (
ID Keyword = "id"
)
// Code implements the Node interface.
func (k Keyword) Code() (string, []any) { return string(k), nil }
// Order of vertex properties.
type Order string
// Order options.
const (
Incr Order = "incr"
Decr Order = "decr"
Shuffle Order = "shuffle"
)
// Code implements the Node interface.
func (o Order) Code() (string, []any) { return string(o), nil }
// Column references a particular type of column in a complex data structure such as a Map, a Map.Entry, or a Path.
type Column string
// Column options.
const (
Keys Column = "keys"
Values Column = "values"
)
// Code implements the Node interface.
func (o Column) Code() (string, []any) { return string(o), nil }
// Scope used for steps that have a variable scope which alter the manner in which the step will behave in relation to how the traverses are processed.
type Scope string
// Scope options.
const (
Local Scope = "local"
Global Scope = "global"
)
// Code implements the Node interface.
func (s Scope) Code() (string, []any) { return string(s), nil }
func codeList(sep string, vs ...any) (string, []any) {
var (
br strings.Builder
args []any
)
for i, node := range vs {
if i > 0 {
br.WriteString(sep)
}
c, nargs := code(node)
br.WriteString(c)
args = append(args, nargs...)
}
return br.String(), args
}
func code(v any) (string, []any) {
switch n := v.(type) {
case Node:
return n.Code()
case *Traversal:
var (
b strings.Builder
args []any
)
for i := range n.nodes {
code, nargs := n.nodes[i].Code()
b.WriteString(code)
args = append(args, nargs...)
}
return b.String(), args
default:
return "%s", []any{v}
}
}
func sface(args []string) (v []any) {
for _, s := range args {
v = append(v, s)
}
return
}

View File

@@ -0,0 +1,238 @@
// 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 dsl_test
import (
"strconv"
"testing"
"entgo.io/ent/dialect/gremlin/graph/dsl"
"entgo.io/ent/dialect/gremlin/graph/dsl/__"
"entgo.io/ent/dialect/gremlin/graph/dsl/g"
"entgo.io/ent/dialect/gremlin/graph/dsl/p"
"github.com/stretchr/testify/require"
)
func TestTraverse(t *testing.T) {
tests := []struct {
input dsl.Querier
wantQuery string
wantBinds dsl.Bindings
}{
{
input: g.V(5),
wantQuery: "g.V($0)",
wantBinds: dsl.Bindings{"$0": 5},
},
{
input: g.V(2).Both("knows"),
wantQuery: "g.V($0).both($1)",
wantBinds: dsl.Bindings{"$0": 2, "$1": "knows"},
},
{
input: g.V(49).BothE("knows").OtherV().ValueMap(),
wantQuery: "g.V($0).bothE($1).otherV().valueMap()",
wantBinds: dsl.Bindings{"$0": 49, "$1": "knows"},
},
{
input: g.AddV("person").Property("name", "a8m").Next(),
wantQuery: "g.addV($0).property($1, $2).next()",
wantBinds: dsl.Bindings{"$0": "person", "$1": "name", "$2": "a8m"},
},
{
input: dsl.Each([]any{1, 2, 3}, func(it *dsl.Traversal) *dsl.Traversal {
return g.V(it)
}),
wantQuery: "[$0, $1, $2].each { g.V(it) }",
wantBinds: dsl.Bindings{"$0": 1, "$1": 2, "$2": 3},
},
{
input: dsl.Each([]any{g.V(1).Next()}, func(it *dsl.Traversal) *dsl.Traversal {
return it.ID()
}),
wantQuery: "[g.V($0).next()].each { it.id() }",
wantBinds: dsl.Bindings{"$0": 1},
},
{
input: g.AddV("person").AddE("knows").To(g.V(2)),
wantQuery: "g.addV($0).addE($1).to(g.V($2))",
wantBinds: dsl.Bindings{"$0": "person", "$1": "knows", "$2": 2},
},
{
input: func() *dsl.Traversal {
v1 := g.V(2).Next()
v2 := g.AddV("person").Property("name", "a8m")
e1 := g.V(v1).AddE("knows").To(v2)
return dsl.Group(v1, v2, e1)
}(),
wantQuery: "t0 = g.V($0).next(); t1 = g.addV($1).property($2, $3); t2 = g.V(t0).addE($4).to(t1); t2",
wantBinds: dsl.Bindings{"$0": 2, "$1": "person", "$2": "name", "$3": "a8m", "$4": "knows"},
},
{
input: func() *dsl.Traversal {
v1 := g.AddV("person")
each := dsl.Each([]any{1, 2, 3}, func(it *dsl.Traversal) *dsl.Traversal {
return g.V(v1).AddE("knows").To(g.V(it)).Next()
})
return dsl.Group(v1, each)
}(),
wantQuery: "t0 = g.addV($0); t1 = [$1, $2, $3].each { g.V(t0).addE($4).to(g.V(it)).next() }; t1",
wantBinds: dsl.Bindings{"$0": "person", "$1": 1, "$2": 2, "$3": 3, "$4": "knows"},
},
{
input: g.V().HasLabel("person").
Choose(__.Values("age").Is(p.LTE(20))),
wantQuery: "g.V().hasLabel($0).choose(__.values($1).is(lte($2)))",
wantBinds: dsl.Bindings{"$0": "person", "$1": "age", "$2": 20},
},
{
input: g.AddV("person").Property("name", "a8m").Properties(),
wantQuery: "g.addV($0).property($1, $2).properties()",
wantBinds: dsl.Bindings{"$0": "person", "$1": "name", "$2": "a8m"},
},
{
input: func() *dsl.Traversal {
v1 := g.AddV("person").Next()
e1 := g.V(v1).AddE("knows").To(g.V(2).Next())
return dsl.Group(v1, e1, g.V(v1).ValueMap(true))
}(),
wantQuery: "t0 = g.addV($0).next(); t1 = g.V(t0).addE($1).to(g.V($2).next()); t2 = g.V(t0).valueMap($3); t2",
wantBinds: dsl.Bindings{"$0": "person", "$1": "knows", "$2": 2, "$3": true},
},
{
input: func() *dsl.Traversal {
vs := g.V().HasLabel("person").ToList()
edge := g.V(vs).AddE("assoc").To(g.V(1)).Iterate()
each := dsl.Each(vs, func(it *dsl.Traversal) *dsl.Traversal {
return g.V(1).AddE("inverse").To(it).Next()
})
return dsl.Group(vs, edge, each)
}(),
wantQuery: "t0 = g.V().hasLabel($0).toList(); t1 = g.V(t0).addE($1).to(g.V($2)).iterate(); t2 = t0.each { g.V($3).addE($4).to(it).next() }; t2",
wantBinds: dsl.Bindings{"$0": "person", "$1": "assoc", "$2": 1, "$3": 1, "$4": "inverse"},
},
{
input: g.V().Where(__.Or(__.Has("age", 29), __.Has("age", 30))),
wantQuery: "g.V().where(__.or(__.has($0, $1), __.has($2, $3)))",
wantBinds: dsl.Bindings{"$0": "age", "$1": 29, "$2": "age", "$3": 30},
},
{
input: g.V().Has("name", p.Containing("le")).Has("name", p.StartingWith("A")),
wantQuery: `g.V().has($0, containing($1)).has($2, startingWith($3))`,
wantBinds: dsl.Bindings{"$0": "name", "$1": "le", "$2": "name", "$3": "A"},
},
{
input: g.AddV().Property(dsl.Single, "age", 32).ValueMap(),
wantQuery: "g.addV().property(single, $0, $1).valueMap()",
wantBinds: dsl.Bindings{"$0": "age", "$1": 32},
},
{
input: g.V().Count(),
wantQuery: "g.V().count()",
wantBinds: dsl.Bindings{},
},
{
input: g.V().HasNot("age"),
wantQuery: "g.V().hasNot($0)",
wantBinds: dsl.Bindings{"$0": "age"},
},
{
input: func() *dsl.Traversal {
v := g.V().HasID(1)
u := v.Clone().InE().Drop()
return dsl.Join(v, u)
}(),
wantQuery: "g.V().hasId($0); g.V().hasId($1).inE().drop()",
wantBinds: dsl.Bindings{"$0": 1, "$1": 1},
},
{
input: func() *dsl.Traversal {
v := g.V().HasID(1)
u := v.Clone().InE().Drop()
w := u.Clone()
return dsl.Join(v, u, w)
}(),
wantQuery: "g.V().hasId($0); g.V().hasId($1).inE().drop(); g.V().hasId($2).inE().drop()",
wantBinds: dsl.Bindings{"$0": 1, "$1": 1, "$2": 1},
},
{
input: g.V().OutE("knows").Where(__.InV().Has("name", "a8m")).OutV(),
wantQuery: "g.V().outE($0).where(__.inV().has($1, $2)).outV()",
wantBinds: dsl.Bindings{"$0": "knows", "$1": "name", "$2": "a8m"},
},
{
input: g.V().Has("name", p.Within("a8m", "alex")),
wantQuery: "g.V().has($0, within($1, $2))",
wantBinds: dsl.Bindings{"$0": "name", "$1": "a8m", "$2": "alex"},
},
{
input: g.V().HasID(p.Within(1, 2)),
wantQuery: "g.V().hasId(within($0, $1))",
wantBinds: dsl.Bindings{"$0": 1, "$1": 2},
},
{
input: g.V().HasID(p.Without(1, 2)),
wantQuery: "g.V().hasId(without($0, $1))",
wantBinds: dsl.Bindings{"$0": 1, "$1": 2},
},
{
input: g.V().Order().By("name"),
wantQuery: "g.V().order().by($0)",
wantBinds: dsl.Bindings{"$0": "name"},
},
{
input: g.V().Order().By("name", dsl.Incr),
wantQuery: "g.V().order().by($0, incr)",
wantBinds: dsl.Bindings{"$0": "name"},
},
{
input: g.V().Order().By("name", dsl.Incr).Undo(),
wantQuery: "g.V().order()",
wantBinds: dsl.Bindings{},
},
{
input: g.V().OutE("knows").Where(__.InV().Has("name", "a8m")).Undo(),
wantQuery: "g.V().outE($0)",
wantBinds: dsl.Bindings{"$0": "knows"},
},
{
input: g.V().Has("name").Group().By("name").By("age").Select(dsl.Values),
wantQuery: "g.V().has($0).group().by($1).by($2).select(values)",
wantBinds: dsl.Bindings{"$0": "name", "$1": "name", "$2": "age"},
},
{
input: g.V().Fold().Unfold(),
wantQuery: "g.V().fold().unfold()",
wantBinds: dsl.Bindings{},
},
{
input: g.V().Has("person", "name", "a8m").Count().Coalesce(
__.Is(p.NEQ(0)).Constant("unique constraint failed"),
g.AddV("person").Property("name", "a8m").ValueMap(true),
),
wantQuery: "g.V().has($0, $1, $2).count().coalesce(__.is(neq($3)).constant($4), g.addV($5).property($6, $7).valueMap($8))",
wantBinds: dsl.Bindings{"$0": "person", "$1": "name", "$2": "a8m", "$3": 0, "$4": "unique constraint failed", "$5": "person", "$6": "name", "$7": "a8m", "$8": true},
},
{
input: g.V().Has("age").Property("age", __.Union(__.Values("age"), __.Constant(10)).Sum()).ValueMap(),
wantQuery: "g.V().has($0).property($1, __.union(__.values($2), __.constant($3)).sum()).valueMap()",
wantBinds: dsl.Bindings{"$0": "age", "$1": "age", "$2": "age", "$3": 10},
},
{
input: g.V().Has("age").SideEffect(__.Properties("name").Drop()).ValueMap(),
wantQuery: "g.V().has($0).sideEffect(__.properties($1).drop()).valueMap()",
wantBinds: dsl.Bindings{"$0": "age", "$1": "name"},
},
}
for i, tt := range tests {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
query, bindings := tt.input.Query()
require.Equal(t, tt.wantQuery, query)
require.Equal(t, tt.wantBinds, bindings)
})
}
}

View File

@@ -0,0 +1,19 @@
// 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 g
import "entgo.io/ent/dialect/gremlin/graph/dsl"
// V is the api for calling g.V().
func V(args ...any) *dsl.Traversal { return dsl.NewTraversal().V(args...) }
// E is the api for calling g.E().
func E(args ...any) *dsl.Traversal { return dsl.NewTraversal().E(args...) }
// AddV is the api for calling g.AddV().
func AddV(args ...any) *dsl.Traversal { return dsl.NewTraversal().AddV(args...) }
// AddE is the api for calling g.AddE().
func AddE(args ...any) *dsl.Traversal { return dsl.NewTraversal().AddE(args...) }

View File

@@ -0,0 +1,93 @@
// 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 p
import (
"entgo.io/ent/dialect/gremlin/graph/dsl"
)
// EQ is the equal predicate.
func EQ(v any) *dsl.Traversal {
return op("eq", v)
}
// NEQ is the not-equal predicate.
func NEQ(v any) *dsl.Traversal {
return op("neq", v)
}
// GT is the greater than predicate.
func GT(v any) *dsl.Traversal {
return op("gt", v)
}
// GTE is the greater than or equal predicate.
func GTE(v any) *dsl.Traversal {
return op("gte", v)
}
// LT is the less than predicate.
func LT(v any) *dsl.Traversal {
return op("lt", v)
}
// LTE is the less than or equal predicate.
func LTE(v any) *dsl.Traversal {
return op("lte", v)
}
// Between is the between/contains predicate.
func Between(v, u any) *dsl.Traversal {
return op("between", v, u)
}
// StartingWith is the prefix test predicate.
func StartingWith(prefix string) *dsl.Traversal {
return op("startingWith", prefix)
}
// EndingWith is the suffix test predicate.
func EndingWith(suffix string) *dsl.Traversal {
return op("endingWith", suffix)
}
// Containing is the sub string test predicate.
func Containing(substr string) *dsl.Traversal {
return op("containing", substr)
}
// NotStartingWith is the negation of StartingWith.
func NotStartingWith(prefix string) *dsl.Traversal {
return op("notStartingWith", prefix)
}
// NotEndingWith is the negation of EndingWith.
func NotEndingWith(suffix string) *dsl.Traversal {
return op("notEndingWith", suffix)
}
// NotContaining is the negation of Containing.
func NotContaining(substr string) *dsl.Traversal {
return op("notContaining", substr)
}
// Within Determines if a value is within the specified list of values.
func Within[T any](args ...T) *dsl.Traversal {
return op("within", args...)
}
// Without determines if a value is not within the specified list of values.
func Without[T any](args ...T) *dsl.Traversal {
return op("without", args...)
}
func op[T any](name string, args ...T) *dsl.Traversal {
t := &dsl.Traversal{}
vs := make([]any, len(args))
for i, arg := range args {
vs[i] = arg
}
return t.Add(dsl.NewFunc(name, vs...))
}

View File

@@ -0,0 +1,426 @@
// 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 dsl
import (
"errors"
"fmt"
"strings"
)
// Traversal mimics the TinkerPop graph traversal.
type Traversal struct {
// nodes holds the dsl nodes. first element is the reference name
// of the TinkerGraph. defaults to "g".
nodes []Node
errs []error
}
// NewTraversal returns a new default traversal with "g" as a reference name to the Graph.
func NewTraversal() *Traversal {
return &Traversal{nodes: []Node{G}}
}
// Group groups a list of traversals into one. all traversals are assigned into a temporary
// variables named by their index. The last variable functions as a return value of the query.
// Note that, this "temporary hack" is not perfect and may not work in some cases because of
// the limitation of evaluation order.
func Group(trs ...*Traversal) *Traversal {
var (
b = Block{}
names = make(map[*Traversal]Token)
)
for i, tr := range trs {
if _, ok := names[tr]; ok {
continue
}
v := &Var{Name: fmt.Sprintf("t%d", i), Elem: &Traversal{nodes: tr.nodes}}
b.Nodes = append(b.Nodes, v)
names[tr] = Token(v.Name)
}
for _, tr := range trs {
tr.nodes = []Node{names[tr]}
}
b.Nodes = append(b.Nodes, names[trs[len(trs)-1]])
return &Traversal{nodes: []Node{b}}
}
// Join joins a list of traversals with a semicolon separator.
func Join(trs ...*Traversal) *Traversal {
b := Block{}
for _, tr := range trs {
b.Nodes = append(b.Nodes, &Traversal{nodes: tr.nodes})
}
return &Traversal{nodes: []Node{b}}
}
// AddError adds an error to the traversal.
func (t *Traversal) AddError(err error) *Traversal {
t.errs = append(t.errs, err)
return t
}
// Err returns a concatenated error of all errors encountered during
// the query-building, or were added manually by calling AddError.
func (t *Traversal) Err() error {
if len(t.errs) == 0 {
return nil
}
br := strings.Builder{}
for i := range t.errs {
if i > 0 {
br.WriteString("; ")
}
br.WriteString(t.errs[i].Error())
}
return errors.New(br.String())
}
// V step is usually used to start a traversal but it may also be used mid-traversal.
func (t *Traversal) V(args ...any) *Traversal {
t.Add(Dot, NewFunc("V", args...))
return t
}
// OtherV maps the Edge to the incident vertex that was not just traversed from in the path history.
func (t *Traversal) OtherV() *Traversal {
t.Add(Dot, NewFunc("otherV"))
return t
}
// E step is usually used to start a traversal but it may also be used mid-traversal.
func (t *Traversal) E(args ...any) *Traversal {
t.Add(Dot, NewFunc("E", args...))
return t
}
// AddV adds a vertex.
func (t *Traversal) AddV(args ...any) *Traversal {
t.Add(Dot, NewFunc("addV", args...))
return t
}
// AddE adds an edge.
func (t *Traversal) AddE(args ...any) *Traversal {
t.Add(Dot, NewFunc("addE", args...))
return t
}
// Next gets the next n-number of results from the traversal.
func (t *Traversal) Next() *Traversal {
return t.Add(Dot, NewFunc("next"))
}
// Drop removes elements and properties from the graph.
func (t *Traversal) Drop() *Traversal {
return t.Add(Dot, NewFunc("drop"))
}
// Property sets a Property value and related meta properties if supplied,
// if supported by the Graph and if the Element is a VertexProperty.
func (t *Traversal) Property(args ...any) *Traversal {
return t.Add(Dot, NewFunc("property", args...))
}
// Both maps the Vertex to its adjacent vertices given the edge labels.
func (t *Traversal) Both(args ...any) *Traversal {
return t.Add(Dot, NewFunc("both", args...))
}
// BothE maps the Vertex to its incident edges given the edge labels.
func (t *Traversal) BothE(args ...any) *Traversal {
return t.Add(Dot, NewFunc("bothE", args...))
}
// Has filters vertices, edges and vertex properties based on their properties.
// See: http://tinkerpop.apache.org/docs/current/reference/#has-step.
func (t *Traversal) Has(args ...any) *Traversal {
return t.Add(Dot, NewFunc("has", args...))
}
// HasNot filters vertices, edges and vertex properties based on the non-existence of properties.
// See: http://tinkerpop.apache.org/docs/current/reference/#has-step.
func (t *Traversal) HasNot(args ...any) *Traversal {
return t.Add(Dot, NewFunc("hasNot", args...))
}
// HasID filters vertices, edges and vertex properties based on their identifier.
func (t *Traversal) HasID(args ...any) *Traversal {
return t.Add(Dot, NewFunc("hasId", args...))
}
// HasLabel filters vertices, edges and vertex properties based on their label.
func (t *Traversal) HasLabel(args ...any) *Traversal {
return t.Add(Dot, NewFunc("hasLabel", args...))
}
// HasNext returns true if the iteration has more elements.
func (t *Traversal) HasNext() *Traversal {
return t.Add(Dot, NewFunc("hasNext"))
}
// Match maps the Traverser to a Map of bindings as specified by the provided match traversals.
func (t *Traversal) Match(args ...any) *Traversal {
return t.Add(Dot, NewFunc("match", args...))
}
// Choose routes the current traverser to a particular traversal branch option which allows the creation of if-then-else like semantics within a traversal.
func (t *Traversal) Choose(args ...any) *Traversal {
return t.Add(Dot, NewFunc("choose", args...))
}
// Select arbitrary values from the traversal.
func (t *Traversal) Select(args ...any) *Traversal {
return t.Add(Dot, NewFunc("select", args...))
}
// Group organizes objects in the stream into a Map.Calls to group() are typically accompanied with by() modulators which help specify how the grouping should occur.
func (t *Traversal) Group() *Traversal {
return t.Add(Dot, NewFunc("group"))
}
// Values maps the Element to the values of the associated properties given the provide property keys.
func (t *Traversal) Values(args ...string) *Traversal {
return t.Add(Dot, NewFunc("values", sface(args)...))
}
// ValueMap maps the Element to a Map of the property values key'd according to their Property.key().
func (t *Traversal) ValueMap(args ...any) *Traversal {
return t.Add(Dot, NewFunc("valueMap", args...))
}
// Properties maps the Element to its associated properties given the provide property keys.
func (t *Traversal) Properties(args ...any) *Traversal {
return t.Add(Dot, NewFunc("properties", args...))
}
// Range filters the objects in the traversal by the number of them to pass through the stream.
func (t *Traversal) Range(args ...any) *Traversal {
return t.Add(Dot, NewFunc("range", args...))
}
// Limit filters the objects in the traversal by the number of them to pass through the stream, where only the first n objects are allowed as defined by the limit argument.
func (t *Traversal) Limit(args ...any) *Traversal {
return t.Add(Dot, NewFunc("limit", args...))
}
// ID maps the Element to its Element.id().
func (t *Traversal) ID() *Traversal {
return t.Add(Dot, NewFunc("id"))
}
// Label maps the Element to its Element.label().
func (t *Traversal) Label() *Traversal {
return t.Add(Dot, NewFunc("label"))
}
// From provides from()-modulation to respective steps.
func (t *Traversal) From(args ...any) *Traversal {
return t.Add(Dot, NewFunc("from", args...))
}
// To used as a modifier to addE(String) this method specifies the traversal to use for selecting the incoming vertex of the newly added Edge.
func (t *Traversal) To(args ...any) *Traversal {
return t.Add(Dot, NewFunc("to", args...))
}
// As provides a label to the step that can be accessed later in the traversal by other steps.
func (t *Traversal) As(args ...any) *Traversal {
return t.Add(Dot, NewFunc("as", args...))
}
// Or ensures that at least one of the provided traversals yield a result.
func (t *Traversal) Or(args ...any) *Traversal {
return t.Add(Dot, NewFunc("or", args...))
}
// And ensures that all of the provided traversals yield a result.
func (t *Traversal) And(args ...any) *Traversal {
return t.Add(Dot, NewFunc("and", args...))
}
// Is filters the E object if it is not P.eq(V) to the provided value.
func (t *Traversal) Is(args ...any) *Traversal {
return t.Add(Dot, NewFunc("is", args...))
}
// Not removes objects from the traversal stream when the traversal provided as an argument does not return any objects.
func (t *Traversal) Not(args ...any) *Traversal {
return t.Add(Dot, NewFunc("not", args...))
}
// In maps the Vertex to its incoming adjacent vertices given the edge labels.
func (t *Traversal) In(args ...any) *Traversal {
return t.Add(Dot, NewFunc("in", args...))
}
// Where filters the current object based on the object itself or the path history.
func (t *Traversal) Where(args ...any) *Traversal {
return t.Add(Dot, NewFunc("where", args...))
}
// Out maps the Vertex to its outgoing adjacent vertices given the edge labels.
func (t *Traversal) Out(args ...any) *Traversal {
return t.Add(Dot, NewFunc("out", args...))
}
// OutE maps the Vertex to its outgoing incident edges given the edge labels.
func (t *Traversal) OutE(args ...any) *Traversal {
return t.Add(Dot, NewFunc("outE", args...))
}
// InE maps the Vertex to its incoming incident edges given the edge labels.
func (t *Traversal) InE(args ...any) *Traversal {
return t.Add(Dot, NewFunc("inE", args...))
}
// OutV maps the Edge to its outgoing/tail incident Vertex.
func (t *Traversal) OutV(args ...any) *Traversal {
return t.Add(Dot, NewFunc("outV", args...))
}
// InV maps the Edge to its incoming/head incident Vertex.
func (t *Traversal) InV(args ...any) *Traversal {
return t.Add(Dot, NewFunc("inV", args...))
}
// ToList puts all the results into a Groovy list.
func (t *Traversal) ToList() *Traversal {
return t.Add(Dot, NewFunc("toList"))
}
// Iterate iterates the traversal presumably for the generation of side-effects.
func (t *Traversal) Iterate() *Traversal {
return t.Add(Dot, NewFunc("iterate"))
}
// Count maps the traversal stream to its reduction as a sum of the Traverser.bulk() values
// (i.e. count the number of traversers up to this point).
func (t *Traversal) Count(args ...any) *Traversal {
return t.Add(Dot, NewFunc("count", args...))
}
// Order all the objects in the traversal up to this point and then emit them one-by-one in their ordered sequence.
func (t *Traversal) Order(args ...any) *Traversal {
return t.Add(Dot, NewFunc("order", args...))
}
// By can be applied to a number of different step to alter their behaviors.
// This form is essentially an identity() modulation.
func (t *Traversal) By(args ...any) *Traversal {
return t.Add(Dot, NewFunc("by", args...))
}
// Fold rolls up objects in the stream into an aggregate list..
func (t *Traversal) Fold() *Traversal {
return t.Add(Dot, NewFunc("fold"))
}
// Unfold unrolls a Iterator, Iterable or Map into a linear form or simply emits the object if it is not one of those types.
func (t *Traversal) Unfold() *Traversal {
return t.Add(Dot, NewFunc("unfold"))
}
// Sum maps the traversal stream to its reduction as a sum of the Traverser.get() values multiplied by their Traverser.bulk().
func (t *Traversal) Sum(args ...any) *Traversal {
return t.Add(Dot, NewFunc("sum", args...))
}
// Mean determines the mean value in the stream.
func (t *Traversal) Mean(args ...any) *Traversal {
return t.Add(Dot, NewFunc("mean", args...))
}
// Min determines the smallest value in the stream.
func (t *Traversal) Min(args ...any) *Traversal {
return t.Add(Dot, NewFunc("min", args...))
}
// Max determines the greatest value in the stream.
func (t *Traversal) Max(args ...any) *Traversal {
return t.Add(Dot, NewFunc("max", args...))
}
// Coalesce evaluates the provided traversals and returns the result of the first traversal to emit at least one object.
func (t *Traversal) Coalesce(args ...any) *Traversal {
return t.Add(Dot, NewFunc("coalesce", args...))
}
// Dedup removes all duplicates in the traversal stream up to this point.
func (t *Traversal) Dedup(args ...any) *Traversal {
return t.Add(Dot, NewFunc("dedup", args...))
}
// Constant maps any object to a fixed E value.
func (t *Traversal) Constant(args ...any) *Traversal {
return t.Add(Dot, NewFunc("constant", args...))
}
// Union merges the results of an arbitrary number of traversals.
func (t *Traversal) Union(args ...any) *Traversal {
return t.Add(Dot, NewFunc("union", args...))
}
// SideEffect allows the traverser to proceed unchanged, but yield some computational
// sideEffect in the process.
func (t *Traversal) SideEffect(args ...any) *Traversal {
return t.Add(Dot, NewFunc("sideEffect", args...))
}
// Each is a Groovy each-loop function.
func Each(v any, cb func(it *Traversal) *Traversal) *Traversal {
t := &Traversal{}
switch v := v.(type) {
case *Traversal:
t.Add(&Var{Elem: v})
case []any:
t.Add(NewList(v...))
default:
t.Add(Token("undefined"))
}
t.Add(Dot, Token("each"), Token(" { "))
t.Add(cb(&Traversal{nodes: []Node{Token("it")}}).nodes...)
t.Add(Token(" }"))
return t
}
// Add is the public API for adding new nodes to the traversal by its sub packages.
func (t *Traversal) Add(n ...Node) *Traversal {
t.nodes = append(t.nodes, n...)
return t
}
// Query returns the query-representation and its binding of this traversal object.
func (t *Traversal) Query() (string, Bindings) {
var (
names []any
query strings.Builder
bindings = Bindings{}
)
for _, n := range t.nodes {
code, args := n.Code()
query.WriteString(code)
for _, arg := range args {
names = append(names, bindings.Add(arg))
}
}
return fmt.Sprintf(query.String(), names...), bindings
}
// Clone creates a deep copy of an existing traversal.
func (t *Traversal) Clone() *Traversal {
if t == nil {
return nil
}
return &Traversal{nodes: append(make([]Node, 0, len(t.nodes)), t.nodes...)}
}
// Undo reverts the last-step of the traversal.
func (t *Traversal) Undo() *Traversal {
if n := len(t.nodes); n > 2 {
t.nodes = t.nodes[:n-2]
}
return t
}

View File

@@ -0,0 +1,94 @@
// 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 graph
import (
"fmt"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
)
type (
// An Edge between two vertices.
Edge struct {
Element
OutV, InV Vertex
}
// graphson edge repr.
edge struct {
Element
OutV any `json:"outV"`
OutVLabel string `json:"outVLabel"`
InV any `json:"inV"`
InVLabel string `json:"inVLabel"`
}
)
// NewEdge create a new graph edge.
func NewEdge(id any, label string, outV, inV Vertex) Edge {
return Edge{
Element: NewElement(id, label),
OutV: outV,
InV: inV,
}
}
// String implements fmt.Stringer interface.
func (e Edge) String() string {
return fmt.Sprintf("e[%v][%v-%s->%v]", e.ID, e.OutV.ID, e.Label, e.InV.ID)
}
// MarshalGraphson implements graphson.Marshaler interface.
func (e Edge) MarshalGraphson() ([]byte, error) {
return graphson.Marshal(edge{
Element: e.Element,
OutV: e.OutV.ID,
OutVLabel: e.OutV.Label,
InV: e.InV.ID,
InVLabel: e.InV.Label,
})
}
// UnmarshalGraphson implements graphson.Unmarshaler interface.
func (e *Edge) UnmarshalGraphson(data []byte) error {
var edge edge
if err := graphson.Unmarshal(data, &edge); err != nil {
return fmt.Errorf("unmarshalling edge: %w", err)
}
*e = NewEdge(
edge.ID, edge.Label,
NewVertex(edge.OutV, edge.OutVLabel),
NewVertex(edge.InV, edge.InVLabel),
)
return nil
}
// GraphsonType implements graphson.Typer interface.
func (edge) GraphsonType() graphson.Type {
return "g:Edge"
}
// Property denotes a key/value pair associated with an edge.
type Property struct {
Key string `json:"key"`
Value any `json:"value"`
}
// NewProperty create a new graph edge property.
func NewProperty(key string, value any) Property {
return Property{key, value}
}
// GraphsonType implements graphson.Typer interface.
func (Property) GraphsonType() graphson.Type {
return "g:Property"
}
// String implements fmt.Stringer interface.
func (p Property) String() string {
return fmt.Sprintf("p[%s->%v]", p.Key, p.Value)
}

View File

@@ -0,0 +1,108 @@
// 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 graph
import (
"fmt"
"testing"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEdgeString(t *testing.T) {
e := NewEdge(
13, "develops",
NewVertex(1, ""),
NewVertex(10, ""),
)
assert.Equal(t, "e[13][1-develops->10]", fmt.Sprint(e))
}
func TestEdgeEncoding(t *testing.T) {
t.Parallel()
e := NewEdge(13, "develops",
NewVertex(1, "person"),
NewVertex(10, "software"),
)
got, err := graphson.MarshalToString(e)
require.NoError(t, err)
want := `{
"@type" : "g:Edge",
"@value" : {
"id" : {
"@type" : "g:Int64",
"@value" : 13
},
"label" : "develops",
"inVLabel" : "software",
"outVLabel" : "person",
"inV" : {
"@type" : "g:Int64",
"@value" : 10
},
"outV" : {
"@type" : "g:Int64",
"@value" : 1
}
}
}`
assert.JSONEq(t, want, got)
e = Edge{}
err = graphson.UnmarshalFromString(got, &e)
require.NoError(t, err)
assert.Equal(t, NewElement(int64(13), "develops"), e.Element)
assert.Equal(t, NewVertex(int64(1), "person"), e.OutV)
assert.Equal(t, NewVertex(int64(10), "software"), e.InV)
}
func TestPropertyEncoding(t *testing.T) {
t.Parallel()
props := []Property{
NewProperty("from", int32(2017)),
NewProperty("to", int32(2019)),
}
got, err := graphson.MarshalToString(props)
require.NoError(t, err)
want := `{
"@type" : "g:List",
"@value" : [
{
"@type" : "g:Property",
"@value" : {
"key" : "from",
"value" : {
"@type" : "g:Int32",
"@value" : 2017
}
}
},
{
"@type" : "g:Property",
"@value" : {
"key" : "to",
"value" : {
"@type" : "g:Int32",
"@value" : 2019
}
}
}
]
}`
assert.JSONEq(t, want, got)
}
func TestPropertyString(t *testing.T) {
p := NewProperty("since", 2019)
assert.Equal(t, "p[since->2019]", fmt.Sprint(p))
}

View File

@@ -0,0 +1,16 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graph
// Element defines a base struct for graph elements.
type Element struct {
ID any `json:"id"`
Label string `json:"label"`
}
// NewElement create a new graph element.
func NewElement(id any, label string) Element {
return Element{id, label}
}

View File

@@ -0,0 +1,57 @@
// 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 graph
import (
"errors"
"fmt"
"reflect"
"github.com/mitchellh/mapstructure"
)
// ValueMap models a .valueMap() gremlin response.
type ValueMap []map[string]any
// Decode decodes a value map into v.
func (m ValueMap) Decode(v any) error {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr {
return errors.New("cannot unmarshal into a non pointer")
}
if rv.IsNil() {
return errors.New("cannot unmarshal into a nil pointer")
}
if rv.Elem().Kind() != reflect.Slice {
v = &[]any{v}
}
return m.decode(v)
}
func (m ValueMap) decode(v any) error {
cfg := mapstructure.DecoderConfig{
DecodeHook: func(f, t reflect.Kind, data any) (any, error) {
if f == reflect.Slice && t != reflect.Slice {
rv := reflect.ValueOf(data)
if rv.Len() == 1 {
data = rv.Index(0).Interface()
}
}
return data, nil
},
Result: v,
TagName: "json",
}
dec, err := mapstructure.NewDecoder(&cfg)
if err != nil {
return fmt.Errorf("creating structure decoder: %w", err)
}
if err := dec.Decode(m); err != nil {
return fmt.Errorf("decoding value map: %w", err)
}
return nil
}

View File

@@ -0,0 +1,74 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package graph
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValueMapDecodeOne(t *testing.T) {
vm := ValueMap{map[string]any{
"id": int64(1),
"label": "person",
"name": []any{"marko"},
"age": []any{int32(29)},
}}
var ent struct {
ID uint64 `json:"id"`
Label string `json:"label"`
Name string `json:"name"`
Age uint8 `json:"age"`
}
err := vm.Decode(&ent)
require.NoError(t, err)
assert.Equal(t, uint64(1), ent.ID)
assert.Equal(t, "person", ent.Label)
assert.Equal(t, "marko", ent.Name)
assert.Equal(t, uint8(29), ent.Age)
}
func TestValueMapDecodeMany(t *testing.T) {
vm := ValueMap{
map[string]any{
"id": int64(1),
"label": "person",
"name": []any{"chico"},
},
map[string]any{
"id": int64(2),
"label": "person",
"name": []any{"dico"},
},
}
ents := []struct {
ID int `json:"id"`
Label string `json:"label"`
Name string `json:"name"`
}{}
err := vm.Decode(&ents)
require.NoError(t, err)
require.Len(t, ents, 2)
assert.Equal(t, 1, ents[0].ID)
assert.Equal(t, "person", ents[0].Label)
assert.Equal(t, "chico", ents[0].Name)
assert.Equal(t, 2, ents[1].ID)
assert.Equal(t, "person", ents[1].Label)
assert.Equal(t, "dico", ents[1].Name)
}
func TestValueMapDecodeBadInput(t *testing.T) {
type s struct{ Name string }
err := ValueMap{}.Decode(s{})
assert.Error(t, err)
err = ValueMap{}.Decode((*s)(nil))
assert.Error(t, err)
}

View File

@@ -0,0 +1,62 @@
// 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 graph
import (
"fmt"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
)
// Vertex represents a graph vertex.
type Vertex struct {
Element
}
// NewVertex create a new graph vertex.
func NewVertex(id any, label string) Vertex {
if label == "" {
label = "vertex"
}
return Vertex{
Element: NewElement(id, label),
}
}
// GraphsonType implements graphson.Typer interface.
func (Vertex) GraphsonType() graphson.Type {
return "g:Vertex"
}
// String implements fmt.Stringer interface.
func (v Vertex) String() string {
return fmt.Sprintf("v[%v]", v.ID)
}
// VertexProperty denotes a key/value pair associated with a vertex.
type VertexProperty struct {
ID any `json:"id"`
Key string `json:"label"`
Value any `json:"value"`
}
// NewVertexProperty create a new graph vertex property.
func NewVertexProperty(id any, key string, value any) VertexProperty {
return VertexProperty{
ID: id,
Key: key,
Value: value,
}
}
// GraphsonType implements graphson.Typer interface.
func (VertexProperty) GraphsonType() graphson.Type {
return "g:VertexProperty"
}
// String implements fmt.Stringer interface.
func (vp VertexProperty) String() string {
return fmt.Sprintf("vp[%s->%v]", vp.Key, vp.Value)
}

View File

@@ -0,0 +1,86 @@
// 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 graph
import (
"fmt"
"testing"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVertexCreation(t *testing.T) {
v := NewVertex(45, "person")
assert.Equal(t, 45, v.ID)
assert.Equal(t, "person", v.Label)
v = NewVertex(46, "")
assert.Equal(t, "vertex", v.Label)
}
func TestVertexString(t *testing.T) {
v := NewVertex(42, "")
assert.Equal(t, "v[42]", fmt.Sprint(v))
}
func TestVertexEncoding(t *testing.T) {
t.Parallel()
v := NewVertex(1, "user")
got, err := graphson.MarshalToString(v)
require.NoError(t, err)
want := `{
"@type" : "g:Vertex",
"@value" : {
"id" : {
"@type" : "g:Int64",
"@value" : 1
},
"label" : "user"
}
}`
assert.JSONEq(t, want, got)
v = Vertex{}
err = graphson.UnmarshalFromString(got, &v)
require.NoError(t, err)
assert.Equal(t, int64(1), v.ID)
assert.Equal(t, "user", v.Label)
}
func TestVertexPropertyEncoding(t *testing.T) {
t.Parallel()
vp := NewVertexProperty("46ab60c2-918c-4cc4-a13b-350510e8908a", "name", "alex")
got, err := graphson.MarshalToString(vp)
require.NoError(t, err)
want := `{
"@type" : "g:VertexProperty",
"@value" : {
"id" : "46ab60c2-918c-4cc4-a13b-350510e8908a",
"label": "name",
"value": "alex"
}
}`
assert.JSONEq(t, want, got)
vp = VertexProperty{}
err = graphson.UnmarshalFromString(got, &vp)
require.NoError(t, err)
assert.Equal(t, "46ab60c2-918c-4cc4-a13b-350510e8908a", vp.ID)
assert.Equal(t, "name", vp.Key)
assert.Equal(t, "alex", vp.Value)
}
func TestVertexPropertyString(t *testing.T) {
vp := NewVertexProperty(55, "country", "israel")
assert.Equal(t, "vp[country->israel]", fmt.Sprint(vp))
}

85
dialect/gremlin/http.go Normal file
View File

@@ -0,0 +1,85 @@
// 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 gremlin
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
jsoniter "github.com/json-iterator/go"
)
type httpTransport struct {
client *http.Client
url string
}
// NewHTTPTransport returns a new http transport.
func NewHTTPTransport(urlStr string, client *http.Client) (RoundTripper, error) {
u, err := url.Parse(urlStr)
if err != nil {
return nil, fmt.Errorf("gremlin/http: parsing url: %w", err)
}
if client == nil {
client = http.DefaultClient
}
return &httpTransport{client, u.String()}, nil
}
// RoundTrip implements RouterTripper interface.
func (t *httpTransport) RoundTrip(ctx context.Context, req *Request) (*Response, error) {
if req.Operation != OpsEval {
return nil, fmt.Errorf("gremlin/http: unsupported operation: %q", req.Operation)
}
if _, ok := req.Arguments[ArgsGremlin]; !ok {
return nil, errors.New("gremlin/http: missing query expression")
}
pr, pw := io.Pipe()
defer pr.Close()
go func() {
err := jsoniter.NewEncoder(pw).Encode(req.Arguments)
if err != nil {
err = fmt.Errorf("gremlin/http: encoding request: %w", err)
}
_ = pw.CloseWithError(err)
}()
var br io.Reader
{
req, err := http.NewRequest(http.MethodPost, t.url, pr)
if err != nil {
return nil, fmt.Errorf("gremlin/http: creating http request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
rsp, err := t.client.Do(req.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("gremlin/http: posting http request: %w", err)
}
defer rsp.Body.Close()
if rsp.StatusCode < http.StatusOK || rsp.StatusCode > http.StatusPartialContent {
body, _ := io.ReadAll(rsp.Body)
return nil, fmt.Errorf("gremlin/http: status=%q, body=%q", rsp.Status, body)
}
if rsp.ContentLength > MaxResponseSize {
return nil, errors.New("gremlin/http: context length exceeds limit")
}
br = rsp.Body
}
var rsp Response
if err := graphson.NewDecoder(io.LimitReader(br, MaxResponseSize)).Decode(&rsp); err != nil {
return nil, fmt.Errorf("gremlin/http: decoding response: %w", err)
}
return &rsp, nil
}

View File

@@ -0,0 +1,130 @@
// 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 gremlin
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHTTPTransportRoundTripper(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Header.Get("Content-Type"), "application/json")
got, err := io.ReadAll(r.Body)
require.NoError(t, err)
assert.JSONEq(t, `{"gremlin": "g.V(1)", "language": "gremlin-groovy"}`, string(got))
_, err = io.WriteString(w, `{
"requestId": "f679127f-8701-425c-af55-049a44720db6",
"result": {
"data": {
"@type": "g:List",
"@value": [
{
"@type": "g:Vertex",
"@value": {
"id": {
"@type": "g:Int64",
"@value": 1
},
"label": "person"
}
}
]
},
"meta": {
"@type": "g:Map",
"@value": []
}
},
"status": {
"attributes": {
"@type": "g:Map",
"@value": []
},
"code": 200,
"message": ""
}
}`)
require.NoError(t, err)
}))
defer srv.Close()
transport, err := NewHTTPTransport(srv.URL, nil)
require.NoError(t, err)
rsp, err := transport.RoundTrip(context.Background(), NewEvalRequest("g.V(1)"))
require.NoError(t, err)
assert.Equal(t, "f679127f-8701-425c-af55-049a44720db6", rsp.RequestID)
assert.Equal(t, 200, rsp.Status.Code)
assert.Empty(t, rsp.Status.Message)
v := jsoniter.Get(rsp.Result.Data, graphson.ValueKey, 0, graphson.ValueKey)
require.NoError(t, v.LastError())
assert.Equal(t, 1, v.Get("id", graphson.ValueKey).ToInt())
assert.Equal(t, "person", v.Get("label").ToString())
}
func TestNewHTTPTransportBadURL(t *testing.T) {
transport, err := NewHTTPTransport(":", nil)
assert.Nil(t, transport)
assert.Error(t, err)
}
func TestHTTPTransportBadRequest(t *testing.T) {
transport, err := NewHTTPTransport("example.com", nil)
require.NoError(t, err)
req := NewEvalRequest("g.V()")
req.Operation = ""
rsp, err := transport.RoundTrip(context.Background(), req)
assert.EqualError(t, err, `gremlin/http: unsupported operation: ""`)
assert.Nil(t, rsp)
req = NewEvalRequest("g.V()")
delete(req.Arguments, ArgsGremlin)
rsp, err = transport.RoundTrip(context.Background(), req)
assert.EqualError(t, err, "gremlin/http: missing query expression")
assert.Nil(t, rsp)
}
func TestHTTPTransportBadResponseStatus(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
transport, err := NewHTTPTransport(srv.URL, nil)
require.NoError(t, err)
_, err = transport.RoundTrip(context.Background(), NewEvalRequest("g.E()."))
require.Error(t, err)
assert.Contains(t, err.Error(), http.StatusText(http.StatusInternalServerError))
}
func TestHTTPTransportBadResponseBody(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := io.WriteString(w, "{{{")
require.NoError(t, err)
}))
defer srv.Close()
transport, err := NewHTTPTransport(srv.URL, nil)
require.NoError(t, err)
_, err = transport.RoundTrip(context.Background(), NewEvalRequest("g.E()."))
require.Error(t, err)
assert.Contains(t, err.Error(), "decoding response")
}

View File

@@ -0,0 +1,315 @@
// 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 ws
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"sync"
"time"
"entgo.io/ent/dialect/gremlin"
"entgo.io/ent/dialect/gremlin/encoding"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
"github.com/gorilla/websocket"
"golang.org/x/sync/errgroup"
)
const (
// Time allowed to write a message to the peer.
writeWait = 5 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 10 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
)
type (
// A Dialer contains options for connecting to Gremlin server.
Dialer struct {
// Underlying websocket dialer.
websocket.Dialer
// Gremlin server basic auth credentials.
user, pass string
}
// Conn performs operations on a gremlin server.
Conn struct {
// Underlying websocket connection.
conn *websocket.Conn
// Credentials for basic authentication.
user, pass string
// Goroutine tracking.
ctx context.Context
grp *errgroup.Group
// Channel of outbound requests.
send chan io.Reader
// Map of in flight requests.
inflight sync.Map
}
// inflight tracks request state.
inflight struct {
// partially received data
frags []graphson.RawMessage
// response channel
result chan<- result
}
// represents an execution result.
result struct {
rsp *gremlin.Response
err error
}
)
var (
// DefaultDialer is a dialer with all fields set to the default values.
DefaultDialer = &Dialer{
Dialer: websocket.Dialer{
Proxy: http.ProxyFromEnvironment,
HandshakeTimeout: 5 * time.Second,
WriteBufferSize: 8192,
ReadBufferSize: 8192,
},
}
// ErrConnClosed is returned by the Conn's Execute method when
// the underlying gremlin server connection is closed.
ErrConnClosed = errors.New("gremlin: server connection closed")
// ErrDuplicateRequest is returned by the Conns Execute method on
// request identifier key collision.
ErrDuplicateRequest = errors.New("gremlin: duplicate request")
)
// Dial creates a new connection by calling DialContext with a background context.
func (d *Dialer) Dial(uri string) (*Conn, error) {
return d.DialContext(context.Background(), uri)
}
// DialContext creates a new Gremlin connection.
func (d *Dialer) DialContext(ctx context.Context, uri string) (*Conn, error) {
c, rsp, err := d.Dialer.DialContext(ctx, uri, nil)
if err != nil {
return nil, fmt.Errorf("gremlin: dialing uri %s: %w", uri, err)
}
defer rsp.Body.Close()
conn := &Conn{
conn: c,
user: d.user,
pass: d.pass,
send: make(chan io.Reader),
}
conn.grp, conn.ctx = errgroup.WithContext(context.Background())
conn.grp.Go(conn.sender)
conn.grp.Go(conn.receiver)
return conn, nil
}
// Execute executes a request against a Gremlin server.
func (c *Conn) Execute(ctx context.Context, req *gremlin.Request) (*gremlin.Response, error) {
// buffered result channel prevents receiver block on context cancellation
result := make(chan result, 1)
// request id must be unique across inflight request
if _, loaded := c.inflight.LoadOrStore(req.RequestID, &inflight{result: result}); loaded {
return nil, ErrDuplicateRequest
}
pr, pw := io.Pipe()
defer pr.Close()
// stream graphson encoding into request
c.grp.Go(func() error {
err := graphson.NewEncoder(pw).Encode(req)
if err != nil {
err = fmt.Errorf("encoding request: %w", err)
}
pw.CloseWithError(err)
return err
})
// local copy for single write
send := c.send
for {
select {
case <-c.ctx.Done():
c.inflight.Delete(req.RequestID)
return nil, ErrConnClosed
case <-ctx.Done():
c.inflight.Delete(req.RequestID)
return nil, ctx.Err()
case send <- pr:
send = nil
case result := <-result:
return result.rsp, result.err
}
}
}
// Close connection with a Gremlin server.
func (c *Conn) Close() error {
c.grp.Go(func() error { return ErrConnClosed })
_ = c.grp.Wait()
return nil
}
func (c *Conn) sender() error {
pinger := time.NewTicker(pingPeriod)
defer pinger.Stop()
// closing connection terminates receiver
defer c.conn.Close()
for {
select {
case r := <-c.send:
// ensure write completes within a window
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
// fetch next message writer
w, err := c.conn.NextWriter(websocket.BinaryMessage)
if err != nil {
return fmt.Errorf("getting message writer: %w", err)
}
// write mime header
if _, err := w.Write(encoding.GraphSON3Mime); err != nil {
return fmt.Errorf("writing mime header: %w", err)
}
// write request body
if _, err := io.Copy(w, r); err != nil {
return fmt.Errorf("writing request: %w", err)
}
// finish message write
if err := w.Close(); err != nil {
return fmt.Errorf("closing message writer: %w", err)
}
case <-c.ctx.Done():
// connection closing
return c.conn.WriteControl(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
time.Time{},
)
case <-pinger.C:
// periodic connection keepalive
if err := c.conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(writeWait)); err != nil {
return fmt.Errorf("writing ping message: %w", err)
}
}
}
}
func (c *Conn) receiver() error {
// handle keepalive responses
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
return c.conn.SetReadDeadline(time.Now().Add(pongWait))
})
// complete all in flight requests on termination
defer c.inflight.Range(func(id, ifr any) bool {
ifr.(*inflight).result <- result{err: ErrConnClosed}
c.inflight.Delete(id)
return true
})
for {
// rely on sender connection close during termination
_, r, err := c.conn.NextReader()
if err != nil {
return fmt.Errorf("writing ping message: %w", err)
}
// decode received response
var rsp gremlin.Response
if err := graphson.NewDecoder(r).Decode(&rsp); err != nil {
return fmt.Errorf("reading response: %w", err)
}
ifr, ok := c.inflight.Load(rsp.RequestID)
if !ok {
// context cancellation aborts inflight requests
continue
}
// handle incoming response
if done := c.receive(ifr.(*inflight), &rsp); done {
// stop tracking finished requests
c.inflight.Delete(rsp.RequestID)
}
}
}
func (c *Conn) receive(ifr *inflight, rsp *gremlin.Response) bool {
result := result{rsp: rsp}
switch rsp.Status.Code {
case gremlin.StatusSuccess:
// quickly handle non fragmented responses
if ifr.frags == nil {
break
}
// handle fragment
fallthrough
case gremlin.StatusPartialContent:
// append received fragment
var frag []graphson.RawMessage
if err := graphson.Unmarshal(rsp.Result.Data, &frag); err != nil {
result.err = fmt.Errorf("decoding response fragment: %w", err)
break
}
ifr.frags = append(ifr.frags, frag...)
// partial response requires additional fragments
if rsp.Status.Code == gremlin.StatusPartialContent {
return false
}
// reassemble fragmented response
if rsp.Result.Data, result.err = graphson.Marshal(ifr.frags); result.err != nil {
result.err = fmt.Errorf("assembling fragmented response: %w", result.err)
}
case gremlin.StatusAuthenticate:
// receiver should never block
c.grp.Go(func() error {
var buf bytes.Buffer
if err := graphson.NewEncoder(&buf).Encode(
gremlin.NewAuthRequest(rsp.RequestID, c.user, c.pass),
); err != nil {
return fmt.Errorf("encoding auth request: %w", err)
}
select {
case c.send <- &buf:
case <-c.ctx.Done():
}
return c.ctx.Err()
})
return false
}
ifr.result <- result
return true
}

View File

@@ -0,0 +1,390 @@
// 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 ws
import (
"context"
"net/http"
"net/http/httptest"
"strconv"
"sync"
"testing"
"entgo.io/ent/dialect/gremlin"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type conn struct{ *websocket.Conn }
func (c conn) ReadRequest() (*gremlin.Request, error) {
_, data, err := c.ReadMessage()
if err != nil {
return nil, err
}
var req gremlin.Request
if err := graphson.Unmarshal(data[data[0]+1:], &req); err != nil {
return nil, err
}
return &req, nil
}
func (c conn) WriteResponse(rsp *gremlin.Response) error {
data, err := graphson.Marshal(rsp)
if err != nil {
return err
}
return c.WriteMessage(websocket.BinaryMessage, data)
}
func serve(handler func(conn)) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
c, _ := upgrader.Upgrade(w, r, nil)
defer c.Close()
handler(conn{c})
for {
_, _, err := c.ReadMessage()
if err != nil {
break
}
}
}))
}
func TestConnectClosure(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
srv := serve(func(conn conn) {
defer wg.Done()
_, _, err := conn.ReadMessage()
assert.True(t, websocket.IsCloseError(err, websocket.CloseNormalClosure))
})
defer srv.Close()
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
require.NoError(t, err)
err = conn.Close()
assert.NoError(t, err)
_, err = conn.Execute(context.Background(), gremlin.NewEvalRequest("g.V()"))
assert.EqualError(t, err, ErrConnClosed.Error())
}
func TestSimpleQuery(t *testing.T) {
srv := serve(func(conn conn) {
typ, data, err := conn.ReadMessage()
require.NoError(t, err)
assert.Equal(t, websocket.BinaryMessage, typ)
var req gremlin.Request
err = graphson.Unmarshal(data[data[0]+1:], &req)
require.NoError(t, err)
assert.Equal(t, "g.V()", req.Arguments["gremlin"])
rsp := gremlin.Response{RequestID: req.RequestID}
rsp.Status.Code = gremlin.StatusNoContent
err = conn.WriteResponse(&rsp)
require.NoError(t, err)
})
defer srv.Close()
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
require.NoError(t, err)
defer assert.Condition(t, func() bool { return assert.NoError(t, conn.Close()) })
rsp, err := conn.Execute(context.Background(), gremlin.NewEvalRequest("g.V()"))
assert.NoError(t, err)
require.NotNil(t, rsp)
assert.Equal(t, gremlin.StatusNoContent, rsp.Status.Code)
}
func TestDuplicateRequest(t *testing.T) {
// skip until flakiness will be fixed.
t.SkipNow()
srv := serve(func(conn conn) {
req, err := conn.ReadRequest()
require.NoError(t, err)
rsp := gremlin.Response{RequestID: req.RequestID}
rsp.Status.Code = gremlin.StatusNoContent
err = conn.WriteResponse(&rsp)
require.NoError(t, err)
})
defer srv.Close()
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
require.NoError(t, err)
defer conn.Close()
var errors [2]error
req := gremlin.NewEvalRequest("g.V()")
var wg sync.WaitGroup
wg.Add(len(errors))
for i := range errors {
go func(i int) {
_, errors[i] = conn.Execute(context.Background(), req)
wg.Done()
}(i)
}
wg.Wait()
err = errors[0]
if err == nil {
err = errors[1]
}
assert.EqualError(t, err, ErrDuplicateRequest.Error())
}
func TestConnectCancellation(t *testing.T) {
srv := serve(func(conn) {})
defer srv.Close()
ctx, cancel := context.WithCancel(context.Background())
cancel()
conn, err := DefaultDialer.DialContext(ctx, "ws://"+srv.Listener.Addr().String())
assert.Error(t, err)
assert.Nil(t, conn)
}
func TestQueryCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
srv := serve(func(conn conn) {
if _, _, err := conn.ReadMessage(); err == nil {
cancel()
}
})
defer srv.Close()
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
require.NoError(t, err)
defer conn.Close()
_, err = conn.Execute(ctx, gremlin.NewEvalRequest("g.E()"))
assert.EqualError(t, err, context.Canceled.Error())
}
func TestBadResponse(t *testing.T) {
tests := []struct {
name string
mangle func(*gremlin.Response) *gremlin.Response
}{
{
name: "NoStatus",
mangle: func(rsp *gremlin.Response) *gremlin.Response {
return rsp
},
},
{
name: "Malformed",
mangle: func(rsp *gremlin.Response) *gremlin.Response {
rsp.Status.Code = gremlin.StatusMalformedRequest
rsp.Status.Message = "bad request"
return rsp
},
},
{
name: "Unknown",
mangle: func(rsp *gremlin.Response) *gremlin.Response {
rsp.Status.Code = 424242
return rsp
},
},
}
srv := serve(func(conn conn) {
for {
req, err := conn.ReadRequest()
if err != nil {
break
}
idx, err := strconv.ParseInt(req.Arguments["gremlin"].(string), 10, 0)
require.NoError(t, err)
err = conn.WriteResponse(tests[idx].mangle(&gremlin.Response{RequestID: req.RequestID}))
require.NoError(t, err)
}
})
defer srv.Close()
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
require.NoError(t, err)
defer conn.Close()
var wg sync.WaitGroup
wg.Add(len(tests))
ctx := context.Background()
for i, tc := range tests {
i, tc := i, tc
t.Run(tc.name, func(t *testing.T) {
defer wg.Done()
rsp, err := conn.Execute(ctx, gremlin.NewEvalRequest(strconv.FormatInt(int64(i), 10)))
assert.NoError(t, err)
assert.True(t, rsp.IsErr())
})
}
wg.Wait()
}
func TestServerHangup(t *testing.T) {
// skip until flakiness will be fixed.
t.SkipNow()
srv := serve(func(conn conn) { _ = conn.Close() })
defer srv.Close()
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
require.NoError(t, err)
defer conn.Close()
_, err = conn.Execute(context.Background(), gremlin.NewEvalRequest("g.V()"))
assert.EqualError(t, err, ErrConnClosed.Error())
assert.Error(t, conn.ctx.Err())
}
func TestCanceledLongRequest(t *testing.T) {
// skip until flakiness will be fixed.
t.SkipNow()
ctx, cancel := context.WithCancel(context.Background())
srv := serve(func(conn conn) {
var responses [3]*gremlin.Response
for i := 0; i < len(responses); i++ {
req, err := conn.ReadRequest()
require.NoError(t, err)
rsp := gremlin.Response{RequestID: req.RequestID}
rsp.Status.Code = gremlin.StatusSuccess
rsp.Result.Data = graphson.RawMessage(`"ok"`)
responses[i] = &rsp
}
cancel()
responses[0], responses[2] = responses[2], responses[0]
for i := 0; i < len(responses); i++ {
err := conn.WriteResponse(responses[i])
require.NoError(t, err)
}
})
defer srv.Close()
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
require.NoError(t, err)
defer conn.Close()
var wg sync.WaitGroup
wg.Add(3)
defer wg.Wait()
for i := 0; i < 3; i++ {
go func(ctx context.Context, idx int) {
defer wg.Done()
rsp, err := conn.Execute(ctx, gremlin.NewEvalRequest("g.V()"))
if idx > 0 {
assert.NoError(t, err)
assert.EqualValues(t, []byte(`"ok"`), rsp.Result.Data)
} else {
assert.EqualError(t, err, context.Canceled.Error())
}
}(ctx, i)
ctx = context.Background()
}
}
func TestPartialResponse(t *testing.T) {
type kv struct {
Key string
Value int
}
kvs := []kv{
{"one", 1},
{"two", 2},
{"three", 3},
}
srv := serve(func(conn conn) {
req, err := conn.ReadRequest()
require.NoError(t, err)
for i := range kvs {
data, err := graphson.Marshal([]kv{kvs[i]})
require.NoError(t, err)
rsp := gremlin.Response{RequestID: req.RequestID}
rsp.Result.Data = graphson.RawMessage(data)
if i != len(kvs)-1 {
rsp.Status.Code = gremlin.StatusPartialContent
} else {
rsp.Status.Code = gremlin.StatusSuccess
}
err = conn.WriteResponse(&rsp)
require.NoError(t, err)
}
})
defer srv.Close()
conn, err := DefaultDialer.Dial("ws://" + srv.Listener.Addr().String())
require.NoError(t, err)
defer conn.Close()
rsp, err := conn.Execute(context.Background(), gremlin.NewEvalRequest("g.E()"))
assert.NoError(t, err)
var result []kv
err = graphson.Unmarshal(rsp.Result.Data, &result)
require.NoError(t, err)
assert.Equal(t, kvs, result)
}
func TestAuthentication(t *testing.T) {
user, pass := "username", "password"
srv := serve(func(conn conn) {
req, err := conn.ReadRequest()
require.NoError(t, err)
rsp := gremlin.Response{RequestID: req.RequestID}
rsp.Status.Code = gremlin.StatusAuthenticate
err = conn.WriteResponse(&rsp)
require.NoError(t, err)
areq, err := conn.ReadRequest()
require.NoError(t, err)
var acreds gremlin.Credentials
err = acreds.UnmarshalText([]byte(areq.Arguments["sasl"].(string)))
assert.NoError(t, err)
areq.Arguments["sasl"] = acreds
assert.Equal(t, gremlin.NewAuthRequest(req.RequestID, user, pass), areq)
rsp = gremlin.Response{RequestID: req.RequestID}
rsp.Status.Code = gremlin.StatusNoContent
err = conn.WriteResponse(&rsp)
require.NoError(t, err)
})
defer srv.Close()
dialer := *DefaultDialer
dialer.user = user
dialer.pass = pass
client, err := dialer.Dial("ws://" + srv.Listener.Addr().String())
require.NoError(t, err)
defer client.Close()
_, err = client.Execute(context.Background(), gremlin.NewEvalRequest("g.E().drop()"))
assert.NoError(t, err)
}

View File

@@ -0,0 +1,64 @@
// 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 ocgremlin
import (
"context"
"entgo.io/ent/dialect/gremlin"
"go.opencensus.io/trace"
)
// Transport is an gremlin.RoundTripper that instruments all outgoing requests with
// OpenCensus stats and tracing.
type Transport struct {
// Base is a wrapped gremlin.RoundTripper that does the actual requests.
Base gremlin.RoundTripper
// StartOptions are applied to the span started by this Transport around each
// request.
//
// StartOptions.SpanKind will always be set to trace.SpanKindClient
// for spans started by this transport.
StartOptions trace.StartOptions
// GetStartOptions allows to set start options per request. If set,
// StartOptions is going to be ignored.
GetStartOptions func(context.Context, *gremlin.Request) trace.StartOptions
// NameFromRequest holds the function to use for generating the span name
// from the information found in the outgoing Gremlin Request. By default the
// name equals the URL Path.
FormatSpanName func(context.Context, *gremlin.Request) string
// WithQuery, if set to true, will enable recording of gremlin queries in spans.
// Only allow this if it is safe to have queries recorded with respect to
// security.
WithQuery bool
}
// RoundTrip implements gremlin.RoundTripper, delegating to Base and recording stats and traces for the request.
func (t *Transport) RoundTrip(ctx context.Context, req *gremlin.Request) (*gremlin.Response, error) {
spanNameFormatter := t.FormatSpanName
if spanNameFormatter == nil {
spanNameFormatter = func(context.Context, *gremlin.Request) string {
return "gremlin:traversal"
}
}
startOpts := t.StartOptions
if t.GetStartOptions != nil {
startOpts = t.GetStartOptions(ctx, req)
}
var rt gremlin.RoundTripper = &traceTransport{
base: t.Base,
formatSpanName: spanNameFormatter,
startOptions: startOpts,
withQuery: t.WithQuery,
}
rt = statsTransport{rt}
return rt.RoundTrip(ctx, req)
}

View File

@@ -0,0 +1,74 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package ocgremlin
import (
"context"
"errors"
"testing"
"entgo.io/ent/dialect/gremlin"
"github.com/stretchr/testify/mock"
"go.opencensus.io/trace"
)
type mockExporter struct {
mock.Mock
}
func (e *mockExporter) ExportSpan(s *trace.SpanData) {
e.Called(s)
}
func TestTransportOptions(t *testing.T) {
tests := []struct {
name string
spanName string
wantName string
}{
{
name: "Default formatter",
wantName: "gremlin:traversal",
},
{
name: "Custom formatter",
spanName: "tester",
wantName: "tester",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
var exporter mockExporter
exporter.On(
"ExportSpan",
mock.MatchedBy(func(s *trace.SpanData) bool { return s.Name == tt.wantName })).
Once()
defer exporter.AssertExpectations(t)
trace.RegisterExporter(&exporter)
defer trace.UnregisterExporter(&exporter)
transport := &mockTransport{}
transport.On("RoundTrip", mock.Anything, mock.Anything).
Return(nil, errors.New("noop")).
Once()
defer transport.AssertExpectations(t)
rt := &Transport{
Base: transport,
GetStartOptions: func(context.Context, *gremlin.Request) trace.StartOptions {
return trace.StartOptions{Sampler: trace.AlwaysSample()}
},
}
if tt.spanName != "" {
rt.FormatSpanName = func(context.Context, *gremlin.Request) string {
return tt.spanName
}
}
_, _ = rt.RoundTrip(context.Background(), gremlin.NewEvalRequest("g.E()"))
})
}
}

View File

@@ -0,0 +1,117 @@
// 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 ocgremlin
import (
"context"
"strconv"
"time"
"entgo.io/ent/dialect/gremlin"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
)
// The following measures are supported for use in custom views.
var (
RequestCount = stats.Int64(
"gremlin/request_count",
"Number of Gremlin requests started",
stats.UnitDimensionless,
)
ResponseBytes = stats.Int64(
"gremlin/response_bytes",
"Total number of bytes in response data",
stats.UnitBytes,
)
RoundTripLatency = stats.Float64(
"gremlin/roundtrip_latency",
"End-to-end latency",
stats.UnitMilliseconds,
)
)
// The following tags are applied to stats recorded by this package.
var (
// StatusCode is the numeric Gremlin response status code,
// or "error" if a transport error occurred and no status code was read.
StatusCode, _ = tag.NewKey("gremlin_status_code")
)
// Default distributions used by views in this package.
var (
DefaultSizeDistribution = view.Distribution(32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576)
DefaultLatencyDistribution = view.Distribution(1, 2, 3, 4, 5, 6, 8, 10, 13, 16, 20, 25, 30, 40, 50, 65, 80, 100, 130, 160, 200, 250, 300, 400, 500, 650, 800, 1000, 2000, 5000, 10000, 20000, 50000, 100000)
)
// Package ocgremlin provides some convenience views for measures.
// You still need to register these views for data to actually be collected.
var (
RequestCountView = &view.View{
Name: "gremlin/request_count",
Measure: RequestCount,
Aggregation: view.Count(),
Description: "Count of Gremlin requests started",
}
ResponseCountView = &view.View{
Name: "gremlin/response_count",
Measure: RoundTripLatency,
Aggregation: view.Count(),
Description: "Count of responses received, by response status",
TagKeys: []tag.Key{StatusCode},
}
ResponseBytesView = &view.View{
Name: "gremlin/response_bytes",
Measure: ResponseBytes,
Aggregation: DefaultSizeDistribution,
Description: "Total number of bytes in response data",
}
RoundTripLatencyView = &view.View{
Name: "gremlin/roundtrip_latency",
Measure: RoundTripLatency,
Aggregation: DefaultLatencyDistribution,
Description: "End-to-end latency, by response code",
TagKeys: []tag.Key{StatusCode},
}
)
// Views are the default views provided by this package.
func Views() []*view.View {
return []*view.View{
RequestCountView,
ResponseCountView,
ResponseBytesView,
RoundTripLatencyView,
}
}
// statsTransport is an gremlin.RoundTripper that collects stats for the outgoing requests.
type statsTransport struct {
base gremlin.RoundTripper
}
func (t statsTransport) RoundTrip(ctx context.Context, req *gremlin.Request) (*gremlin.Response, error) {
stats.Record(ctx, RequestCount.M(1))
start := time.Now()
rsp, err := t.base.RoundTrip(ctx, req)
latency := float64(time.Since(start)) / float64(time.Millisecond)
var (
tags = make([]tag.Mutator, 1)
ms = []stats.Measurement{RoundTripLatency.M(latency)}
)
if err == nil {
tags[0] = tag.Upsert(StatusCode, strconv.Itoa(rsp.Status.Code))
ms = append(ms, ResponseBytes.M(int64(len(rsp.Result.Data))))
} else {
tags[0] = tag.Upsert(StatusCode, "error")
}
_ = stats.RecordWithTags(ctx, tags, ms...)
return rsp, err
}

View File

@@ -0,0 +1,94 @@
// 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 ocgremlin
import (
"context"
"strings"
"testing"
"entgo.io/ent/dialect/gremlin"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.opencensus.io/stats/view"
)
func TestStatsCollection(t *testing.T) {
err := view.Register(
RequestCountView,
ResponseCountView,
ResponseBytesView,
RoundTripLatencyView,
)
require.NoError(t, err)
req := gremlin.NewEvalRequest("g.E()")
rsp := &gremlin.Response{RequestID: req.RequestID}
rsp.Status.Code = gremlin.StatusSuccess
rsp.Result.Data = graphson.RawMessage(
`{"@type": "g:List", "@value": [{"@type": "g:Int32", "@value": 42}]}`,
)
transport := &mockTransport{}
transport.On("RoundTrip", mock.Anything, mock.Anything).
Return(rsp, nil).
Once()
defer transport.AssertExpectations(t)
rt := &statsTransport{transport}
_, _ = rt.RoundTrip(context.Background(), req)
tests := []struct {
name string
expect func(*testing.T, *view.Row)
}{
{
name: "gremlin/request_count",
expect: func(t *testing.T, row *view.Row) {
count, ok := row.Data.(*view.CountData)
require.True(t, ok)
assert.Equal(t, int64(1), count.Value)
},
},
{
name: "gremlin/response_count",
expect: func(t *testing.T, row *view.Row) {
count, ok := row.Data.(*view.CountData)
require.True(t, ok)
assert.Equal(t, int64(1), count.Value)
},
},
{
name: "gremlin/response_bytes",
expect: func(t *testing.T, row *view.Row) {
data, ok := row.Data.(*view.DistributionData)
require.True(t, ok)
assert.EqualValues(t, len(rsp.Result.Data), data.Sum())
},
},
{
name: "gremlin/roundtrip_latency",
expect: func(t *testing.T, row *view.Row) {
data, ok := row.Data.(*view.DistributionData)
require.True(t, ok)
assert.NotZero(t, data.Sum())
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name[strings.Index(tt.name, "/")+1:], func(t *testing.T) {
v := view.Find(tt.name)
assert.NotNil(t, v)
rows, err := view.RetrieveData(tt.name)
require.NoError(t, err)
require.Len(t, rows, 1)
tt.expect(t, rows[0])
})
}
}

View File

@@ -0,0 +1,133 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package ocgremlin
import (
"context"
"fmt"
"entgo.io/ent/dialect/gremlin"
"go.opencensus.io/trace"
)
// Attributes recorded on the span for the requests.
const (
RequestIDAttribute = "gremlin.request_id"
OperationAttribute = "gremlin.operation"
QueryAttribute = "gremlin.query"
BindingAttribute = "gremlin.binding"
CodeAttribute = "gremlin.code"
MessageAttribute = "gremlin.message"
)
type traceTransport struct {
base gremlin.RoundTripper
startOptions trace.StartOptions
formatSpanName func(context.Context, *gremlin.Request) string
withQuery bool
}
func (t *traceTransport) RoundTrip(ctx context.Context, req *gremlin.Request) (*gremlin.Response, error) {
ctx, span := trace.StartSpan(ctx,
t.formatSpanName(ctx, req),
trace.WithSampler(t.startOptions.Sampler),
trace.WithSpanKind(trace.SpanKindClient),
)
defer span.End()
span.AddAttributes(requestAttrs(req, t.withQuery)...)
rsp, err := t.base.RoundTrip(ctx, req)
if err != nil {
span.SetStatus(trace.Status{Code: trace.StatusCodeUnknown, Message: err.Error()})
return rsp, err
}
span.AddAttributes(responseAttrs(rsp)...)
span.SetStatus(TraceStatus(rsp.Status.Code))
return rsp, err
}
func requestAttrs(req *gremlin.Request, withQuery bool) []trace.Attribute {
attrs := []trace.Attribute{
trace.StringAttribute(RequestIDAttribute, req.RequestID),
trace.StringAttribute(OperationAttribute, req.Operation),
}
if withQuery {
query, _ := req.Arguments[gremlin.ArgsGremlin].(string)
attrs = append(attrs, trace.StringAttribute(QueryAttribute, query))
if bindings, ok := req.Arguments[gremlin.ArgsBindings].(map[string]any); ok {
attrs = append(attrs, bindingsAttrs(bindings)...)
}
}
return attrs
}
func bindingsAttrs(bindings map[string]any) []trace.Attribute {
attrs := make([]trace.Attribute, 0, len(bindings))
for key, val := range bindings {
key = BindingAttribute + "." + key
attrs = append(attrs, bindingToAttr(key, val))
}
return attrs
}
func bindingToAttr(key string, val any) trace.Attribute {
switch v := val.(type) {
case nil:
return trace.StringAttribute(key, "")
case int64:
return trace.Int64Attribute(key, v)
case float64:
return trace.Float64Attribute(key, v)
case string:
return trace.StringAttribute(key, v)
case bool:
return trace.BoolAttribute(key, v)
default:
s := fmt.Sprintf("%v", v)
if len(s) > 256 {
s = s[:256]
}
return trace.StringAttribute(key, s)
}
}
func responseAttrs(rsp *gremlin.Response) []trace.Attribute {
attrs := []trace.Attribute{
trace.Int64Attribute(CodeAttribute, int64(rsp.Status.Code)),
}
if rsp.Status.Message != "" {
attrs = append(attrs, trace.StringAttribute(MessageAttribute, rsp.Status.Message))
}
return attrs
}
// TraceStatus is a utility to convert the gremlin status code to a trace.Status.
func TraceStatus(status int) trace.Status {
var code int32
switch status {
case gremlin.StatusSuccess,
gremlin.StatusNoContent,
gremlin.StatusPartialContent:
code = trace.StatusCodeOK
case gremlin.StatusUnauthorized:
code = trace.StatusCodePermissionDenied
case gremlin.StatusAuthenticate:
code = trace.StatusCodeUnauthenticated
case gremlin.StatusMalformedRequest,
gremlin.StatusInvalidRequestArguments,
gremlin.StatusScriptEvaluationError:
code = trace.StatusCodeInvalidArgument
case gremlin.StatusServerError,
gremlin.StatusServerSerializationError:
code = trace.StatusCodeInternal
case gremlin.StatusServerTimeout:
code = trace.StatusCodeDeadlineExceeded
default:
code = trace.StatusCodeUnknown
}
return trace.Status{Code: code, Message: gremlin.StatusText(status)}
}

View File

@@ -0,0 +1,254 @@
// 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 ocgremlin
import (
"bytes"
"context"
"errors"
"fmt"
"testing"
"entgo.io/ent/dialect/gremlin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.opencensus.io/trace"
)
type mockTransport struct {
mock.Mock
}
func (t *mockTransport) RoundTrip(ctx context.Context, req *gremlin.Request) (*gremlin.Response, error) {
args := t.Called(ctx, req)
rsp, _ := args.Get(0).(*gremlin.Response)
return rsp, args.Error(1)
}
func TestTraceTransportRoundTrip(t *testing.T) {
_, parent := trace.StartSpan(context.Background(), "parent")
tests := []struct {
name string
parent *trace.Span
}{
{
name: "no parent",
parent: nil,
},
{
name: "parent",
parent: parent,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
transport := &mockTransport{}
transport.On("RoundTrip", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
span := trace.FromContext(args.Get(0).(context.Context))
require.NotNil(t, span)
if tt.parent != nil {
assert.Equal(t, tt.parent.SpanContext().TraceID, span.SpanContext().TraceID)
}
}).
Return(nil, errors.New("noop")).
Once()
defer transport.AssertExpectations(t)
ctx, req := context.Background(), gremlin.NewEvalRequest("g.V()")
if tt.parent != nil {
ctx = trace.NewContext(ctx, tt.parent)
}
rt := &Transport{Base: transport}
_, _ = rt.RoundTrip(ctx, req)
})
}
}
type testExporter struct {
spans []*trace.SpanData
}
func (t *testExporter) ExportSpan(s *trace.SpanData) {
t.spans = append(t.spans, s)
}
func TestEndToEnd(t *testing.T) {
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
var exporter testExporter
trace.RegisterExporter(&exporter)
defer trace.UnregisterExporter(&exporter)
req := gremlin.NewEvalRequest("g.V()")
rsp := &gremlin.Response{
RequestID: req.RequestID,
}
rsp.Status.Code = 200
rsp.Status.Message = "OK"
var transport mockTransport
transport.On("RoundTrip", mock.Anything, mock.Anything).
Return(rsp, nil).
Once()
defer transport.AssertExpectations(t)
rt := &Transport{Base: &transport, WithQuery: true}
_, err := rt.RoundTrip(context.Background(), req)
require.NoError(t, err)
require.Len(t, exporter.spans, 1)
attrs := exporter.spans[0].Attributes
assert.Len(t, attrs, 5)
assert.Equal(t, req.RequestID, attrs["gremlin.request_id"])
assert.Equal(t, req.Operation, attrs["gremlin.operation"])
assert.Equal(t, req.Arguments[gremlin.ArgsGremlin], attrs["gremlin.query"])
assert.Equal(t, int64(200), attrs["gremlin.code"])
assert.Equal(t, "OK", attrs["gremlin.message"])
}
func TestRequestAttributes(t *testing.T) {
tests := []struct {
name string
makeReq func() *gremlin.Request
wantAttrs []trace.Attribute
}{
{
name: "Query without bindings",
makeReq: func() *gremlin.Request {
req := gremlin.NewEvalRequest("g.E().count()")
req.RequestID = "a8b5c664-03ca-4175-a9e7-569b46f3551c"
return req
},
wantAttrs: []trace.Attribute{
trace.StringAttribute("gremlin.request_id", "a8b5c664-03ca-4175-a9e7-569b46f3551c"),
trace.StringAttribute("gremlin.operation", "eval"),
trace.StringAttribute("gremlin.query", "g.E().count()"),
},
},
{
name: "Query with bindings",
makeReq: func() *gremlin.Request {
bindings := map[string]any{
"$1": "user", "$2": int64(42),
"$3": 3.14, "$4": bytes.Repeat([]byte{0xff}, 257),
"$5": true, "$6": nil,
}
req := gremlin.NewEvalRequest(
`g.V().hasLabel($1).has("age",$2).has("v",$3).limit($4).valueMap($5)`,
gremlin.WithBindings(bindings),
)
req.RequestID = "d3d986fa-bd22-41bd-b2f7-ef2f1f639260"
return req
},
wantAttrs: []trace.Attribute{
trace.StringAttribute("gremlin.request_id", "d3d986fa-bd22-41bd-b2f7-ef2f1f639260"),
trace.StringAttribute("gremlin.operation", "eval"),
trace.StringAttribute("gremlin.query", `g.V().hasLabel($1).has("age",$2).has("v",$3).limit($4).valueMap($5)`),
trace.StringAttribute("gremlin.binding.$1", "user"),
trace.Int64Attribute("gremlin.binding.$2", 42),
trace.Float64Attribute("gremlin.binding.$3", 3.14),
trace.StringAttribute("gremlin.binding.$4", func() string {
str := fmt.Sprintf("%v", bytes.Repeat([]byte{0xff}, 256))
return str[:256]
}()),
trace.BoolAttribute("gremlin.binding.$5", true),
trace.StringAttribute("gremlin.binding.$6", ""),
},
},
{
name: "Authentication",
makeReq: func() *gremlin.Request {
return gremlin.NewAuthRequest(
"d239d950-59a1-41a7-a103-908f976ebd89",
"user", "pass",
)
},
wantAttrs: []trace.Attribute{
trace.StringAttribute("gremlin.request_id", "d239d950-59a1-41a7-a103-908f976ebd89"),
trace.StringAttribute("gremlin.operation", "authentication"),
trace.StringAttribute("gremlin.query", ""),
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
req := tt.makeReq()
attrs := requestAttrs(req, true)
for _, attr := range attrs {
assert.Contains(t, tt.wantAttrs, attr)
}
assert.Len(t, attrs, len(tt.wantAttrs))
})
}
}
func TestResponseAttributes(t *testing.T) {
tests := []struct {
name string
makeRsp func() *gremlin.Response
wantAttrs []trace.Attribute
}{
{
name: "Success no message",
makeRsp: func() *gremlin.Response {
var rsp gremlin.Response
rsp.Status.Code = 204
return &rsp
},
wantAttrs: []trace.Attribute{
trace.Int64Attribute("gremlin.code", 204),
},
},
{
name: "Authenticate with message",
makeRsp: func() *gremlin.Response {
var rsp gremlin.Response
rsp.Status.Code = 407
rsp.Status.Message = "login required"
return &rsp
},
wantAttrs: []trace.Attribute{
trace.Int64Attribute("gremlin.code", 407),
trace.StringAttribute("gremlin.message", "login required"),
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
rsp := tt.makeRsp()
attrs := responseAttrs(rsp)
assert.Equal(t, tt.wantAttrs, attrs)
})
}
}
func TestTraceStatus(t *testing.T) {
tests := []struct {
in int
want trace.Status
}{
{200, trace.Status{Code: trace.StatusCodeOK, Message: "Success"}},
{204, trace.Status{Code: trace.StatusCodeOK, Message: "No Content"}},
{206, trace.Status{Code: trace.StatusCodeOK, Message: "Partial Content"}},
{401, trace.Status{Code: trace.StatusCodePermissionDenied, Message: "Unauthorized"}},
{407, trace.Status{Code: trace.StatusCodeUnauthenticated, Message: "Authenticate"}},
{498, trace.Status{Code: trace.StatusCodeInvalidArgument, Message: "Malformed Request"}},
{499, trace.Status{Code: trace.StatusCodeInvalidArgument, Message: "Invalid Request Arguments"}},
{500, trace.Status{Code: trace.StatusCodeInternal, Message: "Server Error"}},
{597, trace.Status{Code: trace.StatusCodeInvalidArgument, Message: "Script Evaluation Error"}},
{598, trace.Status{Code: trace.StatusCodeDeadlineExceeded, Message: "Server Timeout"}},
{599, trace.Status{Code: trace.StatusCodeInternal, Message: "Server Serialization Error"}},
{600, trace.Status{Code: trace.StatusCodeUnknown, Message: ""}},
}
for _, tt := range tests {
assert.Equal(t, tt.want, TraceStatus(tt.in))
}
}

110
dialect/gremlin/request.go Normal file
View File

@@ -0,0 +1,110 @@
// 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 gremlin
import (
"bytes"
"encoding/base64"
"errors"
"time"
"github.com/google/uuid"
)
type (
// A Request models a request message sent to the server.
Request struct {
RequestID string `json:"requestId" graphson:"g:UUID"`
Operation string `json:"op"`
Processor string `json:"processor"`
Arguments map[string]any `json:"args"`
}
// RequestOption enables request customization.
RequestOption func(*Request)
// Credentials holds request plain auth credentials.
Credentials struct{ Username, Password string }
)
// NewEvalRequest returns a new evaluation request request.
func NewEvalRequest(query string, opts ...RequestOption) *Request {
r := &Request{
RequestID: uuid.New().String(),
Operation: OpsEval,
Arguments: map[string]any{
ArgsGremlin: query,
ArgsLanguage: "gremlin-groovy",
},
}
for i := range opts {
opts[i](r)
}
return r
}
// NewAuthRequest returns a new auth request.
func NewAuthRequest(requestID, username, password string) *Request {
return &Request{
RequestID: requestID,
Operation: OpsAuthentication,
Arguments: map[string]any{
ArgsSasl: Credentials{
Username: username,
Password: password,
},
ArgsSaslMechanism: "PLAIN",
},
}
}
// WithBindings sets request bindings.
func WithBindings(bindings map[string]any) RequestOption {
return func(r *Request) {
r.Arguments[ArgsBindings] = bindings
}
}
// WithEvalTimeout sets script evaluation timeout.
func WithEvalTimeout(timeout time.Duration) RequestOption {
return func(r *Request) {
r.Arguments[ArgsEvalTimeout] = int64(timeout / time.Millisecond)
}
}
// MarshalText implements encoding.TextMarshaler interface.
func (c Credentials) MarshalText() ([]byte, error) {
var buf bytes.Buffer
buf.WriteByte(0)
buf.WriteString(c.Username)
buf.WriteByte(0)
buf.WriteString(c.Password)
enc := base64.StdEncoding
text := make([]byte, enc.EncodedLen(buf.Len()))
enc.Encode(text, buf.Bytes())
return text, nil
}
// UnmarshalText implements encoding.TextUnmarshaler interface.
func (c *Credentials) UnmarshalText(text []byte) error {
enc := base64.StdEncoding
data := make([]byte, enc.DecodedLen(len(text)))
n, err := enc.Decode(data, text)
if err != nil {
return err
}
data = data[:n]
parts := bytes.SplitN(data, []byte{0}, 3)
if len(parts) != 3 {
return errors.New("bad credentials data")
}
c.Username = string(parts[1])
c.Password = string(parts[2])
return nil
}

View File

@@ -0,0 +1,135 @@
// 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 gremlin
import (
"encoding/json"
"testing"
"time"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEvaluateRequestEncode(t *testing.T) {
req := NewEvalRequest("g.V(x)",
WithBindings(map[string]any{"x": 1}),
WithEvalTimeout(time.Second),
)
data, err := graphson.Marshal(req)
require.NoError(t, err)
var got map[string]any
err = json.Unmarshal(data, &got)
require.NoError(t, err)
assert.Equal(t, map[string]any{
"@type": "g:UUID",
"@value": req.RequestID,
}, got["requestId"])
assert.Equal(t, req.Operation, got["op"])
assert.Equal(t, req.Processor, got["processor"])
args := got["args"].(map[string]any)
assert.Equal(t, "g:Map", args["@type"])
assert.ElementsMatch(t, args["@value"], []any{
"gremlin", "g.V(x)", "language", "gremlin-groovy",
"scriptEvaluationTimeout", map[string]any{
"@type": "g:Int64",
"@value": float64(1000),
},
"bindings", map[string]any{
"@type": "g:Map",
"@value": []any{
"x",
map[string]any{
"@type": "g:Int64",
"@value": float64(1),
},
},
},
})
}
func TestEvaluateRequestWithoutBindingsEncode(t *testing.T) {
req := NewEvalRequest("g.E()")
got, err := graphson.MarshalToString(req)
require.NoError(t, err)
assert.NotContains(t, got, "bindings")
}
func TestAuthenticateRequestEncode(t *testing.T) {
req := NewAuthRequest("41d2e28a-20a4-4ab0-b379-d810dede3786", "user", "pass")
data, err := graphson.Marshal(req)
require.NoError(t, err)
var got map[string]any
err = json.Unmarshal(data, &got)
require.NoError(t, err)
assert.Equal(t, map[string]any{
"@type": "g:UUID",
"@value": req.RequestID,
}, got["requestId"])
assert.Equal(t, req.Operation, got["op"])
assert.Equal(t, req.Processor, got["processor"])
args := got["args"].(map[string]any)
assert.Equal(t, "g:Map", args["@type"])
assert.ElementsMatch(t, args["@value"], []any{
"sasl", "AHVzZXIAcGFzcw==", "saslMechanism", "PLAIN",
})
}
func TestCredentialsMarshaling(t *testing.T) {
want := Credentials{
Username: "username",
Password: "password",
}
text, err := want.MarshalText()
assert.NoError(t, err)
assert.Equal(t, "AHVzZXJuYW1lAHBhc3N3b3Jk", string(text))
var got Credentials
err = got.UnmarshalText(text)
assert.NoError(t, err)
assert.Equal(t, want, got)
}
func TestCredentialsBadEncodingMarshaling(t *testing.T) {
tests := []struct {
name string
text []byte
}{
{
name: "BadBase64",
text: []byte{0x12},
},
{
name: "Empty",
text: []byte{},
},
{
name: "BadPrefix",
text: []byte("Kg=="),
},
{
name: "NoSeparator",
text: []byte("AHVzZXI="),
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
var creds Credentials
err := creds.UnmarshalText(tc.text)
assert.Error(t, err)
})
}
}

127
dialect/gremlin/response.go Normal file
View File

@@ -0,0 +1,127 @@
// 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 gremlin
import (
"errors"
"fmt"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
"entgo.io/ent/dialect/gremlin/graph"
)
// A Response models a response message received from the server.
type Response struct {
RequestID string `json:"requestId" graphson:"g:UUID"`
Status struct {
Code int `json:"code"`
Attributes map[string]any `json:"attributes"`
Message string `json:"message"`
} `json:"status"`
Result struct {
Data graphson.RawMessage `json:"data"`
Meta map[string]any `json:"meta"`
} `json:"result"`
}
// IsErr returns whether response indicates an error.
func (rsp *Response) IsErr() bool {
switch rsp.Status.Code {
case StatusSuccess, StatusNoContent, StatusPartialContent:
return false
default:
return true
}
}
// Err returns an error representing response status.
func (rsp *Response) Err() error {
if rsp.IsErr() {
return fmt.Errorf("gremlin: code=%d, message=%q", rsp.Status.Code, rsp.Status.Message)
}
return nil
}
// ReadVal reads gremlin response data into v.
func (rsp *Response) ReadVal(v any) error {
if err := rsp.Err(); err != nil {
return err
}
if err := graphson.Unmarshal(rsp.Result.Data, v); err != nil {
return fmt.Errorf("gremlin: unmarshal response data: type=%T: %w", v, err)
}
return nil
}
// ReadVertices returns response data as slice of vertices.
func (rsp *Response) ReadVertices() ([]graph.Vertex, error) {
var v []graph.Vertex
err := rsp.ReadVal(&v)
return v, err
}
// ReadVertexProperties returns response data as slice of vertex properties.
func (rsp *Response) ReadVertexProperties() ([]graph.VertexProperty, error) {
var vp []graph.VertexProperty
err := rsp.ReadVal(&vp)
return vp, err
}
// ReadEdges returns response data as slice of edges.
func (rsp *Response) ReadEdges() ([]graph.Edge, error) {
var e []graph.Edge
err := rsp.ReadVal(&e)
return e, err
}
// ReadProperties returns response data as slice of properties.
func (rsp *Response) ReadProperties() ([]graph.Property, error) {
var p []graph.Property
err := rsp.ReadVal(&p)
return p, err
}
// ReadValueMap returns response data as a value map.
func (rsp *Response) ReadValueMap() (graph.ValueMap, error) {
var m graph.ValueMap
err := rsp.ReadVal(&m)
return m, err
}
// ReadBool returns response data as a bool.
func (rsp *Response) ReadBool() (bool, error) {
var b [1]*bool
if err := rsp.ReadVal(&b); err != nil {
return false, err
}
if b[0] == nil {
return false, errors.New("gremlin: no boolean value")
}
return *b[0], nil
}
// ReadInt returns response data as an int.
func (rsp *Response) ReadInt() (int, error) {
var v [1]*int
if err := rsp.ReadVal(&v); err != nil {
return 0, err
}
if v[0] == nil {
return 0, errors.New("gremlin: no integer value")
}
return *v[0], nil
}
// ReadString returns response data as a string.
func (rsp *Response) ReadString() (string, error) {
var v [1]*string
if err := rsp.ReadVal(&v); err != nil {
return "", err
}
if v[0] == nil {
return "", errors.New("gremlin: no string value")
}
return *v[0], nil
}

View File

@@ -0,0 +1,564 @@
// 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 gremlin
import (
"reflect"
"testing"
"entgo.io/ent/dialect/gremlin/encoding/graphson"
"entgo.io/ent/dialect/gremlin/graph"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDecodeResponse(t *testing.T) {
in := `{
"requestId": "a65f2d39-1efa-45d2-a06a-c736476500fc",
"result": {
"data": {
"@type": "g:List",
"@value": [
{
"@type": "g:Map",
"@value": [
{
"@type": "g:T",
"@value": "id"
},
{
"@type": "g:Int64",
"@value": 1
},
{
"@type": "g:T",
"@value": "label"
},
"person",
"name",
{
"@type": "g:List",
"@value": [
"marko"
]
},
"age",
{
"@type": "g:List",
"@value": [
{
"@type": "g:Int32",
"@value": 29
}
]
}
]
},
{
"@type": "g:Map",
"@value": [
{
"@type": "g:T",
"@value": "id"
},
{
"@type": "g:Int64",
"@value": 6
},
{
"@type": "g:T",
"@value": "label"
},
"person",
"name",
{
"@type": "g:List",
"@value": [
"peter"
]
},
"age",
{
"@type": "g:List",
"@value": [
{
"@type": "g:Int32",
"@value": 35
}
]
}
]
}
]
},
"meta": {
"@type": "g:Map",
"@value": []
}
},
"status": {
"attributes": {
"@type": "g:Map",
"@value": []
},
"code": 200,
"message": ""
}
}`
var rsp Response
err := graphson.UnmarshalFromString(in, &rsp)
require.NoError(t, err)
assert.Equal(t, "a65f2d39-1efa-45d2-a06a-c736476500fc", rsp.RequestID)
assert.Equal(t, 200, rsp.Status.Code)
assert.Empty(t, rsp.Status.Message)
assert.Empty(t, rsp.Status.Attributes)
assert.Empty(t, rsp.Result.Meta)
var vm graph.ValueMap
err = graphson.Unmarshal(rsp.Result.Data, &vm)
require.NoError(t, err)
require.Len(t, vm, 2)
type person struct {
ID int64 `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
var people []person
err = vm.Decode(&people)
require.NoError(t, err)
assert.Equal(t, []person{
{1, "marko", 29},
{6, "peter", 35},
}, people)
}
func TestDecodeResponseWithError(t *testing.T) {
in := `{
"requestId": "41d2e28a-20a4-4ab0-b379-d810dede3786",
"result": {
"data": null,
"meta": {
"@type": "g:Map",
"@value": []
}
},
"status": {
"attributes": {
"@type": "g:Map",
"@value": []
},
"code": 500,
"message": "Database Down"
}
}`
var rsp Response
err := graphson.UnmarshalFromString(in, &rsp)
require.NoError(t, err)
err = rsp.Err()
require.Error(t, err)
assert.Contains(t, err.Error(), "Database Down")
rsp = Response{}
err = graphson.UnmarshalFromString(`{"status": null}`, &rsp)
require.NoError(t, err)
assert.Error(t, rsp.Err())
}
func TestResponseReadVal(t *testing.T) {
var rsp Response
rsp.Status.Code = StatusSuccess
rsp.Result.Data = []byte(`{"@type": "g:Int32", "@value": 15}`)
var v int32
err := rsp.ReadVal(&v)
assert.NoError(t, err)
assert.Equal(t, int32(15), v)
var s string
err = rsp.ReadVal(&s)
assert.Error(t, err)
rsp.Status.Code = StatusServerError
err = rsp.ReadVal(&v)
assert.Error(t, err)
}
func TestResponseReadGraphElements(t *testing.T) {
tests := []struct {
method string
data string
want any
}{
{
method: "ReadVertices",
data: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Vertex",
"@value": {
"id": {
"@type": "g:Int64",
"@value": 1
},
"label": "person"
}
},
{
"@type": "g:Vertex",
"@value": {
"id": {
"@type": "g:Int64",
"@value": 6
},
"label": "person"
}
}
]
}`,
want: []graph.Vertex{
graph.NewVertex(int64(1), "person"),
graph.NewVertex(int64(6), "person"),
},
},
{
method: "ReadVertexProperties",
data: `{
"@type": "g:List",
"@value": [
{
"@type": "g:VertexProperty",
"@value": {
"id": {
"@type": "g:Int64",
"@value": 0
},
"label": "name",
"value": "marko"
}
},
{
"@type": "g:VertexProperty",
"@value": {
"id": {
"@type": "g:Int64",
"@value": 2
},
"label": "age",
"value": {
"@type": "g:Int32",
"@value": 29
}
}
}
]
}`,
want: []graph.VertexProperty{
graph.NewVertexProperty(int64(0), "name", "marko"),
graph.NewVertexProperty(int64(2), "age", int32(29)),
},
},
{
method: "ReadEdges",
data: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Edge",
"@value": {
"id": {
"@type": "g:Int32",
"@value": 12
},
"inV": {
"@type": "g:Int64",
"@value": 3
},
"inVLabel": "software",
"label": "created",
"outV": {
"@type": "g:Int64",
"@value": 6
},
"outVLabel": "person"
}
}
]
}`,
want: []graph.Edge{
graph.NewEdge(int32(12), "created",
graph.NewVertex(int64(6), "person"),
graph.NewVertex(int64(3), "software"),
),
},
},
{
method: "ReadProperties",
data: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Property",
"@value": {
"key": "weight",
"value": {
"@type": "g:Double",
"@value": 0.2
}
}
}
]
}`,
want: []graph.Property{
graph.NewProperty("weight", float64(0.2)),
},
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.method, func(t *testing.T) {
t.Parallel()
var rsp Response
rsp.Status.Code = StatusSuccess
rsp.Result.Data = []byte(tc.data)
vals := reflect.ValueOf(&rsp).MethodByName(tc.method).Call(nil)
require.Len(t, vals, 2)
require.True(t, vals[1].IsNil())
assert.Equal(t, tc.want, vals[0].Interface())
})
}
}
func TestResponseReadValueMap(t *testing.T) {
t.Parallel()
var rsp Response
rsp.Status.Code = StatusSuccess
rsp.Result.Data = []byte(`{
"@type": "g:List",
"@value": [
{
"@type": "g:Map",
"@value": [
"name",
{
"@type": "g:List",
"@value": [
"alex"
]
}
]
}
]
}`)
m, err := rsp.ReadValueMap()
require.NoError(t, err)
var name string
err = m.Decode(&struct {
Name *string `json:"name"`
}{&name})
require.NoError(t, err)
assert.Equal(t, "alex", name)
}
func TestResponseReadBool(t *testing.T) {
tests := []struct {
name string
data string
want bool
wantErr bool
}{
{
name: "Simple",
data: `{
"@type": "g:List",
"@value": [
true
]
}`,
want: true,
},
{
name: "Multi",
data: `{
"@type": "g:List",
"@value": [
false,
true
]
}`,
want: false,
},
{
name: "Empty",
data: `{
"@type": "g:List",
"@value": []
}`,
wantErr: true,
},
{
name: "BadType",
data: `{
"@type": "g:List",
"@value": [
"user"
]
}`,
wantErr: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var rsp Response
rsp.Status.Code = StatusSuccess
rsp.Result.Data = []byte(tc.data)
got, err := rsp.ReadBool()
if tc.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.want, got)
}
})
}
}
func TestResponseReadInt(t *testing.T) {
tests := []struct {
name string
data string
want int
wantErr bool
}{
{
name: "Simple",
data: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Int64",
"@value": 42
}
]
}`,
want: 42,
},
{
name: "Multi",
data: `{
"@type": "g:List",
"@value": [
{
"@type": "g:Int64",
"@value": 55
},
{
"@type": "g:Int64",
"@value": 13
}
]
}`,
want: 55,
},
{
name: "Empty",
data: `{
"@type": "g:List",
"@value": []
}`,
wantErr: true,
},
{
name: "BadType",
data: `{
"@type": "g:List",
"@value": [
true
]
}`,
wantErr: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var rsp Response
rsp.Status.Code = StatusSuccess
rsp.Result.Data = []byte(tc.data)
got, err := rsp.ReadInt()
if tc.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.want, got)
}
})
}
}
func TestResponseReadString(t *testing.T) {
tests := []struct {
name string
data string
want string
wantErr bool
}{
{
name: "Simple",
data: `{
"@type": "g:List",
"@value": ["foo"]
}`,
want: "foo",
},
{
name: "Empty",
data: `{
"@type": "g:List",
"@value": []
}`,
wantErr: true,
},
{
name: "BadType",
data: `{
"@type": "g:List",
"@value": [
true
]
}`,
wantErr: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var rsp Response
rsp.Status.Code = StatusSuccess
rsp.Result.Data = []byte(tc.data)
got, err := rsp.ReadString()
if tc.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.want, got)
}
})
}
}

69
dialect/gremlin/status.go Normal file
View File

@@ -0,0 +1,69 @@
// 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 gremlin
const (
// StatusSuccess is returned on success.
StatusSuccess = 200
// StatusNoContent means the server processed the request but there is no result to return.
StatusNoContent = 204
// StatusPartialContent indicates the server successfully returned some content, but there
// is more in the stream to arrive wait for a success code to signify the end.
StatusPartialContent = 206
// StatusUnauthorized means the request attempted to access resources that
// the requesting user did not have access to.
StatusUnauthorized = 401
// StatusAuthenticate denotes a challenge from the server for the client to authenticate its request.
StatusAuthenticate = 407
// StatusMalformedRequest means the request message was not properly formatted which means it could not be parsed at
// all or the "op" code was not recognized such that Gremlin Server could properly route it for processing.
// Check the message format and retry the request.
StatusMalformedRequest = 498
// StatusInvalidRequestArguments means the request message was parsable, but the arguments supplied in the message
// were in conflict or incomplete. Check the message format and retry the request.
StatusInvalidRequestArguments = 499
// StatusServerError indicates a general server error occurred that prevented the request from being processed.
StatusServerError = 500
// StatusScriptEvaluationError is returned when the script submitted for processing evaluated in the ScriptEngine
// with errors and could not be processed. Check the script submitted for syntax errors or other problems
// and then resubmit.
StatusScriptEvaluationError = 597
// StatusServerTimeout means the server exceeded one of the timeout settings for the request and could therefore
// only partially responded or did not respond at all.
StatusServerTimeout = 598
// StatusServerSerializationError means the server was not capable of serializing an object that was returned from the
// script supplied on the request. Either transform the object into something Gremlin Server can process within
// the script or install mapper serialization classes to Gremlin Server.
StatusServerSerializationError = 599
)
var statusText = map[int]string{
StatusSuccess: "Success",
StatusNoContent: "No Content",
StatusPartialContent: "Partial Content",
StatusUnauthorized: "Unauthorized",
StatusAuthenticate: "Authenticate",
StatusMalformedRequest: "Malformed Request",
StatusInvalidRequestArguments: "Invalid Request Arguments",
StatusServerError: "Server Error",
StatusScriptEvaluationError: "Script Evaluation Error",
StatusServerTimeout: "Server Timeout",
StatusServerSerializationError: "Server Serialization Error",
}
// StatusText returns status text of code.
func StatusText(code int) string {
return statusText[code]
}

View File

@@ -0,0 +1,16 @@
// Copyright 2019-present Facebook Inc. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package gremlin
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestStatusText(t *testing.T) {
assert.NotEmpty(t, StatusText(StatusSuccess))
assert.Empty(t, StatusText(4242))
}

69
dialect/gremlin/tokens.go Normal file
View File

@@ -0,0 +1,69 @@
// 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 gremlin
// Gremlin server operations.
const (
// OpsAuthentication used by the client to authenticate itself.
OpsAuthentication = "authentication"
// OpsBytecode used for a request that contains the Bytecode representation of a Traversal.
OpsBytecode = "bytecode"
// OpsEval used to evaluate a Gremlin script provided as a string.
OpsEval = "eval"
// OpsGather used to get a particular side-effect as produced by a previously executed Traversal.
OpsGather = "gather"
// OpsKeys used to get all the keys of all side-effects as produced by a previously executed Traversal.
OpsKeys = "keys"
// OpsClose used to get all the keys of all side-effects as produced by a previously executed Traversal.
OpsClose = "close"
)
// Gremlin server operation processors.
const (
// ProcessorTraversal is the default operation processor.
ProcessorTraversal = "traversal"
)
const (
// ArgsBatchSize allows to defines the number of iterations each ResponseMessage should contain
ArgsBatchSize = "batchSize"
// ArgsBindings allows to provide a map of key/value pairs to apply
// as variables in the context of the Gremlin script.
ArgsBindings = "bindings"
// ArgsAliases allows to define aliases that represent globally bound Graph and TraversalSource objects.
ArgsAliases = "aliases"
// ArgsGremlin corresponds to the Traversal to evaluate.
ArgsGremlin = "gremlin"
// ArgsSideEffect allows to specify the unique identifier for the request.
ArgsSideEffect = "sideEffect"
// ArgsSideEffectKey allows to specify the key for a specific side-effect.
ArgsSideEffectKey = "sideEffectKey"
// ArgsAggregateTo describes how side-effect data should be treated.
ArgsAggregateTo = "aggregateTo"
// ArgsLanguage allows to change the flavor of Gremlin used (e.g. gremlin-groovy).
ArgsLanguage = "language"
// ArgsEvalTimeout allows to override the server setting that determines
// the maximum time to wait for a script to execute on the server.
ArgsEvalTimeout = "scriptEvaluationTimeout"
// ArgsSasl defines the response to the server authentication challenge.
ArgsSasl = "sasl"
// ArgsSaslMechanism defines the SASL mechanism (e.g. PLAIN).
ArgsSaslMechanism = "saslMechanism"
)