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) + } }