From 56dbb1827eecbe40af568b6846f4dfea0aee8a84 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Mon, 19 Mar 2018 17:14:50 +0000 Subject: [PATCH] implement basic mrpc functionality --- mrpc/mrpc.go | 138 ++++++++++++++++++++++++++++++++++++++++++++++ mrpc/mrpc_test.go | 67 ++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 mrpc/mrpc.go create mode 100644 mrpc/mrpc_test.go diff --git a/mrpc/mrpc.go b/mrpc/mrpc.go new file mode 100644 index 0000000..9bd8609 --- /dev/null +++ b/mrpc/mrpc.go @@ -0,0 +1,138 @@ +// Package mrpc contains types and functionality to facilitate creating RPC +// interfaces and for making calls against those same interfaces +// +// This package contains a few fundamental types: Handler, Call, and +// Client. Together these form the components needed to implement nearly any RPC +// system. +// +// TODO an example of an implementation of these interfaces can be found in the +// m package +package mrpc + +import ( + "context" + "fmt" + "reflect" +) + +// Handler is a type which serves RPC calls. For each incoming Call the ServeRPC +// method is called, and the return from the method is used as the response. If +// an error is returned the response return is ignored. +type Handler interface { + ServeRPC(Call) (interface{}, error) +} + +// HandlerFunc can be used to wrap an individual function which fits the +// ServeRPC signature, and use that function as a Handler +type HandlerFunc func(Call) (interface{}, error) + +// ServeRPC implements the method for the Handler interface by calling the +// underlying function +func (hf HandlerFunc) ServeRPC(c Call) (interface{}, error) { + return hf(c) +} + +// Call is passed into the ServeRPC method and contains all information about +// the incoming RPC call which is being made +type Call interface { + Context() context.Context + + // Method returns the name of the RPC method being called + Method() string + + // UnmarshalArgs takes in a pointer and unmarshals the RPC call's arguments + // into it. The properties of the unmarshaling are dependent on the + // underlying implementation of the codec types. + UnmarshalArgs(interface{}) error +} + +type call struct { + ctx context.Context + method string + unmarshalArgs func(interface{}) error +} + +func (c call) Context() context.Context { + return c.ctx +} + +func (c call) Method() string { + return c.method +} + +func (c call) UnmarshalArgs(i interface{}) error { + return c.unmarshalArgs(i) +} + +// WithContext returns the same Call it's given, but the new Call will return +// the given context when Context() is called +func WithContext(c Call, ctx context.Context) Call { + return call{ctx: ctx, method: c.Method(), unmarshalArgs: c.UnmarshalArgs} +} + +// WithMethod returns the same Call it's given, but the new Call will return the +// given method name when Method() is called +func WithMethod(c Call, method string) Call { + return call{ctx: c.Context(), method: method, unmarshalArgs: c.UnmarshalArgs} +} + +// Client is an entity which can perform RPC calls against a remote endpoint. +// +// res should be a pointer into which the result of the RPC call will be +// unmarshaled according to Client's implementation. args will be marshaled and +// sent to the remote endpoint according to Client's implementation. +type Client interface { + CallRPC(ctx context.Context, res interface{}, method string, args interface{}) error +} + +// ClientFunc can be used to wrap an individual function which fits the CallRPC +// signature, and use that function as a Client +type ClientFunc func(context.Context, interface{}, string, interface{}) error + +// CallRPC implements the method for the Client interface by calling the +// underlying function +func (cf ClientFunc) CallRPC( + ctx context.Context, + res interface{}, + method string, + args interface{}, +) error { + return cf(ctx, res, method, args) +} + +// ReflectClient returns a Client whose CallRPC method will use reflection to +// call the given Handler's ServeRPC method directly, using reflect.Value's Set +// method to copy CallRPC's args parameter into UnmarshalArgs' receiver +// parameter, and similarly to copy the result from ServeRPC into CallRPC's +// receiver parameter. +func ReflectClient(h Handler) Client { + into := func(dst, src interface{}) error { + dstV, srcV := reflect.ValueOf(dst), reflect.ValueOf(src) + dstVi, srcVi := reflect.Indirect(dstV), reflect.Indirect(srcV) + if !dstVi.CanSet() || dstVi.Type() != srcVi.Type() { + return fmt.Errorf("can't set value of type %v into type %v", srcV.Type(), dstV.Type()) + } + dstVi.Set(srcVi) + return nil + } + + return ClientFunc(func( + ctx context.Context, + resInto interface{}, + method string, + args interface{}, + ) error { + c := call{ + ctx: ctx, + method: method, + unmarshalArgs: func(i interface{}) error { return into(i, args) }, + } + + res, err := h.ServeRPC(c) + if err != nil { + return err + } + + return into(resInto, res) + }) +} diff --git a/mrpc/mrpc_test.go b/mrpc/mrpc_test.go new file mode 100644 index 0000000..a39b778 --- /dev/null +++ b/mrpc/mrpc_test.go @@ -0,0 +1,67 @@ +package mrpc + +import ( + "context" + . "testing" + + "github.com/mediocregopher/mediocre-go-lib/mtest" + "github.com/stretchr/testify/assert" +) + +func TestReflectClient(t *T) { + type argT struct { + In string + } + + type resT struct { + Out string + } + + ctx := context.Background() + + { // test with handler returning non-pointer + client := ReflectClient(HandlerFunc(func(c Call) (interface{}, error) { + var args argT + assert.NoError(t, c.UnmarshalArgs(&args)) + assert.Equal(t, "foo", c.Method()) + return resT{Out: args.In}, nil + })) + + { // test with arg being non-pointer + in := mtest.RandHex(8) + var res resT + assert.NoError(t, client.CallRPC(ctx, &res, "foo", argT{In: in})) + assert.Equal(t, in, res.Out) + } + + { // test with arg being pointer + in := mtest.RandHex(8) + var res resT + assert.NoError(t, client.CallRPC(ctx, &res, "foo", &argT{In: in})) + assert.Equal(t, in, res.Out) + } + } + + { // test with handler returning pointer + client := ReflectClient(HandlerFunc(func(c Call) (interface{}, error) { + var args argT + assert.NoError(t, c.UnmarshalArgs(&args)) + assert.Equal(t, "foo", c.Method()) + return &resT{Out: args.In}, nil + })) + + { // test with arg being non-pointer + in := mtest.RandHex(8) + var res resT + assert.NoError(t, client.CallRPC(ctx, &res, "foo", argT{In: in})) + assert.Equal(t, in, res.Out) + } + + { // test with arg being pointer + in := mtest.RandHex(8) + var res resT + assert.NoError(t, client.CallRPC(ctx, &res, "foo", &argT{In: in})) + assert.Equal(t, in, res.Out) + } + } +}