Merge branch 'main' into block-downloader

This commit is contained in:
Boog900 2024-06-13 22:05:58 +01:00
commit 3a7a8563e6
No known key found for this signature in database
GPG key ID: 42AB1287CB0041C2
17 changed files with 2740 additions and 189 deletions

View file

@ -18,7 +18,7 @@ jobs:
steps:
- name: Cache
uses: actions/cache@v3.2.3
uses: actions/cache@v4
with:
path: |
~/.cargo

View file

@ -83,7 +83,7 @@ jobs:
components: clippy
- name: Cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: target
key: ${{ matrix.os }}

View file

@ -18,7 +18,7 @@ jobs:
steps:
- name: Cache
uses: actions/cache@v3.2.3
uses: actions/cache@v4
with:
path: |
~/.cargo

649
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -120,6 +120,7 @@ allow = [
# Font licenses.
"Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html
"Unicode-3.0", # https://spdx.org/licenses/Unicode-3.0.html
# "LicenseRef-UFL-1.0", # https://tldrlegal.com/license/ubuntu-font-license,-1.0
# "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html
]

View file

@ -7,9 +7,14 @@ license = "MIT"
authors = ["hinto-janai"]
repository = "https://github.com/Cuprate/cuprate/tree/main/rpc/json-rpc"
keywords = ["json", "rpc"]
categories = ["encoding"]
[features]
[dependencies]
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["std"] }
thiserror = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }

197
rpc/json-rpc/README.md Normal file
View file

@ -0,0 +1,197 @@
# `json-rpc`
JSON-RPC 2.0 types and (de)serialization.
## What
This crate implements the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification)
for usage in [Cuprate](https://github.com/Cuprate/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](https://www.jsonrpc.org/specification#batch).
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](https://www.jsonrpc.org/specification#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:
```rust
# 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:
```rust
# 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`](https://github.com/monero-project/monero) | ✅ | ✅ | ✅
| [`jsonrpsee`](https://docs.rs/jsonrpsee) | ❌ | ✅ | ❌
| This crate | ❌ | ✅ | ✅
Allows any case for key fields excluding `method/params`:
```rust
# 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:
```rust
# 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)
```rust
# 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");
```

View file

@ -0,0 +1,219 @@
//! Error codes.
//---------------------------------------------------------------------------------------------------- Use
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::error::constants::{
INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR, SERVER_ERROR,
};
//---------------------------------------------------------------------------------------------------- ErrorCode
/// [Error object code](https://www.jsonrpc.org/specification#error_object).
///
/// This `enum` encapsulates JSON-RPC 2.0's error codes
/// found in [`ErrorObject`](crate::error::ErrorObject).
///
/// It associates the code integer ([`i32`]) with its defined message.
///
/// # Application defined errors
/// The custom error codes past `-32099` (`-31000, -31001`, ...)
/// defined in JSON-RPC 2.0 are not supported by this enum because:
///
/// 1. The `(i32, &'static str)` required makes the enum more than 3x larger
/// 2. It is not used by Cuprate/Monero[^1]
///
/// [^1]: Defined errors used by Monero (also excludes the last defined error `-32000 to -32099 Server error`): <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/contrib/epee/include/net/http_server_handlers_map2.h#L150>
///
/// # Display
/// ```rust
/// use json_rpc::error::ErrorCode;
/// use serde_json::{to_value, from_value, Value};
///
/// for e in [
/// ErrorCode::ParseError,
/// ErrorCode::InvalidRequest,
/// ErrorCode::MethodNotFound,
/// ErrorCode::InvalidParams,
/// ErrorCode::InternalError,
/// ErrorCode::ServerError(0),
/// ] {
/// // The formatting is `$CODE: $MSG`.
/// let expected_fmt = format!("{}: {}", e.code(), e.msg());
/// assert_eq!(expected_fmt, format!("{e}"));
/// }
/// ```
///
/// # (De)serialization
/// This type gets (de)serialized as the associated `i32`, for example:
/// ```rust
/// use json_rpc::error::ErrorCode;
/// use serde_json::{to_value, from_value, Value};
///
/// for e in [
/// ErrorCode::ParseError,
/// ErrorCode::InvalidRequest,
/// ErrorCode::MethodNotFound,
/// ErrorCode::InvalidParams,
/// ErrorCode::InternalError,
/// ErrorCode::ServerError(0),
/// ErrorCode::ServerError(1),
/// ErrorCode::ServerError(2),
/// ] {
/// // Gets serialized into a JSON integer.
/// let value = to_value(&e).unwrap();
/// assert_eq!(value, Value::Number(e.code().into()));
///
/// // Expects a JSON integer when deserializing.
/// assert_eq!(e, from_value(value).unwrap());
/// }
/// ```
///
/// ```rust,should_panic
/// # use json_rpc::error::ErrorCode;
/// # use serde_json::from_value;
/// // A JSON string that contains an integer won't work.
/// from_value::<ErrorCode>("-32700".into()).unwrap();
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)]
pub enum ErrorCode {
#[error("{}: {}", PARSE_ERROR.0, PARSE_ERROR.1)]
/// Invalid JSON was received by the server.
///
/// An error occurred on the server while parsing the JSON text.
ParseError,
#[error("{}: {}", INVALID_REQUEST.0, INVALID_REQUEST.1)]
/// The JSON sent is not a valid Request object.
InvalidRequest,
#[error("{}: {}", METHOD_NOT_FOUND.0, METHOD_NOT_FOUND.1)]
/// The method does not exist / is not available.
MethodNotFound,
#[error("{}: {}", INVALID_PARAMS.0, INVALID_PARAMS.1)]
/// Invalid method parameters.
InvalidParams,
#[error("{}: {}", INTERNAL_ERROR.0, INTERNAL_ERROR.1)]
/// Internal JSON-RPC error.
InternalError,
#[error("{0}: {SERVER_ERROR}")]
/// Reserved for implementation-defined server-errors.
ServerError(i32),
}
impl ErrorCode {
/// Creates [`Self`] from a [`i32`] code.
///
/// [`From<i32>`] is the same as this function.
///
/// ```rust
/// use json_rpc::error::{
/// ErrorCode,
/// INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR,
/// };
///
/// assert_eq!(ErrorCode::from_code(PARSE_ERROR.0), ErrorCode::ParseError);
/// assert_eq!(ErrorCode::from_code(INVALID_REQUEST.0), ErrorCode::InvalidRequest);
/// assert_eq!(ErrorCode::from_code(METHOD_NOT_FOUND.0), ErrorCode::MethodNotFound);
/// assert_eq!(ErrorCode::from_code(INVALID_PARAMS.0), ErrorCode::InvalidParams);
/// assert_eq!(ErrorCode::from_code(INTERNAL_ERROR.0), ErrorCode::InternalError);
///
/// // Non-defined code inputs will default to a custom `ServerError`.
/// assert_eq!(ErrorCode::from_code(0), ErrorCode::ServerError(0));
/// assert_eq!(ErrorCode::from_code(1), ErrorCode::ServerError(1));
/// assert_eq!(ErrorCode::from_code(2), ErrorCode::ServerError(2));
/// ```
pub const fn from_code(code: i32) -> Self {
// FIXME: you cannot `match` on tuple fields
// so use `if` (seems to compile to the same
// assembly as matching directly on `i32`s).
if code == PARSE_ERROR.0 {
Self::ParseError
} else if code == INVALID_REQUEST.0 {
Self::InvalidRequest
} else if code == METHOD_NOT_FOUND.0 {
Self::MethodNotFound
} else if code == INVALID_PARAMS.0 {
Self::InvalidParams
} else if code == INTERNAL_ERROR.0 {
Self::InternalError
} else {
Self::ServerError(code)
}
}
/// Returns `self`'s [`i32`] code representation.
///
/// ```rust
/// use json_rpc::error::{
/// ErrorCode,
/// INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR,
/// };
///
/// assert_eq!(ErrorCode::ParseError.code(), PARSE_ERROR.0);
/// assert_eq!(ErrorCode::InvalidRequest.code(), INVALID_REQUEST.0);
/// assert_eq!(ErrorCode::MethodNotFound.code(), METHOD_NOT_FOUND.0);
/// assert_eq!(ErrorCode::InvalidParams.code(), INVALID_PARAMS.0);
/// assert_eq!(ErrorCode::InternalError.code(), INTERNAL_ERROR.0);
/// assert_eq!(ErrorCode::ServerError(0).code(), 0);
/// assert_eq!(ErrorCode::ServerError(1).code(), 1);
/// ```
pub const fn code(&self) -> i32 {
match self {
Self::ParseError => PARSE_ERROR.0,
Self::InvalidRequest => INVALID_REQUEST.0,
Self::MethodNotFound => METHOD_NOT_FOUND.0,
Self::InvalidParams => INVALID_PARAMS.0,
Self::InternalError => INTERNAL_ERROR.0,
Self::ServerError(code) => *code,
}
}
/// Returns `self`'s human readable [`str`] message.
///
/// ```rust
/// use json_rpc::error::{
/// ErrorCode,
/// INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR, SERVER_ERROR,
/// };
///
/// assert_eq!(ErrorCode::ParseError.msg(), PARSE_ERROR.1);
/// assert_eq!(ErrorCode::InvalidRequest.msg(), INVALID_REQUEST.1);
/// assert_eq!(ErrorCode::MethodNotFound.msg(), METHOD_NOT_FOUND.1);
/// assert_eq!(ErrorCode::InvalidParams.msg(), INVALID_PARAMS.1);
/// assert_eq!(ErrorCode::InternalError.msg(), INTERNAL_ERROR.1);
/// assert_eq!(ErrorCode::ServerError(0).msg(), SERVER_ERROR);
/// ```
pub const fn msg(&self) -> &'static str {
match self {
Self::ParseError => PARSE_ERROR.1,
Self::InvalidRequest => INVALID_REQUEST.1,
Self::MethodNotFound => METHOD_NOT_FOUND.1,
Self::InvalidParams => INVALID_PARAMS.1,
Self::InternalError => INTERNAL_ERROR.1,
Self::ServerError(_) => SERVER_ERROR,
}
}
}
//---------------------------------------------------------------------------------------------------- Trait impl
impl<N: Into<i32>> From<N> for ErrorCode {
fn from(code: N) -> Self {
Self::from_code(code.into())
}
}
//---------------------------------------------------------------------------------------------------- Serde impl
impl<'a> Deserialize<'a> for ErrorCode {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::from_code(Deserialize::deserialize(deserializer)?))
}
}
impl Serialize for ErrorCode {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_i32(self.code())
}
}

