ddf-blocking-http-client 0.0.3

Convenience crate for DiamondDDF projects requiring a blocking http client.
Documentation
//! This is a convenience crate for handling http requests and errors.
//! # Examples
//! ```
//! use ddf_blocking_http_client::Client;
//! use ddf_blocking_http_client::HttpResult;
//! use serde::Deserialize;
//! 
//! use dotenv::dotenv;
//! use std::env;
//!
//! // dotenv is being used for lack of an example not requiring private keys and public wallet addresses.
//! // dotenv is not required for this crate to work.
//! dotenv::dotenv().ok();
//! let api_key = env::var("API_KEY").unwrap();
//! let wallet_address = env::var("PUBLIC_WALLET_ADDRESS").unwrap();
//! 
//! #[derive(Debug, Deserialize)]
//! struct Balance {
//!     result: String
//! }
//! 
//! let client = Client::new();
//! 
//! let url = 
//!     &[
//!         "https://api.etherscan.io/api",
//!         "?module=account",
//!         "&action=balance",
//!         "&address=", &wallet_address,
//!         "&tag=latest",
//!         "&apikey=", &api_key
//!     ].concat();
//! 
//! // Here is the main feature, Balance is the data structure expected back, defined above.
//! // You define your own struct and use the serde crate and tag the struct with #[derive(Deserialize)]
//! // and put the name of your struct where our example is using "Balance".
//! // the 10 means we are going to try 10 times, if there is a too_many_requests response, or 
//! // an unknown error (perhaps our crate just doesn't handle it yet) then it will wait 1 second and try again.
//! // each time it will double the wait time. The return type is HttpResult<T>
//! 
//! let balance = client.get::<Balance>(url, 10).unwrap().result;
//! 
//! dbg!(&balance);
//! 
//! // If you are unsure the shape of the data, you can at first use the "get_as_text" function.
//! // Here is an example of that "get_as_text" function, so you can get a look at all the fields to base your struct off of:
//! 
//! let balance = client.get_as_text(&url);
//! 
//! dbg!(&balance);
//! ```

use reqwest::header::*;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::thread::sleep;
use std::time::Duration;
use serde_json::json;

pub type HttpResult<T> = Result<T, HttpError>;

#[derive(Debug, Clone)]
pub enum HttpError {
    ParseJson{message: String},
    BadToken,
    TooManyTries{num_tries: u32},
    NotFound{message: String},
    TooManyRequests{message: String}
}
#[derive(Debug)]
pub struct Client {
    client: reqwest::blocking::Client
}

/// You can call get from an instance or a direct function.
/// * `url` - full url with args if you have them.
/// * `times_to_try` - times to try before giving up. It starts with 1 second delay and doubles the delay each time.
pub fn get<T: DeserializeOwned + Send + 'static>(url: &str, times_to_try: u32) -> HttpResult<T> {
    let url_copy = url.to_string();
    let result = std::thread::spawn( move || {
        Client::new().get::<T>(&url_copy.to_string(), times_to_try)
    }).join().unwrap();
    result
}
impl Client {
    pub fn new() -> Client {
        Client {
            client: reqwest::blocking::Client::new()
        }
    }
    /// This is the main function to use.
    /// Just define a struct with #[derive(Serialize, Deserialize)] that matches the JSON that will be returned
    /// from the request.
    /// * `url` - full url with args if you have them.
    /// * `times_to_try` - times to try before giving up. It starts with 1 second delay and doubles the delay each time.
    pub fn get<T: DeserializeOwned>(&self, url: &str, times_to_try: u32) -> HttpResult<T> {
        self._get(url, times_to_try, 0, Duration::from_secs(1))
    }

    /// This is if you want to see the shape of the return data as text so you know how to define things.
    pub fn get_as_text(&self, url: &str) -> String {
        return 
            reqwest::blocking::get(url).unwrap()
                .text().unwrap()       
    }

