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 ¶
- func Exchange(ctx context.Context, e Exchanger, r RequestSetReader, w ResponseWriter, ...) (err error)
- func NoResult[P any](h func(context.Context, P) error) func(context.Context, P) (any, error)
- type BatchRequestMarshaler
- type Error
- func InvalidParameters(options ...ErrorOption) Error
- func MethodNotFound(options ...ErrorOption) Error
- func NewClientSideError(code ErrorCode, message string, data json.RawMessage) Error
- func NewError(code ErrorCode, options ...ErrorOption) Error
- func NewErrorWithReservedCode(code ErrorCode, options ...ErrorOption) Error
- type ErrorCode
- type ErrorInfo
- type ErrorOption
- type ErrorResponse
- type ExchangeLogger
- type Exchanger
- type Request
- type RequestSet
- type RequestSetReader
- type Response
- type ResponseSet
- type ResponseWriter
- type Router
- type RouterOption
- type SuccessResponse
- type UnmarshalOption
- type UntypedHandler
- type Validatable
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
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) 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) 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.
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 ¶
IsPredefined returns true if c is an error code defined by the JSON-RPC specification.
func (ErrorCode) IsReserved ¶
IsReserved returns true if c falls within the range of error codes reserved for pre-defined errors.
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.
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
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
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 ¶
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
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
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 ¶
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.
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
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.
Source Files ¶
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. |