harpy

package module
v0.10.3 Latest Latest
Warning

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

Go to latest
Published: May 25, 2023 License: MIT Imports: 14 Imported by: 1

README

Harpy

A toolkit for writing JSON-RPC v2.0 clients and servers in Go.

Documentation Latest Version Build Status Code Coverage

Example

The included example demonstrates how to implement a very simple in-memory key/value store with a JSON-RPC API.

Transports

Harpy provides an HTTP transport out of the box, however JSON-RPC 2.0 is a transport-agnostic protocol and as such Harpy's API attempts to make it easy to implement other transports.

Documentation

Overview

Package harpy is a toolkit for writing JSON-RPC v2.0 servers.

Example

Example shows how to implement a very basic JSON-RPC key/value server using Harpy's HTTP transport.

package main

import (
	"context"
	"net/http"
	"sync"

	"github.com/dogmatiq/harpy"
	"github.com/dogmatiq/harpy/transport/httptransport"
)

// Example shows how to implement a very basic JSON-RPC key/value server using
// Harpy's HTTP transport.
func main() {
	var server KeyValueServer

	// Start the HTTP server.
	http.ListenAndServe(
		":8080",
		httptransport.NewHandler(
			harpy.NewRouter(
				harpy.WithRoute("Get", server.Get),
				harpy.WithRoute("Set", harpy.NoResult(server.Set)),
			),
		),
	)
}

// KeyValueServer is a very basic key/value store with a JSON-RPC interface.
type KeyValueServer struct {
	m      sync.RWMutex
	values map[string]string
}

// GetParams contains parameters for the "Get" method.
type GetParams struct {
	Key string `json:"key"`
}

// SetParams contains parameters for the "Set" method.
type SetParams struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

// Get returns the value associated with a key.
//
// It returns an application-defined error if there is no value associated with
// this key.
func (s *KeyValueServer) Get(_ context.Context, params GetParams) (string, error) {
	s.m.RLock()
	defer s.m.RUnlock()

	if value, ok := s.values[params.Key]; ok {
		return value, nil
	}

	return "", harpy.NewError(
		100, // 100 is our example application's error code for "no such key"
		harpy.WithMessage("no such key"),
	)
}

// Set associates a value with a key.
func (s *KeyValueServer) Set(_ context.Context, params SetParams) error {
	s.m.Lock()
	defer s.m.Unlock()

	if s.values == nil {
		s.values = map[string]string{}
	}

	s.values[params.Key] = params.Value

	return nil
}
Output:

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Exchange

func Exchange(
	ctx context.Context,
	e Exchanger,
	r RequestSetReader,
	w ResponseWriter,
	l ExchangeLogger,
) (err error)

Exchange performs a JSON-RPC exchange, whether for a single request or a batch of requests.

e is the exchanger used to obtain a response to each request. If there are multiple requests each request is passed to the exchanger on its own goroutine.

r is used to obtain a the next RequestSet to process, and w is used to write responses to each request in that set. Calls to the methods on w are serialized and do not require further synchronization.

If ctx is canceled or exceeds its deadline, e is responsible for aborting execution and returning a suitable JSON-RPC response describing the cancelation.

If w produces an error, the context passed to e is canceled and Exchange() returns the ResponseWriter's error. Execution blocks until all goroutines are completed, but no more responses are written.

func NoResult added in v0.3.0

func NoResult[P any](
	h func(context.Context, P) error,
) func(context.Context, P) (any, error)

NoResult adapts a "typed" handler function that does not return a JSON-RPC result value so that it can be used with the WithRoute() function.

Example
package main

import (
	"context"
	"fmt"

	"github.com/dogmatiq/harpy"
)

func main() {
	// Define a handler that does not return a result value (just an error).
	handler := func(ctx context.Context, params []string) error {
		// perform some action
		return nil
	}

	router := harpy.NewRouter(
		// Create a route for the "PerformAction" that routes to the handler
		// function defined above.
		harpy.WithRoute("PerformAction", harpy.NoResult(handler)),
	)

	fmt.Println(router.HasRoute("PerformAction"))
}
Output:

true

Types

type BatchRequestMarshaler added in v0.2.0

