cuprate-hinto-janai/rpc/json-rpc/README.md
hinto-janai a3e34c3ba8
rpc: implement json-rpc crate (#148)
* 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
2024-06-12 02:12:31 +01:00

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");