Skip to content

Commit

Permalink
client: first draft of system.multicall support
Browse files Browse the repository at this point in the history
  • Loading branch information
decathorpe committed Jul 4, 2023
1 parent 5b12eb9 commit 24af6db
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 17 deletions.
3 changes: 3 additions & 0 deletions dxr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ pub use fault::*;

mod impls;

mod multicall;
pub use multicall::*;

mod traits;
pub use traits::*;

Expand Down
24 changes: 24 additions & 0 deletions dxr/src/multicall.rs
Original file line number Diff line number Diff line change
@@ -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<P>(calls: Vec<(String, P)>) -> Result<Value, DxrError>
where
P: TryToParams,
{
let params: Vec<Value> = 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::<Result<Vec<Value>, DxrError>>()?;

params.try_to_value()
}
11 changes: 11 additions & 0 deletions dxr/src/tests/multicall.rs
Original file line number Diff line number Diff line change
@@ -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 = "<methodCall><methodName>system.multicall</methodName></methodCall>";

assert_eq!(to_string(&value).unwrap(), expected);
}
61 changes: 58 additions & 3 deletions dxr_client/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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<Vec<Value>, DxrError> {
pub(crate) fn params(&self) -> Result<Vec<Value>, DxrError> {
self.params.try_to_params()
}
}

impl<P> Call<'static, P, Vec<Value>>
where
P: TryToParams,
{
/// Constructor for [`Call`] values for `system.multicall` calls.
// FIXME: adapt type of return value: `Vec<Result<Value, Fault>>`
pub fn multicall(calls: Vec<(String, P)>) -> Result<Call<'static, Value, Vec<Value>>, 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 = "\
<methodCall>
<methodName>system.multicall</methodName>
<params>
<param>
<value>
<array><data>
<value>
<struct>
<member><name>methodName</name><value><string>add</string></value></member>
<member><name>params</name><value><array><data><value><i4>1</i4></value><value><i4>2</i4></value></data></array></value></member>
</struct>
</value>
<value>
<struct>
<member><name>methodName</name><value><string>sub</string></value></member>
<member><name>params</name><value><array><data><value><i4>2</i4></value><value><i4>1</i4></value></data></array></value></member>
</struct>
</value>
</data></array>
</value>
</param>
</params>
</methodCall>".replace('\n', "");
assert_eq!(string, expected);
}
}
81 changes: 67 additions & 14 deletions dxr_client/src/reqwest_support.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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`].
Expand All @@ -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<P: TryToParams>(
&self,
call: Call<'_, P, Vec<Value>>,
) -> Result<Vec<Result<Value, Fault>>, 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) = <HashMap<String, 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)
}
}


Expand Down

0 comments on commit 24af6db

Please sign in to comment.