jsonrpc/
client.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! # Client support
4//!
5//! Support for connecting to JSONRPC servers over HTTP, sending requests,
6//! and parsing responses
7
8use std::borrow::Cow;
9use std::collections::HashMap;
10use std::fmt;
11use std::hash::{Hash, Hasher};
12use std::sync::atomic;
13
14use serde_json::value::RawValue;
15use serde_json::Value;
16
17use crate::error::Error;
18use crate::{Request, Response};
19
20/// An interface for a transport over which to use the JSONRPC protocol.
21pub trait Transport: Send + Sync + 'static {
22    /// Sends an RPC request over the transport.
23    fn send_request(&self, _: Request) -> Result<Response, Error>;
24    /// Sends a batch of RPC requests over the transport.
25    fn send_batch(&self, _: &[Request]) -> Result<Vec<Response>, Error>;
26    /// Formats the target of this transport. I.e. the URL/socket/...
27    fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result;
28}
29
30/// A JSON-RPC client.
31///
32/// Creates a new Client using one of the transport-specific constructors e.g.,
33/// [`Client::simple_http`] for a bare-minimum HTTP transport.
34pub struct Client {
35    pub(crate) transport: Box<dyn Transport>,
36    nonce: atomic::AtomicUsize,
37}
38
39impl Client {
40    /// Creates a new client with the given transport.
41    pub fn with_transport<T: Transport>(transport: T) -> Client {
42        Client {
43            transport: Box::new(transport),
44            nonce: atomic::AtomicUsize::new(1),
45        }
46    }
47
48    /// Builds a request.
49    ///
50    /// To construct the arguments, one can use one of the shorthand methods
51    /// [`crate::arg`] or [`crate::try_arg`].
52    pub fn build_request<'a>(&self, method: &'a str, params: &'a [Box<RawValue>]) -> Request<'a> {
53        let nonce = self.nonce.fetch_add(1, atomic::Ordering::Relaxed);
54        Request {
55            method,
56            params,
57            id: serde_json::Value::from(nonce),
58            jsonrpc: Some("2.0"),
59        }
60    }
61
62    /// Sends a request to a client.
63    pub fn send_request(&self, request: Request) -> Result<Response, Error> {
64        self.transport.send_request(request)
65    }
66
67    /// Sends a batch of requests to the client.
68    ///
69    /// Note that the requests need to have valid IDs, so it is advised to create the requests
70    /// with [`Client::build_request`].
71    ///
72    /// # Returns
73    ///
74    /// The return vector holds the response for the request at the corresponding index. If no
75    /// response was provided, it's [`None`].
76    pub fn send_batch(&self, requests: &[Request]) -> Result<Vec<Option<Response>>, Error> {
77        if requests.is_empty() {
78            return Err(Error::EmptyBatch);
79        }
80
81        // If the request body is invalid JSON, the response is a single response object.
82        // We ignore this case since we are confident we are producing valid JSON.
83        let responses = self.transport.send_batch(requests)?;
84        if responses.len() > requests.len() {
85            return Err(Error::WrongBatchResponseSize);
86        }
87
88        //TODO(stevenroose) check if the server preserved order to avoid doing the mapping
89
90        // First index responses by ID and catch duplicate IDs.
91        let mut by_id = HashMap::with_capacity(requests.len());
92        for resp in responses.into_iter() {
93            let id = HashableValue(Cow::Owned(resp.id.clone()));
94            if let Some(dup) = by_id.insert(id, resp) {
95                return Err(Error::BatchDuplicateResponseId(dup.id));
96            }
97        }
98        // Match responses to the requests.
99        let results =
100            requests.iter().map(|r| by_id.remove(&HashableValue(Cow::Borrowed(&r.id)))).collect();
101
102        // Since we're also just producing the first duplicate ID, we can also just produce the
103        // first incorrect ID in case there are multiple.
104        if let Some(id) = by_id.keys().next() {
105            return Err(Error::WrongBatchResponseId((*id.0).clone()));
106        }
107
108        Ok(results)
109    }
110
111    /// Makes a request and deserializes the response.
112    ///
113    /// To construct the arguments, one can use one of the shorthand methods
114    /// [`crate::arg`] or [`crate::try_arg`].
115    pub fn call<R: for<'a> serde::de::Deserialize<'a>>(
116        &self,
117        method: &str,
118        args: &[Box<RawValue>],
119    ) -> Result<R, Error> {
120        let request = self.build_request(method, args);
121        let id = request.id.clone();
122
123        let response = self.send_request(request)?;
124        if response.jsonrpc.is_some() && response.jsonrpc != Some(From::from("2.0")) {
125            return Err(Error::VersionMismatch);
126        }
127        if response.id != id {
128            return Err(Error::NonceMismatch);
129        }
130
131        response.result()
132    }
133}
134
135impl fmt::Debug for crate::Client {
136    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137        write!(f, "jsonrpc::Client(")?;
138        self.transport.fmt_target(f)?;
139        write!(f, ")")
140    }
141}
142
143impl<T: Transport> From<T> for Client {
144    fn from(t: T) -> Client {
145        Client::with_transport(t)
146    }
147}
148
149/// Newtype around `Value` which allows hashing for use as hashmap keys,
150/// this is needed for batch requests.
151///
152/// The reason `Value` does not support `Hash` or `Eq` by itself
153/// is that it supports `f64` values; but for batch requests we
154/// will only be hashing the "id" field of the request/response
155/// pair, which should never need decimal precision and therefore
156/// never use `f64`.
157#[derive(Clone, PartialEq, Debug)]
158struct HashableValue<'a>(pub Cow<'a, Value>);
159
160impl<'a> Eq for HashableValue<'a> {}
161
162impl<'a> Hash for HashableValue<'a> {
163    fn hash<H: Hasher>(&self, state: &mut H) {
164        match *self.0.as_ref() {
165            Value::Null => "null".hash(state),
166            Value::Bool(false) => "false".hash(state),
167            Value::Bool(true) => "true".hash(state),
168            Value::Number(ref n) => {
169                "number".hash(state);
170                if let Some(n) = n.as_i64() {
171                    n.hash(state);
172                } else if let Some(n) = n.as_u64() {
173                    n.hash(state);
174                } else {
175                    n.to_string().hash(state);
176                }
177            }
178            Value::String(ref s) => {
179                "string".hash(state);
180                s.hash(state);
181            }
182            Value::Array(ref v) => {
183                "array".hash(state);
184                v.len().hash(state);
185                for obj in v {
186                    HashableValue(Cow::Borrowed(obj)).hash(state);
187                }
188            }
189            Value::Object(ref m) => {
190                "object".hash(state);
191                m.len().hash(state);
192                for (key, val) in m {
193                    key.hash(state);
194                    HashableValue(Cow::Borrowed(val)).hash(state);
195                }
196            }
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    use std::borrow::Cow;
206    use std::collections::HashSet;
207    use std::str::FromStr;
208    use std::sync;
209
210    struct DummyTransport;
211    impl Transport for DummyTransport {
212        fn send_request(&self, _: Request) -> Result<Response, Error> {
213            Err(Error::NonceMismatch)
214        }
215        fn send_batch(&self, _: &[Request]) -> Result<Vec<Response>, Error> {
216            Ok(vec![])
217        }
218        fn fmt_target(&self, _: &mut fmt::Formatter) -> fmt::Result {
219            Ok(())
220        }
221    }
222
223    #[test]
224    fn sanity() {
225        let client = Client::with_transport(DummyTransport);
226        assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 1);
227        let req1 = client.build_request("test", &[]);
228        assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 2);
229        let req2 = client.build_request("test", &[]);
230        assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 3);
231        assert!(req1.id != req2.id);
232    }
233
234    #[test]
235    fn hash_value() {
236        let val = HashableValue(Cow::Owned(Value::from_str("null").unwrap()));
237        let t = HashableValue(Cow::Owned(Value::from_str("true").unwrap()));
238        let f = HashableValue(Cow::Owned(Value::from_str("false").unwrap()));
239        let ns =
240            HashableValue(Cow::Owned(Value::from_str("[0, -0, 123.4567, -100000000]").unwrap()));
241        let m =
242            HashableValue(Cow::Owned(Value::from_str("{ \"field\": 0, \"field\": -0 }").unwrap()));
243
244        let mut coll = HashSet::new();
245
246        assert!(!coll.contains(&val));
247        coll.insert(val.clone());
248        assert!(coll.contains(&val));
249
250        assert!(!coll.contains(&t));
251        assert!(!coll.contains(&f));
252        coll.insert(t.clone());
253        assert!(coll.contains(&t));
254        assert!(!coll.contains(&f));
255        coll.insert(f.clone());
256        assert!(coll.contains(&t));
257        assert!(coll.contains(&f));
258
259        assert!(!coll.contains(&ns));
260        coll.insert(ns.clone());
261        assert!(coll.contains(&ns));
262
263        assert!(!coll.contains(&m));
264        coll.insert(m.clone());
265        assert!(coll.contains(&m));
266    }
267}