Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -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`.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user