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}