Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions examples/jsonrpc/client.v
Original file line number Diff line number Diff line change
@@ -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'
})!
}
213 changes: 213 additions & 0 deletions examples/jsonrpc/server.v
Original file line number Diff line number Diff line change
@@ -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)
}
}
126 changes: 126 additions & 0 deletions vlib/net/jsonrpc/README.md
Original file line number Diff line number Diff line change
@@ -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`.
Loading