Skip to content

net.jsonrpc #

JSONRPC

JSON-RPC 2.0 client+server implementation in pure V.

Limitaions

  • Request/Response use only string id
  • JSON-RPC 1.0 incompatible

Features

  • Request/Response single/batch json encoding/decoding
  • Server to work with any io.ReaderWriter
  • Server automatically manages batch Requests and builds batch Response
  • Client to work with any io.ReaderWriter
  • Interceptors for custom events, raw Request, Request, Response, raw Response

Usage

Request/Response operations

For both Request/Response constructors are provided and must be used for initialization.

import net.jsonrpc

// jsonrpc.new_request(method, params, id)
mut req := jsonrpc.new_request('kv.create', {
    'key':   'key'
    'value': 'value'
}, 'kv.create.1')

println(req.encode())
// '{"jsonrpc":"2.0","method":"kv.create","params":{"key":"key","value":"value"},"id":"kv.create.1"}'

// jsonrpc.new_response(result, error, id)
mut resp := jsonrpc.new_response({
    'key':   'key'
    'value': 'value'
}, jsonrpc.ResponseError{}, 'kv.create.1')

println(resp.encode())
// '{"jsonrpc":"2.0","result":{"key":"key","value":"value"},"id":"kv.create.1"}'

To create a Notification, pass empty string as Request.id (jsonrpc.Empty{}.str() or jsonrpc.empty.str() can be used) (e.g. jsonrpc.new_reponse('method', 'params', jsonrpc.empty.str())). To omit Response.params in encoded json string pass jsonrpc.Empty{} or jsonrpc.empty as value in constructor (e.g. jsonrpc.new_reponse('method', jsonrpc.empty, 'id')). For Response only result or error fields can exist at the same time and not both simultaniously. If error passed to jsonrpc.new_response() the result value will be ignored on Response.encode(). The error field is not generated if jsonrpc.ResponseError{} provided as error into jsonrpc.new_response() (e.g. jsonrpc.new_response("result", jsonrpc.ResponseError{}, "id")). If the empty string passed as Result.id it will use jsonrpc.null as id (translates to json null)

Client

For full usage check client in example

import net
import net.jsonrpc

addr := '127.0.0.1:42228'
mut stream := net.dial_tcp(addr)!
mut c := jsonrpc.new_client(jsonrpc.ClientConfig{
    stream: stream
})

c.notify('kv.create', {
    'key':   'bazz'
    'value': 'barr'
})!

Client can work with any io.ReaderWriter provided into stream field value.

Server

For ready key/value im-memory storage realized with server check this example

import net
import net.jsonrpc

fn handle_test(req &jsonrpc.Request, mut wr jsonrpc.ResponseWriter) {
    p := req.decode_params[string]() or {
        wr.write_error(jsonrpc.invalid_params)
        return
    }

    wr.write(p)
}

fn handle_conn(mut conn net.TcpConn) {
    defer { conn.close() or {} }

    mut srv := jsonrpc.new_server(jsonrpc.ServerConfig{
        stream:  conn
        handler: handle_test
    })

    srv.start()
}

addr := '127.0.0.1:42228'
mut l := net.listen_tcp(.ip, addr)!
println('TCP JSON-RPC server on ${addr} (Content-Length framing)')

for {
    mut c := l.accept()!
    println('Accepted')
    go handle_conn(mut c)
}

Server can work with any io.ReaderWriter provided into stream field value. Server requires jsonrpc.Handler = fn(req &jsonrpc.Request, mut wr jsonrpc.ResponseWriter) to pass decoded jsonrpc.Request and to write jsonrpc.Response into jsonrpc.ResponseWriter. On Notification Server does call jsonrpc.Handler but it ingores written jsonrpc.Response.

Handler

jsonrpc.Handler = fn(req &jsonrpc.Request, mut wr jsonrpc.ResponseWriter) is the function that operates the decoded jsonrpc.Request and writes jsonrpc.Response into jsonrpc.ResponseWriter. Before every return wr.write() or wr.write_error() must be called so the server do not stuck waiting for jsonrpc.Response to be written. Also only wr.write() or wr.write_error() must be called before return and not both.

Router

The simple jsonrpc.Router is provided to register jsonrpc.Handler to handle specific method. The jsonrpc.Router.handle_jsonrpc must be passed into jsonrpc.Server.handler to handle requests. If jsonrpc.Request.method has no registered jsonrpc.Handler, the router will respond with jsonrpc.method_not_found error