type BatchRequestMarshaler struct {
	// Target is the target writer to which the JSON-RPC batch is marshaled.
	Target io.Writer
	// contains filtered or unexported fields
}

BatchRequestMarshaler marshals a batch of JSON-RPC requests to an io.Writer.

func (*BatchRequestMarshaler) Close added in v0.2.0

func (m *BatchRequestMarshaler) Close() error

Close finishes writing the batch to m.Writer.

If no requests have been marshaled, Close() is a no-op. This means that no data will have been written to m.Target at all.

func (*BatchRequestMarshaler) MarshalRequest added in v0.2.0

func (m *BatchRequestMarshaler) MarshalRequest(req Request) error

MarshalRequest marshals the next JSON-RPC request in the batch to m.Writer.

It panics if the marshaler is already closed.

type Error

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

Error is a Go error that describes a JSON-RPC error.

func InvalidParameters

func InvalidParameters(options ...ErrorOption) Error

InvalidParameters returns an error that indicates the provided parameters are malformed or invalid.

func MethodNotFound

func MethodNotFound(options ...ErrorOption) Error

MethodNotFound returns an error that indicates the requested method does not exist.

func NewClientSideError added in v0.2.0

func NewClientSideError(
	code ErrorCode,
	message string,
	data json.RawMessage,
) Error

NewClientSideError returns a new client-side error that represents a JSON-RPC error returned as part of an ErrorResponse.

func NewError

func NewError(code ErrorCode, options ...ErrorOption) Error

NewError returns a new JSON-RPC error with an application-defined error code.

The error codes from and including -32768 to -32000 are reserved for pre-defined errors by the JSON-RPC specification. Use of a code within this range causes a panic.

func NewErrorWithReservedCode

func NewErrorWithReservedCode(code ErrorCode, options ...ErrorOption) Error

NewErrorWithReservedCode returns a new JSON-RPC error that uses a reserved error code.

The error codes from and including -32768 to -32000 are reserved for pre-defined errors by the JSON-RPC specification. Use of a code outside this range causes a panic.

This function is provided to allow user-defined handlers to produce errors with reserved codes if necessary, but forces the developer to be explicit about doing so.

Consider using MethodNotFound(), InvalidParameters() or NewError() instead.

func (Error) Code

func (e Error) Code() ErrorCode

Code returns the JSON-RPC error code.

func (Error) Error

func (e Error) Error() string

Error returns the error message.

func (Error) MarshalData added in v0.2.0

func (e Error) MarshalData() (_ json.RawMessage, ok bool, _ error)

MarshalData returns the JSON representation user-defined data value associated with the error.

ok is false if there is no user-defined data associated with the error.

func (Error) Message

func (e Error) Message() string

Message returns the error message.

func (Error) UnmarshalData added in v0.2.0

func (e Error) UnmarshalData(v any, options ...UnmarshalOption) (ok bool, _ error)

UnmarshalData unmarshals the user-defined data into v.

ok is false if there is no user-defined data associated with the error.

func (Error) Unwrap

func (e Error) Unwrap() error

Unwrap returns the cause of e, if known.

type ErrorCode

type ErrorCode int

ErrorCode is a JSON-RPC error code.

As per the JSON-RPC specification, the error codes from and including -32768 to -32000 are reserved for pre-defined errors. These known set of predefined errors are defined as constants below.

const (
	// ParseErrorCode indicates that the server failed to parse a JSON-RPC
	// request.
	ParseErrorCode ErrorCode = -32700

	// InvalidRequestCode indicates that the server received a well-formed but
	// otherwise invalid JSON-RPC request.
	InvalidRequestCode ErrorCode = -32600

	// MethodNotFoundCode indicates that the server received a request for an
	// RPC method that does not exist.
	MethodNotFoundCode ErrorCode = -32601

	// InvalidParametersCode indicates that the server received a request that
	// contained malformed or invalid parameters.
	InvalidParametersCode ErrorCode = -32602

	// InternalErrorCode indicates that some other error condition was raised
	// within the RPC server.
	InternalErrorCode ErrorCode = -32603
)

func (ErrorCode) IsPredefined

func (c ErrorCode) IsPredefined() bool