    /// Used to send a post request
    /// * `url` - base url that might include an api key
    /// * `data` - most likely going to be json. Use serde_json::json macro if passing as a string.
    ///# Examples
    /// ```
    /// use ddf_blocking_http_client::Client;
    /// use serde_json::json;
    /// use dotenv::dotenv;
    /// use std::env;
    ///
    /// // dotenv is being used for lack of an example not requiring private keys and public wallet addresses.
    /// // dotenv is not required for this crate to work.
    /// dotenv::dotenv().ok();
    /// let api_key = env::var("INFURA_API_KEY").unwrap();
    /// let client = Client::new();
    /// let url = [
    ///     "https://mainnet.infura.io/v3/",
    ///     &api_key
    /// ].concat();
    /// let res = client.post(&
    ///     url, 
    ///     json!(
    ///         {"jsonrpc":"2.0",
    ///         "method":"eth_estimateGas",
    ///         "params": [
    ///             {"from": "0xb60e8dd61c5d32be8058bb8eb970870f07233155",
    ///             "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
    ///             "gas": "0x76c0",
    ///             "gasPrice": "0x9184e72a000",
    ///             "value": "0x9184e72a",
    ///             "data": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}
    ///     )
    /// );
    /// dbg!(&res);
    /// ```
    
    pub fn post<T: Serialize>(&self, url: &str, data: T)        
    where T: std::fmt::Debug
    {
        let response = self.client
            .post(url)
            .header(CONTENT_TYPE, "application/json")
            .header(ACCEPT, "application/json")
            .json(&data)
            .send()
            .unwrap()
            .text().unwrap();
        dbg!(response);
    }

    // this adds some params the end user doesn't need to be concerned with.
    // * `tries` - the number of times the  client has actually tried.
    // * `incremental_wait_time` - starts at a second and doubles each time.
    fn _get<T: DeserializeOwned>(
        &self, 
        url: &str, 
        times_to_try: u32, 
        mut tries: u32, 
        mut incremental_wait_time: Duration
    ) 
    -> HttpResult<T> {
        if tries >= times_to_try { return Err(HttpError::TooManyTries{num_tries: tries})}
        let response = reqwest::blocking::Client::new()
            .get(url)
            .header(CONTENT_TYPE, "application/json")
            //.header(ACCEPT, "application/json")
            .send()
            .unwrap();
       
        match response.status() {
            
            reqwest::StatusCode::OK => {
                // on success, parse our JSON to an APIResponse
                match response.json::<T>() {
                    Ok(parsed) => Ok(parsed),
                    Err(_) => Err(HttpError::ParseJson{message: "Hm, the response didn't match the shape we expected.".to_string()}),
                }
            }
            reqwest::StatusCode::UNAUTHORIZED => {
                Err(HttpError::BadToken)
            }
            reqwest::StatusCode::NOT_FOUND => {
                Err(
                    HttpError::NotFound {
                        message: 
                            "Response 404, probably means the server couldn't find the data you were looking for.".to_string()
                    }
                )
            }
            reqwest::StatusCode::BAD_REQUEST => {
                println!("💥 Caught BAD_REQUEST");
                dbg!(&response.status());
                tries += 1;
                sleep(Duration::from_secs(1));
                self._get(url, times_to_try, tries, incremental_wait_time)
            }
            reqwest::StatusCode::TOO_MANY_REQUESTS => {
                println!("🥷🏾 {} be complainin' 'bout too many requests!", url);
                println!("🥷🏾 So I'm waiting {} from_secs and trying again.", incremental_wait_time.as_secs());
                println!("🥷🏾 This was try {} of {}.", tries, times_to_try);
                sleep(incremental_wait_time);
                incremental_wait_time *= 2;
                tries += 1;
                self._get(url, times_to_try, tries, incremental_wait_time)
            }
            _ => {
                dbg!("💥 Uncaught http error, response.status() was: ", response.status(),"/n");
                tries += 1;
                sleep(incremental_wait_time);
                incremental_wait_time *= 2;
                self._get(url, times_to_try, tries, incremental_wait_time)
            }
        }
    }
}