api.rs
1 use async_recursion::async_recursion; 2 use crate::types::{ 3 Comment, StoryItem, StoryPageData, StorySorting, UserData, 4 }; 5 use futures::future::join_all; 6 use thiserror::Error; 7 8 #[cfg(feature = "caching")] 9 use std::sync::Mutex; 10 11 const BASE_URL: &str = "https://hacker-news.firebaseio.com/v0"; 12 const TOP_STORIES: &str = "/topstories.json"; 13 const NEW_STORIES: &str = "/newstories.json"; 14 const BEST_STORIES: &str = "/beststories.json"; 15 const SHOW_STORIES: &str = "/showstories.json"; 16 const ASK_STORIES: &str = "/askstories.json"; 17 const JOB_STORIES: &str = "/jobstories.json"; 18 const ITEM_API: &str = "/item"; 19 const USER_API: &str = "/user"; 20 21 const STORIES_COUNT: usize = 20; 22 const COMMENT_DEPTH: i64 = 3; 23 24 #[cfg(feature = "caching")] 25 lazy_static::lazy_static! { 26 static ref STORY_CACHE: Mutex<lru::LruCache<i64, StoryPageData>> = 27 Mutex::new(lru::LruCache::new(1000)); 28 static ref STORY_PREVIEW_CACHE: Mutex<lru::LruCache<i64, StoryItem>> = 29 Mutex::new(lru::LruCache::new(1000)); 30 } 31 32 pub async fn get_stories() -> Result<Vec<StoryItem>, ServerError>{ 33 get_stories_with_sorting(StorySorting::default()).await 34 } 35 36 pub async fn get_stories_with_sorting( 37 sort: StorySorting, 38 ) -> Result<Vec<StoryItem>, ServerError> { 39 let stories_api = match sort { 40 StorySorting::Best => BEST_STORIES, 41 StorySorting::Top => TOP_STORIES, 42 StorySorting::New => NEW_STORIES, 43 StorySorting::Show => SHOW_STORIES, 44 StorySorting::Ask => ASK_STORIES, 45 StorySorting::Job => JOB_STORIES, 46 }; 47 48 let url = format!("{}{}", BASE_URL, stories_api); 49 let story_ids = make_json_get_request::<Vec<i64>>(&url).await?; 50 println!("story_ids:({}) {:#?}", story_ids.len(), story_ids); 51 let first_story_ids = &story_ids[..story_ids.len().min(STORIES_COUNT)]; 52 let story_futures = first_story_ids 53 .iter() 54 .map(|story_id| get_story_preview(*story_id)); 55 56 let mut stories = join_all(story_futures) 57 .await 58 .into_iter() 59 .filter_map(|c| c.ok()) 60 .collect::<Vec<_>>(); 61 62 stories.sort_unstable_by(|a,b|a.id.cmp(&b.id)); 63 64 Ok(stories) 65 } 66 67 pub async fn get_story(story_id: i64) -> Result<StoryPageData, ServerError> { 68 #[cfg(feature = "caching")] 69 if let Some(cached_story) = STORY_CACHE.lock().unwrap().get(&story_id) { 70 return Ok(cached_story.clone()); 71 } 72 73 let url = format!("{}{}/{}.json", BASE_URL, ITEM_API, story_id); 74 let mut story = make_json_get_request::<StoryPageData>(&url).await?; 75 let comment_ids = &story.kids[..story.kids.len().min(3)]; 76 let comments = join_all( 77 comment_ids 78 .iter() 79 .map(|story_id| get_comment_with_depth(*story_id, COMMENT_DEPTH)), 80 ) 81 .await 82 .into_iter() 83 .filter_map(|c| c.ok()) 84 .collect(); 85 86 story.comments = comments; 87 88 #[cfg(feature = "caching")] 89 STORY_CACHE.lock().unwrap().put(story_id, story.clone()); 90 91 Ok(story) 92 } 93 94 // Same as get_story but does not add comments 95 pub async fn get_story_preview(story_id: i64) -> Result<StoryItem, ServerError> { 96 #[cfg(feature = "caching")] 97 if let Some(cached_story) = 98 STORY_PREVIEW_CACHE.lock().unwrap().get(&story_id) 99 { 100 return Ok(cached_story.clone()); 101 } 102 103 let url = format!("{}{}/{}.json", BASE_URL, ITEM_API, story_id); 104 let story_preview = make_json_get_request::<StoryItem>(&url).await?; 105 106 #[cfg(feature = "caching")] 107 STORY_PREVIEW_CACHE 108 .lock() 109 .unwrap() 110 .put(story_id, story_preview.clone()); 111 112 Ok(story_preview) 113 } 114 115 116 117 #[cfg_attr(target_arch = "wasm32", async_recursion(?Send))] 118 #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] 119 pub async fn get_comment_with_depth( 120 story_id: i64, 121 depth: i64, 122 ) -> Result<Comment, ServerError> { 123 let url = format!("{}{}/{}.json", BASE_URL, ITEM_API, story_id); 124 let mut comment = make_json_get_request::<Comment>(&url).await?; 125 if depth > 0 { 126 let sub_comment_ids = &comment.kids[..comment.kids.len().min(3)]; 127 let sub_comments = join_all( 128 sub_comment_ids 129 .iter() 130 .map(|story_id| get_comment_with_depth(*story_id, depth - 1)), 131 ) 132 .await 133 .into_iter() 134 .filter_map(|c| c.ok()) 135 .collect(); 136 137 comment.sub_comments = sub_comments; 138 } 139 Ok(comment) 140 } 141 142 pub async fn get_comment(comment_id: i64) -> Result<Comment, ServerError> { 143 let comment = get_comment_with_depth(comment_id, COMMENT_DEPTH).await?; 144 Ok(comment) 145 } 146 147 148 pub async fn get_user_page(user_id: &str) -> Result<UserData, ServerError> { 149 let url = format!("{}{}/{}.json", BASE_URL, USER_API, user_id); 150 let mut user = make_json_get_request::<UserData>(&url).await?; 151 //submitted could be comments or story post 152 let first_story_ids = &user.submitted[..user.submitted.len().min(30)]; 153 let story_futures = first_story_ids 154 .iter() 155 .map(|story_id| get_story_preview(*story_id)); 156 157 // we only filter the success where other types such as comments fail 158 let stories = join_all(story_futures) 159 .await 160 .into_iter() 161 .filter_map(|story| story.ok()) 162 .collect(); 163 164 user.stories = stories; 165 166 dbg!(&user); 167 Ok(user) 168 } 169 170 #[derive(Error, Debug)] 171 pub enum ServerError { 172 #[error("reqwest error: {0}")] 173 Reqwest(#[from] reqwest::Error), 174 #[error("json error: {0}")] 175 SerdeJson(#[from] serde_json::Error), 176 } 177 178 179 pub async fn make_json_get_request<T: serde::de::DeserializeOwned>( 180 url: &str, 181 ) -> Result<T, ServerError> { 182 dbg!(url); 183 let response = reqwest::get(url).await?; 184 Ok(response.json::<T>().await?) 185 }