IsPredefined returns true if c is an error code defined by the JSON-RPC specification.

func (ErrorCode) IsReserved

func (c ErrorCode) IsReserved() bool

IsReserved returns true if c falls within the range of error codes reserved for pre-defined errors.

func (ErrorCode) String

func (c ErrorCode) String() string

String returns a brief description of the error.

type ErrorInfo

type ErrorInfo struct {
	Code    ErrorCode       `json:"code"`
	Message string          `json:"message"`
	Data    json.RawMessage `json:"data,omitempty"`
}

ErrorInfo describes a JSON-RPC error. It is included in an ErrorResponse, but it is not a Go error.

func (ErrorInfo) String

func (e ErrorInfo) String() string

type ErrorOption

type ErrorOption func(*Error)

ErrorOption is an option that provides further information about an error.

func WithCause

func WithCause(c error) ErrorOption

WithCause is an ErrorOption that associates a causal error with a JSON-RPC error.

c is wrapped by the resulting JSON-RPC error, such as it can be used with errors.Is() and errors.As().

If the JSON-RPC error does not already have a user-defined message, c.Error() is used as the user-defined message.

func WithData

func WithData(data any) ErrorOption

WithData is an ErrorOption that associates additional data with an error.

The data is provided to the RPC caller via the "data" field of the error object in the JSON-RPC response.

func WithMessage

func WithMessage(format string, values ...any) ErrorOption

WithMessage is an ErrorOption that provides a user-defined error message for a JSON-RPC error.

This message should be used to provide additional information that can help diagnose the error.

type ErrorResponse

type ErrorResponse struct {
	// Version is the JSON-RPC version.
	//
	// As per the JSON-RPC specification it MUST be exactly "2.0".
	Version string `json:"jsonrpc"`

	// RequestID is the ID of the request that produced this response.
	RequestID json.RawMessage `json:"id"`

	// Error describes the error produced in response to the request.
	Error ErrorInfo `json:"error"`

	// ServerError provides more context to internal errors. The value is never
	// sent to the client.
	ServerError error `json:"-"`
}

ErrorResponse encapsulates a failed JSON-RPC response.

func NewErrorResponse

func NewErrorResponse(requestID json.RawMessage, err error) ErrorResponse

NewErrorResponse returns a new ErrorResponse for the given error.

func (ErrorResponse) UnmarshalRequestID added in v0.2.0

func (r ErrorResponse) UnmarshalRequestID(v any) error

UnmarshalRequestID unmarshals the request ID in the response into v.

func (ErrorResponse) Validate added in v0.2.0

func (r ErrorResponse) Validate() error

Validate checks that the response conforms to the JSON-RPC specification.

It returns nil if the response is valid.

type ExchangeLogger

type ExchangeLogger interface {
	// LogError logs about an error that is a result of some problem with the
	// request set as a whole.
	LogError(ctx context.Context, res ErrorResponse)

	// LogWriterError logs about an error that occured when attempting to use a
	// ResponseWriter.
	LogWriterError(ctx context.Context, err error)

	// LogNotification logs about a notification request.
	LogNotification(ctx context.Context, req Request, err error)

	// LogCall logs about a call request/response pair.
	LogCall(ctx context.Context, req Request, res Response)
}

ExchangeLogger is an interface for logging JSON-RPC requests, responses and errors.

func NewSLogExchangeLogger added in v0.9.0

func NewSLogExchangeLogger(t *slog.Logger) ExchangeLogger

NewSLogExchangeLogger returns an ExchangeLogger that targets the given slog.Logger.

func NewZapExchangeLogger added in v0.9.0

func NewZapExchangeLogger(t *zap.Logger) ExchangeLogger

NewZapExchangeLogger returns an ExchangeLogger that targets the given zap.Logger.

type Exchanger

type Exchanger interface {
	// Call handles call request and returns its response.
	Call(context.Context, Request) Response

	// Notify handles a notification request, which does not expect a response.
	//
	// It may return an error to be logged, but it is not sent to the caller.
	Notify(context.Context, Request) error
}

An Exchanger performs a JSON-RPC exchange, wherein a request is "exchanged" for its response.

