Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+126
View File
@@ -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`.
+84
View File
@@ -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
}
+100
View File
@@ -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
}
+288
View File
@@ -0,0 +1,288 @@
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 }
}
}
+196
View File
@@ -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
}
+48
View File
@@ -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())
}
+235
View File
@@ -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()
}
+102
View File
@@ -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 {
data := s.buf.str().bytes()
len := copy(mut buf, data)
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'
}
srv.stream.write(new_request(method, params, id).encode().bytes())!
srv.respond()!
mut enc_resp := []u8{len: 4096}
srv.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'
}
srv.stream.write(new_request(method, params, id).encode().bytes())!
srv.respond()!
mut enc_resp := []u8{len: 4096}
srv.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
srv.stream.write(new_request('unknown', params, id).encode().bytes())!
srv.respond()!
enc_resp = []u8{len: 4096}
srv.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
}