Skip to content

Commit

Permalink
server: first draft of system.multicall support
Browse files Browse the repository at this point in the history
  • Loading branch information
decathorpe committed Jul 5, 2023
1 parent 6ed11a5 commit 72ed489
Show file tree
Hide file tree
Showing 16 changed files with 285 additions and 24 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"dxr_server",
"dxr_tests",
]
resolver = "2"

[workspace.package]
version = "0.6.0-dev"
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,32 @@ returns the number of times the `countme()` method has been called since the ser
The `dxr` crate provides functionality for deriving the `TryFromDXR` and `TryToDXR` traits
if the `derive` feature is enabled.

There is also optional support for two common, non-standard XML-RPC extensions:
There is also optional support for common, non-standard XML-RPC extensions:

- "long" 64-bit integers (`<i8>`): mapped to `i64`, enabled with the `i8` feature
- "null" values (`<nil/>`): mapped to `Option<T>`, enabled with the `nil` feature
- "system.multicall" support for processing multiple RPC calls within a single request,
enabled with the `multicall` feature

## Development

This repository contains two helper scripts for helping with development:

- `./checkall.py`: Runs `cargo check`, `cargo clippy`, and `cargo test` for all crates and all
combinations of enabled optional features. There should be no warnings or errors for any
combination of enabled features. When adding new feature to one of the crates, the list of
features of each crate needs to be updated in this script as well.
- `./coverage.sh`: Builds the crate with coverage instrumentation enabled, runs
`cargo test --workspace --all-features`, and generates a test coverage report with `grcov`, which
can then be viewed in `target/debug/coverage/`.

## License

This project is licensed under either of

* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
https://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
https://opensource.org/licenses/MIT)

at your option.
6 changes: 3 additions & 3 deletions checkall.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ def check(package: str, feature_list: list[str]):
def main():
os.environ["QUICKCHECK_TESTS"] = "100000"

check("dxr", ["derive", "i8", "nil"])
check("dxr", ["derive", "multicall", "i8", "nil"])
check("dxr_derive", [])
check("dxr_client", ["default", "reqwest", "default-tls", "native-tls", "rustls-tls"])
check("dxr_server", ["default", "axum"])
check("dxr_client", ["default", "multicall", "reqwest", "default-tls", "native-tls", "rustls-tls"])
check("dxr_server", ["default", "multicall", "axum"])
check("dxr_tests", [])


Expand Down
5 changes: 5 additions & 0 deletions dxr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ trybuild = "1"
[features]
# support for derive macros
derive =["dep:dxr_derive"]

# utilities for multicall support
multicall = []

# support non-standard <i8> XML-RPC values
i8 = []

# support non-standard <nil> XML-RPC values
nil = []

Expand Down
2 changes: 1 addition & 1 deletion dxr/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use thiserror::Error;

use crate::fault::Fault;

