139 lines
3.3 KiB
Rust
139 lines
3.3 KiB
Rust
|
//! Minimal JSON-RPC 2.0 Client
|
||
|
//! The request/response code is taken from [here](https://github.com/althea-net/guac_rs/blob/master/web3/src/jsonrpc)
|
||
|
use reqwest::{Client, Error as ReqwestError};
|
||
|
use serde::{Deserialize, Serialize};
|
||
|
use serde_json::Value;
|
||
|
use std::fmt;
|
||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||
|
use thiserror::Error;
|
||
|
use url::Url;
|
||
|
|
||
|
#[derive(Debug)]
|
||
|
/// JSON-RPC 2.0 Client
|
||
|
pub struct HttpClient {
|
||
|
id: AtomicU64,
|
||
|
client: Client,
|
||
|
url: Url,
|
||
|
}
|
||
|
|
||
|
impl HttpClient {
|
||
|
/// Initializes a new HTTP Client
|
||
|
pub fn new(url: impl Into<Url>) -> Self {
|
||
|
Self {
|
||
|
id: AtomicU64::new(0),
|
||
|
client: Client::new(),
|
||
|
url: url.into(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Sends a POST request with the provided method and the params serialized as JSON
|
||
|
pub async fn request<T: Serialize, R: for<'a> Deserialize<'a>>(
|
||
|
&self,
|
||
|
method: &str,
|
||
|
params: Option<T>,
|
||
|
) -> Result<R, ClientError> {
|
||
|
let next_id = self.id.load(Ordering::SeqCst) + 1;
|
||
|
self.id.store(next_id, Ordering::SeqCst);
|
||
|
|
||
|
let payload = Request::new(next_id, method, params);
|
||
|
|
||
|
let res = self
|
||
|
.client
|
||
|
.post(self.url.as_ref())
|
||
|
.json(&payload)
|
||
|
.send()
|
||
|
.await?;
|
||
|
let res = res.json::<Response<R>>().await?;
|
||
|
|
||
|
Ok(res.data.into_result()?)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[derive(Error, Debug)]
|
||
|
pub enum ClientError {
|
||
|
#[error(transparent)]
|
||
|
ReqwestError(#[from] ReqwestError),
|
||
|
#[error(transparent)]
|
||
|
JsonRpcError(#[from] JsonRpcError),
|
||
|
}
|
||
|
|
||
|
#[derive(Serialize, Deserialize, Debug, Clone, Error)]
|
||
|
/// A JSON-RPC 2.0 error
|
||
|
pub struct JsonRpcError {
|
||
|
/// The error code
|
||
|
pub code: i64,
|
||
|
/// The error message
|
||
|
pub message: String,
|
||
|
/// Additional data
|
||
|
pub data: Option<Value>,
|
||
|
}
|
||
|
|
||
|
impl fmt::Display for JsonRpcError {
|
||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
write!(
|
||
|
f,
|
||
|
"(code: {}, message: {}, data: {:?})",
|
||
|
self.code, self.message, self.data
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[derive(Serialize, Deserialize, Debug)]
|
||
|
/// A JSON-RPC request
|
||
|
struct Request<'a, T> {
|
||
|
id: u64,
|
||
|
jsonrpc: &'a str,
|
||
|
method: &'a str,
|
||
|
params: Option<T>,
|
||
|
}
|
||
|
|
||
|
impl<'a, T> Request<'a, T> {
|
||
|
/// Creates a new JSON RPC request
|
||
|
fn new(id: u64, method: &'a str, params: Option<T>) -> Self {
|
||
|
Self {
|
||
|
id,
|
||
|
jsonrpc: "2.0",
|
||
|
method,
|
||
|
params,
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||
|
struct Response<T> {
|
||
|
id: u64,
|
||
|
jsonrpc: String,
|
||
|
#[serde(flatten)]
|
||
|
data: ResponseData<T>,
|
||
|
}
|
||
|
|
||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||
|
#[serde(untagged)]
|
||
|
enum ResponseData<R> {
|
||
|
Error { error: JsonRpcError },
|
||
|
Success { result: R },
|
||
|
}
|
||
|
|
||
|
impl<R> ResponseData<R> {
|
||
|
/// Consume response and return value
|
||
|
fn into_result(self) -> Result<R, JsonRpcError> {
|
||
|
match self {
|
||
|
ResponseData::Success { result } => Ok(result),
|
||
|
ResponseData::Error { error } => Err(error),
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[cfg(test)]
|
||
|
mod tests {
|
||
|
use super::*;
|
||
|
|
||
|
#[test]
|
||
|
fn response() {
|
||
|
let response: Response<u64> =
|
||
|
serde_json::from_str(r#"{"jsonrpc": "2.0", "result": 19, "id": 1}"#).unwrap();
|
||
|
assert_eq!(response.id, 1);
|
||
|
assert_eq!(response.data.into_result().unwrap(), 19);
|
||
|
}
|
||
|
}
|