Skip to content

Commit

Permalink
Merge PR #6325: Add GRPCRouter
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronc authored Jun 3, 2020
1 parent 1f5626b commit e94b673
Show file tree
Hide file tree
Showing 10 changed files with 1,201 additions and 151 deletions.
76 changes: 76 additions & 0 deletions baseapp/grpcrouter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package baseapp

import (
"fmt"

gogogrpc "github.com/gogo/protobuf/grpc"
abci "github.com/tendermint/tendermint/abci/types"
"google.golang.org/grpc"
"google.golang.org/grpc/encoding"
"google.golang.org/grpc/encoding/proto"

sdk "github.com/cosmos/cosmos-sdk/types"
)

var protoCodec = encoding.GetCodec(proto.Name)

// GRPCRouter routes ABCI Query requests to GRPC handlers
type GRPCRouter struct {
routes map[string]GRPCQueryHandler
}

var _ gogogrpc.Server

// NewGRPCRouter creates a new GRPCRouter
func NewGRPCRouter() *GRPCRouter {
return &GRPCRouter{
routes: map[string]GRPCQueryHandler{},
}
}

// GRPCQueryHandler defines a function type which handles ABCI Query requests
// using gRPC
type GRPCQueryHandler = func(ctx sdk.Context, req abci.RequestQuery) (abci.ResponseQuery, error)

// Route returns the GRPCQueryHandler for a given query route path or nil
// if not found
func (qrt *GRPCRouter) Route(path string) GRPCQueryHandler {
handler, found := qrt.routes[path]
if !found {
return nil
}
return handler
}

// RegisterService implements the gRPC Server.RegisterService method. sd is a gRPC
// service description, handler is an object which implements that gRPC service
func (qrt *GRPCRouter) RegisterService(sd *grpc.ServiceDesc, handler interface{}) {
// adds a top-level query handler based on the gRPC service name
for _, method := range sd.Methods {
fqName := fmt.Sprintf("/%s/%s", sd.ServiceName, method.MethodName)
methodHandler := method.Handler

qrt.routes[fqName] = func(ctx sdk.Context, req abci.RequestQuery) (abci.ResponseQuery, error) {
// call the method handler from the service description with the handler object,
// a wrapped sdk.Context with proto-unmarshaled data from the ABCI request data
res, err := methodHandler(handler, sdk.WrapSDKContext(ctx), func(i interface{}) error {
return protoCodec.Unmarshal(req.Data, i)
}, nil)
if err != nil {
return abci.ResponseQuery{}, err
}

// proto marshal the result bytes
resBytes, err := protoCodec.Marshal(res)
if err != nil {
return abci.ResponseQuery{}, err
}

// return the result bytes as the response value
return abci.ResponseQuery{
Height: req.Height,
Value: resBytes,
}, nil
}
}
}
53 changes: 53 additions & 0 deletions baseapp/grpcrouter_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package baseapp

import (
gocontext "context"
"fmt"

gogogrpc "github.com/gogo/protobuf/grpc"
abci "github.com/tendermint/tendermint/abci/types"
"google.golang.org/grpc"

sdk "github.com/cosmos/cosmos-sdk/types"
)

// QueryServiceTestHelper provides a helper for making grpc query service
// rpc calls in unit tests. It implements both the grpc Server and ClientConn
// interfaces needed to register a query service server and create a query
// service client.
type QueryServiceTestHelper struct {
*GRPCRouter
ctx sdk.Context
}

// NewQueryServerTestHelper creates a new QueryServiceTestHelper that wraps
// the provided sdk.Context
func NewQueryServerTestHelper(ctx sdk.Context) *QueryServiceTestHelper {
return &QueryServiceTestHelper{GRPCRouter: NewGRPCRouter(), ctx: ctx}
}

// Invoke implements the grpc ClientConn.Invoke method
func (q *QueryServiceTestHelper) Invoke(_ gocontext.Context, method string, args, reply interface{}, _ ...grpc.CallOption) error {
querier := q.Route(method)
if querier == nil {
return fmt.Errorf("handler not found for %s", method)
}
reqBz, err := protoCodec.Marshal(args)
if err != nil {
return err
}
res, err := querier(q.ctx, abci.RequestQuery{Data: reqBz})

if err != nil {
return err
}
return protoCodec.Unmarshal(res.Value, reply)
}

// NewStream implements the grpc ClientConn.NewStream method
func (q *QueryServiceTestHelper) NewStream(gocontext.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, fmt.Errorf("not supported")
}

var _ gogogrpc.Server = &QueryServiceTestHelper{}
var _ gogogrpc.ClientConn = &QueryServiceTestHelper{}
49 changes: 49 additions & 0 deletions baseapp/grpcrouter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package baseapp

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/require"

"github.com/cosmos/cosmos-sdk/codec/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
)

type testServer struct{}

func (e testServer) Echo(_ context.Context, req *testdata.EchoRequest) (*testdata.EchoResponse, error) {
return &testdata.EchoResponse{Message: req.Message}, nil
}

func (e testServer) SayHello(_ context.Context, request *testdata.SayHelloRequest) (*testdata.SayHelloResponse, error) {
greeting := fmt.Sprintf("Hello %s!", request.Name)
return &testdata.SayHelloResponse{Greeting: greeting}, nil
}

var _ testdata.TestServiceServer = testServer{}

func TestGRPCRouter(t *testing.T) {
qr := NewGRPCRouter()
testdata.RegisterTestServiceServer(qr, testServer{})
helper := &QueryServiceTestHelper{
GRPCRouter: qr,
ctx: sdk.Context{},
}
client := testdata.NewTestServiceClient(helper)

res, err := client.Echo(context.Background(), &testdata.EchoRequest{Message: "hello"})
require.Nil(t, err)
require.NotNil(t, res)
require.Equal(t, "hello", res.Message)

require.Panics(t, func() {
_, _ = client.Echo(context.Background(), nil)
})

res2, err := client.SayHello(context.Background(), &testdata.SayHelloRequest{Name: "Foo"})
require.Nil(t, err)
require.NotNil(t, res)
require.Equal(t, "Hello Foo!", res2.Greeting)
}
Loading

0 comments on commit e94b673

Please sign in to comment.