The Exchanger is responsible for resolving any error conditions. In the case of a JSON-RPC call it must also provide the response. It therefore has no facility to return an error.

type Request

type Request struct {
	// Version is the JSON-RPC version.
	//
	// As per the JSON-RPC specification it MUST be exactly "2.0".
	Version string `json:"jsonrpc"`

	// ID uniquely identifies requests that expect a response, that is RPC calls
	// as opposed to notifications.
	//
	// As per the JSON-RPC specification, it MUST be a JSON string, number, or
	// null value. It SHOULD NOT normally not be null. Numbers SHOULD NOT
	// contain fractional parts.
	//
	// If the ID field itself is nil, the request is a notification.
	ID json.RawMessage `json:"id,omitempty"`

	// Method is the name of the RPC method to be invoked.
	//
	// As per the JSON-RPC specification, method names that begin with "rpc."
	// are reserved for system extensions, and MUST NOT be used for anything
	// else. Each system extension is defined in a separate specification. All
	// system extensions are OPTIONAL.
	//
	// Any requests for extension methods that are not handled internally by
	// this package are treated just like any other request, thus allowing
	// extension methods to be implemented by user-defined handlers.
	//
	// This package does not currently handle any extension methods internally.
	//
	// In accordance with the JSON-RPC specification there are no requirements
	// placed on the format of the method name. This allows server
	// implementations that provide methods with an empty name, non-ASCII names,
	// or any other value that can be represented as a JSON string.
	Method string `json:"method"`

	// Parameters holds the parameter values to be used during the invocation of
	// the method.
	//
	// As per the JSON-RPC specification it MUST be a structured value, that is
	// either a JSON array or object.
	//
	// Validation of the parameters is the responsibility of the user-defined
	// handlers.
	Parameters json.RawMessage `json:"params,omitempty"`
}

Request encapsulates a JSON-RPC request.

func NewCallRequest added in v0.2.0

func NewCallRequest(
	id any,
	method string,
	params any,
) (Request, error)

NewCallRequest returns a new JSON-RPC call request.

The returned request is not necessarily valid; it should be validated by calling Request.ValidateClientSide() before sending to a server.

func NewNotifyRequest added in v0.2.0

func NewNotifyRequest(
	method string,
	params any,
) (Request, error)

NewNotifyRequest returns a new JSON-RPC notify request.

The returned request is not necessarily valid; it should be validated by calling Request.ValidateClientSide() before sending to a server.

func (Request) IsNotification

func (r Request) IsNotification() bool

IsNotification returns true if r is a notification, as opposed to an RPC call that expects a response.

func (Request) UnmarshalParameters

func (r Request) UnmarshalParameters(v any, options ...UnmarshalOption) error

UnmarshalParameters is a convenience method for unmarshaling request parameters into a Go value.

It returns the appropriate native JSON-RPC error if r.Parameters can not be unmarshaled into v.

If v implements the Validatable interface, it calls v.Validate() after unmarshaling successfully. If validation fails it wraps the validation error in the appropriate native JSON-RPC error.

func (Request) ValidateClientSide added in v0.2.0

func (r Request) ValidateClientSide() (err Error, ok bool)

ValidateClientSide checks that the request conforms to the JSON-RPC specification.

It is intended to be called before sending the request to a server; if the request is invalid ok is false and err is the error that a server would return upon receiving the invalid request.

func (Request) ValidateServerSide added in v0.2.0

func (r Request) ValidateServerSide() (err Error, ok bool)

ValidateServerSide checks that the request conforms to the JSON-RPC specification.

If the request is invalid ok is false and err is a JSON-RPC error intended to be sent to the caller in an ErrorResponse.

type RequestSet

type RequestSet struct {
	// Requests contains the requests parsed from the message.
	Requests []Request

	// IsBatch is true if the requests are part of a batch.
	//
	// This is used to disambiguate between a single request and a batch that
	// contains only one request.
	IsBatch bool
}

RequestSet encapsulates one or more JSON-RPC requests that were parsed from a single JSON message.

func UnmarshalRequestSet added in v0.2.0

func UnmarshalRequestSet(r io.Reader) (RequestSet, error)