View file

@ -0,0 +1,22 @@
//! [`JSON-RPC 2.0`](https://www.jsonrpc.org/specification#error_object) defined errors as constants.
//---------------------------------------------------------------------------------------------------- JSON-RPC spec errors.
/// Code and message for [`ErrorCode::ParseError`](crate::error::ErrorCode::ParseError).
pub const PARSE_ERROR: (i32, &str) = (-32700, "Parse error");
/// Code and message for [`ErrorCode::InvalidRequest`](crate::error::ErrorCode::InvalidRequest).
pub const INVALID_REQUEST: (i32, &str) = (-32600, "Invalid Request");
/// Code and message for [`ErrorCode::MethodNotFound`](crate::error::ErrorCode::MethodNotFound).
pub const METHOD_NOT_FOUND: (i32, &str) = (-32601, "Method not found");
/// Code and message for [`ErrorCode::InvalidParams`](crate::error::ErrorCode::InvalidParams).
pub const INVALID_PARAMS: (i32, &str) = (-32602, "Invalid params");
/// Code and message for [`ErrorCode::InternalError`](crate::error::ErrorCode::InternalError).
pub const INTERNAL_ERROR: (i32, &str) = (-32603, "Internal error");
/// Message for [`ErrorCode::ServerError`](crate::error::ErrorCode::ServerError).
///
/// The [`i32`] error code is the caller's choice, this is only the message.
pub const SERVER_ERROR: &str = "Server error";

View file

@ -0,0 +1,14 @@
//! [Error codes and objects](https://www.jsonrpc.org/specification#error_object).
//!
//! This module contains JSON-RPC 2.0's error object and codes,
//! as well as some associated constants.
mod code;
mod constants;
mod object;
pub use code::ErrorCode;
pub use constants::{
INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR, SERVER_ERROR,
};
pub use object::ErrorObject;

View file

@ -0,0 +1,258 @@
//! Error object.
//---------------------------------------------------------------------------------------------------- Use
use std::{borrow::Cow, error::Error, fmt::Display};
use serde::{Deserialize, Serialize};
use serde_json::value::Value;
use crate::error::{
constants::{
INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR,
SERVER_ERROR,
},
ErrorCode,
};
//---------------------------------------------------------------------------------------------------- ErrorObject
/// [The error object](https://www.jsonrpc.org/specification).
///
/// This is the object sent back in a [`Response`](crate::Response)
/// if the method call errored.
///
/// # Display
/// ```rust
/// use json_rpc::error::ErrorObject;
///
/// // The format is `$CODE: $MESSAGE`.
/// // If a message was not passed during construction,
/// // the error code's message will be used.
/// assert_eq!(format!("{}", ErrorObject::parse_error()), "-32700: Parse error");
/// assert_eq!(format!("{}", ErrorObject::invalid_request()), "-32600: Invalid Request");
/// assert_eq!(format!("{}", ErrorObject::method_not_found()), "-32601: Method not found");
/// assert_eq!(format!("{}", ErrorObject::invalid_params()), "-32602: Invalid params");
/// assert_eq!(format!("{}", ErrorObject::internal_error()), "-32603: Internal error");
/// assert_eq!(format!("{}", ErrorObject::server_error(0)), "0: Server error");
///
/// // Set a custom message.
/// let mut e = ErrorObject::server_error(1);
/// e.message = "hello".into();
/// assert_eq!(format!("{e}"), "1: hello");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorObject {
/// The error code.
pub code: ErrorCode,
/// A custom message for this error, distinct from [`ErrorCode::msg`].
///
/// A JSON `string` value.
///
/// This is a `Cow<'static, str>` to support both 0-allocation for
/// `const` string ID's commonly found in programs, as well as support
/// for runtime [`String`]'s.
pub message: Cow<'static, str>,
/// Optional data associated with the error.
///
/// # `None` vs `Some(Value::Null)`
/// This field will be completely omitted during serialization if [`None`],
/// however if it is `Some(Value::Null)`, it will be serialized as `"data": null`.
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl ErrorObject {
/// Creates a new error, deriving the message from the code.
///
/// Same as `ErrorObject::from(ErrorCode)`.
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// for code in [
/// ErrorCode::ParseError,
/// ErrorCode::InvalidRequest,
/// ErrorCode::MethodNotFound,
/// ErrorCode::InvalidParams,
/// ErrorCode::InternalError,
/// ErrorCode::ServerError(0),
/// ] {
/// let object = ErrorObject::from_code(code);
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
///
/// }
/// ```
pub const fn from_code(code: ErrorCode) -> Self {
Self {
code,
message: Cow::Borrowed(code.msg()),
data: None,
}
}
/// Creates a new error using [`PARSE_ERROR`].
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::ParseError;
/// let object = ErrorObject::parse_error();
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn parse_error() -> Self {
Self {
code: ErrorCode::ParseError,
message: Cow::Borrowed(PARSE_ERROR.1),
data: None,
}
}
/// Creates a new error using [`INVALID_REQUEST`].
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::InvalidRequest;
/// let object = ErrorObject::invalid_request();
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn invalid_request() -> Self {
Self {
code: ErrorCode::InvalidRequest,
message: Cow::Borrowed(INVALID_REQUEST.1),
data: None,
}
}
/// Creates a new error using [`METHOD_NOT_FOUND`].
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::MethodNotFound;
/// let object = ErrorObject::method_not_found();
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn method_not_found() -> Self {
Self {
code: ErrorCode::MethodNotFound,
message: Cow::Borrowed(METHOD_NOT_FOUND.1),
data: None,
}
}
/// Creates a new error using [`INVALID_PARAMS`].
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::InvalidParams;
/// let object = ErrorObject::invalid_params();
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn invalid_params() -> Self {
Self {
code: ErrorCode::InvalidParams,
message: Cow::Borrowed(INVALID_PARAMS.1),
data: None,
}
}
/// Creates a new error using [`INTERNAL_ERROR`].
///
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::InternalError;
/// let object = ErrorObject::internal_error();
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn internal_error() -> Self {
Self {
code: ErrorCode::InternalError,
message: Cow::Borrowed(INTERNAL_ERROR.1),
data: None,
}
}
/// Creates a new error using [`SERVER_ERROR`].
///
/// You must provide the custom [`i32`] error code.
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::ServerError(0);
/// let object = ErrorObject::server_error(0);
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn server_error(error_code: i32) -> Self {
Self {
code: ErrorCode::ServerError(error_code),
message: Cow::Borrowed(SERVER_ERROR),
data: None,
}
}
}
//---------------------------------------------------------------------------------------------------- Trait impl
impl From<ErrorCode> for ErrorObject {
fn from(code: ErrorCode) -> Self {
Self::from_code(code)
}
}
impl Display for ErrorObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Using `self.code`'s formatting will write the
// message twice, so prefer the built-in message.
write!(f, "{}: {}", self.code.code(), self.message)
}
}
impl Error for ErrorObject {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.code)
}
fn description(&self) -> &str {
&self.message
}
}

