mod invalid;
use serde::{Deserialize, Serialize};
use tor_rpcbase as rpc;
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum RequestId {
Str(Box<str>),
Int(i64),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub(crate) struct ReqMeta {
#[serde(default)]
pub(crate) updates: bool,
#[serde(default)]
pub(crate) require: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct Request {
pub(crate) id: RequestId,
pub(crate) obj: rpc::ObjectId,
#[serde(default)]
pub(crate) meta: ReqMeta,
#[serde(flatten)]
pub(crate) method: Box<dyn rpc::DeserMethod>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub(crate) enum FlexibleRequest {
Valid(Request),
Invalid(invalid::InvalidRequest),
}
#[derive(Debug, Serialize)]
pub(crate) struct BoxedResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) id: Option<RequestId>,
#[serde(flatten)]
pub(crate) body: ResponseBody,
}
impl BoxedResponse {
pub(crate) fn from_error<E>(id: Option<RequestId>, error: E) -> Self
where
E: Into<rpc::RpcError>,
{
let error: rpc::RpcError = error.into();
let body = ResponseBody::Error(Box::new(error));
Self { id, body }
}
}
#[derive(Serialize)]
pub(crate) enum ResponseBody {
#[serde(rename = "error")]
Error(Box<rpc::RpcError>),
#[serde(rename = "result")]
Success(Box<dyn erased_serde::Serialize + Send>),
#[serde(rename = "update")]
Update(Box<dyn erased_serde::Serialize + Send>),
}
impl ResponseBody {
#[cfg(test)]
pub(crate) fn is_final(&self) -> bool {
match self {
ResponseBody::Error(_) | ResponseBody::Success(_) => true,
ResponseBody::Update(_) => false,
}
}
}
impl From<rpc::RpcError> for ResponseBody {
fn from(inp: rpc::RpcError) -> ResponseBody {
ResponseBody::Error(Box::new(inp))
}
}
impl std::fmt::Debug for ResponseBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let json = |x| match serde_json::to_string(x) {
Ok(s) => s,
Err(e) => format!("«could not serialize: {}»", e),
};
match self {
Self::Error(arg0) => f.debug_tuple("Error").field(arg0).finish(),
Self::Update(arg0) => f.debug_tuple("Update").field(&json(arg0)).finish(),
Self::Success(arg0) => f.debug_tuple("Success").field(&json(arg0)).finish(),
}
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::string_slice)] use super::*;
use derive_deftly::Deftly;
use tor_rpcbase::templates::*;
macro_rules! assert_dbg_eq {
($a:expr, $b:expr) => {
assert_eq!(format!("{:?}", $a), format!("{:?}", $b));
};
}
#[derive(Debug, serde::Deserialize, Deftly)]
#[derive_deftly(DynMethod)]
#[deftly(rpc(method_name = "x-test:dummy"))]
struct DummyMethod {
#[serde(default)]
#[allow(dead_code)]
stuff: u64,
}
impl rpc::RpcMethod for DummyMethod {
type Output = DummyResponse;
type Update = rpc::NoUpdates;
}
#[derive(Serialize)]
struct DummyResponse {
hello: i64,
world: String,
}
#[test]
fn valid_requests() {
let parse_request = |s| match serde_json::from_str::<FlexibleRequest>(s) {
Ok(FlexibleRequest::Valid(req)) => req,
other => panic!("{:?}", other),
};
let r =
parse_request(r#"{"id": 7, "obj": "hello", "method": "x-test:dummy", "params": {} }"#);
assert_dbg_eq!(
r,
Request {
id: RequestId::Int(7),
obj: rpc::ObjectId::from("hello"),
meta: ReqMeta::default(),
method: Box::new(DummyMethod { stuff: 0 })
}
);
}
#[test]
fn invalid_requests() {
use crate::err::RequestParseError as RPE;
fn parsing_error(s: &str) -> RPE {
match serde_json::from_str::<FlexibleRequest>(s) {
Ok(FlexibleRequest::Invalid(req)) => req.error(),
x => panic!("Didn't expect {:?}", x),
}
}
macro_rules! expect_err {
($p:pat, $e:expr) => {
let err = parsing_error($e);
assert!(matches!(err, $p), "Unexpected error type {:?}", err);
};
}
expect_err!(
RPE::IdMissing,
r#"{ "obj": "hello", "method": "x-test:dummy", "params": {} }"#
);
expect_err!(
RPE::IdType,
r#"{ "id": {}, "obj": "hello", "method": "x-test:dummy", "params": {} }"#
);
expect_err!(
RPE::ObjMissing,
r#"{ "id": 3, "method": "x-test:dummy", "params": {} }"#
);
expect_err!(
RPE::ObjType,
r#"{ "id": 3, "obj": 9, "method": "x-test:dummy", "params": {} }"#
);
expect_err!(
RPE::MethodMissing,
r#"{ "id": 3, "obj": "hello", "params": {} }"#
);
expect_err!(
RPE::MethodType,
r#"{ "id": 3, "obj": "hello", "method": [], "params": {} }"#
);
expect_err!(
RPE::MetaType,
r#"{ "id": 3, "obj": "hello", "meta": 7, "method": "x-test:dummy", "params": {} }"#
);
expect_err!(
RPE::MetaType,
r#"{ "id": 3, "obj": "hello", "meta": { "updates": 3}, "method": "x-test:dummy", "params": {} }"#
);
expect_err!(
RPE::NoSuchMethod,
r#"{ "id": 3, "obj": "hello", "method": "arti:this-is-not-a-method", "params": {} }"#
);
expect_err!(
RPE::MissingParams,
r#"{ "id": 3, "obj": "hello", "method": "x-test:dummy" }"#
);
expect_err!(
RPE::ParamType,
r#"{ "id": 3, "obj": "hello", "method": "x-test:dummy", "params": 7 }"#
);
}
#[test]
fn fmt_replies() {
let resp = BoxedResponse {
id: Some(RequestId::Int(7)),
body: ResponseBody::Success(Box::new(DummyResponse {
hello: 99,
world: "foo".into(),
})),
};
let s = serde_json::to_string(&resp).unwrap();
assert_eq!(s, r#"{"id":7,"result":{"hello":99,"world":"foo"}}"#);
let resp = BoxedResponse::from_error(
None,
rpc::RpcError::from(crate::err::RequestParseError::IdMissing),
);
let s = serde_json::to_string(&resp).unwrap();
assert_eq!(
s,
r#"{"error":{"message":"Request did not have any `id` field.","code":-32600,"kinds":["rpc:InvalidRequest"]}}"#
);
}
#[test]
fn response_body_is_final() {
let response_body_error = ResponseBody::from(rpc::RpcError::new(
"This is a test!".to_string(),
rpc::RpcErrorKind::ObjectNotFound,
));
assert!(response_body_error.is_final());
let response_body_success = ResponseBody::Success(Box::new(DummyResponse {
hello: 99,
world: "foo".into(),
}));
assert!(response_body_success.is_final());
let response_body_update = ResponseBody::Update(Box::new(DummyResponse {
hello: 99,
world: "foo".into(),
}));
assert!(!response_body_update.is_final());
}
}