hacker_news/client/
json_client.rs

1use std::error::Error;
2use log;
3use serde_json;
4use reqwest;
5use reqwest::blocking::Client;
6use reqwest::blocking::Request;
7use reqwest::blocking::Response;
8use crate::error::HnError;
9use crate::error::HttpError;
10use crate::model::Id;
11use crate::model::firebase::User;
12use crate::model::firebase::Item;
13use crate::model::firebase::ItemsAndProfiles;
14    
15pub struct JsonClient {
16    http_client: reqwest::blocking::Client,
17}
18
19const BASE_URL: &str = "https://hacker-news.firebaseio.com/v0";
20
21#[allow(clippy::new_without_default)]
22impl JsonClient {
23
24    pub fn new() -> Self {
25        Self {
26            http_client: Client::new(), 
27        }
28    }
29    
30    fn send(&self, req: Request) -> Result<Response, Box<dyn Error>> {
31        let resp = self.http_client.execute(req)?;
32        let status = resp.status().as_u16();
33        if status != 200 {
34            let err = HttpError {
35                url:  resp.url().to_string(),
36                code: status,
37            };
38            log::error!("Recieved non 200 status: {:?}", err);
39            return Err(Box::new(HnError::HttpError(err)));
40        }
41        log::debug!("Recieved 200 status, response = {:?}", resp);
42
43        Ok(resp)
44    }
45    
46    fn get_url(&self, url: &str) -> Result<Response, Box<dyn Error>> {
47        let req = self.http_client.get(url);
48        let resp = self.send(req.build()?)?;
49
50        Ok(resp)
51    }
52
53    pub fn item(&self, id: Id) -> Result <Item, Box<dyn Error>> {
54        let url = format!("{base_url}/item/{id}.json",
55            base_url=BASE_URL,
56            id=id
57        );
58
59        let resp = self.get_url(&url)?;
60        let text = resp.text()?;
61        log::debug!("text = {:?}", text);
62        let item: Item = serde_json::from_str(&text)?;
63        log::debug!("item = {:?}", item);
64
65        Ok(item)
66    }
67    
68    pub fn max_item(&self) -> Result <Id, Box<dyn Error>> {
69        let url = format!("{base_url}/maxitem.json",
70            base_url=BASE_URL,
71        );
72        
73        let resp = self.get_url(&url)?;
74        let text = resp.text()?;
75        log::debug!("text = {:?}", text);
76        let id: Id = serde_json::from_str(&text)?;
77        log::debug!("maxitem = {:?}", id);
78
79        Ok(id)
80    }
81
82    pub fn user(&self, username: String) -> Result<User, Box<dyn Error>> {
83        let url = format!("{base_url}/user/{id}.json",
84            base_url=BASE_URL,
85            id=username
86        );
87        
88        let resp = self.get_url(&url)?;
89        let text = resp.text()?;
90        log::debug!("text = {:?}", text);
91        let user: User = serde_json::from_str(&text)?;
92        log::debug!("user = {:?}", user);
93
94        Ok(user)
95    }
96
97    // TODO: newstorires and topstories are the exact same method with one substring changed.
98    // Consider code consolidation, both with respect to these but all the API methods. For
99    // example, would it be better for HackerNews Client API methods to create a request object,
100    // and then have a single `request` method which executes a request?
101
102    pub fn new_stories(&self) -> Result<Vec<Id>, Box<dyn Error>> {
103        let url = format!("{base_url}/newstories.json",
104            base_url=BASE_URL,
105        );
106        
107        let resp = self.get_url(&url)?;
108        let text = resp.text()?;
109        log::debug!("text = {:?}", text);
110        let ids: Vec<Id> = serde_json::from_str(&text)?;
111        log::debug!("ids = {:?}", ids);
112
113        Ok(ids)
114    }
115
116    pub fn top_stories(&self) -> Result<Vec<Id>, Box<dyn Error>> {
117        let url = format!("{base_url}/topstories.json",
118            base_url=BASE_URL,
119        );
120        
121        let resp = self.get_url(&url)?;
122        let text = resp.text()?;
123        log::debug!("text = {:?}", text);
124        let ids: Vec<Id> = serde_json::from_str(&text)?;
125        log::debug!("ids = {:?}", ids);
126
127        Ok(ids)
128    }
129
130    pub fn updates(&self) -> Result<(Vec<Id>, Vec<String>), Box<dyn Error>> {
131        let url = format!("{base_url}/updates.json",
132            base_url=BASE_URL,
133        );
134        
135        let resp = self.get_url(&url)?;
136        let text = resp.text()?;
137        log::debug!("text = {:?}", text);
138        let items_and_profiles: ItemsAndProfiles = serde_json::from_str(&text)?;
139        let items = items_and_profiles.items;
140        let profiles = items_and_profiles.profiles;
141        let updates = (items, profiles);
142        log::debug!("updates = {:?}", updates);
143
144        Ok(updates)
145    }
146
147    pub fn ask_stories(&self) -> Result<Vec<Id>, Box<dyn Error>> {
148        let url = format!("{base_url}/askstories.json",
149            base_url=BASE_URL,
150        );
151        let resp = self.get_url(&url)?;
152        let text = resp.text()?;
153        log::debug!("text = {:?}", text);
154        let ids: Vec<Id> = serde_json::from_str(&text)?;
155        log::debug!("ids = {:?}", ids);
156
157        Ok(ids)
158    }
159
160    pub fn show_stories(&self) -> Result<Vec<Id>, Box<dyn Error>> {
161        let url = format!("{base_url}/showstories.json",
162            base_url=BASE_URL,
163        );
164        
165        let resp = self.get_url(&url)?;
166        let text = resp.text()?;
167        log::debug!("text = {:?}", text);
168        let ids: Vec<Id> = serde_json::from_str(&text)?;
169        log::debug!("ids = {:?}", ids);
170
171        Ok(ids)
172    }
173    
174    pub fn job_stories(&self) -> Result<Vec<Id>, Box<dyn Error>> {
175        let url = format!("{base_url}/jobstories.json",
176            base_url=BASE_URL,
177        );
178        
179        let resp = self.get_url(&url)?;
180        let text = resp.text()?;
181        log::debug!("text = {:?}", text);
182        let ids: Vec<Id> = serde_json::from_str(&text)?;
183        log::debug!("ids = {:?}", ids);
184
185        Ok(ids)
186    }
187
188}
189
190#[cfg(test)]
191mod tests {
192
193    use super::JsonClient;
194    use std::error::Error;
195    use crate::util::setup;
196
197    #[test]
198    fn test_item() -> Result<(), Box<dyn Error>> {
199        setup();
200
201        let id_story = 27476206;
202        let id_comment = 27509155;
203
204        let client = JsonClient::new();
205        let story = client.item(id_story)?;
206        log::debug!("item = {:?}", story);
207        assert!(story.is_story());
208        
209        let comment = client.item(id_comment)?;
210        log::debug!("item = {:?}", comment);
211        assert!(comment.is_comment());
212
213        Ok(())
214    }
215
216    #[test]
217    fn test_max_item() -> Result<(), Box<dyn Error>> {
218        setup();
219
220        let client = JsonClient::new();
221        let item = client.max_item()?;
222        log::debug!("maxitem = {:?}", item);
223
224        Ok(())
225    }
226    
227    #[test]
228    fn test_user() -> Result<(), Box<dyn Error>> {
229        setup();
230
231        let client = JsonClient::new();
232        let user = client.user("pg".to_string())?;
233        log::debug!("user = {:?}", user);
234
235        Ok(())
236    }
237    
238    #[test]
239    fn test_new_stories() -> Result<(), Box<dyn Error>> {
240        setup();
241
242        let client = JsonClient::new();
243        let ids = client.new_stories()?;
244        log::debug!("ids = {:?}", ids);
245
246        Ok(())
247    }
248    
249    #[test]
250    fn test_top_stories() -> Result<(), Box<dyn Error>> {
251        setup();
252
253        let client = JsonClient::new();
254        let ids = client.top_stories()?;
255        log::debug!("ids = {:?}", ids);
256
257        Ok(())
258    }
259    
260    #[test]
261    fn test_updates() -> Result<(), Box<dyn Error>> {
262        setup();
263
264        let client = JsonClient::new();
265        let updates = client.updates()?;
266        log::debug!("updates = {:?}", updates);
267
268        Ok(())
269    }
270    
271    #[test]
272    fn test_ask_stories() -> Result<(), Box<dyn Error>> {
273        setup();
274
275        let client = JsonClient::new();
276        let ids = client.ask_stories()?;
277        log::debug!("ids = {:?}", ids);
278
279        Ok(())
280    }
281    
282    #[test]
283    fn test_show_stories() -> Result<(), Box<dyn Error>> {
284        setup();
285
286        let client = JsonClient::new();
287        let ids = client.show_stories()?;
288        log::debug!("ids = {:?}", ids);
289
290        Ok(())
291    }
292
293    #[test]
294    fn test_job_stories() -> Result<(), Box<dyn Error>> {
295        setup();
296
297        let client = JsonClient::new();
298        let ids = client.job_stories()?;
299        log::debug!("ids = {:?}", ids);
300
301        Ok(())
302    }
303}