242
rpc/json-rpc/src/id.rs Normal file
View file

@ -0,0 +1,242 @@
//! [`Id`]: request/response identification.
//---------------------------------------------------------------------------------------------------- Use
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
//---------------------------------------------------------------------------------------------------- Id
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(untagged)]
/// [Request](crate::Request)/[Response](crate::Response) identification.
///
/// This is the [JSON-RPC 2.0 `id` field](https://www.jsonrpc.org/specification)
/// type found in `Request/Response`s.
///
/// # From
/// This type implements [`From`] on:
/// - [`String`]
/// - [`str`]
/// - [`u8`], [`u16`], [`u32`], [`u64`]
///
/// and all of those wrapped in [`Option`].
///
/// If the `Option` is [`None`], [`Id::Null`] is returned.
///
/// Note that the `&str` implementations will allocate, use [`Id::from_static_str`]
/// (or just manually create the `Cow`) for a non-allocating `Id`.
///
/// ```rust
/// use json_rpc::Id;
///
/// assert_eq!(Id::from(String::new()), Id::Str("".into()));
/// assert_eq!(Id::from(Some(String::new())), Id::Str("".into()));
/// assert_eq!(Id::from(None::<String>), Id::Null);
/// assert_eq!(Id::from(123_u64), Id::Num(123_u64));
/// assert_eq!(Id::from(Some(123_u64)), Id::Num(123_u64));
/// assert_eq!(Id::from(None::<u64>), Id::Null);
/// ```
pub enum Id {
/// A JSON `null` value.
///
/// ```rust
/// use json_rpc::Id;
/// use serde_json::{from_value,to_value,json,Value};
///
/// assert_eq!(from_value::<Id>(json!(null)).unwrap(), Id::Null);
/// assert_eq!(to_value(Id::Null).unwrap(), Value::Null);
///
/// // Not a real `null`, but a string.
/// assert_eq!(from_value::<Id>(json!("null")).unwrap(), Id::Str("null".into()));
/// ```
Null,
/// A JSON `number` value.
Num(u64),
/// A JSON `string` value.
///
/// This is a `Cow<'static, str>` to support both 0-allocation for
/// `const` string ID's commonly found in programs, as well as support
/// for runtime [`String`]'s.
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::Id;
///
/// /// A program's static ID.
/// const ID: &'static str = "my_id";
///
/// // No allocation.
/// let s = Id::Str(Cow::Borrowed(ID));
///
/// // Runtime allocation.
/// let s = Id::Str(Cow::Owned("runtime_id".to_string()));
/// ```
Str(Cow<'static, str>),
}
impl Id {
/// This returns `Some(u64)` if [`Id`] is a number.
///
/// ```rust
/// use json_rpc::Id;
///
/// assert_eq!(Id::Num(0).as_u64(), Some(0));
/// assert_eq!(Id::Str("0".into()).as_u64(), None);
/// assert_eq!(Id::Null.as_u64(), None);
/// ```
pub const fn as_u64(&self) -> Option<u64> {
match self {
Self::Num(n) => Some(*n),
_ => None,
}
}
/// This returns `Some(&str)` if [`Id`] is a string.
///
/// ```rust
/// use json_rpc::Id;
///
/// assert_eq!(Id::Str("0".into()).as_str(), Some("0"));
/// assert_eq!(Id::Num(0).as_str(), None);
/// assert_eq!(Id::Null.as_str(), None);
/// ```
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Str(s) => Some(s.as_ref()),
_ => None,
}
}
/// Returns `true` if `self` is [`Id::Null`].
///
/// ```rust
/// use json_rpc::Id;
///
/// assert!(Id::Null.is_null());
/// assert!(!Id::Num(0).is_null());
/// assert!(!Id::Str("".into()).is_null());
/// ```
pub fn is_null(&self) -> bool {
*self == Self::Null
}
/// Create a new [`Id::Str`] from a static string.
///
/// ```rust
/// use json_rpc::Id;
///
/// assert_eq!(Id::from_static_str("hi"), Id::Str("hi".into()));
/// ```
pub const fn from_static_str(s: &'static str) -> Self {
Self::Str(Cow::Borrowed(s))
}
/// Inner infallible implementation of [`FromStr::from_str`]
const fn from_string(s: String) -> Self {
Self::Str(Cow::Owned(s))
}
}
impl std::str::FromStr for Id {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, std::convert::Infallible> {
Ok(Self::from_string(s.to_string()))
}
}
impl From<String> for Id {
fn from(s: String) -> Self {
Self::from_string(s)
}
}
impl From<&str> for Id {
fn from(s: &str) -> Self {
Self::from_string(s.to_string())
}
}
impl From<Option<String>> for Id {
fn from(s: Option<String>) -> Self {
match s {
Some(s) => Self::from_string(s),
None => Self::Null,
}
}
}
impl From<Option<&str>> for Id {
fn from(s: Option<&str>) -> Self {
let s = s.map(ToString::to_string);
s.into()
}
}
/// Implement `From<unsigned integer>` for `Id`.
///
/// Not a generic since that clashes with `From<String>`.
macro_rules! impl_u {
($($u:ty),*) => {
$(
impl From<$u> for Id {
fn from(u: $u) -> Self {
Self::Num(u64::from(u))
}
}
impl From<&$u> for Id {
fn from(u: &$u) -> Self {
Self::Num(u64::from(*u))
}
}
impl From<Option<$u>> for Id {
fn from(u: Option<$u>) -> Self {
match u {
Some(u) => Self::Num(u64::from(u)),
None => Self::Null,
}
}
}
)*
}
}
impl_u!(u8, u16, u32);
#[cfg(target_pointer_width = "64")]
impl_u!(u64);
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod test {
use super::*;
/// Basic [`Id::as_u64()`] tests.
#[test]
fn __as_u64() {
let id = Id::Num(u64::MIN);
assert_eq!(id.as_u64().unwrap(), u64::MIN);
let id = Id::Num(u64::MAX);
assert_eq!(id.as_u64().unwrap(), u64::MAX);
let id = Id::Null;
assert!(id.as_u64().is_none());
let id = Id::Str("".into());
assert!(id.as_u64().is_none());
}
/// Basic [`Id::as_str()`] tests.
#[test]
fn __as_str() {
let id = Id::Str("str".into());
assert_eq!(id.as_str().unwrap(), "str");
let id = Id::Null;
assert!(id.as_str().is_none());
let id = Id::Num(0);
assert!(id.as_str().is_none());
}
}

View file

