Reputation: 143
I'm building a library in Rust that has a send
method that performs HTTP requests against a local RPC server using reqwest.
This method returns a generic type R
in a Result
where R: DeserializeOwned
. After making the correct types for every response, serde_json::from_str()
can get me the type.
If there is no response upon a request, how can I make send
still return something meaningful?
This is the code I have now:
fn send<R, T>(
&self,
request: &RpcRequest<T>,
) -> Result<R, ApiError>
where
T: Serialize + Debug,
R: DeserializeOwned + Debug,
let res = serde_json::from_str(&buf).map_err(|err| ClientError::Json(err))
I am now forced to create and return an Err
, but technically, the request returning no response is expected behavior, so I want to return something other than an Err
.
I tried to work around this by wrapping R
with Option
, but that means I have to double unwrap every response, and 98% of the responses from reqwest do have data in their response, so it feels a bit like overkill.
I also tried to return a self-made EmptyResponse
type, but the compiler complains: expected type R, found type EmptyResponse
. I think returning a type EmptyResponse
would be what I want, but maybe someone can shed some tips on how to maybe do this even better.
Upvotes: 0
Views: 819
Reputation: 430861
The pragmatic answer is to have two functions:
fn send<R, T>(&self, request: &RpcRequest<T>) -> Result<R, ApiError>
where
T: Serialize + Debug,
R: DeserializeOwned + Debug,
fn send_no_response<T>(&self, request: &RpcRequest<T>) -> Result<(), ApiError>
where
T: Serialize + Debug,
If your server happens to return a value that can be deserialized into the type ()
, then you can avoid the overhead of two functions. However, this is not the case for JSON, one of the most common formats:
use serde::de::DeserializeOwned; // 1.0.85
use serde_json; // 1.0.37
type Error = Box<std::error::Error>;
type Result<T, E = Error> = std::result::Result<T, E>;
fn send<R>() -> Result<R, Error>
where
R: DeserializeOwned,
{
serde_json::from_str("").map_err(Into::into)
}
fn main() {
let _r: () = send().expect("Unable to deserialize");
}
This panics:
Unable to deserialize: Error("EOF while parsing a value", line: 1, column: 0)
In a world with specialization, you can use it and a helper trait to reduce back to one function:
#![feature(specialization)]
use serde::de::DeserializeOwned; // 1.0.85
use serde_json; // 1.0.37
type Error = Box<std::error::Error>;
type Result<T, E = Error> = std::result::Result<T, E>;
type ApiResponse = &'static str;
trait FromApi: Sized {
fn convert(response: ApiResponse) -> Result<Self, Error>;
}
impl<R> FromApi for R
where
R: DeserializeOwned,
{
default fn convert(response: ApiResponse) -> Result<R, Error> {
eprintln!("deserializing the response");
serde_json::from_str(response).map_err(Into::into)
}
}
impl FromApi for () {
fn convert(_response: ApiResponse) -> Result<Self, Error> {
eprintln!("Ignoring the response");
Ok(())
}
}
fn send<R: FromApi>() -> Result<R> {
eprintln!(r#""sending" the request"#);
let api_response = "";
R::convert(api_response)
}
fn main() {
let _r: () = send().expect("Unable to deserialize");
}
Upvotes: 1
Reputation: 1649
You can return an Result<Option<R>, ApiError>
as shown in the documentation, then match it like this:
match sender.send(request) {
Ok(Some(r)) => {
// process response
}
Ok(None) => {
// process empty response
}
Err(e) => {
// process error
}
}
// or
if let Ok(Some(r)) = sender.send(request) {
// process response
}
I tried to work around this by wrapping
R
withOption
, but that means I have to double unwrap every response, and 98% of the responses from reqwest do have data in their response, so it feels a bit like overkill.
Unwrapping the Option
is a very cheap operation, there's nothing to be worried about.
Upvotes: 1