#[derive(Debug, Error)]
#[derive(Debug, Error, PartialEq)]
/// Error type representing conversion errors between XML-RPC values and Rust values.
pub enum DxrError {
/// Error variant for XML parser errors.
Expand Down
2 changes: 2 additions & 0 deletions dxr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ pub use fault::*;

mod impls;

#[cfg(feature = "multicall")]
mod multicall;
#[cfg(feature = "multicall")]
pub use multicall::*;

mod traits;
Expand Down
162 changes: 160 additions & 2 deletions dxr/src/multicall.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use crate::{DxrError, Member, Struct, TryToParams, TryToValue, Value};
use std::collections::HashMap;

use crate::{Array, DxrError, Fault, Member, Struct, TryFromValue, 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>
pub fn into_multicall_params<P>(calls: Vec<(String, P)>) -> Result<Value, DxrError>
where
P: TryToParams,
{
Expand All @@ -22,3 +24,159 @@ where

params.try_to_value()
}

/// Convenience method for reconstructing method calls from "system.multicall" arguments.
#[allow(clippy::type_complexity)]
pub fn from_multicall_params(mut values: Vec<Value>) -> Result<Vec<Result<(String, Vec<Value>), DxrError>>, DxrError> {
// system.multicall calls take an array of arguments as single argument
let value = match values.pop() {
Some(value) => value,
None => return Err(DxrError::parameter_mismatch(0, 1)),
};

// check if there are more than one arguments
if !values.is_empty() {
return Err(DxrError::parameter_mismatch(values.len() + 1, 1));
}

// extract vector of argument values
let params = <Vec<Value>>::try_from_value(&value)?;

let calls: Vec<Result<(String, Vec<Value>), DxrError>> = params
.into_iter()
.map(|v| {
let mut members: HashMap<String, Value> = HashMap::try_from_value(&v)?;

if members.len() != 2 {
return Err(DxrError::parameter_mismatch(members.len(), 2));
}

let name = match members.remove("methodName") {
Some(name) => name,
None => return Err(DxrError::missing_field("system.multicall", "methodName")),
};

let params = match members.remove("params") {
Some(params) => params,
None => return Err(DxrError::missing_field("system.multicall", "params")),
};

Ok((String::try_from_value(&name)?, <Vec<Value>>::try_from_value(&params)?))
})
.collect();

Ok(calls)
}

/// Convenience method for constructing return values for "system.multicall" calls.
pub fn into_multicall_response(results: Vec<Result<Value, Fault>>) -> Value {
let values: Vec<Value> = results
.into_iter()
.map(|r| match r {
Ok(value) => Value::array(Array::new(vec![value])),
Err(fault) => {
let members = vec![
Member::new(String::from("faultCode"), Value::i4(fault.code())),
Member::new(String::from("faultString"), Value::string(fault.string().to_owned())),
];
Value::structure(Struct::new(members))
},
})
.collect();

Value::array(Array::new(values))
}

#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::MethodCall;

#[test]
fn from_multicall() {
let string = "\
<methodCall>
<methodName>system.multicall</methodName>
<params>
<param>
<value>
<array>
<data>
<value>
<struct>
<member>
<name>methodName</name>
<value>event</value>
</member>
<member>
<name>params</name>
<value>
<array>
<data>
<value>foo</value>
<value>bar</value>
<value>baz</value>
<value><boolean>1</boolean></value>
</data>
</array>
</value>
</member>
</struct>
</value>
<value>
<struct>
<member>
<name>methodName</name>
<value>event</value>
</member>
<member>
<name>params</name>
<value>
<array>
<data>
<value>another</value>
<value>call</value>
<value>hi</value>
<value><boolean>1</boolean></value>
</data>
</array>
</value>
</member>
</struct>
</value>
</data>
</array>
</value>
</param>
</params>
</methodCall>";

let call: MethodCall = quick_xml::de::from_str(string).unwrap();

let params = from_multicall_params(call.params()).unwrap();

let expected: Vec<Result<(String, Vec<Value>), DxrError>> = vec![
Ok((
String::from("event"),
vec![
Value::string(String::from("foo")),
Value::string(String::from("bar")),
Value::string(String::from("baz")),
Value::boolean(true),
],
)),
Ok((
String::from("event"),
vec![
Value::string(String::from("another")),
Value::string(String::from("call")),
Value::string(String::from("hi")),
Value::boolean(true),
],
)),
];

assert_eq!(params, expected);
}
}
3 changes: 3 additions & 0 deletions dxr_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ url = { version = "2.2", optional = true }
# use the default TLS backend by default
default = ["reqwest?/default-tls"]

# multicall support
multicall = ["dxr/multicall"]

reqwest = ["dep:http", "dep:reqwest", "dep:thiserror", "dep:url"]

default-tls = ["reqwest?/default-tls"]
Expand Down
2 changes: 2 additions & 0 deletions dxr_client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ This crate contains a building blocks for writing XML-RPC clients based on `dxr`

It also includes an implementation of an `async` XML-RPC client using `reqwest`, which is disabled
by default. To enable the `reqwest` support, enable the `"reqwest"` feature of this crate.

To enable convenience functionality for "system.multicall" support, enable the `multicall` feature.
8 changes: 6 additions & 2 deletions dxr_client/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,26 @@ where
}
}

#[cfg(feature = "multicall")]
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)?;
let calls = dxr::into_multicall_params(calls)?;
Ok(Call::new("system.multicall", calls))
}
}

#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]

#[cfg(feature = "multicall")]
use super::*;

#[cfg(feature = "multicall")]
#[test]
fn to_multicall() {
let call = Call::multicall(vec![(String::from("add"), (1, 2)), (String::from("sub"), (2, 1))]).unwrap();
Expand Down Expand Up @@ -109,6 +112,7 @@ mod tests {
</params>
</methodCall>".replace('\n', "");

assert_eq!(string, expected);
}
}
18 changes: 8 additions & 10 deletions dxr_client/src/reqwest_support.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#[cfg(feature = "multicall")]
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, Value};
#[cfg(feature = "multicall")]
use dxr::Value;
use dxr::{DxrError, Fault, FaultResponse, MethodCall, MethodResponse, TryFromValue, TryToParams};

use crate::{Call, DEFAULT_USER_AGENT};

Expand Down Expand Up @@ -132,20 +135,16 @@ impl Client {
}

/// Asynchronous method for handling "system.multicall" calls.
///
/// *Note*: This method does not check if the number of method calls matches the number of
/// returned results.
#[cfg(feature = "multicall")]
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
Expand Down Expand Up @@ -178,7 +177,6 @@ impl Client {
}
}


fn request_to_body(call: &MethodCall) -> Result<String, DxrError> {
let body = [
r#"<?xml version="1.0"?>"#,
Expand Down
4 changes: 4 additions & 0 deletions dxr_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ tokio = { version = "1.14", features = ["sync"], optional = true }

[features]
default = []

# multicall support
multicall = ["dxr/multicall"]

axum = ["dep:axum", "dep:hyper", "dep:thiserror", "dep:tokio"]

[package.metadata.docs.rs]
Expand Down
2 changes: 2 additions & 0 deletions dxr_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ This crate contains a building blocks for writing XML-RPC servers based on `dxr`

It also includes a complete XML-RPC server implementation based on the `axum` web framework, which
is disabled by default. To enable the `axum` support, enable the `"axum"` feature of this crate.

To enable "system.multicall" support, enable the `multicall` feature.

0 comments on commit 72ed489

Please sign in to comment.