Documentation ¶
Overview ¶
Package jsonrpc provides a stream-based implementation of the JSON-RPC 2.0 specification, inspired by the Language Server Protocol (LSP) framing format.
Example ¶
package main import ( "context" "encoding/json" "fmt" "io" "net" "strconv" "zb.256lights.llc/pkg/internal/jsonrpc" ) func main() { // Set up a server handler. srv := jsonrpc.ServeMux{ "subtract": jsonrpc.HandlerFunc(subtractHandler), } // Start a goroutine to serve JSON-RPC on an in-memory pipe. srvConn, clientConn := net.Pipe() srvDone := make(chan struct{}) go func() { defer close(srvDone) defer srvConn.Close() jsonrpc.Serve(context.Background(), newCodec(srvConn), srv) }() defer func() { // Wait for server to finish. <-srvDone }() // Create a client that communicates on the in-memory pipe. client := jsonrpc.NewClient(func(ctx context.Context) (jsonrpc.ClientCodec, error) { return newCodec(clientConn), nil }) defer client.Close() // Call the server using the client. response, err := client.JSONRPC(context.Background(), &jsonrpc.Request{ Method: "subtract", Params: json.RawMessage(`[42, 23]`), }) if err != nil { panic(err) } var x int64 if err := json.Unmarshal(response.Result, &x); err != nil { panic(err) } fmt.Println("Server returned", x) } // subtractHandler is a [jsonrpc.HandlerFunc] // that takes in an array of one or more integers // and returns the result of subtracting the subsequent integers from the first argument. func subtractHandler(ctx context.Context, req *jsonrpc.Request) (*jsonrpc.Response, error) { // Parse the arguments into the desired JSON structure. var params []int64 if err := json.Unmarshal(req.Params, ¶ms); err != nil { return nil, err } // Input validation. // We can use jsonrpc.Error to specify the error code to use. if len(params) == 0 { return nil, jsonrpc.Error(jsonrpc.InvalidParams, fmt.Errorf("params must include at least one number")) } // Perform the calculation. result := params[0] for _, x := range params[1:] { result -= x } return &jsonrpc.Response{ Result: json.RawMessage(strconv.FormatInt(result, 10)), }, nil } // codec is a simple implementation of [jsonrpc.ServerCodec] and [jsonrpc.ClientCodec] // that reads and writes JSON messages with no framing. type codec struct { enc *json.Encoder dec *json.Decoder c io.Closer } // newCodec returns a new codec that reads, writes, and closes the given stream. func newCodec(rwc io.ReadWriteCloser) *codec { c := &codec{ enc: json.NewEncoder(rwc), dec: json.NewDecoder(rwc), c: rwc, } c.dec.UseNumber() return c } // ReadRequest implements [jsonrpc.ServerCodec]. func (c *codec) ReadRequest() (json.RawMessage, error) { var msg json.RawMessage if err := c.dec.Decode(&msg); err != nil { return nil, err } return msg, nil } // ReadResponse implements [jsonrpc.ClientCodec]. func (c *codec) ReadResponse() (json.RawMessage, error) { var msg json.RawMessage if err := c.dec.Decode(&msg); err != nil { return nil, err } return msg, nil } // WriteRequest implements [jsonrpc.ClientCodec]. func (c *codec) WriteRequest(request json.RawMessage) error { return c.enc.Encode(request) } // WriteResponse implements [jsonrpc.ServerCodec]. func (c *codec) WriteResponse(response json.RawMessage) error { return c.enc.Encode(response) } // Close closes the underlying connection. // (Part of [jsonrpc.ClientCodec].) func (c *codec) Close() error { return c.c.Close() }
Output: Server returned 19
Index ¶
- func Do(ctx context.Context, h Handler, method string, response, request any) error
- func Error(code ErrorCode, err error) error
- func Notify(ctx context.Context, h Handler, method string, params any) error
- func Serve(ctx context.Context, codec ServerCodec, handler Handler) error
- type Client
- type ClientCodec
- type ErrorCode
- type Handler
- type HandlerFunc
- type Header
- type MethodNotFoundHandler
- type OpenFunc
- type Reader
- type Request
- type RequestWriter
- type Response
- type ServeMux
- type ServerCodec
- type Writer
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Do ¶
Do makes a single JSON-RPC to the given Handler. request should be any Go value that can be passed to json.Marshal. response should be any Go value (usually a pointer) that can be passed to json.Unmarshal. If response is the nil interface, then any result data is ignored (but Do will still wait for the call to complete).
Example ¶
ctx := context.Background() handler := jsonrpc.ServeMux{ "subtract": jsonrpc.HandlerFunc(subtractHandler), } var result int64 if err := jsonrpc.Do(ctx, handler, "subtract", &result, []int64{42, 23}); err != nil { panic(err) } fmt.Println("Handler returned", result)
Output: Handler returned 19
func Error ¶
Error returns a new error that wraps the given error and will return the given code from CodeFromError. Error panics if it is given a nil error.
func Notify ¶
Notify makes a single JSON-RPC to the given Handler. params should be any Go value that can be passed to json.Marshal.
Example ¶
package main import ( "context" "encoding/json" "fmt" "zb.256lights.llc/pkg/internal/jsonrpc" ) func main() { ctx := context.Background() handler := jsonrpc.ServeMux{ "update": jsonrpc.HandlerFunc(func(ctx context.Context, req *jsonrpc.Request) (*jsonrpc.Response, error) { var params []int64 if err := json.Unmarshal(req.Params, ¶ms); err != nil { return nil, err } fmt.Println(params) return nil, nil }), } if err := jsonrpc.Notify(ctx, handler, "update", []int64{1, 2, 3, 4, 5}); err != nil { panic(err) } }
Output: [1 2 3 4 5]
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
A Client represents a JSON-RPC client that automatically reconnects after I/O errors. Methods on Client are safe to call from multiple goroutines concurrently.
func NewClient ¶
NewClient returns a new Client that opens connections using the given function. The caller is responsible for calling Client.Close when the Client is no longer in use.
NewClient will start opening a connection in the background, but will return before the connection is established. The first call to Client.JSONRPC will block on the connection.
func (*Client) Codec ¶
func (c *Client) Codec(ctx context.Context) (codec RequestWriter, release func(), err error)
Codec obtains the client's currently active codec, waiting for a connection to be established if necessary. The caller is responsible for calling the release function when it is no longer using the codec: all new calls to Client.JSONRPC will block to avoid concurrent message writes. After the first call to the release function, subsequent calls are no-ops. Client will continue to read responses from the codec while the caller is holding onto the codec.
Codec guarantees that if it succeeds, it returns a value that was returned by the OpenFunc provided to NewClient. However, applications should not call either ReadResponse or Close on the returned codec.
Example ¶
package main import ( "context" "encoding/json" "zb.256lights.llc/pkg/internal/jsonrpc" ) func main() { // Assuming that we have Context and client from elsewhere. ctx := context.Background() var client *jsonrpc.Client // Obtain a codec. codec, releaseCodec, err := client.Codec(ctx) if err != nil { // handle error... } defer releaseCodec() // Send a notification manually. err = codec.WriteRequest(json.RawMessage(`{"jsonrpc": "2.0", "method": "foobar"}`)) if err != nil { // handle error... } }
Output:
type ClientCodec ¶
type ClientCodec interface { RequestWriter ReadResponse() (json.RawMessage, error) Close() error }
ClientCodec represents a single connection from a client to a server. WriteRequest and ReadResponse must be safe to call concurrently with each other, but Client guarantees that it will never make multiple concurrent WriteRequest calls nor multiple concurrent ReadResponse calls.
Close is called when the Client no longer intends to use the connection. Close may be called concurrently with WriteRequest or ReadResponse: doing so should interrupt either call and cause the call to return an error.
type ErrorCode ¶
type ErrorCode int
ErrorCode is a number that indicates the type of error that occurred during a JSON-RPC.
const ( // ParseError indicates that invalid JSON was received by the server. ParseError ErrorCode = -32700 // InvalidRequest indicates that the JSON sent is not a valid request object. InvalidRequest ErrorCode = -32600 // MethodNotFound indicates that the requested method does not exist or is not available. MethodNotFound ErrorCode = -32601 // InvalidParams indicates that the request's method parameters are invalid. InvalidParams ErrorCode = -32602 // InternalError indicates an internal JSON-RPC error. InternalError ErrorCode = -32603 )
Error codes defined in JSON-RPC 2.0.
Language Server Protocol error codes.
func CodeFromError ¶
CodeFromError returns the error's ErrorCode, if one has been assigned using Error.
As a special case, if there is a context.Canceled or context.DeadlineExceeded error in the error's Unwrap() chain, then CodeFromError returns RequestCancelled.
type Handler ¶
A type that implements Handler responds to JSON-RPC requests. Implementations of JSONRPC must be safe to call from multiple goroutines concurrently.
The jsonrpc package provides Client, ServeMux, and HandlerFunc as implementations of Handler.
type HandlerFunc ¶
HandlerFunc is a function that implements Handler.
type MethodNotFoundHandler ¶
type MethodNotFoundHandler struct{}
MethodNotFoundHandler implements Handler by returning a MethodNotFound error for all requests.
func (MethodNotFoundHandler) JSONRPC ¶
JSONRPC returns an error for which ErrorCode returns MethodNotFound.
type OpenFunc ¶
type OpenFunc func(ctx context.Context) (ClientCodec, error)
OpenFunc opens a connection for a Client.
type Reader ¶
type Reader struct {
// contains filtered or unexported fields
}
A Reader reads framed messages from an underlying io.Reader. Reader introduces its own buffering, so it may consume more bytes than needed to read a message.
func (*Reader) NextMessage ¶
NextMessage reads the next message header from the underlying reader. If the previous message's body was not fully read as determined by the Content-Length of the previous message, it will be discarded.
The message's Content-Length will be returned as bodySize, or -1 if the current message does not contain a valid Content-Length. If bodySize is -1, then the caller should decide whether it wants to consume the message based on the header, since the caller will be responsible for reading exactly the number of bytes that the body consumes.
func (*Reader) Read ¶
Read reads bytes from the body of a message. If the message header had a valid Content-Length, then Read will return io.EOF once the body's end has been reached.
func (*Reader) ReadByte ¶
ReadByte reads a single byte from the body of a message. If the message header had a valid Content-Length, then ReadByte will return io.EOF once the body's end has been reached.
func (*Reader) UnreadByte ¶
UnreadByte unreads the last byte from the body of a message.
type Request ¶
type Request struct { // Method is the name of the method to be invoked. Method string // Params is the raw JSON of the parameters. // If len(Params) == 0, then the parameters are omitted on the wire. // Otherwise, Params must hold a valid JSON array or a valid JSON object. Params json.RawMessage // Notification is true if the client does not care about a response. Notification bool // Extra holds a map of additional top-level fields on the request object. Extra map[string]json.RawMessage }
Request represents a parsed JSON-RPC request.
type RequestWriter ¶
type RequestWriter interface {
WriteRequest(request json.RawMessage) error
}
RequestWriter holds the WriteRequest method of ClientCodec.
type Response ¶
type Response struct { // Result is the result of invoking the method. // This may be any JSON. Result json.RawMessage // Extra holds a map of additional top-level fields on the response object. Extra map[string]json.RawMessage }
Response represents a parsed JSON-RPC response.
type ServerCodec ¶
type ServerCodec interface { ReadRequest() (json.RawMessage, error) WriteResponse(response json.RawMessage) error }
ServerCodec represents a single connection from a server to a client. ReadRequest and WriteResponse must be safe to call concurrently with each other, but [Server] guarantees that it will never make multiple concurrent ReadRequest calls nor multiple concurrent WriteResponse calls.