@ -1 +1,110 @@
#![doc = include_str!("../README.md")]
//---------------------------------------------------------------------------------------------------- Lints
// Forbid lints.
// Our code, and code generated (e.g macros) cannot overrule these.
#![forbid(
// `unsafe` is allowed but it _must_ be
// commented with `SAFETY: reason`.
clippy::undocumented_unsafe_blocks,
// Never.
unused_unsafe,
redundant_semicolons,
unused_allocation,
coherence_leak_check,
while_true,
// Maybe can be put into `#[deny]`.
unconditional_recursion,
for_loops_over_fallibles,
unused_braces,
unused_labels,
keyword_idents,
non_ascii_idents,
variant_size_differences,
single_use_lifetimes,
// Probably can be put into `#[deny]`.
future_incompatible,
let_underscore,
break_with_label_and_loop,
duplicate_macro_attributes,
exported_private_dependencies,
large_assignments,
overlapping_range_endpoints,
semicolon_in_expressions_from_macros,
noop_method_call,
unreachable_pub,
)]
// Deny lints.
// Some of these are `#[allow]`'ed on a per-case basis.
#![deny(
clippy::all,
clippy::correctness,
clippy::suspicious,
clippy::style,
clippy::complexity,
clippy::perf,
clippy::pedantic,
clippy::nursery,
clippy::cargo,
clippy::missing_docs_in_private_items,
unused_mut,
missing_docs,
deprecated,
unused_comparisons,
nonstandard_style
)]
#![allow(
// FIXME: this lint affects crates outside of
// `database/` for some reason, allow for now.
clippy::cargo_common_metadata,
// FIXME: adding `#[must_use]` onto everything
// might just be more annoying than useful...
// although it is sometimes nice.
clippy::must_use_candidate,
// FIXME: good lint but too many false positives
// with our `Env` + `RwLock` setup.
clippy::significant_drop_tightening,
// FIXME: good lint but is less clear in most cases.
clippy::items_after_statements,
clippy::module_name_repetitions,
clippy::module_inception,
clippy::redundant_pub_crate,
clippy::option_if_let_else,
)]
// Allow some lints when running in debug mode.
#![cfg_attr(debug_assertions, allow(clippy::todo, clippy::multiple_crate_versions))]
// Allow some lints in tests.
#![cfg_attr(
test,
allow(
clippy::cognitive_complexity,
clippy::needless_pass_by_value,
clippy::cast_possible_truncation,
clippy::too_many_lines
)
)]
//---------------------------------------------------------------------------------------------------- Mod/Use
pub mod error;
mod id;
pub use id::Id;
mod version;
pub use version::Version;
mod request;
pub use request::Request;
mod response;
pub use response::Response;
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod tests;

354
rpc/json-rpc/src/request.rs Normal file
View file

@ -0,0 +1,354 @@
//! JSON-RPC 2.0 request object.
//---------------------------------------------------------------------------------------------------- Use
use serde::{Deserialize, Serialize};
use crate::{id::Id, version::Version};
//---------------------------------------------------------------------------------------------------- Request
/// [The request object](https://www.jsonrpc.org/specification#request_object).
///
/// The generic `T` is the body type of the request, i.e. it is the
/// type that holds both the `method` and `params`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Request<T> {
/// JSON-RPC protocol version; always `2.0`.
pub jsonrpc: Version,
/// An identifier established by the Client.
///
/// If it is not included it is assumed to be a
/// [notification](https://www.jsonrpc.org/specification#notification).
///
/// ### `None` vs `Some(Id::Null)`
/// This field will be completely omitted during serialization if [`None`],
/// however if it is `Some(Id::Null)`, it will be serialized as `"id": null`.
///
/// Note that the JSON-RPC 2.0 specification discourages the use of `Id::NUll`,
/// so if there is no ID needed, consider using `None`.
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Id>,
#[serde(flatten)]
/// The `method` and `params` fields.
///
/// - `method`: A type that serializes as the name of the method to be invoked.
/// - `params`: A structured value that holds the parameter values to be used during the invocation of the method.
///
/// As mentioned in the library documentation, there are no `method/params` fields in [`Request`],
/// they are both merged in this `body` field which is `#[serde(flatten)]`ed.
///
/// ### Invariant
/// Your `T` must serialize as `method` and `params` to comply with the specification.
pub body: T,
}
impl<T> Request<T> {
/// Create a new [`Self`] with no [`Id`].
///
/// ```rust
/// use json_rpc::Request;
///
/// assert_eq!(Request::new("").id, None);
/// ```
pub const fn new(body: T) -> Self {
Self {
jsonrpc: Version,
id: None,
body,
}
}
/// Create a new [`Self`] with an [`Id`].
///
/// ```rust
/// use json_rpc::{Id, Request};
///
/// assert_eq!(Request::new_with_id(Id::Num(0), "").id, Some(Id::Num(0)));
/// ```
pub const fn new_with_id(id: Id, body: T) -> Self {
Self {
jsonrpc: Version,
id: Some(id),
body,
}
}
/// Returns `true` if the request is [notification](https://www.jsonrpc.org/specification#notification).
///
/// In other words, if `id` is [`None`], this returns `true`.
///
/// ```rust
/// use json_rpc::{Id, Request};
///
/// assert!(Request::new("").is_notification());
/// assert!(!Request::new_with_id(Id::Null, "").is_notification());
/// ```
pub const fn is_notification(&self) -> bool {
self.id.is_none()
}
}
//---------------------------------------------------------------------------------------------------- Trait impl
impl<T> std::fmt::Display for Request<T>
where
T: std::fmt::Display + Serialize,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match serde_json::to_string_pretty(self) {
Ok(json) => write!(f, "{json}"),
Err(_) => Err(std::fmt::Error),
}
}
}
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod test {
use super::*;
use crate::{
id::Id,
tests::{assert_ser, Body},
};
use pretty_assertions::assert_eq;
use serde_json::{json, Value};
/// Basic serde tests.
#[test]
fn serde() {
let id = Id::Num(123);
let body = Body {
method: "a_method".into(),
params: [0, 1, 2],
};
let req = Request::new_with_id(id, body);
assert!(!req.is_notification());
let ser: String = serde_json::to_string(&req).unwrap();
let de: Request<Body<[u8; 3]>> = serde_json::from_str(&ser).unwrap();
assert_eq!(req, de);
}
/// Asserts that fields must be `lowercase`.
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"missing field `jsonrpc`\", line: 1, column: 63)"
)]
fn lowercase() {
let id = Id::Num(123);
let body = Body {
method: "a_method".into(),
params: [0, 1, 2],
};
let req = Request::new_with_id(id, body);
let ser: String = serde_json::to_string(&req).unwrap();
assert_eq!(
ser,
r#"{"jsonrpc":"2.0","id":123,"method":"a_method","params":[0,1,2]}"#,
);
let mixed_case = r#"{"jSoNRPC":"2.0","ID":123,"method":"a_method","params":[0,1,2]}"#;
let de: Request<Body<[u8; 3]>> = serde_json::from_str(mixed_case).unwrap();
assert_eq!(de, req);
}
/// Tests that null `id` shows when serializing.
#[test]
fn request_null_id() {
let req = Request::new_with_id(
Id::Null,
Body {
method: "m".into(),
params: "p".to_string(),
},
);
let json = json!({
"jsonrpc": "2.0",
"id": null,
"method": "m",
"params": "p",
});
assert_ser(&req, &json);
}
/// Tests that a `None` `id` omits the field when serializing.
#[test]
fn request_none_id() {
let req = Request::new(Body {
method: "a".into(),
params: "b".to_string(),
});
let json = json!({
"jsonrpc": "2.0",
"method": "a",
"params": "b",
});
assert_ser(&req, &json);
}
/// Tests that omitting `params` omits the field when serializing.
#[test]
fn request_no_params() {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct NoParamMethod {
method: String,
}
let req = Request::new_with_id(
Id::Num(123),
NoParamMethod {
method: "asdf".to_string(),
},
);
let json = json!({
"jsonrpc": "2.0",
"id": 123,
"method": "asdf",
});
assert_ser(&req, &json);
}
/// Tests that tagged enums serialize correctly.
#[test]
fn request_tagged_enums() {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct GetHeight {
height: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
#[serde(rename_all = "snake_case")]
enum Methods {
GetHeight(/* param: */ GetHeight),
}
let req = Request::new_with_id(Id::Num(123), Methods::GetHeight(GetHeight { height: 0 }));
let json = json!({
"jsonrpc": "2.0",
"id": 123,
"method": "get_height",
"params": {
"height": 0,
},
});
assert_ser(&req, &json);
}
/// Tests that requests serialize into the expected JSON value.
#[test]
fn request_is_expected_value() {
// Test values: (request, expected_value)
let array: [(Request<Body<[u8; 3]>>, Value); 3] = [
(
Request::new_with_id(
Id::Num(123),
Body {
method: "method_1".into(),
params: [0, 1, 2],
},
),
json!({
"jsonrpc": "2.0",
"id": 123,
"method": "method_1",
"params": [0, 1, 2],
}),
),
(
Request::new_with_id(
Id::Null,
Body {
method: "method_2".into(),
params: [3, 4, 5],
},
),
json!({
"jsonrpc": "2.0",
"id": null,
"method": "method_2",
"params": [3, 4, 5],
}),
),
(
Request::new_with_id(
Id::Str("string_id".into()),
Body {
method: "method_3".into(),
params: [6, 7, 8],
},
),
json!({
"jsonrpc": "2.0",
"method": "method_3",
"id": "string_id",
"params": [6, 7, 8],
}),
),
];
for (request, expected_value) in array {
assert_ser(&request, &expected_value);
}
}
/// Tests that non-ordered fields still deserialize okay.
#[test]
fn deserialize_out_of_order_keys() {
let expected = Request::new_with_id(
Id::Str("id".into()),
Body {
method: "method".into(),
params: [0, 1, 2],
},
);
let json = json!({
"method": "method",
"id": "id",
"params": [0, 1, 2],
"jsonrpc": "2.0",
});
let resp = serde_json::from_value::<Request<Body<[u8; 3]>>>(json).unwrap();
assert_eq!(resp, expected);
}
/// Tests that unknown fields are ignored, and deserialize continues.
/// Also that unicode and backslashes work.
#[test]
fn unknown_fields_and_unicode() {
let expected = Request::new_with_id(
Id::Str("id".into()),
Body {
method: "method".into(),
params: [0, 1, 2],
},
);
let json = json!({
"unknown_field": 123,
"method": "method",
"unknown_field": 123,
"id": "id",
"\nhello": 123,
"params": [0, 1, 2],
"\u{00f8}": 123,
"jsonrpc": "2.0",
"unknown_field": 123,
});
let resp = serde_json::from_value::<Request<Body<[u8; 3]>>>(json).unwrap();
assert_eq!(resp, expected);
}
}