UnmarshalRequestSet unmarshals a JSON-RPC request or request batch from r.

If there is a problem parsing the request or the request is malformed, an Error is returned. Any other non-nil error should be considered an IO error.

On success it returns a request set containing well-formed (but not necessarily valid) requests.

func (RequestSet) ValidateClientSide added in v0.2.0

func (rs RequestSet) ValidateClientSide() (err Error, ok bool)

ValidateClientSide checks that the request set is valid and that the requests within conform to the JSON-RPC specification.

It is intended to be called before sending the request set to a server; if the request is invalid ok is false and err is the error that a server would return upon receiving the invalid request set.

func (RequestSet) ValidateServerSide added in v0.2.0

func (rs RequestSet) ValidateServerSide() (err Error, ok bool)

ValidateServerSide checks that the request set is valid and that the requests within conform to the JSON-RPC specification.

If the request set is invalid ok is false and err is a JSON-RPC error intended to be sent to the caller in an ErrorResponse.

type RequestSetReader

type RequestSetReader interface {
	// Read reads the next RequestSet that is to be processed.
	//
	// It returns ctx.Err() if ctx is canceled while waiting to read the next
	// request set. If request set data is read but cannot be parsed a native
	// JSON-RPC Error is returned. Any other error indicates an IO error.
	Read(ctx context.Context) (RequestSet, error)
}

RequestSetReader reads requests sets in order to perform an exchange.

Implementations are typically provided by the transport layer.

type Response

type Response interface {
	// Validate checks that the response conforms to the JSON-RPC specification.
	//
	// It returns nil if the response is valid.
	Validate() error

	// UnmarshalRequestID unmarshals the request ID in the response into v.
	UnmarshalRequestID(v any) error
	// contains filtered or unexported methods
}

Response is an interface for a JSON-RPC response object.

func NewSuccessResponse

func NewSuccessResponse(requestID json.RawMessage, result any) Response

NewSuccessResponse returns a new SuccessResponse containing the given result.

If the result can not be marshaled an ErrorResponse is returned instead.

type ResponseSet added in v0.2.0

type ResponseSet struct {
	// Responses contains the responses parsed from the message.
	Responses []Response

	// IsBatch is true if the responses are part of a batch.
	//
	// This is used to disambiguate between a single response and a batch that
	// contains only one response.
	IsBatch bool
}

ResponseSet encapsulates one or more JSON-RPC responses that were parsed from a single JSON message.

func UnmarshalResponseSet added in v0.2.0

func UnmarshalResponseSet(r io.Reader) (ResponseSet, error)

UnmarshalResponseSet parses a set of JSON-RPC response set.

func (ResponseSet) Validate added in v0.2.0

func (rs ResponseSet) Validate() error

Validate checks that the response set is valid and that the responses within conform to the JSON-RPC specification.

It returns nil if the response set is valid.

type ResponseWriter

type ResponseWriter interface {
	// WriteError writes an error response that is a result of some problem with
	// the request set as a whole.
	WriteError(ErrorResponse) error

	// WriteUnbatched writes a response to an individual request that was not
	// part of a batch.
	WriteUnbatched(Response) error

	// WriteBatched writes a response to an individual request that was part of
	// a batch.
	WriteBatched(Response) error

	// Close is called to signal that there are no more responses to be sent.
	Close() error
}

A ResponseWriter writes responses to requests.

Implementations are typically provided by the transport layer.

type Router

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

Router is a Exchanger that dispatches to different handlers based on the JSON-RPC method name.

func NewRouter added in v0.3.0

func NewRouter(options ...RouterOption) *Router

NewRouter returns a new router containing the given routes.

Example
package main

import (
	"context"
	"fmt"

	"github.com/dogmatiq/harpy"
)

func main() {
	// Define a handler that returns the length of "positional" parameters.
	handler := func(ctx context.Context, params []string) (int, error) {
		return len(params), nil
	}

	// Create a router that routes requests for the "Len" method to the handler
	// function defined above.
	router := harpy.NewRouter(
		harpy.WithRoute("Len", handler),
	)

	fmt.Println(router.HasRoute("Len"))
}
Output:

true

func (*Router) Call

