Skip to content

Commit

Permalink
Replace the Handler interface with a plain function type. (#90)
Browse files Browse the repository at this point in the history
In practice there turns out to be no practical advantage to having the Handler
type be an interface. All the existing usage that I can find, including in the
handler support package, is based on explicit functions.

This change replaces the Handler interface with a type alias to the expected
function signature for the interface's Handle method. Any existing use based on
the interface can be updated by extracting the method directly. For example,
given a type like:

    type T struct{ ...
    func (t *T) Handle(ctx context.Context, req *jrpc2.Request) (any, error) { ... }
    h := &T{ ... }

Replace usage like:

    m := handler.Map{"Method": h}

with:

    m := handler.Map{"Method": h.Handle}

This is a breaking change to the package API.
  • Loading branch information
creachadair authored Dec 12, 2022
1 parent 4abecb2 commit 70c3cd8
Show file tree
Hide file tree
Showing 6 changed files with 39 additions and 51 deletions.
43 changes: 19 additions & 24 deletions base.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,25 @@ type Namer interface {
Names() []string
}

// A Handler handles a single request.
type Handler interface {
// Handle invokes the method with the specified request. The response value
// must be JSON-marshalable or nil. In case of error, the handler can
// return a value of type *jrpc2.Error to control the response code sent
// back to the caller; otherwise the server will wrap the resulting value.
//
// The context passed to the handler by a *jrpc2.Server includes two special
// values that the handler may extract.
//
// To obtain the server instance running the handler, write:
//
// srv := jrpc2.ServerFromContext(ctx)
//
// To obtain the inbound request message, write:
//
// req := jrpc2.InboundRequest(ctx)
//
// The latter is primarily useful for handlers generated by handler.New,
// which do not receive the request directly. For a handler that implements
// the Handle method directly, req is the same value passed as a parameter
// to Handle.
Handle(context.Context, *Request) (any, error)
}
// A Handler function implements a method given a request. The response value
// must be JSON-marshalable or nil. In case of error, the handler can return a
// value of type *jrpc2.Error to control the response code sent back to the
// caller; otherwise the server will wrap the resulting value.
//
// The context passed to the handler by a *jrpc2.Server includes two special
// values that the handler may extract.
//
// To obtain the server instance running the handler, write:
//
// srv := jrpc2.ServerFromContext(ctx)
//
// To obtain the inbound request message, write:
//
// req := jrpc2.InboundRequest(ctx)
//
// The latter is primarily useful for wrappers generated by handler.New, which
// do not receive the request directly.
type Handler = func(context.Context, *Request) (any, error)

// A Request is a request message from a client to a server.
type Request struct {
Expand Down
12 changes: 5 additions & 7 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ defined by http://www.jsonrpc.org/specification.
The *Server type implements a JSON-RPC server. A server communicates with a
client over a channel.Channel, and dispatches client requests to user-defined
method handlers. Handlers satisfy the jrpc2.Handler interface by exporting a
Handle method with this signature:
method handlers. Method handlers are functions with this signature:
Handle(ctx Context.Context, req *jrpc2.Request) (any, error)
func(ctx Context.Context, req *jrpc2.Request) (any, error)
A server finds the handler for a request by looking up its method name in a
jrpc2.Assigner provided when the server is set up. A Handler can decode the
Expand All @@ -26,8 +25,8 @@ request parameters using the UnmarshalParams method on the request:
}
The handler package makes it easier to use functions that do not have this
exact type signature as handlers, by using reflection to lift functions into
the Handler interface. For example, suppose we want to export this Add function:
exact type signature as handlers, using reflection to wrap them in a Handler
function. For example, suppose we want to export this Add function:
// Add returns the sum of a slice of integers.
func Add(ctx context.Context, values []int) int {
Expand All @@ -38,8 +37,7 @@ the Handler interface. For example, suppose we want to export this Add function
return sum
}
To convert Add to a handler, call handler.New, which wraps its argument in a a
handler.Func, which satisfies the jrpc2.Handler interface:
To convert Add to a handler, call handler.New, which returns a handler.Func:
h := handler.New(Add) // h is now a handler.Func that calls Add
Expand Down
19 changes: 7 additions & 12 deletions handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,12 @@ import (
"github.com/creachadair/jrpc2/code"
)

// A Func adapts a function having the correct signature to a jrpc2.Handler.
// A Func is wrapper for a jrpc2.Handler function.
type Func func(context.Context, *jrpc2.Request) (any, error)

// Handle implements the jrpc2.Handler interface by calling m.
func (m Func) Handle(ctx context.Context, req *jrpc2.Request) (any, error) {
return m(ctx, req)
}

// A Map is a trivial implementation of the jrpc2.Assigner interface that looks
// up method names in a map of static jrpc2.Handler values.
type Map map[string]jrpc2.Handler
// up method names in a static map of function values.
type Map map[string]Func

// Assign implements part of the jrpc2.Assigner interface.
func (m Map) Assign(_ context.Context, method string) jrpc2.Handler { return m[method] }
Expand Down Expand Up @@ -78,7 +73,7 @@ func (m ServiceMap) Names() []string {
return all
}

// New adapts a function to a jrpc2.Handler. The concrete value of fn must be
// New adapts a function to a .Handler. The concrete value of fn must be
// function accepted by Check. The resulting Func will handle JSON encoding and
// decoding, call fn, and report appropriate errors.
//
Expand Down Expand Up @@ -124,9 +119,9 @@ type FuncInfo struct {
// for non-struct arguments.
func (fi *FuncInfo) SetStrict(strict bool) *FuncInfo { fi.strictFields = strict; return fi }

// Wrap adapts the function represented by fi in a Func that satisfies the
// jrpc2.Handler interface. The wrapped function can obtain the *jrpc2.Request
// value from its context argument using the jrpc2.InboundRequest helper.
// Wrap adapts the function represented by fi in a Func. The wrapped function
// can obtain the *jrpc2.Request value from its context argument using the
// jrpc2.InboundRequest helper.
//
// This method panics if fi == nil or if it does not represent a valid function
// type. A FuncInfo returned by a successful call to Check is always valid.
Expand Down
10 changes: 5 additions & 5 deletions handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ func TestFuncInfo_wrapDecode(t *testing.T) {
fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"x","params":%s}`, test.p))
got, err := test.fn(ctx, req)
if err != nil {
t.Errorf("Call %+v failed: %v", test.fn, err)
t.Errorf("Call %v failed: %v", test.fn, err)
} else if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("Call %+v: wrong result (-want, +got)\n%s", test.fn, diff)
t.Errorf("Call %v: wrong result (-want, +got)\n%s", test.fn, diff)
}
}
}
Expand Down Expand Up @@ -263,7 +263,7 @@ func TestFuncInfo_SetStrict(t *testing.T) {
// introduce another pointer indirection.
func TestNew_pointerRegression(t *testing.T) {
var got argStruct
call := handler.New(func(_ context.Context, arg *argStruct) error {
method := handler.New(func(_ context.Context, arg *argStruct) error {
got = *arg
t.Logf("Got argument struct: %+v", got)
return nil
Expand All @@ -276,8 +276,8 @@ func TestNew_pointerRegression(t *testing.T) {
"alpha": "xyzzy",
"bravo": 23
}}`)
if _, err := call.Handle(context.Background(), req); err != nil {
t.Errorf("Handle failed: %v", err)
if _, err := method(context.Background(), req); err != nil {
t.Errorf("Handler failed: %v", err)
}
want := argStruct{A: "xyzzy", B: 23}
if diff := cmp.Diff(want, got); diff != "" {
Expand Down
4 changes: 2 additions & 2 deletions internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ func TestServer_specialMethods(t *testing.T) {
}
}
if got := s.assign(ctx, "rpc.nonesuch"); got != nil {
t.Errorf("s.assign(rpc.nonesuch): got %v, want nil", got)
t.Errorf("s.assign(rpc.nonesuch): got %p, want nil", got)
}
}

Expand All @@ -252,7 +252,7 @@ func TestServer_disableBuiltinHook(t *testing.T) {
// With builtins disabled, the default rpc.* methods should not get assigned.
for _, name := range []string{rpcServerInfo} {
if got := s.assign(ctx, name); got != nil {
t.Errorf("s.assign(%s): got %+v, wanted nil", name, got)
t.Errorf("s.assign(%s): got %p, wanted nil", name, got)
}
}

Expand Down
2 changes: 1 addition & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ func (s *Server) invoke(base context.Context, h Handler, req *Request) (json.Raw
defer s.sem.Release(1)

s.rpcLog.LogRequest(ctx, req)
v, err := h.Handle(ctx, req)
v, err := h(ctx, req)
if err != nil {
if req.IsNotification() {
s.log("Discarding error from notification to %q: %v", req.Method(), err)
Expand Down

0 comments on commit 70c3cd8

Please sign in to comment.