n41r0j
n41r0j

Reputation: 143

How can I return something meaningful from a generic function if there is nothing to return?

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

Answers (2)

Shepmaster
Shepmaster

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

Laney
Laney

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 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.

Unwrapping the Option is a very cheap operation, there's nothing to be worried about.

Upvotes: 1

Related Questions