mirror of
https://github.com/Cuprate/cuprate.git
synced 2025-03-12 09:29:11 +00:00
Merge branch 'main' into block-downloader
This commit is contained in:
commit
3a7a8563e6
17 changed files with 2740 additions and 189 deletions
2
.github/workflows/audit.yml
vendored
2
.github/workflows/audit.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Cache
|
||||
uses: actions/cache@v3.2.3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
|
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -83,7 +83,7 @@ jobs:
|
|||
components: clippy
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target
|
||||
key: ${{ matrix.os }}
|
||||
|
|
2
.github/workflows/deny.yml
vendored
2
.github/workflows/deny.yml
vendored
|
@ -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
649
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
]
|
||||
|
|
|
@ -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
197
rpc/json-rpc/README.md
Normal 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");
|
||||
```
|
219
rpc/json-rpc/src/error/code.rs
Normal file
219
rpc/json-rpc/src/error/code.rs
Normal 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())
|
||||
}
|
||||
}
|
22
rpc/json-rpc/src/error/constants.rs
Normal file
22
rpc/json-rpc/src/error/constants.rs
Normal 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";
|
14
rpc/json-rpc/src/error/mod.rs
Normal file
14
rpc/json-rpc/src/error/mod.rs
Normal 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;
|
258
rpc/json-rpc/src/error/object.rs
Normal file
258
rpc/json-rpc/src/error/object.rs
Normal 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
242
rpc/json-rpc/src/id.rs
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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
354
rpc/json-rpc/src/request.rs
Normal 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);
|
||||
}
|
||||
}
|
485
rpc/json-rpc/src/response.rs
Normal file
485
rpc/json-rpc/src/response.rs
Normal 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
249
rpc/json-rpc/src/tests.rs
Normal 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
119
rpc/json-rpc/src/version.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue