* rpc: add `json-rpc` from https://github.com/Cuprate/cuprate/pull/43 Maintains all the changes made in that branch * workspace: add `rpc/json-rpc` * json-rpc: fix cargo.toml * add todo * satisfy clippy * `method/params` -> `body` switch, adjust input types * add test helpers, test tagged enums and flatten structs * fix id type `None` <-> `Some(Id::Null)` difference * lib.rs: add docs * impl `Version` * impl `Id` * impl `Request` * impl `Response` * impl `ErrorCode` * impl `ErrorObject` * fixes * add monero jsonrpc tests * response: add id test * add display docs to `ErrorObject` * remove `#[inline]` * add id null test * cleanup * code: clarify Monero's error code usage in docs * id: fix macro indentation * readme: fix `Response` -> `Request` * request: add `lowercase` test * tests: formatting, more string tests * readme: add `Serialization changes` * code: ugly `match` -> `if` * response: manual deserialization impl - lowercase keys only - enforce either `result/error` but not both * remove unneeded clone bounds * readme: add implementation comparison tests * request/response: more tests * readme: formatting, assert error messages are expected * request: add unknown field test * request/response: add unknown field and unicode test
6.3 KiB
json-rpc
JSON-RPC 2.0 types and (de)serialization.
What
This crate implements the JSON-RPC 2.0 specification for usage in Cuprate.
It contains slight modifications catered towards Cuprate and isn't necessarily a general purpose implementation of the specification (see below).
This crate expects you to read the brief JSON-RPC 2.0 specification for context.
Batching
This crate does not have any types for JSON-RPC 2.0 batching.
This is because monerod
does not support this,
as such, neither does Cuprate.
TODO: citation needed on monerod
not supporting batching.
Request changes
JSON-RPC 2.0's Request
object usually contains these 2 fields:
method
params
This crate replaces those two with a body
field that is #[serde(flatten)]
ed,
and assumes the type within that body
field is tagged properly, for example:
# use pretty_assertions::assert_eq;
use serde::{Deserialize, Serialize};
use json_rpc::{Id, Request};
// Parameter type.
#[derive(Deserialize, Serialize)]
struct GetBlock {
height: u64,
}
// Method enum containing all enums.
// All methods are tagged as `method`
// and their inner parameter types are
// tagged with `params` (in snake case).
#[derive(Deserialize, Serialize)]
#[serde(tag = "method", content = "params")] // INVARIANT: these tags are needed
#[serde(rename_all = "snake_case")] // for proper (de)serialization.
enum Methods {
GetBlock(GetBlock),
/* other methods */
}
// Create the request object.
let request = Request::new_with_id(
Id::Str("hello".into()),
Methods::GetBlock(GetBlock { height: 123 }),
);
// Serializing properly shows the `method/params` fields
// even though `Request` doesn't contain those fields.
let json = serde_json::to_string_pretty(&request).unwrap();
let expected_json =
r#"{
"jsonrpc": "2.0",
"id": "hello",
"method": "get_block",
"params": {
"height": 123
}
}"#;
assert_eq!(json, expected_json);
This is how the method/param types are done in Cuprate.
For reasoning, see: https://github.com/Cuprate/cuprate/pull/146#issuecomment-2145734838.
Serialization changes
This crate's serialized field order slightly differs compared to monerod
.
monerod
's JSON objects are serialized in alphabetically order, where as this crate serializes the fields in their defined order (due to [serde
]).
With that said, parsing should be not affected at all since a key-value map is used:
# use pretty_assertions::assert_eq;
use json_rpc::{Id, Response};
let response = Response::ok(Id::Num(123), "OK");
let response_json = serde_json::to_string_pretty(&response).unwrap();
// This crate's `Response` result type will _always_
// serialize fields in the following order:
let expected_json =
r#"{
"jsonrpc": "2.0",
"id": 123,
"result": "OK"
}"#;
assert_eq!(response_json, expected_json);
// Although, `monerod` will serialize like such:
let monerod_json =
r#"{
"id": 123,
"jsonrpc": "2.0",
"result": "OK"
}"#;
///---
let response = Response::<()>::invalid_request(Id::Num(123));
let response_json = serde_json::to_string_pretty(&response).unwrap();
// This crate's `Response` error type will _always_
// serialize fields in the following order:
let expected_json =
r#"{
"jsonrpc": "2.0",
"id": 123,
"error": {
"code": -32600,
"message": "Invalid Request"
}
}"#;
assert_eq!(response_json, expected_json);
// Although, `monerod` will serialize like such:
let monerod_json =
r#"{
"error": {
"code": -32600,
"message": "Invalid Request"
},
"id": 123
"jsonrpc": "2.0",
}"#;
Compared to other implementations
A quick table showing some small differences between this crate and other JSON-RPC 2.0 implementations.
Implementation | Allows any case for key fields excluding method/params |
Allows unknown fields in main {} , and response/request objects |
Allows overwriting previous values upon duplicate fields (except [Response ]'s result/error field) |
---|---|---|---|
monerod |
✅ | ✅ | ✅ |
jsonrpsee |
❌ | ✅ | ❌ |
This crate | ❌ | ✅ | ✅ |
Allows any case for key fields excluding method/params
:
# use json_rpc::Response;
# use serde_json::from_str;
# use pretty_assertions::assert_eq;
let json = r#"{"jsonrpc":"2.0","id":123,"result":"OK"}"#;
from_str::<Response<String>>(&json).unwrap();
// Only `lowercase` is allowed.
let json = r#"{"jsonRPC":"2.0","id":123,"result":"OK"}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "missing field `jsonrpc` at line 1 column 40");
Allows unknown fields in main {}
, and response/request objects:
# use json_rpc::Response;
# use serde_json::from_str;
// unknown fields are allowed in main `{}`
// v
let json = r#"{"unknown_field":"asdf","jsonrpc":"2.0","id":123,"result":"OK"}"#;
from_str::<Response<String>>(&json).unwrap();
// and within objects
// v
let json = r#"{"jsonrpc":"2.0","id":123,"error":{"code":-1,"message":"","unknown_field":"asdf"}}"#;
from_str::<Response<String>>(&json).unwrap();
Allows overwriting previous values upon duplicate fields (except [Response
]'s result/error
field)
# use json_rpc::{Id, Response};
# use serde_json::from_str;
# use pretty_assertions::assert_eq;
// duplicate fields will get overwritten by the latest one
// v v
let json = r#"{"jsonrpc":"2.0","id":123,"id":321,"result":"OK"}"#;
let response = from_str::<Response<String>>(&json).unwrap();
assert_eq!(response.id, Id::Num(321));
// But 2 results are not allowed.
let json = r#"{"jsonrpc":"2.0","id":123,"result":"OK","result":"OK"}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "duplicate field `result/error` at line 1 column 48");
// Same with errors.
let json = r#"{"jsonrpc":"2.0","id":123,"error":{"code":-1,"message":""},"error":{"code":-1,"message":""}}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "duplicate field `result/error` at line 1 column 66");