/ src / api.rs
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  }