View file

@ -0,0 +1,485 @@
//! JSON-RPC 2.0 response object.
//---------------------------------------------------------------------------------------------------- Use
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
use crate::{error::ErrorObject, id::Id, version::Version};
//---------------------------------------------------------------------------------------------------- Response
/// [The response object](https://www.jsonrpc.org/specification#response_object).
///
/// The generic `T` is the response payload, i.e. it is the
/// type that holds both the `method` and `params`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response<T> {
/// JSON-RPC protocol version; always `2.0`.
pub jsonrpc: Version,
/// This field must always be present in serialized JSON.
///
/// ### JSON-RPC 2.0 rules
/// - The [`Response`]'s ID must be the same as the [`Request`](crate::Request)
/// - If the `Request` omitted the `id` field, there should be no `Response`
/// - If there was an error in detecting the `Request`'s ID, the `Response` must contain an [`Id::Null`]
pub id: Id,
/// The response payload.
///
/// ### JSON-RPC 2.0 rules
/// - This must be [`Ok`] upon success
/// - This must be [`Err`] upon error
/// - This can be any (de)serializable data `T` on success
/// - This must be [`ErrorObject`] on errors
pub payload: Result<T, ErrorObject>,
}
impl<T> Response<T> {
/// Creates a successful response.
///
/// ```rust
/// use json_rpc::{Id, Response};
///
/// let ok = Response::ok(Id::Num(123), "OK");
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":123,"result":"OK"}"#);
/// ```
pub const fn ok(id: Id, result: T) -> Self {
Self {
jsonrpc: Version,
id,
payload: Ok(result),
}
}
/// Creates an error response.
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let err = ErrorObject {
/// code: 0.into(),
/// message: "m".into(),
/// data: Some("d".into()),
/// };
///
/// let ok = Response::<()>::err(Id::Num(123), err);
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":123,"error":{"code":0,"message":"m","data":"d"}}"#);
/// ```
pub const fn err(id: Id, error: ErrorObject) -> Self {
Self {
jsonrpc: Version,
id,
payload: Err(error),
}
}
/// Creates an error response using [`ErrorObject::parse_error`].
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let ok = Response::<()>::parse_error(Id::Num(0));
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32700,"message":"Parse error"}}"#);
/// ```
pub const fn parse_error(id: Id) -> Self {
Self {
jsonrpc: Version,
payload: Err(ErrorObject::parse_error()),
id,
}
}
/// Creates an error response using [`ErrorObject::invalid_request`].
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let ok = Response::<()>::invalid_request(Id::Num(0));
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32600,"message":"Invalid Request"}}"#);
/// ```
pub const fn invalid_request(id: Id) -> Self {
Self {
jsonrpc: Version,
payload: Err(ErrorObject::invalid_request()),
id,
}
}
/// Creates an error response using [`ErrorObject::method_not_found`].
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let ok = Response::<()>::method_not_found(Id::Num(0));
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32601,"message":"Method not found"}}"#);
/// ```
pub const fn method_not_found(id: Id) -> Self {
Self {
jsonrpc: Version,
payload: Err(ErrorObject::method_not_found()),
id,
}
}
/// Creates an error response using [`ErrorObject::invalid_params`].
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let ok = Response::<()>::invalid_params(Id::Num(0));
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32602,"message":"Invalid params"}}"#);
/// ```
pub const fn invalid_params(id: Id) -> Self {
Self {
jsonrpc: Version,
payload: Err(ErrorObject::invalid_params()),
id,
}
}
/// Creates an error response using [`ErrorObject::internal_error`].
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let ok = Response::<()>::internal_error(Id::Num(0));
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32603,"message":"Internal error"}}"#);
/// ```
pub const fn internal_error(id: Id) -> Self {
Self {
jsonrpc: Version,
payload: Err(ErrorObject::internal_error()),
id,
}
}
}
//---------------------------------------------------------------------------------------------------- Trait impl
impl<T> std::fmt::Display for Response<T>
where
T: Serialize,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match serde_json::to_string_pretty(self) {
Ok(json) => write!(f, "{json}"),
Err(_) => Err(std::fmt::Error),
}
}
}
//---------------------------------------------------------------------------------------------------- Serde impl
impl<T> Serialize for Response<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("Response", 3)?;
s.serialize_field("jsonrpc", &self.jsonrpc)?;
// This member is required.
//
// Even if `null`, or the client `Request` didn't include one.
s.serialize_field("id", &self.id)?;
match &self.payload {
Ok(r) => s.serialize_field("result", r)?,
Err(e) => s.serialize_field("error", e)?,
}
s.end()
}
}
// [`Response`] has a manual deserialization implementation because
// we need to confirm `result` and `error` don't both exist:
//
// > Either the result member or error member MUST be included, but both members MUST NOT be included.
//
// <https://www.jsonrpc.org/specification#error_object>
impl<'de, T> Deserialize<'de> for Response<T>
where
T: Deserialize<'de> + 'de,
{
fn deserialize<D: Deserializer<'de>>(der: D) -> Result<Self, D::Error> {
use std::marker::PhantomData;
use serde::de::{Error, MapAccess, Visitor};
/// This type represents the key values within [`Response`].
enum Key {
/// "jsonrpc" field.
JsonRpc,
/// "result" field.
Result,
/// "error" field.
Error,
/// "id" field.
Id,
/// Any other unknown field (ignored).
Unknown,
}
// Deserialization for [`Response`]'s key fields.
//
// This ignores unknown keys.
impl<'de> Deserialize<'de> for Key {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
/// Serde visitor for [`Response`]'s key fields.
struct KeyVisitor;
impl Visitor<'_> for KeyVisitor {
type Value = Key;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("`jsonrpc`, `id`, `result`, `error`")
}
fn visit_str<E>(self, string: &str) -> Result<Key, E>
where
E: Error,
{
// PERF: this match is in order of how this library serializes fields.
match string {
"jsonrpc" => Ok(Key::JsonRpc),
"id" => Ok(Key::Id),
"result" => Ok(Key::Result),
"error" => Ok(Key::Error),
// Ignore any other keys that appear
// and continue deserialization.
_ => Ok(Key::Unknown),
}
}
}
deserializer.deserialize_identifier(KeyVisitor)
}
}
/// Serde visitor for the key-value map of [`Response`].
struct MapVisit<T>(PhantomData<T>);
// Deserialization for [`Response`]'s key and values (the JSON map).
impl<'de, T> Visitor<'de> for MapVisit<T>
where
T: Deserialize<'de> + 'de,
{
type Value = Response<T>;
fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
formatter.write_str("JSON-RPC 2.0 Response")
}
/// This is a loop that goes over every key-value pair
/// and fills out the necessary fields.
///
/// If both `result/error` appear then this
/// deserialization will error, as to
/// follow the JSON-RPC 2.0 specification.
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
// Initialize values.
let mut jsonrpc = None;
let mut payload = None;
let mut id = None;
// Loop over map, filling values.
while let Some(key) = map.next_key::<Key>()? {
// PERF: this match is in order of how this library serializes fields.
match key {
Key::JsonRpc => jsonrpc = Some(map.next_value::<Version>()?),
Key::Id => id = Some(map.next_value::<Id>()?),
Key::Result => {
if payload.is_none() {
payload = Some(Ok(map.next_value::<T>()?));
} else {
return Err(serde::de::Error::duplicate_field("result/error"));
}
}
Key::Error => {
if payload.is_none() {
payload = Some(Err(map.next_value::<ErrorObject>()?));
} else {
return Err(serde::de::Error::duplicate_field("result/error"));
}
}
Key::Unknown => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
// Make sure all our key-value pairs are set and correct.
match (jsonrpc, id, payload) {
// Response with a single `result` or `error`.
(Some(jsonrpc), Some(id), Some(payload)) => Ok(Response {
jsonrpc,
id,
payload,
}),
// No fields existed.
(None, None, None) => Err(Error::missing_field("jsonrpc + id + result/error")),
// Some field was missing.
(None, _, _) => Err(Error::missing_field("jsonrpc")),
(_, None, _) => Err(Error::missing_field("id")),
(_, _, None) => Err(Error::missing_field("result/error")),
}
}
}
/// All expected fields of the [`Response`] type.
const FIELDS: &[&str; 4] = &["jsonrpc", "id", "result", "error"];
der.deserialize_struct("Response", FIELDS, MapVisit(PhantomData))
}
}
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod test {
use serde_json::json;
use super::*;
use crate::id::Id;
/// Basic serde test on OK results.
#[test]
fn serde_result() {
let result = String::from("result_ok");
let id = Id::Num(123);
let req = Response::ok(id.clone(), result.clone());
let ser: String = serde_json::to_string(&req).unwrap();
let de: Response<String> = serde_json::from_str(&ser).unwrap();
assert_eq!(de.payload.unwrap(), result);
assert_eq!(de.id, id);
}
/// Basic serde test on errors.
#[test]
fn serde_error() {
let error = ErrorObject::internal_error();
let id = Id::Num(123);
let req: Response<String> = Response::err(id.clone(), error.clone());
let ser: String = serde_json::to_string(&req).unwrap();
let de: Response<String> = serde_json::from_str(&ser).unwrap();
assert_eq!(de.payload.unwrap_err(), error);
assert_eq!(de.id, id);
}
/// Test that the `result` and `error` fields are mutually exclusive.
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"duplicate field `result/error`\", line: 0, column: 0)"
)]
fn result_error_mutually_exclusive() {
let e = ErrorObject::internal_error();
let j = json!({
"jsonrpc": "2.0",
"id": 0,
"result": "",
"error": e
});
serde_json::from_value::<Response<String>>(j).unwrap();
}
/// Test that the `result` and `error` fields can repeat (and get overwritten).
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"duplicate field `result/error`\", line: 1, column: 45)"
)]
fn result_repeat() {
// `result`
let json = r#"{"jsonrpc":"2.0","id":0,"result":"a","result":"b"}"#;
serde_json::from_str::<Response<String>>(json).unwrap();
}
/// Test that the `error` field cannot repeat.
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"duplicate field `result/error`\", line: 1, column: 83)"
)]
fn error_repeat() {
let e = ErrorObject::invalid_request();
let e = serde_json::to_string(&e).unwrap();
let json = format!(r#"{{"jsonrpc":"2.0","id":0,"error":{e},"error":{e}}}"#);
serde_json::from_str::<Response<String>>(&json).unwrap();
}
/// Test that the `id` field must exist.
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"missing field `id`\", line: 0, column: 0)"
)]
fn id_must_exist() {
let j = json!({
"jsonrpc": "2.0",
"result": "",
});
serde_json::from_value::<Response<String>>(j).unwrap();
}
/// Tests that non-ordered fields still deserialize okay.
#[test]
fn deserialize_out_of_order_keys() {
let e = ErrorObject::internal_error();
let j = json!({
"error": e,
"id": 0,
"jsonrpc": "2.0"
});
let resp = serde_json::from_value::<Response<String>>(j).unwrap();
assert_eq!(resp, Response::internal_error(Id::Num(0)));
let ok = Response::ok(Id::Num(0), "OK".to_string());
let j = json!({
"result": "OK",
"id": 0,
"jsonrpc": "2.0"
});
let resp = serde_json::from_value::<Response<String>>(j).unwrap();
assert_eq!(resp, ok);
}
/// Asserts that fields must be `lowercase`.
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"missing field `jsonrpc`\", line: 1, column: 40)"
)]
fn lowercase() {
let mixed_case = r#"{"jSoNRPC":"2.0","id":123,"result":"OK"}"#;
serde_json::from_str::<Response<String>>(mixed_case).unwrap();
}
/// Tests that unknown fields are ignored, and deserialize continues.
/// Also that unicode and backslashes work.
#[test]
fn unknown_fields_and_unicode() {
let e = ErrorObject::internal_error();
let j = json!({
"error": e,
"\u{00f8}": 123,
"id": 0,
"unknown_field": 123,
"jsonrpc": "2.0",
"unknown_field": 123
});
let resp = serde_json::from_value::<Response<String>>(j).unwrap();
assert_eq!(resp, Response::internal_error(Id::Num(0)));
}
}