Interceptors

Both jsonrpc.Client and jsonrpc.Server support jsonrpc.Interceptors - the collection of on event handlers. There is implementation of all supported interceptors called jsonrpc.LoggingInterceptor.

Constants #

const version = '2.0'
const parse_error = error_with_code('Invalid JSON.', -32700)

JSON-RPC standard-ish errors :contentReference[oaicite:3]{index=3}

const invalid_request = error_with_code('Invalid request.', -32600)
const method_not_found = error_with_code('Method not found.', -32601)
const invalid_params = error_with_code('Invalid params', -32602)
const internal_error = error_with_code('Internal error.', -32693)
const server_error_start = error_with_code('Error occurred when starting server.',
	-32099)
const server_not_initialized = error_with_code('Server not initialized.', -32002)
const unknown_error = error_with_code('Unknown error.', -32001)
const server_error_end = error_with_code('Error occurred when stopping the server.',
	-32000)
const error_codes = [
	parse_error.code(),
	invalid_request.code(),
	method_not_found.code(),
	invalid_params.code(),
	internal_error.code(),
	server_error_start.code(),
	server_not_initialized.code(),
	server_error_end.code(),
	unknown_error.code(),
]
const null = Null{}
const empty = Empty{}

fn decode_batch_request #

fn decode_batch_request(raw string) ![]Request

decode_batch_request decodes raw batch request into []jsonrpc.Request by reading after \r\n\r\n.

fn decode_batch_response #

fn decode_batch_response(raw string) ![]Response

decode_batch_response decodes raw batch request into []jsonrpc.Request by reading after \r\n\r\n.

fn decode_request #

fn decode_request(raw string) !Request

decode_request decodes raw request into JSONRPC Request by reading after \r\n\r\n.

fn decode_response #

fn decode_response(raw string) !Response

decode_response decodes raw response into JSONRPC Response by reading after \r\n\r\n.

fn dispatch_event #

fn dispatch_event(ints []EventInterceptor, event_name string, data string)

dispatch_event sends event_name and data to provided jsonrpc.EventInterceptors

fn error_with_code #

fn error_with_code(message string, code int) ResponseError

error_with_code returns jsonrpc.ResponseError with empty data field

fn intercept_encoded_request #

fn intercept_encoded_request(ints []EncodedRequestInterceptor, req []u8) !

intercept_encoded_request sends raw request data before attempting to decode it into jsonrpc.Request to provided jsonrpc.EncodedRequestInterceptors

fn intercept_encoded_response #

fn intercept_encoded_response(ints []EncodedResponseInterceptor, resp []u8)

intercept_encoded_response sends raw encoded data representing jsonrpc.Response to provided jsonrpc.EncodedResponseInterceptors

fn intercept_request #

fn intercept_request(ints []RequestInterceptor, req &Request) !

intercept_request sends decoded jsonrpc.Request to provided jsonrpc.RequestInterceptors

fn intercept_response #

fn intercept_response(ints []ResponseInterceptor, resp &Response)

intercept_response sends decoded jsonrpc.Response to provided jsonrpc.ResponseInterceptors

fn new_client #

fn new_client(cfg ClientConfig) Client

new_client creates new jsonrpc.Client with stream to read/write and interceptors

fn new_request #

fn new_request[T](method string, params T, id string) Request

new_request is the constructor for Request. ALWAYS use this to initialize new Request. if id is empty string ('') then the Request will be notification (no id field on encode). Pass jsonrpc.Empty{} as params to not generate params field on encode. jsonrpc.null can be used as params to set params field to json null on encode. Limitations: id is always string.

fn new_response #

fn new_response[T](result T, error ResponseError, id string) Response

new_response is the constructor for Response. ALWAYS use this to initialize new Response. if id is empty string ('') then the Result id will be a json null. Pass jsonrpc.ResponseError{} as error to not generate error field on encode. jsonrpc.null can be used as result to set field to json null on encode. Limitations: id is always string.

fn new_server #

fn new_server(cfg ServerConfig) Server

new_server creates new jsonrpc.Server with stream to read/write, the jsonrpc.Handler to handle Requests/Responses and interceptors

fn response_error #

fn response_error(params ResponseErrorGeneratorParams) ResponseError

response_error returns jsonrpc.ResponseError created from passed error and data