func (r *Router) Call(ctx context.Context, req Request) Response

Call handles a call request and returns the response.

It invokes the handler associated with the method specified by the request. If no such method has been registered it returns a JSON-RPC "method not found" error response.

func (*Router) HasRoute added in v0.3.0

func (r *Router) HasRoute(method string) bool

HasRoute returns true if the router has a route for the given method.

func (*Router) Notify

func (r *Router) Notify(ctx context.Context, req Request) error

Notify handles a notification request.

It invokes the handler associated with the method specified by the request. If no such method has been registered it does nothing.

type RouterOption added in v0.3.0

type RouterOption func(*Router)

RouterOption represents a single route within a router.

func WithRoute added in v0.3.0

func WithRoute[P, R any](
	m string,
	h func(context.Context, P) (R, error),
	options ...UnmarshalOption,
) RouterOption

WithRoute it a router option that adds a route from the method m to the "typed" handler function h.

P is the type into which the JSON-RPC request parameters are unmarshaled. R is the type of the result included in a successful JSON-RPC response.

func WithUntypedRoute added in v0.3.0

func WithUntypedRoute(
	m string,
	h func(context.Context, Request) (result any, _ error),
) RouterOption

WithUntypedRoute is a RouterOption that adds a route from the method m to the "untyped" handler function h.

type SuccessResponse

type SuccessResponse struct {
	// Version is the JSON-RPC version.
	//
	// As per the JSON-RPC specification it MUST be exactly "2.0".
	Version string `json:"jsonrpc"`

	// RequestID is the ID of the request that produced this response.
	RequestID json.RawMessage `json:"id"`

	// Result is the user-defined result value produce in response to the
	// request.
	Result json.RawMessage `json:"result"`
}

SuccessResponse encapsulates a successful JSON-RPC response.

func (SuccessResponse) UnmarshalRequestID added in v0.2.0

func (r SuccessResponse) UnmarshalRequestID(v any) error

UnmarshalRequestID unmarshals the request ID in the response into v.

func (SuccessResponse) Validate added in v0.2.0

func (r SuccessResponse) Validate() error

Validate checks that the response conforms to the JSON-RPC specification.

It returns nil if the response is valid.

type UnmarshalOption added in v0.8.1

type UnmarshalOption = jsonx.UnmarshalOption

UnmarshalOption is an option that changes the behavior of JSON unmarshaling.

func AllowUnknownFields added in v0.8.1

func AllowUnknownFields(allow bool) UnmarshalOption

AllowUnknownFields is an UnmarshalOption that controls whether parameters, results and error data may contain unknown fields.

Unknown fields are disallowed by default.

type UntypedHandler added in v0.3.0

type UntypedHandler func(ctx context.Context, req Request) (res any, err error)

A UntypedHandler is a function that produces a result value (or error) in response to a JSON-RPC request for a specific method.

It is "untyped" because it is passed a complete JSON-RPC request object, as opposed to a specific type of parameter value.

res is the result value to include in the JSON-RPC response; it is not the JSON-RPC response itself. If err is non-nil, a JSON-RPC error response is sent instead and res is ignored.

If req is a notification (that is, it does not have a request ID) res is always ignored.

type Validatable

type Validatable interface {
	// Validate returns a non-nil error if the value is invalid.
	//
	// The returned error, if non-nil, is always wrapped in a JSON-RPC "invalid
	// parameters" error, and therefore should not itself be a JSON-RPC error.
	Validate() error
}

Validatable is an interface for parameter values that provide their own validation.

Directories

Path Synopsis
internal
jsonx
Package jsonx contains utilities for dealing with JSON and the encoding/json package.
Package jsonx contains utilities for dealing with JSON and the encoding/json package.
middleware
otelharpy
Package otelharpy provides middleware that instruments JSON-RPC servers with OpenTelemetry tracing and metrics.
Package otelharpy provides middleware that instruments JSON-RPC servers with OpenTelemetry tracing and metrics.
transport
httptransport
Package httptransport provides a simple HTTP-based JSON-RPC transport.
Package httptransport provides a simple HTTP-based JSON-RPC transport.

Jump to

Keyboard shortcuts

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