249
rpc/json-rpc/src/tests.rs Normal file
View file

@ -0,0 +1,249 @@
//! Tests and utilities.
#![allow(
clippy::unreadable_literal,
clippy::manual_string_new,
clippy::struct_field_names
)]
//---------------------------------------------------------------------------------------------------- Use
use std::borrow::Cow;
use pretty_assertions::assert_eq;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{to_string, to_string_pretty, to_value, Value};
use crate::{Id, Request, Response};
//---------------------------------------------------------------------------------------------------- Body
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) struct Body<P> {
pub(crate) method: Cow<'static, str>,
pub(crate) params: P,
}
//---------------------------------------------------------------------------------------------------- Free functions
/// Assert input and output of serialization are the same.
pub(crate) fn assert_ser<T>(t: &T, expected_value: &Value)
where
T: Serialize + std::fmt::Debug + Clone + PartialEq,
{
let value = to_value(t).unwrap();
assert_eq!(value, *expected_value);
}
/// Assert input and output of string serialization are the same.
pub(crate) fn assert_ser_string<T>(t: &T, expected_string: &str)
where
T: Serialize + std::fmt::Debug + Clone + PartialEq,
{
let string = to_string(t).unwrap();
assert_eq!(string, expected_string);
}
/// Assert input and output of (pretty) string serialization are the same.
pub(crate) fn assert_ser_string_pretty<T>(t: &T, expected_string: &str)
where
T: Serialize + std::fmt::Debug + Clone + PartialEq,
{
let string = to_string_pretty(t).unwrap();
assert_eq!(string, expected_string);
}
/// Tests an input JSON string matches an expected type `T`.
fn assert_de<T>(json: &'static str, expected: T)
where
T: DeserializeOwned + std::fmt::Debug + Clone + PartialEq,
{
let t = serde_json::from_str::<T>(json).unwrap();
assert_eq!(t, expected);
}
//---------------------------------------------------------------------------------------------------- Types
// Parameter type.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct GetBlock {
height: u64,
}
// Method enum containing all params.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "method", content = "params")]
#[serde(rename_all = "snake_case")]
enum Methods {
GetBlock(GetBlock),
GetBlockCount,
}
//---------------------------------------------------------------------------------------------------- TESTS
/// Tests that Monero's `get_block` request and response
/// in JSON string form gets correctly (de)serialized.
#[test]
fn monero_jsonrpc_get_block() {
//--- Request
const REQUEST: &str =
r#"{"jsonrpc":"2.0","id":"0","method":"get_block","params":{"height":123}}"#;
let request = Request::new_with_id(
Id::Str("0".into()),
Methods::GetBlock(GetBlock { height: 123 }),
);
assert_ser_string(&request, REQUEST);
assert_de(REQUEST, request);
//--- Response
const RESPONSE: &str = r#"{
"jsonrpc": "2.0",
"id": "0",
"result": {
"blob": "01008cb1c49a0572244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5aaa8f2ba01b70101ff7b08ebcc2202f917ac2dc38c0e0735f2c97df4a307a445b32abaf0ad528c385ae11a7e767d3880897a0215a39af4cf4c67136ecc048d9296b4cb7a6be61275a0ef207eb4cbb427cc216380dac40902dabddeaada9f4ed2512f9b9613a7ced79d3996ad5050ca542f31032bd638193380c2d72f02965651ab4a26264253bb8a4ccb9b33afbc8c8b4f3e331baf50537b8ee80364038088aca3cf020235a7367536243629560b8a40f104352c89a2d4719e86f54175c2e4e3ecfec9938090cad2c60e023b623a01eace71e9b37d2bfac84f9aafc85dbf62a0f452446c5de0ca50cf910580e08d84ddcb010258c370ee02069943e5440294aeafae29656f9782b0a565d26065bb7af07a6af980c0caf384a30202899a53eeb05852a912bcbc6fa78e4c85f0b059726b0b8f0753e7aa54fc9d7ce82101351af203765d1679e2a9458ab6737d289e18c49766d41fc31a2bf0fe32dd196200",
"block_header": {
"block_size": 345,
"block_weight": 345,
"cumulative_difficulty": 4646953,
"cumulative_difficulty_top64": 0,
"depth": 3166236,
"difficulty": 51263,
"difficulty_top64": 0,
"hash": "ff617b489e91f5db76f6f2cc9b3f03236e09fb191b4238f4a1e64185a6c28019",
"height": 123,
"long_term_weight": 345,
"major_version": 1,
"miner_tx_hash": "054ba33024e72dfe8dafabc8af7c0e070e9aca3a9df44569cfa3c96669f9542f",
"minor_version": 0,
"nonce": 3136465066,
"num_txes": 0,
"orphan_status": false,
"pow_hash": "",
"prev_hash": "72244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5",
"reward": 17590122566891,
"timestamp": 1397823628,
"wide_cumulative_difficulty": "0x46e829",
"wide_difficulty": "0xc83f"
},
"credits": 0,
"json": "{\n \"major_version\": 1, \n \"minor_version\": 0, \n \"timestamp\": 1397823628, \n \"prev_id\": \"72244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5\", \n \"nonce\": 3136465066, \n \"miner_tx\": {\n \"version\": 1, \n \"unlock_time\": 183, \n \"vin\": [ {\n \"gen\": {\n \"height\": 123\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 566891, \n \"target\": {\n \"key\": \"f917ac2dc38c0e0735f2c97df4a307a445b32abaf0ad528c385ae11a7e767d38\"\n }\n }, {\n \"amount\": 2000000, \n \"target\": {\n \"key\": \"15a39af4cf4c67136ecc048d9296b4cb7a6be61275a0ef207eb4cbb427cc2163\"\n }\n }, {\n \"amount\": 20000000, \n \"target\": {\n \"key\": \"dabddeaada9f4ed2512f9b9613a7ced79d3996ad5050ca542f31032bd6381933\"\n }\n }, {\n \"amount\": 100000000, \n \"target\": {\n \"key\": \"965651ab4a26264253bb8a4ccb9b33afbc8c8b4f3e331baf50537b8ee8036403\"\n }\n }, {\n \"amount\": 90000000000, \n \"target\": {\n \"key\": \"35a7367536243629560b8a40f104352c89a2d4719e86f54175c2e4e3ecfec993\"\n }\n }, {\n \"amount\": 500000000000, \n \"target\": {\n \"key\": \"3b623a01eace71e9b37d2bfac84f9aafc85dbf62a0f452446c5de0ca50cf9105\"\n }\n }, {\n \"amount\": 7000000000000, \n \"target\": {\n \"key\": \"58c370ee02069943e5440294aeafae29656f9782b0a565d26065bb7af07a6af9\"\n }\n }, {\n \"amount\": 10000000000000, \n \"target\": {\n \"key\": \"899a53eeb05852a912bcbc6fa78e4c85f0b059726b0b8f0753e7aa54fc9d7ce8\"\n }\n }\n ], \n \"extra\": [ 1, 53, 26, 242, 3, 118, 93, 22, 121, 226, 169, 69, 138, 182, 115, 125, 40, 158, 24, 196, 151, 102, 212, 31, 195, 26, 43, 240, 254, 50, 221, 25, 98\n ], \n \"signatures\": [ ]\n }, \n \"tx_hashes\": [ ]\n}",
"miner_tx_hash": "054ba33024e72dfe8dafabc8af7c0e070e9aca3a9df44569cfa3c96669f9542f",
"status": "OK",
"top_hash": "",
"untrusted": false
}
}"#;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct Json {
blob: String,
block_header: BlockHeader,
credits: u64,
json: String,
miner_tx_hash: String,
status: String,
top_hash: String,
untrusted: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct BlockHeader {
block_size: u64,
block_weight: u64,
cumulative_difficulty: u64,
cumulative_difficulty_top64: u64,
depth: u64,
difficulty: u64,
difficulty_top64: u64,
hash: String,
height: u64,
long_term_weight: u64,
major_version: u64,
miner_tx_hash: String,
minor_version: u64,
nonce: u64,
num_txes: u64,
orphan_status: bool,
pow_hash: String,
prev_hash: String,
reward: u64,
timestamp: u64,
wide_cumulative_difficulty: String,
wide_difficulty: String,
}
let payload = Json {
blob: "01008cb1c49a0572244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5aaa8f2ba01b70101ff7b08ebcc2202f917ac2dc38c0e0735f2c97df4a307a445b32abaf0ad528c385ae11a7e767d3880897a0215a39af4cf4c67136ecc048d9296b4cb7a6be61275a0ef207eb4cbb427cc216380dac40902dabddeaada9f4ed2512f9b9613a7ced79d3996ad5050ca542f31032bd638193380c2d72f02965651ab4a26264253bb8a4ccb9b33afbc8c8b4f3e331baf50537b8ee80364038088aca3cf020235a7367536243629560b8a40f104352c89a2d4719e86f54175c2e4e3ecfec9938090cad2c60e023b623a01eace71e9b37d2bfac84f9aafc85dbf62a0f452446c5de0ca50cf910580e08d84ddcb010258c370ee02069943e5440294aeafae29656f9782b0a565d26065bb7af07a6af980c0caf384a30202899a53eeb05852a912bcbc6fa78e4c85f0b059726b0b8f0753e7aa54fc9d7ce82101351af203765d1679e2a9458ab6737d289e18c49766d41fc31a2bf0fe32dd196200".into(),
block_header: BlockHeader {
block_size: 345,
block_weight: 345,
cumulative_difficulty: 4646953,
cumulative_difficulty_top64: 0,
depth: 3166236,
difficulty: 51263,
difficulty_top64: 0,
hash: "ff617b489e91f5db76f6f2cc9b3f03236e09fb191b4238f4a1e64185a6c28019".into(),
height: 123,
long_term_weight: 345,
major_version: 1,
miner_tx_hash: "054ba33024e72dfe8dafabc8af7c0e070e9aca3a9df44569cfa3c96669f9542f".into(),
minor_version: 0,
nonce: 3136465066,
num_txes: 0,
orphan_status: false,
pow_hash: "".into(),
prev_hash: "72244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5".into(),
reward: 17590122566891,
timestamp: 1397823628,
wide_cumulative_difficulty: "0x46e829".into(),
wide_difficulty: "0xc83f".into(),
},
credits: 0,
json: "{\n \"major_version\": 1, \n \"minor_version\": 0, \n \"timestamp\": 1397823628, \n \"prev_id\": \"72244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5\", \n \"nonce\": 3136465066, \n \"miner_tx\": {\n \"version\": 1, \n \"unlock_time\": 183, \n \"vin\": [ {\n \"gen\": {\n \"height\": 123\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 566891, \n \"target\": {\n \"key\": \"f917ac2dc38c0e0735f2c97df4a307a445b32abaf0ad528c385ae11a7e767d38\"\n }\n }, {\n \"amount\": 2000000, \n \"target\": {\n \"key\": \"15a39af4cf4c67136ecc048d9296b4cb7a6be61275a0ef207eb4cbb427cc2163\"\n }\n }, {\n \"amount\": 20000000, \n \"target\": {\n \"key\": \"dabddeaada9f4ed2512f9b9613a7ced79d3996ad5050ca542f31032bd6381933\"\n }\n }, {\n \"amount\": 100000000, \n \"target\": {\n \"key\": \"965651ab4a26264253bb8a4ccb9b33afbc8c8b4f3e331baf50537b8ee8036403\"\n }\n }, {\n \"amount\": 90000000000, \n \"target\": {\n \"key\": \"35a7367536243629560b8a40f104352c89a2d4719e86f54175c2e4e3ecfec993\"\n }\n }, {\n \"amount\": 500000000000, \n \"target\": {\n \"key\": \"3b623a01eace71e9b37d2bfac84f9aafc85dbf62a0f452446c5de0ca50cf9105\"\n }\n }, {\n \"amount\": 7000000000000, \n \"target\": {\n \"key\": \"58c370ee02069943e5440294aeafae29656f9782b0a565d26065bb7af07a6af9\"\n }\n }, {\n \"amount\": 10000000000000, \n \"target\": {\n \"key\": \"899a53eeb05852a912bcbc6fa78e4c85f0b059726b0b8f0753e7aa54fc9d7ce8\"\n }\n }\n ], \n \"extra\": [ 1, 53, 26, 242, 3, 118, 93, 22, 121, 226, 169, 69, 138, 182, 115, 125, 40, 158, 24, 196, 151, 102, 212, 31, 195, 26, 43, 240, 254, 50, 221, 25, 98\n ], \n \"signatures\": [ ]\n }, \n \"tx_hashes\": [ ]\n}".into(),
miner_tx_hash: "054ba33024e72dfe8dafabc8af7c0e070e9aca3a9df44569cfa3c96669f9542f".into(),
status: "OK".into(),
top_hash: "".into(),
untrusted: false
};
let response = Response::ok(Id::Str("0".into()), payload);
assert_ser_string_pretty(&response, RESPONSE);
assert_de(RESPONSE, response);
}
/// Tests that Monero's `get_block_count` request and response
/// in JSON string form gets correctly (de)serialized.
#[test]
fn monero_jsonrpc_get_block_count() {
//--- Request
const REQUEST: &str = r#"{"jsonrpc":"2.0","id":0,"method":"get_block_count"}"#;
let request = Request::new_with_id(Id::Num(0), Methods::GetBlockCount);
assert_ser_string(&request, REQUEST);
assert_de(REQUEST, request);
//--- Response
const RESPONSE: &str = r#"{
"jsonrpc": "2.0",
"id": 0,
"result": {
"count": 3166375,
"status": "OK",
"untrusted": false
}
}"#;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct Json {
count: u64,
status: String,
untrusted: bool,
}
let payload = Json {
count: 3166375,
status: "OK".into(),
untrusted: false,
};
let response = Response::ok(Id::Num(0), payload);
assert_ser_string_pretty(&response, RESPONSE);
assert_de(RESPONSE, response);
}