type EncodedRequestInterceptor #

type EncodedRequestInterceptor = fn (req []u8) !

EncodedRequestInterceptor called on jsonrpc.intercept_encoded_request

type EncodedResponseInterceptor #

type EncodedResponseInterceptor = fn (resp []u8)

EncodedResponseInterceptor called on jsonrpc.intercept_encoded_response

type EventInterceptor #

type EventInterceptor = fn (name string, data string)

EventInterceptor called on jsonrpc.dispatch_event

type Handler #

type Handler = fn (req &Request, mut wr ResponseWriter)

Handler is the function called when jsonrpc.Request is decoded and jsonrpc.Response is required which is written into jsonrpc.ResponseWriter. Before returning from Handler either wr.write() or wr.write_error() must be called or the stream will stuck awaiting writing jsonrpc.Response

type RequestInterceptor #

type RequestInterceptor = fn (req &Request) !

RequestInterceptor called on jsonrpc.intercept_request

type ResponseInterceptor #

type ResponseInterceptor = fn (resp &Response)

ResponseInterceptor called on jsonrpc.intercept_response

fn ([]Request) encode_batch #

fn (reqs []Request) encode_batch() string

encode_batch loops through every jsonrpc.Request in array, calls encode() for each of them and writes into json list [] splitting with ','

fn ([]Response) encode_batch #

fn (resps []Response) encode_batch() string

encode_batch loops through every jsonrpc.Response in array, calls encode() for each of them and writes into json list [] splitting with ','

struct Client #

struct Client {
mut:
	stream       io.ReaderWriter
	interceptors Interceptors
}

fn (Client) notify #

fn (mut c Client) notify[T](method string, params T) !

notify sends JSON-RPC 2.0 Notification and returns without waiting for jsonrpc.Response

fn (Client) request #

fn (mut c Client) request[T](method string, params T, id string) !Response

request new jsonrpc.Request and return jsonrpc.Response

fn (Client) batch #

fn (mut c Client) batch(reqs []Request) ![]Response

batch sends batch of jsonrpc.Request and returns batch ofjsonrpc.Response

struct ClientConfig #

struct ClientConfig {
pub mut:
	stream       io.ReaderWriter
	interceptors Interceptors
}

struct Empty #

struct Empty {}

fn (Empty) str #

fn (e Empty) str() string

str returns empty string: '' (can be passed to jsonrpc.new_request as id or params to omit fields on encoding)

struct Interceptors #

struct Interceptors {
pub mut:
	event            []EventInterceptor
	encoded_request  []EncodedRequestInterceptor
	request          []RequestInterceptor
	response         []ResponseInterceptor
	encoded_response []EncodedResponseInterceptor
}

Interceptors collection of all supported interceptors to be called on events

fn (Interceptors) get_interceptor #

fn (i Interceptors) get_interceptor[T]() ?&T

get_interceptor tries to find and return interceptor of provided type from jsonrpc.Interceptors

struct LoggingInterceptor #

@[heap]
struct LoggingInterceptor {
pub mut:
	log log.Log
}

LoggingInterceptor is simple logging full-fledged Interceptor messages will be written in log.get_level() Level

fn (LoggingInterceptor) on_event #

fn (mut l LoggingInterceptor) on_event(name string, data string)

on_event logs event name and data into provided log

fn (LoggingInterceptor) on_encoded_request #

fn (mut l LoggingInterceptor) on_encoded_request(req []u8) !

on_encoded_request logs json encoded jsonrpc.Request as string

fn (LoggingInterceptor) on_request #

fn (mut l LoggingInterceptor) on_request(req &Request) !

on_request logs jsonrpc.Request method, params and id

fn (LoggingInterceptor) on_response #

fn (mut l LoggingInterceptor) on_response(resp &Response)

on_response logs jsonrpc.Response result, error and id

fn (LoggingInterceptor) on_encoded_response #

fn (mut l LoggingInterceptor) on_encoded_response(resp []u8)

on_encoded_response logs json encoded jsonrpc.Response as string

struct Null #

struct Null {}

Null represents the null value in JSON.

fn (Null) str #

fn (n Null) str() string

str returns string representation of json null: 'null' (can be used for jsonrpc.Request id and params as well as jsonrpc.Response result and id)

struct Request #

