diff --git a/Cargo.toml b/Cargo.toml index c2f18ded..51463768 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,9 @@ edition = "2018" [dependencies] +derive-error = "0.0.5" reqwest = "0.11.*" chrono = "0.4.*" json = "0.12.*" tokio = { version = "1.3.*", features = ["full"] } + diff --git a/src/lib.rs b/src/lib.rs index 02afcb7a..0e2c25aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,30 +7,33 @@ //!extern crate tokio; //!use crate::rapr::{RaprClient,RaSub}; //!#[tokio::main] -//!async fn main() { +//!async fn main() -> Result<(), rapr::Error> { //! let client = RaprClient::new(); //! let mut sub = RaSub::new("pics"); -//! client.fetch(10, &mut sub).await.unwrap(); +//! client.fetch(10, &mut sub).await?; //! for post in sub.posts { //! println!("{}",post.title); //! } +//! Ok(()) //!} +#[macro_use] +extern crate derive_error; mod rapr; -pub use crate::rapr::{RaPost, RaSub, RaprClient}; +pub use crate::rapr::Error; +pub use crate::rapr::{RaPost, RaPostItems, RaSub, RaprClient}; #[cfg(test)] mod tests { use crate::rapr::{RaSub, RaprClient}; #[tokio::test] - async fn subreddit() { + async fn subreddit() -> Result<(), crate::rapr::Error> { let client = RaprClient::new(); let mut sub = RaSub::new("rust"); - client.fetch(10, &mut sub).await.unwrap(); - // println!("{:#?}", sub.posts.len()); - // client.fetch(5, &mut sub).await.unwrap(); + client.fetch(10, &mut sub).await?; for post in sub.posts { println!("{:#?}", post); } + Ok(()) } #[tokio::test] async fn title() { diff --git a/src/rapr.rs b/src/rapr.rs index b3107548..62b14fb8 100644 --- a/src/rapr.rs +++ b/src/rapr.rs @@ -1,20 +1,38 @@ extern crate json; extern crate reqwest; extern crate tokio; -use chrono::{DateTime, Local}; +// use chrono::{DateTime, Local}; use std::fmt; -#[derive(Debug)] +/// Error enum +#[derive(Debug, Error)] pub enum Error { - JsonParseError, - HttpGetError, + UnexpectedJson, + NoneError, + JsonParseError(json::Error), + RequestError(reqwest::Error), } -#[derive(Debug, Clone, Copy)] -pub enum TextType { - HTML, - Raw, - Empty, +/// Items related to the post +#[derive(Debug, Clone)] +pub struct RaPostItems { + pub upvotes: u32, + pub downvotes: u32, + pub permalink: String, + pub url: Option, +} + +impl RaPostItems { + /// Create a new struct of items for a post + /// You probably don't need this fucntion. + pub fn new(upvotes: u32, downvotes: u32, permalink: &str, url: Option) -> Self { + Self { + upvotes, + downvotes, + permalink: permalink.to_string(), + url, + } + } } /// Reddit post object @@ -23,13 +41,9 @@ pub enum TextType { #[derive(Clone)] pub struct RaPost { pub id: String, - pub datetime: DateTime, pub title: String, pub text: Option, - texttype: TextType, - pub permalink: String, - pub upvotes: u32, - pub downvotes: u32, + pub items: RaPostItems, pub json: json::JsonValue, } @@ -37,12 +51,10 @@ impl fmt::Debug for RaPost { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("RaPost") .field("id", &self.id) - .field("datetime", &self.datetime) .field("title", &self.title) .field("text", &self.text) - .field("texttype", &self.texttype) - .field("upvotes", &self.upvotes) - .field("downvotes", &self.downvotes) + .field("permalink", &self.items.permalink) + .field("url", &self.items.url) .finish() } } @@ -53,54 +65,35 @@ impl RaPost { id: &str, title: &str, text: Option<&str>, - texttype: TextType, - permalink: &str, - upvotes: u32, - downvotes: u32, + items: RaPostItems, json: &json::JsonValue, ) -> Self { - let _text = match text { - Some(text) => Some(text.to_string()), - None => None, - }; Self { id: id.to_string(), title: title.to_string(), - datetime: Local::now(), // Temp - text: _text, - texttype, - permalink: String::from(permalink), - upvotes, - downvotes, + text: text.map(String::from), + items, json: json.clone(), } } + /// Parse json from `json['data']['children']` array elements. - pub fn parse(post: &json::JsonValue) -> Result { - let mut text: Option<&str> = post["selftext"].as_str(); - let mut texttype: TextType = TextType::Empty; - // Reddit always returns an empty string on selftext - // if there is no text. So this shouldn't panic! - // Some also have both selftext and selftext_html - // I am only taking selftext from these. - if text.unwrap().is_empty() { - text = post["selftext_html"].as_str(); - if text != None { - texttype = TextType::HTML; - } - } else { - texttype = TextType::Raw; + pub fn parse(post: &json::JsonValue) -> Result { + let id = post["name"].as_str().ok_or(Error::UnexpectedJson)?; + let title = post["title"].as_str().ok_or(Error::UnexpectedJson)?; + let upvotes = post["ups"].as_u32().ok_or(Error::UnexpectedJson)?; + let downvotes = post["downs"].as_u32().ok_or(Error::UnexpectedJson)?; + let permalink = post["permalink"].as_str().ok_or(Error::UnexpectedJson)?; + let url = post["url"].as_str().map(String::from); + + let mut selftext = post["selftext"].as_str(); + if selftext.ok_or(Error::NoneError)?.is_empty() { + // Either unwraps and sets new value or reamins "" + selftext = post["selftext_html"].as_str(); } - Ok(RaPost::new( - post["name"].as_str().unwrap(), - post["title"].as_str().unwrap(), - text, - texttype, - post["permalink"].as_str().unwrap(), - post["ups"].as_u32().unwrap(), - post["downs"].as_u32().unwrap(), - post, - )) + let items = RaPostItems::new(upvotes, downvotes, permalink, url); + + Ok(RaPost::new(id, title, selftext, items, post)) } } @@ -134,6 +127,12 @@ pub struct RaprClient { rwclient: reqwest::Client, } +impl Default for RaprClient { + fn default() -> Self { + Self::new() + } +} + impl RaprClient { pub fn new() -> Self { Self { @@ -141,6 +140,9 @@ impl RaprClient { rwclient: reqwest::Client::new(), } } + pub fn default() -> Self { + Self::new() + } /// Fetch posts from subreddit and store them in the subreddit object. /// Note: First fetch always seems to pull two pinned posts which are not marked pinned in the json @@ -151,34 +153,34 @@ impl RaprClient { }; let res = match &sub.after { - None => self - .rwclient - .get(url) - .query(&[("limit", count)]) - .send() - .await - .unwrap(), - Some(after) => self - .rwclient - .get(url) - .query(&[("limit", count.to_string()), ("after", after.to_string())]) - .send() - .await - .unwrap(), + None => { + self.rwclient + .get(url) + .query(&[("limit", count)]) + .send() + .await? + } + Some(after) => { + self.rwclient + .get(url) + .query(&[("limit", count.to_string()), ("after", after.to_string())]) + .send() + .await? + } }; - let mut parsed = json::parse(res.text().await.unwrap().as_str()).unwrap(); + let mut parsed = json::parse(res.text().await?.as_str())?; let raw_posts: Vec = match parsed["data"]["children"].take() { json::JsonValue::Array(arr) => arr, - _ => return Err(Error::JsonParseError), + _ => return Err(Error::UnexpectedJson), }; let mut parsed_posts: Vec = Vec::new(); for post in raw_posts { - if post["kind"].as_str().unwrap() == "t3" { - parsed_posts.push(RaPost::parse(&post["data"]).unwrap()); + if post["kind"].as_str().ok_or(Error::NoneError)? == "t3" { + parsed_posts.push(RaPost::parse(&post["data"])?); } } if parsed["data"]["after"].is_string() {