Skip to content

Commit

Permalink
Update documentation and examples.
Browse files Browse the repository at this point in the history
  • Loading branch information
creachadair committed Oct 25, 2021
1 parent a527fc4 commit 887b0e7
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 76 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# jrpc2

[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/creachadair/jrpc2)
[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=yellow)](https://pkg.go.dev/github.com/creachadair/jrpc2)
[![Go Report Card](https://goreportcard.com/badge/github.com/creachadair/jrpc2)](https://goreportcard.com/report/github.com/creachadair/jrpc2)

This repository provides Go package that implements a [JSON-RPC 2.0][spec] client and server.
Expand Down
125 changes: 68 additions & 57 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ Handle method with this signature:
Handle(ctx Context.Context, req *jrpc2.Request) (interface{}, error)
The handler package helps adapt existing functions to this interface.
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.
For example, suppose we would like to export the following Add function as a
JSON-RPC method:
For example, suppose we want to export this Add function via JSON-RPC:
// Add returns the sum of a slice of integers.
func Add(ctx context.Context, values []int) int {
Expand All @@ -27,13 +25,14 @@ JSON-RPC method:
return sum
}
The handler package helps adapt existing functions to the Handler interface.
To convert Add to a jrpc2.Handler, call handler.New, which uses reflection to
lift its argument into the jrpc2.Handler interface:
h := handler.New(Add) // h is a jrpc2.Handler that invokes Add
h := handler.New(Add) // h is now a jrpc2.Handler that calls Add
We will advertise this function under the name "Add". For static assignments
we can use a handler.Map, which finds methods by looking them up in a Go map:
The handler package also provides handler.Map, which implements the Assigner
interface with a Go map. To advertise this function under the name "Add":
assigner := handler.Map{
"Add": handler.New(Add),
Expand All @@ -43,19 +42,21 @@ Equipped with an Assigner we can now construct a Server:
srv := jrpc2.NewServer(assigner, nil) // nil for default options
To serve requests, we need a channel.Channel. Implementations of the Channel
To start the server, we need a channel.Channel. Implementations of the Channel
interface handle the framing, transmission, and receipt of JSON messages. The
channel package provides several common framing disciplines and functions to
wrap them around various input and output streams. For this example, we'll use
a channel that delimits messages by newlines, and communicates on os.Stdin and
os.Stdout:
channel package implements some common framing disciplines for byte streams
like pipes and sockets. For this example, we'll use a channel that
communicates over stdin and stdout, with messages delimited by newlines:
ch := channel.Line(os.Stdin, os.Stdout)
Now we can start the server:
srv.Start(ch)
Once started, the running server handles incoming requests until the channel
closes, or until it is stopped explicitly by calling srv.Stop(). To wait for
the server to finish, call:
The Start method does not block. The server runs until the channel closes, or
until it is stopped explicitly by calling srv.Stop(). To wait for the server to
finish, call:
err := srv.Wait()
Expand Down Expand Up @@ -88,12 +89,12 @@ To create a client we need a channel:
To send a single RPC, use the Call method:
rsp, err := cli.Call(ctx, "Add", []int{1, 3, 5, 7})
rsp, err := cli.Call(ctx, "Math.Add", []int{1, 3, 5, 7})
Call blocks until the response is received. Any error returned by the server,
including cancellation or deadline exceeded, has concrete type *jrpc2.Error.
Call blocks until the response is received. Errors returned by the server have
concrete type *jrpc2.Error.
To issue a batch of requests, use the Batch method:
To issue a batch of concurrent requests, use the Batch method:
rsps, err := cli.Batch(ctx, []jrpc2.Spec{
{Method: "Math.Add", Params: []int{1, 2, 3}},
Expand All @@ -106,7 +107,7 @@ call reflects an error in sending the request: The caller must check each
response separately for errors from the server. Responses are returned in the
same order as the Spec values, save that notifications are omitted.
To decode the result from a successful response use its UnmarshalResult method:
To decode the result from a successful response, use its UnmarshalResult method:
var result int
if err := rsp.UnmarshalResult(&result); err != nil {
Expand All @@ -118,21 +119,25 @@ To close a client and discard all its pending work, call cli.Close().
Notifications
The JSON-RPC protocol also supports notifications. Notifications differ from
calls in that they are one-way: The client sends them to the server, but the
server does not reply.
A JSON-RPC notification is a one-way request: The client sends the request to
the server, but the server does not reply. Use the Notify method of a client to
send a notification:
Use the Notify method of a jrpc2.Client to send notifications:
note := handler.Obj{"message": "A fire is burning!"}
err := cli.Notify(ctx, "Alert", note)
err := cli.Notify(ctx, "Alert", handler.Obj{
"message": "A fire is burning!",
})
A notification is complete once it has been sent. Notifications can also be sent
as part of a batch request:
A notification is complete once it has been sent.
rsps, err := cli.Batch(ctx, []jrpc2.Spec{
{Method: "Alert", Params: note, Notify: true}, // this is a notification
{Method: "Math.Add": Params: []int{1, 2}}, // this is a normal call
// ...
})
On server, notifications are handled identically to ordinary requests, except
that the return value is discarded once the handler returns. If a handler does
not want to do anything for a notification, it can query the request:
On the server, notifications are handled just like other requests, except that
the return value is discarded once the handler returns. If a handler does not
want to do anything for a notification, it can query the request:
if req.IsNotification() {
return 0, nil // ignore notifications
Expand All @@ -141,47 +146,50 @@ not want to do anything for a notification, it can query the request:
Services with Multiple Methods
The example above shows a server with one method using handler.New. To
simplify exporting multiple methods, the handler.Map type collects named
methods:
The example above shows a server with one method. A handler.Map works for any
number of distinctly-named methods:
mathService := handler.Map{
"Add": handler.New(Add),
"Mul": handler.New(Mul),
}
Maps may be further combined with the handler.ServiceMap type to allow
different services to work together:
Maps may be further combined with the handler.ServiceMap type to allow multiple
services to be exported from the same server:
func GetStatus(context.Context) string { return "all is well" }
func getStatus(context.Context) string { return "all is well" }
assigner := handler.ServiceMap{
"Math": mathService,
"Status": handler.Map{"Get": handler.New(Status)},
"Status": handler.Map{
"Get": handler.New(Status),
},
}
This assigner dispatches "Math.Add" and "Math.Mul" to the arithmetic functions,
and "Status.Get" to the GetStatus function. A ServiceMap splits the method name
and "Status.Get" to the getStatus function. A ServiceMap splits the method name
on the first period ("."), and you may nest ServiceMaps more deeply if you
require a more complex hierarchy.
Concurrency
A Server issues requests to handlers concurrently, up to the Concurrency limit
given in its ServerOptions. Two requests (either calls or notifications) are
concurrent if they arrive as part of the same batch. In addition, two calls are
concurrent if the time intervals between the arrival of the request objects and
delivery of the response objects overlap.
A Server issues concurrent requests to handlers in parallel, up to the limit
given by the Concurrency field in ServerOptions.
Two requests (either calls or notifications) are concurrent if they arrive as
part of the same batch. In addition, two calls are concurrent if the time
intervals between the arrival of the request objects and delivery of the
response objects overlap.
The server may issue concurrent requests to their handlers in any order.
Non-concurrent requests are processed in order of arrival. Notifications, in
particular, can only be concurrent with other requests in the same batch. This
ensures a client that sends a notification can be sure its notification will be
fully processed before any subsequent calls are issued to their handlers.
These rules imply that the client cannot rely on the order of evaluation for
calls that overlap in time: If the caller needs to ensure that call A completes
These rules imply that the client cannot rely on the execution order of calls
that overlap in time: If the caller needs to ensure that call A completes
before call B starts, it must wait for A to return before invoking B.
Expand All @@ -192,19 +200,19 @@ the implementation. By default, a server does not dispatch these methods to its
assigner. In this configuration, the server exports a "rpc.serverInfo" method
taking no parameters and returning a jrpc2.ServerInfo value.
Setting the DisableBuiltin option to true in the ServerOptions removes special
treatment of "rpc." method names, and disables the rpc.serverInfo handler.
When this option is true, method names beginning with "rpc." will be dispatched
to the assigner like any other method.
Setting the DisableBuiltin server option to true removes special treatment of
"rpc." method names, and disables the rpc.serverInfo handler. When this option
is true, method names beginning with "rpc." will be dispatched to the assigner
like any other method.
Server Push
The AllowPush option in jrpc2.ServerOptions allows a server to "push" requests
back to the client. This is a non-standard extension of JSON-RPC used by some
applications such as the Language Server Protocol (LSP). If this feature is
enabled, the server's Notify and Callback methods send requests back to the
client. Otherwise, those methods will report an error:
The AllowPush server option allows handlers to "push" requests back to the
client. This is a non-standard extension of JSON-RPC used by some applications
such as the Language Server Protocol (LSP). When this option is enabled, the
server's Notify and Callback methods send requests back to the client.
Otherwise, those methods will report an error:
if err := s.Notify(ctx, "methodName", params); err == jrpc2.ErrPushUnsupported {
// server push is not enabled
Expand All @@ -214,10 +222,13 @@ client. Otherwise, those methods will report an error:
}
A method handler may use jrpc2.ServerFromContext to access the server from its
context, and then invoke these methods on it.
context, and then invoke these methods on it. On the client side, the OnNotify
and OnCallback options in jrpc2.ClientOptions provide hooks to which any server
requests are delivered, if they are set.
On the client side, the OnNotify and OnCallback options in jrpc2.ClientOptions
provide hooks to which any server requests are delivered, if they are set.
Since not all clients support server push, handlers should set a timeout when
using the server Callback method; otherwise the callback may block forever for
a client response that will never arrive.
*/
package jrpc2

Expand Down
4 changes: 3 additions & 1 deletion handler/positional.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ func NewPos(fn interface{}, names ...string) Func {
// {"first": 17, "second": 23}
//
// where "first" is mapped to argument x and "second" to argument y. Unknown
// field keys generate an error.
// field keys generate an error. The field names are not required to match the
// parameter names declared by the function; it is the names assigned here that
// determine which object keys are accepted.
func Positional(fn interface{}, names ...string) (*FuncInfo, error) {
if fn == nil {
return nil, errors.New("nil function")
Expand Down
14 changes: 6 additions & 8 deletions tools/examples/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func add(ctx context.Context, cli *jrpc2.Client, vs ...int) (result int, err err
return
}

func div(ctx context.Context, cli *jrpc2.Client, args binarg) (result float64, err error) {
err = cli.CallResult(ctx, "Math.Div", args, &result)
func div(ctx context.Context, cli *jrpc2.Client, x, y int) (result float64, err error) {
err = cli.CallResult(ctx, "Math.Div", handler.Obj{"X": x, "Y": y}, &result)
return
}

Expand All @@ -44,8 +44,6 @@ func alert(ctx context.Context, cli *jrpc2.Client, msg string) error {
return cli.Notify(ctx, "Post.Alert", handler.Obj{"message": msg})
}

type binarg struct{ X, Y int }

func intResult(rsp *jrpc2.Response) int {
var v int
if err := rsp.UnmarshalResult(&v); err != nil {
Expand Down Expand Up @@ -88,7 +86,7 @@ func main() {
} else {
log.Printf("Math.Add result=%d", sum)
}
if quot, err := div(ctx, cli, binarg{82, 19}); err != nil {
if quot, err := div(ctx, cli, 82, 19); err != nil {
log.Fatalln("Math.Div:", err)
} else {
log.Printf("Math.Div result=%.3f", quot)
Expand All @@ -100,7 +98,7 @@ func main() {
}

// An error condition (division by zero)
if quot, err := div(ctx, cli, binarg{15, 0}); err != nil {
if quot, err := div(ctx, cli, 15, 0); err != nil {
log.Printf("Math.Div err=%v", err)
} else {
log.Fatalf("Math.Div succeeded unexpectedly: result=%v", quot)
Expand All @@ -114,7 +112,7 @@ func main() {
y := rand.Intn(100)
specs = append(specs, jrpc2.Spec{
Method: "Math.Mul",
Params: binarg{x, y},
Params: handler.Obj{"X": x, "Y": y},
})
}
}
Expand All @@ -140,7 +138,7 @@ func main() {
go func() {
defer wg.Done()
var result int
if err := cli.CallResult(ctx, "Math.Sub", binarg{x, y}, &result); err != nil {
if err := cli.CallResult(ctx, "Math.Sub", handler.Obj{"X": x, "Y": y}, &result); err != nil {
log.Printf("Req (%d-%d) failed: %v", x, y, err)
return
}
Expand Down
14 changes: 5 additions & 9 deletions tools/examples/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,11 @@ func Add(ctx context.Context, vs []int) int {
return sum
}

// Sub returns the difference arg.X - arg.Y.
func Sub(ctx context.Context, arg binop) int {
return arg.X - arg.Y
}
// Sub returns the difference x - y.
func Sub(ctx context.Context, x, y int) int { return x - y }

// Mul returns the product arg.X * arg.Y.
func Mul(ctx context.Context, arg binop) int {
return arg.X * arg.Y
}
func Mul(ctx context.Context, x, y int) int { return x * y }

// Div converts its arguments to floating point and returns their ratio.
func Div(ctx context.Context, arg binop) (float64, error) {
Expand Down Expand Up @@ -86,8 +82,8 @@ func main() {
mux := handler.ServiceMap{
"Math": handler.Map{
"Add": handler.New(Add),
"Sub": handler.New(Sub),
"Mul": handler.New(Mul),
"Sub": handler.NewPos(Sub, "X", "Y"),
"Mul": handler.NewPos(Mul, "X", "Y"),
"Div": handler.New(Div),
"Status": handler.New(Status),
},
Expand Down

0 comments on commit 887b0e7

Please sign in to comment.