Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stop Rust from enforcing the serde::Deserialize trait on an error type

Tags:

rust

serde

The code below is the beginnings of a small library I'm writing to talk to a web API. Users of the library will instantiate a client MyClient and access the web API through it. Here, I'm trying to get an access token from the API before making requests to it.

In get_new_access() I'm able to make the request and receive the JSON response. I then try to use serde to turn the response into an Access struct, and this is where the problems start.

I've created a library specific error enum MyError which can represent the JSON deserializing and reqwest errors that could occur within get_new_access(). However, when I go to compile I get the trait serde::Deserialize<'_> is not implemented for MyError. My understanding is that this is happening because in the case that I get one of the aforementioned errors, serde does not know how to deserialize it into an Access struct. Of course, I don't want it to do that at all, so my question is what should I do?

I've looked at various serde deserialize examples, but all of them seem to assume that they are running in a main function that can only return a serde error. If I put #[derive(Deserialize)] above MyError's declaration, then I get the same error, but it shifts to reqwest::Error and serde_json::Error instead.

use std::error;
use std::fmt;

extern crate chrono;
extern crate reqwest;

#[macro_use]
extern crate serde_derive;

extern crate serde;
extern crate serde_json;

use chrono::prelude::*;
use reqwest::Client;

pub struct MyClient {
    access: Access,
    token_expires: DateTime<Utc>,
}

#[derive(Deserialize, Debug)]
struct Access {
    access_token: String,
    expires_in: i64,
    token_type: String,
}

fn main() {
    let sc: MyClient = MyClient::new();

    println!("{:?}", &sc.access);
}

impl MyClient {
    pub fn new() -> MyClient {
        let a: Access = MyClient::get_new_access().expect("Couldn't get Access");
        let e: DateTime<Utc> = chrono::Utc::now(); //TODO
        MyClient {
            access: a,
            token_expires: e,
        }
    }

    fn get_new_access() -> Result<Access, MyError> {
        let params = ["test"];
        let client = Client::new();
        let json = client
            .post(&[""].concat())
            .form(&params)
            .send()?
            .text()
            .expect("Couldn't get JSON Response");

        println!("{}", &json);

        serde_json::from_str(&json)?

        //let a = Access {access_token: "Test".to_string(), expires_in: 3600, token_type: "Test".to_string() };

        //serde_json::from_str(&json)?
    }
}

#[derive(Debug)]
pub enum MyError {
    WebRequestError(reqwest::Error),
    ParseError(serde_json::Error),
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "eRROR")
    }
}

impl error::Error for MyError {
    fn description(&self) -> &str {
        "API internal error"
    }

    fn cause(&self) -> Option<&error::Error> {
        // Generic error, underlying cause isn't tracked.
        None
    }
}

impl From<serde_json::Error> for MyError {
    fn from(e: serde_json::Error) -> Self {
        MyError::ParseError(e)
    }
}

impl From<reqwest::Error> for MyError {
    fn from(e: reqwest::Error) -> Self {
        MyError::WebRequestError(e)
    }
}

Playground link here.

like image 396
J.B Avatar asked Mar 05 '23 20:03

J.B


1 Answers

Your first problem is that your fn get_new_access() -> Result<Access, MyError> expects a Result. But in here:

    //...
    serde_json::from_str(&json)?
}

because of using ?(try macro), you are trying to return Result's unwrapped value which is a subtype of serde::Deserialize<'_>. The compiler warns you about this Deserialize is not a Result. What you should do is just return the result without unwrapping it:

    //...
    serde_json::from_str(&json)
}

Or

    //...
    let access = serde_json::from_str(&json)?; // gets access or propagates error 
    Ok(access) //if no error return access in a Result
}

Then you will have a second problem because your function expects MyError in the Result while you are returning serde_json::Error with this call serde_json::from_str(&json). Luckily Result has the function map_err which maps the actual error type to your custom error type.

This code will solve your problem:

    //...
    serde_json::from_str(&json).map_err(MyError::ParseError)
}

For the request in the comment :

For example, if I change the web request line to let json = client.post("").form(&params).send().map_err(MyError::WebRequestError)?.text()?;, is that better practice at all?

Yes but since text() returns a Result you need to map it's error as MyError too. Since both send and text has same error type(reqwest::Error) you can combine the results with and_then :

let json = client
    .post(&[""].concat())
    .form(&params)
    .send()
    .and_then(Response::text) //use reqwest::Response; 
    .map_err(MyError::WebRequestError)?;
like image 133
Ömer Erden Avatar answered Apr 12 '23 23:04

Ömer Erden