diff --git a/dxr/src/lib.rs b/dxr/src/lib.rs
index 09d95b7..ea1793f 100644
--- a/dxr/src/lib.rs
+++ b/dxr/src/lib.rs
@@ -72,6 +72,9 @@ pub use fault::*;
mod impls;
+mod multicall;
+pub use multicall::*;
+
mod traits;
pub use traits::*;
diff --git a/dxr/src/multicall.rs b/dxr/src/multicall.rs
new file mode 100644
index 0000000..0d02313
--- /dev/null
+++ b/dxr/src/multicall.rs
@@ -0,0 +1,24 @@
+use crate::{DxrError, Member, Struct, TryToParams, TryToValue, Value};
+
+/// Convenience method for constructing arguments for "system.multicall" calls.
+///
+/// This method is more efficient than manually constructing the array of XML-RPC
+/// calls as structs (either by using a [`HashMap`] or by deriving [`TryToValue`]
+/// on a custom struct) because this method can rely on crate internals.
+pub fn multicall
(calls: Vec<(String, P)>) -> Result
+where
+ P: TryToParams,
+{
+ let params: Vec = calls
+ .into_iter()
+ .map(|(n, p)| {
+ let members = vec![
+ Member::new(String::from("methodName"), Value::string(n)),
+ Member::new(String::from("params"), p.try_to_params()?.try_to_value()?),
+ ];
+ Ok(Value::structure(Struct::new(members)))
+ })
+ .collect::, DxrError>>()?;
+
+ params.try_to_value()
+}
diff --git a/dxr/src/tests/multicall.rs b/dxr/src/tests/multicall.rs
new file mode 100644
index 0000000..c2ca680
--- /dev/null
+++ b/dxr/src/tests/multicall.rs
@@ -0,0 +1,11 @@
+use quick_xml::{de::from_str, se::to_string};
+
+use crate::values::{MethodCall, Value};
+
+#[test]
+fn to_multicall() {
+ let value = MethodCall::new(String::from("hello"), vec![]);
+ let expected = "system.multicall";
+
+ assert_eq!(to_string(&value).unwrap(), expected);
+}
diff --git a/dxr_client/src/call.rs b/dxr_client/src/call.rs
index 5a6ac55..4cd5a41 100644
--- a/dxr_client/src/call.rs
+++ b/dxr_client/src/call.rs
@@ -22,7 +22,7 @@ where
P: TryToParams,
R: TryFromValue,
{
- /// constructor for [`Call`] values from method name and method parameters
+ /// Constructor for [`Call`] values from method name and method parameters.
///
/// This method accepts every type of value for the `params` argument if it implements the
/// [`TryToParams`] trait. This includes:
@@ -49,11 +49,66 @@ where
Ok(MethodCall::new(self.method(), self.params()?))
}
- fn method(&self) -> String {
+ pub(crate) fn method(&self) -> String {
String::from(self.method)
}
- fn params(&self) -> Result, DxrError> {
+ pub(crate) fn params(&self) -> Result, DxrError> {
self.params.try_to_params()
}
}
+
+impl Call<'static, P, Vec>
+where
+ P: TryToParams,
+{
+ /// Constructor for [`Call`] values for `system.multicall` calls.
+ // FIXME: adapt type of return value: `Vec>`
+ pub fn multicall(calls: Vec<(String, P)>) -> Result>, DxrError> {
+ let calls = dxr::multicall(calls)?;
+ Ok(Call::new("system.multicall", calls))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+ use super::*;
+
+ #[test]
+ fn to_multicall() {
+ let call = Call::multicall(vec![(String::from("add"), (1, 2)), (String::from("sub"), (2, 1))]).unwrap();
+ let string = quick_xml::se::to_string(&call.as_xml_rpc().unwrap()).unwrap();
+
+ let expected = "\
+
+system.multicall
+
+
+
+
+
+
+
+
+methodNameadd
+params12
+
+
+
+
+
+methodNamesub
+params21
+
+
+
+
+
+
+
+
+".replace('\n', "");
+ assert_eq!(string, expected);
+ }
+}
diff --git a/dxr_client/src/reqwest_support.rs b/dxr_client/src/reqwest_support.rs
index be790ad..57d0806 100644
--- a/dxr_client/src/reqwest_support.rs
+++ b/dxr_client/src/reqwest_support.rs
@@ -1,38 +1,40 @@
+use std::collections::HashMap;
+
use http::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE, USER_AGENT};
use thiserror::Error;
use url::Url;
-use dxr::{DxrError, Fault, FaultResponse, MethodCall, MethodResponse, TryFromValue, TryToParams};
+use dxr::{DxrError, Fault, FaultResponse, MethodCall, MethodResponse, TryFromValue, TryToParams, Value};
use crate::{Call, DEFAULT_USER_AGENT};
-/// error type for XML-RPC clients based on [`reqwest`]
+/// Error type for XML-RPC clients based on [`reqwest`].
#[derive(Debug, Error)]
pub enum ClientError {
- /// error variant for XML-RPC server faults
+ /// Error variant for XML-RPC server faults.
#[error("{}", fault)]
Fault {
- /// fault returned by the server
+ /// Fault returned by the server.
#[from]
fault: Fault,
},
- /// error variant for XML-RPC errors
+ /// Error variant for XML-RPC errors.
#[error("{}", error)]
RPC {
- /// XML-RPC parsing error
+ /// XML-RPC parsing error.
#[from]
error: DxrError,
},
- /// error variant for networking errors
+ /// Error variant for networking errors.
#[error("{}", error)]
Net {
- /// networking error returned by [`reqwest`]
+ /// Networking error returned by [`reqwest`].
#[from]
error: reqwest::Error,
},
}
-/// builder that takes parameters for constructing a [`Client`] based on [`reqwest`]
+/// Builder that takes parameters for constructing a [`Client`] based on [`reqwest::Client`].
#[derive(Debug)]
pub struct ClientBuilder {
url: Url,
@@ -41,7 +43,7 @@ pub struct ClientBuilder {
}
impl ClientBuilder {
- /// constructor for [`ClientBuilder`] from the URL of the XML-RPC server
+ /// Constructor for [`ClientBuilder`] from the URL of the XML-RPC server.
///
/// This also sets up the default `Content-Type: text/xml` HTTP header for XML-RPC requests.
pub fn new(url: Url) -> Self {
@@ -55,13 +57,13 @@ impl ClientBuilder {
}
}
- /// method for overriding the default User-Agent header
+ /// Method for overriding the default User-Agent header.
pub fn user_agent(mut self, user_agent: &'static str) -> Self {
self.user_agent = Some(user_agent);
self
}
- /// method for providing additional custom HTTP headers
+ /// Method for providing additional custom HTTP headers.
///
/// Using [`HeaderName`] constants for the header name is recommended. The [`HeaderValue`]
/// argument needs to be parsed (probably from a string) with [`HeaderValue::from_str`] to
@@ -71,7 +73,7 @@ impl ClientBuilder {
self
}
- /// build the [`Client`] by setting up and initializing the internal [`reqwest::Client`]
+ /// Build the [`Client`] by setting up and initializing the internal [`reqwest::Client`].
///
/// If no custom value was provided for `User-Agent`, the default value
/// ([`DEFAULT_USER_AGENT`]) will be used.
@@ -103,7 +105,12 @@ pub struct Client {
}
impl Client {
- /// asynchronous method for handling remote procedure calls with XML-RPC
+ /// Constructor for a [`Client`] from a [`reqwest::Client`] that was already initialized.
+ pub fn with_client(url: Url, client: reqwest::Client) -> Self {
+ Client { url, client }
+ }
+
+ /// Asynchronous method for handling remote procedure calls with XML-RPC.
///
/// Fault responses from the XML-RPC server are transparently converted into [`Fault`] errors.
/// Invalid XML-RPC responses or faults will result in an appropriate [`DxrError`].
@@ -123,6 +130,52 @@ impl Client {
// extract return value
Ok(R::try_from_value(&result.inner())?)
}
+
+ /// Asynchronous method for handling "system.multicall" calls.
+ pub async fn multicall(
+ &self,
+ call: Call<'_, P, Vec>,
+ ) -> Result>, ClientError> {
+ let expected = call.params()?.len();
+ let response = self.call(call).await?;
+
+ if response.len() != expected {
+ // length of results must match number of method calls
+ return Err(ClientError::RPC {
+ error: DxrError::parameter_mismatch(response.len(), expected),
+ });
+ }
+
+ let mut results = Vec::new();
+ for result in response {
+ // return values for successful calls are arrays that contain a single value
+ if let Ok((value,)) = <(Value,)>::try_from_value(&result) {
+ results.push(Ok(value));
+ };
+
+ // return values for failed calls are structs with two members
+ if let Ok(mut value) = >::try_from_value(&result) {
+ let code = match value.remove("faultCode") {
+ Some(code) => code,
+ None => return Err(DxrError::missing_field("Fault", "faultCode").into()),
+ };
+
+ let string = match value.remove("faultString") {
+ Some(string) => string,
+ None => return Err(DxrError::missing_field("Fault", "faultString").into()),
+ };
+
+ // The value might still contain other struct fields:
+ // Rather than return an error because they are unexpected, they are ignored,
+ // since the required "faultCode" and "faultString" members were present.
+
+ let fault = Fault::new(i32::try_from_value(&code)?, String::try_from_value(&string)?);
+ results.push(Err(fault));
+ }
+ }
+
+ Ok(results)
+ }
}