diff --git a/examples/jsonrpc/client.v b/examples/jsonrpc/client.v new file mode 100644 index 00000000000000..9f195bd6f891a9 --- /dev/null +++ b/examples/jsonrpc/client.v @@ -0,0 +1,46 @@ +module main + +import net +import net.jsonrpc + +fn main() { + addr := '127.0.0.1:42228' + mut stream := net.dial_tcp(addr)! + mut log_inter := jsonrpc.LoggingInterceptor{} + mut inters := jsonrpc.Interceptors{ + event: [log_inter.on_event] + encoded_request: [log_inter.on_encoded_request] + request: [log_inter.on_request] + response: [log_inter.on_response] + encoded_response: [log_inter.on_encoded_response] + } + + mut c := jsonrpc.new_client(jsonrpc.ClientConfig{ + stream: stream + interceptors: inters + }) + + println('TCP JSON-RPC client on ${addr}') + + d1 := c.request('kv.delete', { + 'key': 'foo' + }, 'kv.delete')! + println('RESULT: ${d1}') + + res := c.batch([ + jsonrpc.new_request('kv.create', { + 'key': 'foo' + 'value': 'bar' + }, 'kv.create'), + jsonrpc.new_request('kv.create', { + 'key': 'bar' + 'value': 'foo' + }, 'kv.create'), + ])! + println('RESULT: ${res}') + + c.notify('kv.create', { + 'key': 'bazz' + 'value': 'barr' + })! +} diff --git a/examples/jsonrpc/server.v b/examples/jsonrpc/server.v new file mode 100644 index 00000000000000..d544daa4b76074 --- /dev/null +++ b/examples/jsonrpc/server.v @@ -0,0 +1,213 @@ +module main + +import net +import sync +import net.jsonrpc +import log + +// ---- CRUD domain ---- +struct KvItem { + key string + value string +} + +struct KvKey { + key string +} + +// ---- Handler ---- +struct KvStore { +mut: + mu &sync.RwMutex = sync.new_rwmutex() + store map[string]string +} + +fn (mut s KvStore) create(key string, value string) bool { + s.mu.@lock() + defer { s.mu.unlock() } + if key in s.store { + return false + } + s.store[key] = value + return true +} + +fn (mut s KvStore) get(key string) ?string { + s.mu.@rlock() + defer { s.mu.runlock() } + if value := s.store[key] { + return value + } + return none +} + +fn (mut s KvStore) update(key string, value string) bool { + s.mu.@lock() + defer { s.mu.unlock() } + if key in s.store { + s.store[key] = value + return true + } + return false +} + +fn (mut s KvStore) delete(key string) bool { + s.mu.@lock() + defer { s.mu.unlock() } + if key in s.store { + s.store.delete(key) + return true + } + return false +} + +fn (s KvStore) dump() map[string]string { + return s.store +} + +@[heap] +struct KvHandler { +mut: + store KvStore +} + +fn (mut h KvHandler) handle_create(req &jsonrpc.Request, mut wr jsonrpc.ResponseWriter) { + p := req.decode_params[KvItem]() or { + wr.write_error(jsonrpc.invalid_params) + return + } + if p.key.len == 0 { + wr.write_error(jsonrpc.invalid_params) + return + } + log.warn('params=${p}') + if !h.store.create(p.key, p.value) { + wr.write_error(jsonrpc.ResponseError{ // custom app-level error code + code: -32010 + message: 'Key already exists' + data: p.key + }) + return + } + + wr.write({ + 'ok': true + }) +} + +fn (mut h KvHandler) handle_get(req &jsonrpc.Request, mut wr jsonrpc.ResponseWriter) { + p := req.decode_params[KvKey]() or { + wr.write_error(jsonrpc.invalid_params) + return + } + + value := h.store.get(p.key) or { + wr.write_error(jsonrpc.ResponseError{ + code: -32004 + message: 'Not found' + data: p.key + }) + return + } + + wr.write(KvItem{ key: p.key, value: value }) +} + +fn (mut h KvHandler) handle_update(req &jsonrpc.Request, mut wr jsonrpc.ResponseWriter) { + p := req.decode_params[KvItem]() or { + wr.write_error(jsonrpc.invalid_params) + return + } + + if !h.store.update(p.key, p.value) { + wr.write_error(jsonrpc.ResponseError{ + code: -32004 + message: 'Not found' + data: p.key + }) + return + } + + wr.write({ + 'ok': true + }) +} + +fn (mut h KvHandler) handle_delete(req &jsonrpc.Request, mut wr jsonrpc.ResponseWriter) { + p := req.decode_params[KvKey]() or { + wr.write_error(jsonrpc.invalid_params) + return + } + + if !h.store.delete(p.key) { + wr.write_error(jsonrpc.ResponseError{ + code: -32004 + message: 'Not found' + data: p.key + }) + return + } + + wr.write({ + 'ok': true + }) +} + +fn (mut h KvHandler) handle_list(req &jsonrpc.Request, mut wr jsonrpc.ResponseWriter) { + mut items := []KvItem{} + for k, v in h.store.dump() { + items << KvItem{ + key: k + value: v + } + } + items.sort(a.key < b.key) + wr.write(items) +} + +// ---- Per-connection server loop ---- +// The jsonrpc.Server.start() reads from stream and writes to same stream. +fn handle_conn(mut conn net.TcpConn, h jsonrpc.Handler) { + defer { conn.close() or {} } + + mut log_inter := jsonrpc.LoggingInterceptor{} + mut inters := jsonrpc.Interceptors{ + event: [log_inter.on_event] + encoded_request: [log_inter.on_encoded_request] + request: [log_inter.on_request] + response: [log_inter.on_response] + encoded_response: [log_inter.on_encoded_response] + } + + mut srv := jsonrpc.new_server(jsonrpc.ServerConfig{ + stream: conn + handler: h + interceptors: inters + }) + + jsonrpc.dispatch_event(inters.event, 'start', 'server started') + srv.start() +} + +fn main() { + mut s := KvStore{} + mut h := KvHandler{ + store: s + } + mut r := jsonrpc.Router{} + r.register('kv.create', h.handle_create) + r.register('kv.get', h.handle_get) + r.register('kv.update', h.handle_update) + r.register('kv.delete', h.handle_delete) + r.register('kv.list', h.handle_list) + + 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, r.handle_jsonrpc) + } +} diff --git a/vlib/net/jsonrpc/README.md b/vlib/net/jsonrpc/README.md new file mode 100644 index 00000000000000..2bbd8a825890a1 --- /dev/null +++ b/vlib/net/jsonrpc/README.md @@ -0,0 +1,126 @@ +# JSONRPC +[JSON-RPC 2.0](https://www.jsonrpc.org/specification) 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. +```v +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](examples/client.v) +```v +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](examples/main.v) +```v +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`. \ No newline at end of file diff --git a/vlib/net/jsonrpc/client.v b/vlib/net/jsonrpc/client.v new file mode 100644 index 00000000000000..fc937001f038fa --- /dev/null +++ b/vlib/net/jsonrpc/client.v @@ -0,0 +1,84 @@ +module jsonrpc + +import io + +pub struct ClientConfig { +pub mut: + stream io.ReaderWriter + interceptors Interceptors +} + +pub struct Client { +mut: + stream io.ReaderWriter + interceptors Interceptors +} + +// new_client creates new `jsonrpc.Client` with `stream` to read/write and `interceptors` +pub fn new_client(cfg ClientConfig) Client { + return Client{ + stream: cfg.stream + interceptors: cfg.interceptors + } +} + +// notify sends JSON-RPC 2.0 Notification and returns without waiting for `jsonrpc.Response` +pub fn (mut c Client) notify[T](method string, params T) ! { + mut req := new_request(method, params, '') + intercept_request(c.interceptors.request, &req) or { return err } + + mut enc_req := req.encode().bytes() + intercept_encoded_request(c.interceptors.encoded_request, enc_req) or { return err } + + c.stream.write(enc_req) or { return err } +} + +// request new `jsonrpc.Request` and return `jsonrpc.Response` +pub fn (mut c Client) request[T](method string, params T, id string) !Response { + mut req := new_request(method, params, id) + intercept_request(c.interceptors.request, &req) or { return err } + + mut enc_req := req.encode().bytes() + intercept_encoded_request(c.interceptors.encoded_request, enc_req) or { return err } + + c.stream.write(enc_req) or { return err } + + mut enc_resp := []u8{len: 4096} + c.stream.read(mut enc_resp) or { return err } + + intercept_encoded_response(c.interceptors.encoded_response, enc_resp) + + resp := decode_response(enc_resp.bytestr()) or { return err } + + intercept_response(c.interceptors.response, resp) + + return resp +} + +// batch sends batch of `jsonrpc.Request` and returns batch of`jsonrpc.Response` +pub fn (mut c Client) batch(reqs []Request) ![]Response { + mut reqs_str := '[' + for req in reqs { + intercept_request(c.interceptors.request, &req) or { return err } + reqs_str = reqs_str + req.encode() + ', ' + } + + reqs_str = reqs_str.all_before_last(', ') + ']' + enc_reqs := reqs_str.bytes() + intercept_encoded_request(c.interceptors.encoded_request, enc_reqs) or { return err } + + c.stream.write(enc_reqs) or { return err } + + mut enc_resp := []u8{len: 4096} + c.stream.read(mut enc_resp) or { return err } + + intercept_encoded_response(c.interceptors.encoded_response, enc_resp) + + mut resps := decode_batch_response(enc_resp.bytestr()) or { return err } + + for mut resp in resps { + intercept_response(c.interceptors.response, &resp) + } + + return resps +} diff --git a/vlib/net/jsonrpc/interceptors.v b/vlib/net/jsonrpc/interceptors.v new file mode 100644 index 00000000000000..a0b150193c4f99 --- /dev/null +++ b/vlib/net/jsonrpc/interceptors.v @@ -0,0 +1,100 @@ +module jsonrpc + +// EventInterceptor called on `jsonrpc.dispatch_event` +pub type EventInterceptor = fn (name string, data string) + +// EncodedRequestInterceptor called on `jsonrpc.intercept_encoded_request` +pub type EncodedRequestInterceptor = fn (req []u8) ! + +// RequestInterceptor called on `jsonrpc.intercept_request` +pub type RequestInterceptor = fn (req &Request) ! + +// ResponseInterceptor called on `jsonrpc.intercept_response` +pub type ResponseInterceptor = fn (resp &Response) + +// EncodedResponseInterceptor called on `jsonrpc.intercept_encoded_response` +pub type EncodedResponseInterceptor = fn (resp []u8) + +// Interceptors collection of all supported interceptors to be called on events +pub struct Interceptors { +pub mut: + event []EventInterceptor + encoded_request []EncodedRequestInterceptor + request []RequestInterceptor + response []ResponseInterceptor + encoded_response []EncodedResponseInterceptor +} + +// dispatch_event sends `event_name` and `data` to provided `jsonrpc.EventInterceptor`s +pub fn dispatch_event(ints []EventInterceptor, event_name string, data string) { + for i in ints { + i(event_name, data) + } +} + +// intercept_encoded_request sends raw request data before attempting +// to decode it into `jsonrpc.Request` to provided `jsonrpc.EncodedRequestInterceptor`s +pub fn intercept_encoded_request(ints []EncodedRequestInterceptor, req []u8) ! { + for i in ints { + i(req)! + } +} + +// intercept_request sends decoded `jsonrpc.Request` to provided `jsonrpc.RequestInterceptor`s +pub fn intercept_request(ints []RequestInterceptor, req &Request) ! { + for i in ints { + i(req)! + } +} + +// intercept_response sends decoded `jsonrpc.Response` to provided `jsonrpc.ResponseInterceptor`s +pub fn intercept_response(ints []ResponseInterceptor, resp &Response) { + for i in ints { + i(resp) + } +} + +// intercept_encoded_response sends raw encoded data representing +// `jsonrpc.Response` to provided `jsonrpc.EncodedResponseInterceptor`s +pub fn intercept_encoded_response(ints []EncodedResponseInterceptor, resp []u8) { + for i in ints { + i(resp) + } +} + +// is_interceptor_enabled checks if interceptor of provided type is enabled on `jsonrpc.Server` +pub fn (s &Server) is_interceptor_enabled[T]() bool { + s.get_interceptor[T]() or { return false } + return true +} + +// get_interceptor tries to find and return interceptor of provided type from `jsonrpc.Interceptors` +pub fn (i Interceptors) get_interceptor[T]() ?&T { + for inter in i.event { + if inter is T { + return inter + } + } + for inter in i.encoded_request { + if inter is T { + return inter + } + } + for inter in i.request { + if inter is T { + return inter + } + } + for inter in i.response { + if inter is T { + return inter + } + } + for inter in i.encoded_response { + if inter is T { + return inter + } + } + + return none +} diff --git a/vlib/net/jsonrpc/jsonrpc.v b/vlib/net/jsonrpc/jsonrpc.v new file mode 100644 index 00000000000000..31116b93ffa5ff --- /dev/null +++ b/vlib/net/jsonrpc/jsonrpc.v @@ -0,0 +1,290 @@ +module jsonrpc + +import json + +pub const version = '2.0' + +// ---- error helpers ---- + +pub struct ResponseError { +pub mut: + code int + message string + data string +} + +// code returns `jsonrpc.ResponseError.code` field value +pub fn (err ResponseError) code() int { + return err.code +} + +// msg returns `jsonrpc.ResponseError.message` field value +pub fn (err ResponseError) msg() string { + return err.message +} + +// err returns `jsonrpc.ResponseError` casted to `IError` +pub fn (e ResponseError) err() IError { + return IError(e) +} + +// ResponseErrorGeneratorParams & response_error are used by server.v :contentReference[oaicite:2]{index=2} +@[params] +pub struct ResponseErrorGeneratorParams { + error IError @[required] + data string +} + +// response_error returns `jsonrpc.ResponseError` created from passed error and data +@[inline] +pub fn response_error(params ResponseErrorGeneratorParams) ResponseError { + return ResponseError{ + code: params.error.code() + message: params.error.msg() + data: params.data + } +} + +// error_with_code returns `jsonrpc.ResponseError` with empty data field +pub fn error_with_code(message string, code int) ResponseError { + return ResponseError{ + code: code + message: message + data: '' + } +} + +// JSON-RPC standard-ish errors :contentReference[oaicite:3]{index=3} + +pub const parse_error = error_with_code('Invalid JSON.', -32700) +pub const invalid_request = error_with_code('Invalid request.', -32600) +pub const method_not_found = error_with_code('Method not found.', -32601) +pub const invalid_params = error_with_code('Invalid params', -32602) +pub const internal_error = error_with_code('Internal error.', -32693) +pub const server_error_start = error_with_code('Error occurred when starting server.', + -32099) +pub const server_not_initialized = error_with_code('Server not initialized.', -32002) +pub const unknown_error = error_with_code('Unknown error.', -32001) +pub const server_error_end = error_with_code('Error occurred when stopping the server.', + -32000) +pub 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(), +] + +// Null represents the null value in JSON. +pub struct Null {} + +// 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) +pub fn (n Null) str() string { + return 'null' +} + +pub const null = Null{} + +pub struct Empty {} + +// str returns empty string: '' (can be passed to jsonrpc.new_request as id or params to omit fields on encoding) +pub fn (e Empty) str() string { + return '' +} + +pub const empty = Empty{} + +// ---- request/response ---- + +// Request uses raw JSON strings for id and params in the old VLS code. :contentReference[oaicite:4]{index=4} +pub 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) +} + +// 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. +pub fn new_request[T](method string, params T, id string) Request { + return Request{ + method: method + params: try_encode(params) + id: id + } +} + +// (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. +pub fn (req Request) encode() string { + params_payload := if req.params.len == 0 { + '' + } else { + ',"params":${req.params}' + } + id_payload := if req.id.len != 0 { ',"id":"${req.id}"' } else { '' } + return '{"jsonrpc":"${version}","method":"${req.method}"${params_payload}${id_payload}}' +} + +// encode_batch loops through every `jsonrpc.Request` in array, calls encode() for each of them +// and writes into json list [] splitting with ',' +pub fn (reqs []Request) encode_batch() string { + if reqs.len == 0 { + return '[]' + } + mut s := '[' + reqs[0].encode() + for req in reqs[1..] { + s = s + ',' + req.encode() + } + return s + ']' +} + +// decode_params tries to decode Request.params into provided type +pub fn (req Request) decode_params[T]() !T { + return try_decode[T](req.params) +} + +// decode_request decodes raw request into JSONRPC Request by reading after \r\n\r\n. +pub fn decode_request(raw string) !Request { + json_payload := raw.all_after('\r\n\r\n') + return json.decode(Request, json_payload) or { return err } +} + +// decode_batch_request decodes raw batch request into []jsonrpc.Request by reading after \r\n\r\n. +pub fn decode_batch_request(raw string) ![]Request { + json_payload := raw.all_after('\r\n\r\n') + return json.decode([]Request, json_payload) or { return err } +} + +pub struct Response { +pub: + jsonrpc string = version + result string @[raw] + error ResponseError + id string +} + +// 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. +pub fn new_response[T](result T, error ResponseError, id string) Response { + return Response{ + result: if error.code != 0 { '' } else { try_encode(result) } + error: error + id: id + } +} + +// 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. +pub fn (resp Response) encode() string { + mut s := '{"jsonrpc":"${version}"' + if resp.error.code != 0 { + s = s + ',"error":' + json.encode(resp.error) + } else { + s = s + ',"result":' + resp.result + } + s = s + ',"id":' + if resp.id.len == 0 { + s = s + null.str() + } else { + s = s + '"${resp.id}"' + } + return s + '}' +} + +// encode_batch loops through every `jsonrpc.Response` in array, calls encode() for each of them +// and writes into json list [] splitting with ',' +pub fn (resps []Response) encode_batch() string { + if resps.len == 0 { + return '[]' + } + mut s := '[' + resps[0].encode() + for resp in resps[1..] { + s = s + ',' + resp.encode() + } + return s + ']' +} + +// decode_params tries to decode Response.result into provided type +pub fn (resp Response) decode_result[T]() !T { + return try_decode[T](resp.result) +} + +// decode_response decodes raw response into JSONRPC Response by reading after \r\n\r\n. +pub fn decode_response(raw string) !Response { + json_payload := raw.all_after('\r\n\r\n') + return json.decode(Response, json_payload) or { return err } +} + +// decode_batch_response decodes raw batch request into []jsonrpc.Request by reading after \r\n\r\n. +pub fn decode_batch_response(raw string) ![]Response { + json_payload := raw.all_after('\r\n\r\n') + return json.decode([]Response, json_payload) or { return err } +} + +// try_encode tries to encode passed value to json object, array or primitive +// currently only for internal use +fn try_encode[T](data T) string { + return $if data is string { + '"${data}"' + } $else $if data is bool { + data.str() + } $else $if data is int { + data.str() + } $else $if data is Null { + data.str() + } $else $if data is Empty { + data.str() + } $else { + json.encode(data) + } +} + +fn try_decode[T](s string) !T { + $if T is string { + if s[0] == `"` && s[s.len - 1] == `"` { + return s.find_between('"', '"\0') + } + return error('Could not decode data=${s} into type string') + } $else $if T is bool { + if s == 'true' { + return true + } + if s == 'false' { + return false + } + return error('Could not decode data=${s} into type bool') + } $else $if T is int { + res := s.int() + if res == 0 && s != '0' { + return error('Could not decode data=${s} into type int') + } + return res + } $else $if T is Null { + if s != null.str() { + return error('Could not decode data=${s} into type bool') + } + return null + } $else $if T is Empty { + if s.len == 0 { + return Empty{} + } + return error('Params not empty: data=${s}') + } $else { + return json.decode(T, s) or { return err } + } +} diff --git a/vlib/net/jsonrpc/jsonrpc_test.v b/vlib/net/jsonrpc/jsonrpc_test.v new file mode 100644 index 00000000000000..1af7605ff423d5 --- /dev/null +++ b/vlib/net/jsonrpc/jsonrpc_test.v @@ -0,0 +1,196 @@ +module jsonrpc + +import json + +fn test_try_decode_encode() { + obj_data := { + 'key': 'value' + } + mut enc_data := try_encode[map[string]string](obj_data) + assert enc_data == json.encode(obj_data) + assert try_decode[map[string]string](enc_data)! == obj_data + + str_data := 'string' + enc_data = try_encode[string](str_data) + assert enc_data == '"${str_data}"' + assert try_decode[string](enc_data)! == str_data + + bool_data := true + enc_data = try_encode[bool](bool_data) + assert enc_data == bool_data.str() + assert try_decode[bool](enc_data)! == bool_data + + int_data := 123 + enc_data = try_encode[int](int_data) + assert enc_data == int_data.str() + assert try_decode[int](enc_data)! == int_data + + null_data := null + enc_data = try_encode[Null](null_data) + assert enc_data == null_data.str() + assert try_decode[Null](enc_data)! == null_data + + empty_data := empty + enc_data = try_encode[Empty](empty) + assert enc_data == empty_data.str() + assert try_decode[Empty](enc_data)! == empty_data +} + +fn test_request_obj_params() { + id := 'obj' + method := 'params.' + id + params := { + 'key': 'value' + } + mut req := new_request(method, params, id) + assert req.jsonrpc == version + assert req.method == method + assert req.params == json.encode(params) + assert req.id == id + + enc_req := req.encode() + assert enc_req == '{"jsonrpc":"${version}","method":"${method}","params":${json.encode(params)},"id":"${id}"}' + + assert req.decode_params[map[string]string]()! == params + + dec_req := decode_request(enc_req)! + assert req.jsonrpc == dec_req.jsonrpc + assert req.method == dec_req.method + assert req.params == dec_req.params + assert req.id == dec_req.id +} + +fn test_request_notification() { + id := '' + method := 'req.notif' + params := 'notif' + mut req := new_request(method, params, id) + assert req.jsonrpc == version + assert req.method == method + assert req.params == '"${params}"' + assert req.id == id + + assert req.encode() == '{"jsonrpc":"${version}","method":"${method}","params":"${params}"}' + assert req.decode_params[string]()! == params +} + +fn test_request_empty_params() { + id := '' + method := 'req.empty' + params := empty + mut req := new_request(method, params, id) + assert req.jsonrpc == version + assert req.method == method + assert req.params == params.str() + assert req.id == id + + assert req.encode() == '{"jsonrpc":"${version}","method":"${method}"}' + assert req.decode_params[Empty]()! == params +} + +fn test_request_batch() { + n_id := '' + n_method := 'req.notif' + n_params := 'notif' + mut notif := new_request(n_method, n_params, n_id) + + r_id := 'obj' + r_method := 'params.obj' + r_params := { + 'key': 'value' + } + mut req := new_request(r_method, r_params, r_id) + + batch := [notif, req] + enc_batch := batch.encode_batch() + assert enc_batch == '[{"jsonrpc":"${version}","method":"${n_method}","params":"${n_params}"},{"jsonrpc":"${version}","method":"${r_method}","params":${json.encode(r_params)},"id":"${r_id}"}]' + + assert decode_batch_request(enc_batch)! == batch +} + +fn test_response_obj_result() { + id := 'obj' + result := { + 'key': 'value' + } + mut resp := new_response(result, ResponseError{}, id) + assert resp.jsonrpc == version + assert resp.result == json.encode(result) + assert resp.error == ResponseError{} + assert resp.id == id + + enc_resp := resp.encode() + assert enc_resp == '{"jsonrpc":"${version}","result":${json.encode(result)},"id":"${id}"}' + + assert resp.decode_result[map[string]string]()! == result + + dec_resp := decode_response(enc_resp)! + assert resp.jsonrpc == dec_resp.jsonrpc + assert resp.result == dec_resp.result + assert resp.error == dec_resp.error + assert resp.id == dec_resp.id +} + +fn test_response_error() { + id := 'error' + result := 'should be null' + err := parse_error + mut resp := new_response(result, err, id) + assert resp.jsonrpc == version + assert resp.result == '' + assert resp.error == parse_error + assert resp.id == id + + enc_resp := resp.encode() + assert enc_resp == '{"jsonrpc":"${version}","error":${json.encode(err)},"id":"${id}"}' + + assert resp.decode_result[Empty]()! == empty + + dec_resp := decode_response(enc_resp)! + assert resp.jsonrpc == dec_resp.jsonrpc + assert resp.result == dec_resp.result + assert resp.error == dec_resp.error + assert resp.id == dec_resp.id +} + +fn test_response_null_id() { + id := '' + result := 'should be null' + err := parse_error + mut resp := new_response(result, err, id) + assert resp.jsonrpc == version + assert resp.result == '' + assert resp.error == parse_error + assert resp.id == id + + enc_resp := resp.encode() + assert enc_resp == '{"jsonrpc":"${version}","error":${json.encode(err)},"id":${null.str()}}' + + assert resp.decode_result[Empty]()! == empty + + dec_resp := decode_response(enc_resp)! + assert resp.jsonrpc == dec_resp.jsonrpc + assert resp.result == dec_resp.result + assert resp.error == dec_resp.error + assert resp.id == dec_resp.id +} + +fn test_response_batch() { + r_id := 'obj' + r_err := ResponseError{} + r_result := { + 'key': 'value' + } + mut r_resp := new_response(r_result, r_err, r_id) + + e_id := '' + e_result := 'should be null' + e_err := parse_error + mut e_resp := new_response(e_result, e_err, e_id) + + batch := [r_resp, e_resp] + enc_batch := batch.encode_batch() + assert enc_batch == '[{"jsonrpc":"${version}","result":${json.encode(r_result)},"id":"${r_id}"},{"jsonrpc":"${version}","error":${json.encode(e_err)},"id":${null.str()}}]' + + assert decode_batch_response(enc_batch)! == batch +} diff --git a/vlib/net/jsonrpc/logging.v b/vlib/net/jsonrpc/logging.v new file mode 100644 index 00000000000000..d2236653dbee6d --- /dev/null +++ b/vlib/net/jsonrpc/logging.v @@ -0,0 +1,48 @@ +module jsonrpc + +import log + +// LoggingInterceptor is simple logging full-fledged Interceptor +// messages will be written in `log`.get_level() Level +@[heap] +pub struct LoggingInterceptor { +pub mut: + log log.Log +} + +// on_event logs event `name` and `data` into provided `log` +pub fn (mut l LoggingInterceptor) on_event(name string, data string) { + msg := '[EVENT] name=${name} data=${data}' + l.log.send_output(msg, l.log.get_level()) +} + +// on_encoded_request logs json encoded `jsonrpc.Request` as string +pub fn (mut l LoggingInterceptor) on_encoded_request(req []u8) ! { + msg := '[RAW REQ] ${req.bytestr()}' + l.log.send_output(msg, l.log.get_level()) +} + +// on_request logs `jsonrpc.Request` method, params and id +pub fn (mut l LoggingInterceptor) on_request(req &Request) ! { + msg := '[REQ] method=${req.method} params=${req.params} id=${req.id}' + l.log.send_output(msg, l.log.get_level()) +} + +// on_response logs `jsonrpc.Response` result, error and id +pub fn (mut l LoggingInterceptor) on_response(resp &Response) { + mut msg := '[RESP] result=${resp.result} ' + if resp.error.code != 0 { + msg = msg + 'error=${resp.error}' + } else { + msg = msg + 'error=none' + } + msg = msg + ' id=${resp.id}' + + l.log.send_output(msg, l.log.get_level()) +} + +// on_encoded_response logs json encoded `jsonrpc.Response` as string +pub fn (mut l LoggingInterceptor) on_encoded_response(resp []u8) { + msg := '[RAW RESP] ${resp.bytestr()}' + l.log.send_output(msg, l.log.get_level()) +} diff --git a/vlib/net/jsonrpc/server.v b/vlib/net/jsonrpc/server.v new file mode 100644 index 00000000000000..0223eaa048458f --- /dev/null +++ b/vlib/net/jsonrpc/server.v @@ -0,0 +1,235 @@ +module jsonrpc + +import json +import strings +import io + +pub struct ServerConfig { +pub 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} +@[heap] +pub struct Server { +mut: + stream io.ReaderWriter + handler Handler @[required] + interceptors Interceptors +} + +// new_server creates new `jsonrpc.Server` with `stream` to read/write, +// the `jsonrpc.Handler` to handle Requests/Responses and `interceptors` +pub fn new_server(cfg ServerConfig) Server { + return Server{ + stream: cfg.stream + handler: cfg.handler + interceptors: cfg.interceptors + } +} + +// 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 +pub fn (mut s Server) respond() ! { + mut rw := s.writer() + mut rx := []u8{len: 4096} + bytes_read := s.stream.read(mut rx) or { + if err is io.Eof { + return + } + return err + } + + if bytes_read == 0 { + return + } + + intercept_encoded_request(s.interceptors.encoded_request, rx) or { + rw.write_error(response_error(error: err)) + return err + } + + req_str := rx.bytestr() + + mut req_batch := []Request{} + match req_str[0].ascii_str() { + '[' { + req_batch = decode_batch_request(req_str) or { + rw.write_error(response_error(error: parse_error)) + return err + } + rw.start_batch() + } + '{' { + req := decode_request(req_str) or { + rw.write_error(response_error(error: parse_error)) + return err + } + req_batch.prepend(req) + } + else { + rw.write_error(response_error(error: parse_error)) + return parse_error + } + } + + for rq in req_batch { + rw.req_id = rq.id + + intercept_request(s.interceptors.request, &rq) or { + rw.write_error(response_error(error: err)) + return err + } + + s.handler(&rq, mut rw) + } + + if req_batch.len > 1 { + rw.close_batch() + } +} + +fn (s &Server) writer() &ResponseWriter { + return &ResponseWriter{ + writer: s.stream + sb: strings.new_builder(4096) + server: s + } +} + +// start `Server` loop to operate on `stream` passed into constructor +// it calls `Server.respond()` method in loop +pub fn (mut s Server) start() { + for { + s.respond() or { + if err is io.Eof { + return + } + } + } +} + +// 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` +pub type Handler = fn (req &Request, mut wr ResponseWriter) + +// Router is simple map of method names and their `Handler`s +pub struct Router { +mut: + methods map[string]Handler +} + +// 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` +pub fn (r Router) handle_jsonrpc(req &Request, mut wr ResponseWriter) { + if h := r.methods[req.method] { + h(req, mut wr) + return + } + + wr.write_error(method_not_found) +} + +// register `handler` to operate when `method` found in incoming `jsonrpc.Request` +pub fn (mut r Router) register(method string, handler Handler) bool { + if method in r.methods { + return false + } + + r.methods[method] = handler + return true +} + +pub struct ResponseWriter { +mut: + sb strings.Builder + is_batch bool + server &Server +pub mut: + req_id string + writer io.ReaderWriter +} + +fn (mut rw ResponseWriter) start_batch() { + rw.is_batch = true + rw.sb.write_string('[') +} + +fn (mut rw ResponseWriter) close_batch() { + rw.is_batch = false + rw.sb.go_back(2) + rw.sb.write_string(']') + rw.close() +} + +fn (mut rw ResponseWriter) close() { + intercept_encoded_response(rw.server.interceptors.encoded_response, rw.sb) + rw.writer.write(rw.sb) or {} + rw.sb.go_back_to(0) +} + +// write payload into `jsonrpc.Response.result`. +// call when need to send data in response +pub fn (mut rw ResponseWriter) write[T](payload T) { + final_resp := Response{ + id: rw.req_id + result: json.encode(payload) + } + + intercept_response(rw.server.interceptors.response, final_resp) + + if rw.req_id.len == 0 { + return + } + + rw.sb.write_string(final_resp.encode()) + + if rw.is_batch == true { + rw.sb.write_string(', ') + return + } + rw.close() +} + +// write_empty writes `jsonrpc.null` as response +pub fn (mut rw ResponseWriter) write_empty() { + rw.write[Null](null) +} + +// write_error into the `jsonrpc.Response` of current request +pub fn (mut rw ResponseWriter) write_error(err IError) { + mut res_err := err + if err !is ResponseError { + if err.code() !in error_codes { + res_err = response_error(error: unknown_error) + } else { + res_err = response_error(error: err) + } + } + + final_resp := Response{ + id: rw.req_id + error: res_err as ResponseError + } + + intercept_response(rw.server.interceptors.response, final_resp) + + rw.sb.write_string(final_resp.encode()) + if rw.is_batch { + rw.sb.write_string(', ') + return + } + rw.close() +} diff --git a/vlib/net/jsonrpc/server_test.v b/vlib/net/jsonrpc/server_test.v new file mode 100644 index 00000000000000..722630719cb72f --- /dev/null +++ b/vlib/net/jsonrpc/server_test.v @@ -0,0 +1,102 @@ +module jsonrpc + +import strings + +struct StringRW { +mut: + buf strings.Builder = strings.new_builder(4096) +} + +fn (mut s StringRW) read(mut buf []u8) !int { + len := s.buf.len + buf = s.buf.str().bytes() + s.buf = strings.new_builder(4096) + return len +} + +fn (mut s StringRW) write(buf []u8) !int { + return s.buf.write(buf) +} + +struct KVItem { + key string + value string +} + +fn handle_test(req &Request, mut wr ResponseWriter) { + p := req.decode_params[KVItem]() or { + wr.write_error(invalid_params) + return + } + + wr.write(p) +} + +fn test_server_request_response() { + mut stream := StringRW{} + mut srv := new_server(ServerConfig{ + stream: stream + handler: handle_test + }) + + id := 'req' + method := 'kv.item' + params := KVItem{ + key: 'foo' + value: 'bar' + } + stream.write(new_request(method, params, id).encode().bytes())! + + srv.respond()! + + mut enc_resp := []u8{len: 4096} + stream.read(mut enc_resp)! + resp := decode_response(enc_resp.bytestr())! + + assert resp.jsonrpc == version + assert resp.decode_result[KVItem]()! == params + assert resp.error == ResponseError{} + assert resp.id == id +} + +fn test_server_router_request_response() { + mut r := Router{} + method := 'kv.item' + r.register(method, handle_test) + mut stream := StringRW{} + mut srv := new_server(ServerConfig{ + stream: stream + handler: r.handle_jsonrpc + }) + + id := 'req' + params := KVItem{ + key: 'foo' + value: 'bar' + } + stream.write(new_request(method, params, id).encode().bytes())! + + srv.respond()! + + mut enc_resp := []u8{len: 4096} + stream.read(mut enc_resp)! + mut resp := decode_response(enc_resp.bytestr())! + + assert resp.jsonrpc == version + assert resp.decode_result[KVItem]()! == params + assert resp.error == ResponseError{} + assert resp.id == id + + stream.write(new_request('unknown', params, id).encode().bytes())! + + srv.respond()! + + enc_resp = []u8{len: 4096} + stream.read(mut enc_resp)! + resp = decode_response(enc_resp.bytestr())! + + assert resp.jsonrpc == version + assert resp.decode_result[Empty]()! == empty + assert resp.error == method_not_found + assert resp.id == id +}