ddf_blocking_http_client/
lib.rs

1//! This is a convenience crate for handling http requests and errors.
2//! # Examples
3//! ```
4//! use ddf_blocking_http_client::Client;
5//! use ddf_blocking_http_client::HttpResult;
6//! use serde::Deserialize;
7//! 
8//! use dotenv::dotenv;
9//! use std::env;
10//!
11//! // dotenv is being used for lack of an example not requiring private keys and public wallet addresses.
12//! // dotenv is not required for this crate to work.
13//! dotenv::dotenv().ok();
14//! let api_key = env::var("API_KEY").unwrap();
15//! let wallet_address = env::var("PUBLIC_WALLET_ADDRESS").unwrap();
16//! 
17//! #[derive(Debug, Deserialize)]
18//! struct Balance {
19//!     result: String
20//! }
21//! 
22//! let client = Client::new();
23//! 
24//! let url = 
25//!     &[
26//!         "https://api.etherscan.io/api",
27//!         "?module=account",
28//!         "&action=balance",
29//!         "&address=", &wallet_address,
30//!         "&tag=latest",
31//!         "&apikey=", &api_key
32//!     ].concat();
33//! 
34//! // Here is the main feature, Balance is the data structure expected back, defined above.
35//! // You define your own struct and use the serde crate and tag the struct with #[derive(Deserialize)]
36//! // and put the name of your struct where our example is using "Balance".
37//! // the 10 means we are going to try 10 times, if there is a too_many_requests response, or 
38//! // an unknown error (perhaps our crate just doesn't handle it yet) then it will wait 1 second and try again.
39//! // each time it will double the wait time. The return type is HttpResult<T>
40//! 
41//! let balance = client.get::<Balance>(url, 10).unwrap().result;
42//! 
43//! dbg!(&balance);
44//! 
45//! // If you are unsure the shape of the data, you can at first use the "get_as_text" function.
46//! // 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:
47//! 
48//! let balance = client.get_as_text(&url);
49//! 
50//! dbg!(&balance);
51//! ```
52
53use reqwest::header::*;
54use serde::de::DeserializeOwned;
55use serde::Serialize;
56use std::thread::sleep;
57use std::time::Duration;
58use serde_json::json;
59
60pub type HttpResult<T> = Result<T, HttpError>;
61
62#[derive(Debug, Clone)]
63pub enum HttpError {
64    ParseJson{message: String},
65    BadToken,
66    TooManyTries{num_tries: u32},
67    NotFound{message: String},
68    TooManyRequests{message: String}
69}
70#[derive(Debug)]
71pub struct Client {
72    client: reqwest::blocking::Client
73}
74
75/// You can call get from an instance or a direct function.
76/// * `url` - full url with args if you have them.
77/// * `times_to_try` - times to try before giving up. It starts with 1 second delay and doubles the delay each time.
78pub fn get<T: DeserializeOwned + Send + 'static>(url: &str, times_to_try: u32) -> HttpResult<T> {
79    let url_copy = url.to_string();
80    let result = std::thread::spawn( move || {
81        Client::new().get::<T>(&url_copy.to_string(), times_to_try)
82    }).join().unwrap();
83    result
84}
85impl Client {
86    pub fn new() -> Client {
87        Client {
88            client: reqwest::blocking::Client::new()
89        }
90    }
91    /// This is the main function to use.
92    /// Just define a struct with #[derive(Serialize, Deserialize)] that matches the JSON that will be returned
93    /// from the request.
94    /// * `url` - full url with args if you have them.
95    /// * `times_to_try` - times to try before giving up. It starts with 1 second delay and doubles the delay each time.
96    pub fn get<T: DeserializeOwned>(&self, url: &str, times_to_try: u32) -> HttpResult<T> {
97        self._get(url, times_to_try, 0, Duration::from_secs(1))
98    }
99
100    /// This is if you want to see the shape of the return data as text so you know how to define things.
101    pub fn get_as_text(&self, url: &str) -> String {
102        return 
103            reqwest::blocking::get(url).unwrap()
104                .text().unwrap()       
105    }
106
107    /// Used to send a post request
108    /// * `url` - base url that might include an api key
109    /// * `data` - most likely going to be json. Use serde_json::json macro if passing as a string.
110    ///# Examples
111    /// ```
112    /// use ddf_blocking_http_client::Client;
113    /// use serde_json::json;
114    /// use dotenv::dotenv;
115    /// use std::env;
116    ///
117    /// // dotenv is being used for lack of an example not requiring private keys and public wallet addresses.
118    /// // dotenv is not required for this crate to work.
119    /// dotenv::dotenv().ok();
120    /// let api_key = env::var("INFURA_API_KEY").unwrap();
121    /// let client = Client::new();
122    /// let url = [
123    ///     "https://mainnet.infura.io/v3/",
124    ///     &api_key
125    /// ].concat();
126    /// let res = client.post(&
127    ///     url, 
128    ///     json!(
129    ///         {"jsonrpc":"2.0",
130    ///         "method":"eth_estimateGas",
131    ///         "params": [
132    ///             {"from": "0xb60e8dd61c5d32be8058bb8eb970870f07233155",
133    ///             "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
134    ///             "gas": "0x76c0",
135    ///             "gasPrice": "0x9184e72a000",
136    ///             "value": "0x9184e72a",
137    ///             "data": "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}
138    ///     )
139    /// );
140    /// dbg!(&res);
141    /// ```
142    
143    pub fn post<T: Serialize>(&self, url: &str, data: T)        
144    where T: std::fmt::Debug
145    {
146        let response = self.client
147            .post(url)
148            .header(CONTENT_TYPE, "application/json")
149            .header(ACCEPT, "application/json")
150            .json(&data)
151            .send()
152            .unwrap()
153            .text().unwrap();
154        dbg!(response);
155    }
156
157    // this adds some params the end user doesn't need to be concerned with.
158    // * `tries` - the number of times the  client has actually tried.
159    // * `incremental_wait_time` - starts at a second and doubles each time.
160    fn _get<T: DeserializeOwned>(
161        &self, 
162        url: &str, 
163        times_to_try: u32, 
164        mut tries: u32, 
165        mut incremental_wait_time: Duration
166    ) 
167    -> HttpResult<T> {
168        if tries >= times_to_try { return Err(HttpError::TooManyTries{num_tries: tries})}
169        let response = reqwest::blocking::Client::new()
170            .get(url)
171            .header(CONTENT_TYPE, "application/json")
172            //.header(ACCEPT, "application/json")
173            .send()
174            .unwrap();
175       
176        match response.status() {
177            
178            reqwest::StatusCode::OK => {
179                // on success, parse our JSON to an APIResponse
180                match response.json::<T>() {
181                    Ok(parsed) => Ok(parsed),
182                    Err(_) => Err(HttpError::ParseJson{message: "Hm, the response didn't match the shape we expected.".to_string()}),
183                }
184            }
185            reqwest::StatusCode::UNAUTHORIZED => {
186                Err(HttpError::BadToken)
187            }
188            reqwest::StatusCode::NOT_FOUND => {
189                Err(
190                    HttpError::NotFound {
191                        message: 
192                            "Response 404, probably means the server couldn't find the data you were looking for.".to_string()
193                    }
194                )
195            }
196            reqwest::StatusCode::BAD_REQUEST => {
197                println!("💥 Caught BAD_REQUEST");
198                dbg!(&response.status());
199                tries += 1;
200                sleep(Duration::from_secs(1));
201                self._get(url, times_to_try, tries, incremental_wait_time)
202            }
203            reqwest::StatusCode::TOO_MANY_REQUESTS => {
204                println!("🥷🏾 {} be complainin' 'bout too many requests!", url);
205                println!("🥷🏾 So I'm waiting {} from_secs and trying again.", incremental_wait_time.as_secs());
206                println!("🥷🏾 This was try {} of {}.", tries, times_to_try);
207                sleep(incremental_wait_time);
208                incremental_wait_time *= 2;
209                tries += 1;
210                self._get(url, times_to_try, tries, incremental_wait_time)
211            }
212            _ => {
213                dbg!("💥 Uncaught http error, response.status() was: ", response.status(),"/n");
214                tries += 1;
215                sleep(incremental_wait_time);
216                incremental_wait_time *= 2;
217                self._get(url, times_to_try, tries, incremental_wait_time)
218            }
219        }
220    }
221}