119
rpc/json-rpc/src/version.rs Normal file
View file

@ -0,0 +1,119 @@
//! [`Version`]: JSON-RPC 2.0 version marker.
//---------------------------------------------------------------------------------------------------- Use
use serde::de::{Error, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
//---------------------------------------------------------------------------------------------------- Version
/// [Protocol version marker](https://www.jsonrpc.org/specification#compatibility).
///
/// This represents the JSON-RPC version.
///
/// This is an empty marker type that always gets (de)serialized as [`Self::TWO`].
///
/// It is the only valid value for the `jsonrpc` field in the
/// [`Request`](crate::Request) and [`Response`](crate::Request) objects.
///
/// JSON-RPC 2.0 allows for backwards compatibility with `1.0` but this crate
/// (and this type) will not accept that, and will fail in deserialization
/// when encountering anything but [`Self::TWO`].
///
/// # Formatting
/// When using Rust formatting, [`Version`] is formatted as `2.0`.
///
/// When using JSON serialization, `Version` is formatted with quotes indicating
/// it is a JSON string and not a JSON float, i.e. it gets formatted as `"2.0"`, not `2.0`.
///
/// # Example
/// ```rust
/// use json_rpc::Version;
/// use serde_json::{to_string, to_string_pretty, from_str};
///
/// assert_eq!(Version::TWO, "2.0");
/// let version = Version;
///
/// // All debug/display formats are the same.
/// assert_eq!(format!("{version:?}"), Version::TWO);
/// assert_eq!(format!("{version:#?}"), Version::TWO);
/// assert_eq!(format!("{version}"), Version::TWO);
///
/// // JSON serialization will add extra quotes to
/// // indicate it is a string and not a float.
/// assert_eq!(to_string(&Version).unwrap(), "\"2.0\"");
/// assert_eq!(to_string_pretty(&Version).unwrap(), "\"2.0\"");
///
/// // Deserialization only accepts the JSON string "2.0".
/// assert!(from_str::<Version>(&"\"2.0\"").is_ok());
/// // This is JSON float, not a string.
/// assert!(from_str::<Version>(&"2.0").is_err());
///
/// assert!(from_str::<Version>(&"2").is_err());
/// assert!(from_str::<Version>(&"1.0").is_err());
/// assert!(from_str::<Version>(&"20").is_err());
/// assert!(from_str::<Version>(&"two").is_err());
/// assert!(from_str::<Version>(&"2.1").is_err());
/// assert!(from_str::<Version>(&"v2.0").is_err());
/// assert!(from_str::<Version>("").is_err());
/// ```
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Version;
impl Version {
/// The string `2.0`.
///
/// Note that this does not have extra quotes to mark
/// that it's a JSON string and not a float.
/// ```rust
/// use json_rpc::Version;
///
/// let string = format!("{}", Version);
/// assert_eq!(string, "2.0");
/// assert_ne!(string, "\"2.0\"");
/// ```
pub const TWO: &'static str = "2.0";
}
//---------------------------------------------------------------------------------------------------- Trait impl
impl Serialize for Version {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(Self::TWO)
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, r#"{}"#, Self::TWO)
}
}
impl std::fmt::Debug for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, r#"{}"#, Self::TWO)
}
}
//---------------------------------------------------------------------------------------------------- Serde impl
/// Empty serde visitor for [`Version`].
struct VersionVisitor;
impl Visitor<'_> for VersionVisitor {
type Value = Version;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("Identifier must be the exact string: \"2.0\"")
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
if v == Version::TWO {
Ok(Version)
} else {
Err(Error::invalid_value(serde::de::Unexpected::Str(v), &self))
}
}
}
impl<'de> Deserialize<'de> for Version {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
d.deserialize_str(VersionVisitor)
}
}