struct Request {
pub:
	jsonrpc string = version
	method  string
	params  string @[omitempty; raw] // raw JSON object/array/null
	id      string @[omitempty]      // raw JSON (e.g. 1 or "abc") if empty => notification (no id field)
}

Request uses raw JSON strings for id and params in the old VLS code. :contentReference[oaicite:4]{index=4}

fn (Request) encode #

fn (req Request) encode() string

(req Request) encode() returns json string representing Request. In returning string params field can be omited if in new_request was passed jsonrpc.Empty{} as params. In returning string id field can be omited if in new_request was passed empty string ('') as id.

fn (Request) decode_params #

fn (req Request) decode_params[T]() !T

decode_params tries to decode Request.params into provided type

struct Response #

struct Response {
pub:
	jsonrpc string = version
	result  string @[raw]
	error   ResponseError
	id      string
}

fn (Response) encode #

fn (resp Response) encode() string

encode() returns json string representing Response. In returning string result field only generates if in new_response was passed jsonrpc.ResponseError{} as error value. In returning string id field will be json null if in new_request was passed empty string ('') as id.

fn (Response) decode_result #

fn (resp Response) decode_result[T]() !T

decode_params tries to decode Response.result into provided type

struct ResponseError #

struct ResponseError {
pub mut:
	code    int
	message string
	data    string
}

---- error helpers ----

fn (ResponseError) code #

fn (err ResponseError) code() int

code returns jsonrpc.ResponseError.code field value

fn (ResponseError) msg #

fn (err ResponseError) msg() string

msg returns jsonrpc.ResponseError.message field value

fn (ResponseError) err #

fn (e ResponseError) err() IError

err returns jsonrpc.ResponseError casted to IError

struct ResponseErrorGeneratorParams #

@[params]
struct ResponseErrorGeneratorParams {
	error IError @[required]
	data  string
}

ResponseErrorGeneratorParams & response_error are used by server.v :contentReference[oaicite:2]{index=2}

struct ResponseWriter #

struct ResponseWriter {
mut:
	sb       strings.Builder
	is_batch bool
	server   &Server
pub mut:
	req_id string
	writer io.ReaderWriter
}

fn (ResponseWriter) write #

fn (mut rw ResponseWriter) write[T](payload T)

write payload into jsonrpc.Response.result. call when need to send data in response

fn (ResponseWriter) write_empty #

fn (mut rw ResponseWriter) write_empty()

write_empty writes jsonrpc.null as response

fn (ResponseWriter) write_error #

fn (mut rw ResponseWriter) write_error(err IError)

write_error into the jsonrpc.Response of current request

struct Router #

struct Router {
mut:
	methods map[string]Handler
}

Router is simple map of method names and their Handlers

fn (Router) handle_jsonrpc #

fn (r Router) handle_jsonrpc(req &Request, mut wr ResponseWriter)

handle_jsonrpc must be passed into Server handler field to operate it simply tries to invoke registered methods and if none valid found writes jsonrpc.method_not_found error into jsonrpc.ResponseWriter

fn (Router) register #

fn (mut r Router) register(method string, handler Handler) bool

register handler to operate when method found in incoming jsonrpc.Request

struct Server #

@[heap]
struct Server {
mut:
	stream       io.ReaderWriter
	handler      Handler @[required]
	interceptors Interceptors
}

Server represents a JSONRPC server that sends/receives data from a stream (an io.ReaderWriter) and uses Content-Length framing. :contentReference[oaicite:6]{index=6}

fn (Server) is_interceptor_enabled #

fn (s &Server) is_interceptor_enabled[T]() bool

is_interceptor_enabled checks if interceptor of provided type is enabled on jsonrpc.Server

fn (Server) respond #

fn (mut s Server) respond() !

respond reads bytes from stream, pass them to the interceptors.encoded_request, tries to decode into jsonrpc.Request and pass to interceptors.request and on fail it responds with jsonrpc.parse_error after that it calls handlers (batch requests are handled automatically as well as writing batch response) and passes recieved jsonrpc.Response into interceptors.response and the last step is to encode jsonrpc.Response, pass it into interceptors.encoded_response and write to stream

fn (Server) start #

fn (mut s Server) start()

start Server loop to operate on stream passed into constructor it calls Server.respond() method in loop

struct ServerConfig #

struct ServerConfig {
pub mut:
	stream       io.ReaderWriter
	handler      Handler @[required]
	interceptors Interceptors
}