jsonrpc

package
v0.0.0-...-15c7053 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 22, 2025 License: MIT Imports: 16 Imported by: 0

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, &params); 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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Do

func Do(ctx context.Context, h Handler, method string, response, request any) error

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

func Error(code ErrorCode, err error) 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

func Notify(ctx context.Context, h Handler, method string, params any) error

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, &params); 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]

func Serve

func Serve(ctx context.Context, codec ServerCodec, handler Handler) error

Serve serves JSON-RPC requests for a connection. Serve will read requests from the codec until ReadRequest returns an error, which Serve will return once all requests have completed.

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

func NewClient(open OpenFunc) *Client

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) Close

func (c *Client) Close() error

Close closes the client 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:

func (*Client) JSONRPC

func (c *Client) JSONRPC(ctx context.Context, req *Request) (*Response, error)

JSONRPC sends a request to the server.

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.

const (
	UnknownErrorCode ErrorCode = -32001
	RequestCancelled ErrorCode = -32800
)

Language Server Protocol error codes.

func CodeFromError

func CodeFromError(err error) (_ ErrorCode, ok bool)

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

type Handler interface {
	JSONRPC(ctx context.Context, req *Request) (*Response, error)
}

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

type HandlerFunc func(ctx context.Context, req *Request) (*Response, error)

HandlerFunc is a function that implements Handler.

func (HandlerFunc) JSONRPC

func (f HandlerFunc) JSONRPC(ctx context.Context, req *Request) (*Response, error)

JSONRPC calls f.

type Header = textproto.MIMEHeader

Header represents a message's header.

type MethodNotFoundHandler

type MethodNotFoundHandler struct{}

MethodNotFoundHandler implements Handler by returning a MethodNotFound error for all requests.

func (MethodNotFoundHandler) JSONRPC

func (MethodNotFoundHandler) JSONRPC(ctx context.Context, req *Request) (*Response, error)

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 NewReader

func NewReader(r io.Reader) *Reader

NewReader returns a new Reader that reads from r.

func (*Reader) NextMessage

func (r *Reader) NextMessage() (header Header, bodySize int64, err error)

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

func (r *Reader) Read(p []byte) (n int, err error)

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

func (r *Reader) ReadByte() (byte, error)

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

func (r *Reader) UnreadByte() error

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 ServeMux

type ServeMux map[string]Handler

ServeMux is a mapping of method names to JSON-RPC handlers.

func (ServeMux) JSONRPC

func (mux ServeMux) JSONRPC(ctx context.Context, req *Request) (*Response, error)

JSONRPC calls the handler that corresponds to the request's method or returns a MethodNotFound error if no such handler is present.

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.

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

A Writer writes framed messages to an underlying io.Writer.

func NewWriter

func NewWriter(w io.Writer) *Writer

NewWriter returns a new Writer that writes to w.

func (*Writer) WriteMessage

func (c *Writer) WriteMessage(header Header, body io.Reader) error

WriteMessage writes a message to the connection.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL