diff --git a/koucha/src/db.rs b/koucha/src/db.rs index 4964fd9..9be8130 100644 --- a/koucha/src/db.rs +++ b/koucha/src/db.rs @@ -2,6 +2,8 @@ mod user; pub use user::User; mod feed; pub use feed::Feed; +mod feed_channel; +pub use feed_channel::FeedChannel; mod channel; pub use channel::Channel; mod item; @@ -12,9 +14,17 @@ macro_rules! define_key { #[derive(PartialEq, Debug, Copy, Clone)] pub struct $name(i64); }; + + ($name:ident, $($field:ident : $type:ty),* $(,)?) => { + #[derive(PartialEq, Debug, Copy, Clone)] + pub struct $name { + $($field: $type),* + } + }; } define_key!(UserKey); define_key!(FeedKey); +define_key!(FeedChannelKey, feed_key: FeedKey, channel_key: ChannelKey); define_key!(ChannelKey); define_key!(ItemKey); diff --git a/koucha/src/db/channel.rs b/koucha/src/db/channel.rs index 0f38f3e..eaa03be 100644 --- a/koucha/src/db/channel.rs +++ b/koucha/src/db/channel.rs @@ -6,6 +6,8 @@ use crate::{ db::{ ChannelKey, Item, + FeedChannel, + feed_channel::UnparsedFeedChannel, item::UnparsedItem, }, }; @@ -79,6 +81,19 @@ impl Channel { ).fetch_one(&pool.0).await?.parse() } + async fn get_feed_channels( + &self, pool: &AdapterPool + ) -> Result> { + sqlx::query_as!( + UnparsedFeedChannel, + "SELECT channel_id, feed_id, initial_score, gravity, boost + FROM feed_channels + WHERE channel_id = ?", + self.key.0 + ).fetch_all(&pool.0).await?.into_iter() + .map(UnparsedFeedChannel::parse).collect() + } + pub async fn get_items(&self, pool: &AdapterPool) -> Result> { sqlx::query_as!( UnparsedItem, @@ -95,8 +110,10 @@ impl Channel { mod tests { use super::*; use crate::{ + db::{Feed, User}, test_utils::{ - FEED1, FEED2, CHANNEL_TITLE, CHANNEL_DESC, ITEM_GUID, ITEM_GUID2, + FEED1, FEED2, CHANNEL_TITLE, CHANNEL_DESC, USERNAME, FEED_TITLE, + FEED_TITLE2, ITEM_GUID, ITEM_GUID2, setup_adapter, setup_channel, }, @@ -192,6 +209,24 @@ mod tests { assert_eq!(channels.len(), 2); } + #[tokio::test] + async fn get_feed_channels() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let channel = setup_channel(pool).await; + + let user = User::create(pool, USERNAME).await.unwrap(); + let feed1 = Feed::create(pool, user.key(), FEED_TITLE).await.unwrap(); + let feed2 = Feed::create(pool, user.key(), FEED_TITLE2).await.unwrap(); + + feed1.add_channel(pool, channel.key).await.unwrap(); + feed2.add_channel(pool, channel.key).await.unwrap(); + + let fc_list = channel.get_feed_channels(pool).await.unwrap(); + + assert_eq!(fc_list.len(), 2); + } + #[tokio::test] async fn get_items() { let adapter = setup_adapter().await; diff --git a/koucha/src/db/feed_channel.rs b/koucha/src/db/feed_channel.rs new file mode 100644 index 0000000..e2df61a --- /dev/null +++ b/koucha/src/db/feed_channel.rs @@ -0,0 +1,188 @@ +use crate::{ + Result, + AdapterPool, + db::{ + Channel, + ChannelKey, + Feed, + FeedKey, + FeedChannelKey, + Item, + }, + score::{ + Score, + Gravity, + Boost, + }, +}; +use chrono::{Utc, DateTime}; + +pub struct UnparsedFeedChannel { + pub channel_id: i64, + pub feed_id: i64, + pub initial_score: Option, + pub gravity: Option, + pub boost: Option, +} +impl UnparsedFeedChannel { + pub fn parse(self) -> Result { + Ok(FeedChannel { + key: FeedChannelKey { + feed_key: FeedKey(self.feed_id), + channel_key: ChannelKey(self.channel_id), + }, + initial_score: Score::new(self.initial_score), + gravity: Gravity::new(self.gravity), + boost: Boost::new(self.boost), + }) + } +} + +pub struct FeedChannel { + key: FeedChannelKey, + initial_score: Score, + gravity: Gravity, + boost: Boost, +} + +impl FeedChannel { + pub async fn get_channel(&self, pool: &AdapterPool) -> Result { + Channel::get(pool, self.key.channel_key).await + } + pub async fn get_feed(&self, pool: &AdapterPool) -> Result { + Feed::get(pool, self.key.feed_key).await + } + + pub async fn add_item( + &self, pool: &AdapterPool, item: &Item + ) -> Result<()> { + self.add_item_at(pool, item, Utc::now()).await + } + + async fn add_item_at( + &self, pool: &AdapterPool, item: &Item, add_at: DateTime + ) -> Result<()> { + let int_item_id = item.key().0; + let int_initial_score = i64::from(self.initial_score); + let string_last_updated = add_at.to_rfc2822(); + + sqlx::query!( + "INSERT OR IGNORE INTO feed_items (feed_id, item_id, score, last_updated) + VALUES (?, ?, ?, ?)", + self.key.feed_key.0, int_item_id, int_initial_score, string_last_updated + ).execute(&pool.0).await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use reqwest::Url; + use crate::{ + db::{ + Channel, + FeedKey, + User + }, + test_utils::{ + FEED1, setup_adapter, get_datetime + }, + }; + + #[test] + fn parse() { + const CID: i64 = 1; + const FID: i64 = 2; + const IS: i64 = 3; + const G: i64 = 4; + const B: i64 = 5; + let ufc = UnparsedFeedChannel { + channel_id: CID, + feed_id: FID, + initial_score: Some(IS), + gravity: Some(G), + boost: Some(B), + }; + + let fc = ufc.parse().unwrap(); + + assert_eq!(fc.key.channel_key.0, CID); + assert_eq!(fc.key.feed_key.0, FID); + assert_eq!(i64::from(fc.initial_score), IS); + assert_eq!(i64::from(fc.gravity), G); + assert_eq!(i64::from(fc.boost), B); + } + + // FeedChannel Tests + #[tokio::test] + async fn get_channel() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + + let url = Url::parse(FEED1).unwrap(); + let channel = Channel::get_or_create(pool, url).await.unwrap(); + + let fc = FeedChannel { + key: FeedChannelKey { + feed_key: FeedKey(1), // Fake Feed + channel_key: channel.key(), + }, + initial_score: Score::new(None), + gravity: Gravity::new(None), + boost: Boost::new(None), + }; + + let channel_from_fc = fc.get_channel(pool).await.unwrap(); + assert_eq!(channel_from_fc.key(), channel.key()); + } + + #[tokio::test] + async fn get_feed() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + + let user = User::create(pool, "Alice").await.unwrap(); + let feed = Feed::create(pool, user.key(), "My Feed").await.unwrap(); + + let fc = FeedChannel { + key: FeedChannelKey { + feed_key: feed.key(), + channel_key: ChannelKey(1), // Fake Channel + }, + initial_score: Score::new(None), + gravity: Gravity::new(None), + boost: Boost::new(None), + }; + + let feed_from_fc = fc.get_feed(pool).await.unwrap(); + assert_eq!(feed_from_fc.key(), feed.key()); + } + + #[tokio::test] + pub async fn add_item() { + let dt = get_datetime(); + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + + let user = User::create(pool, "Alice").await.unwrap(); + let feed = Feed::create(pool, user.key(), "My Feed").await.unwrap(); + let url = Url::parse(FEED1).unwrap(); + let channel = Channel::get_or_create(pool, url).await.unwrap(); + let fc = FeedChannel { + key: FeedChannelKey { + feed_key: feed.key(), + channel_key: channel.key(), + }, + initial_score: Score::new(None), + gravity: Gravity::new(None), + boost: Boost::new(None), + }; + + let item = Item::get_or_create(pool, channel.key(), "item-guid").await.unwrap(); + fc.add_item_at(pool, &item, dt).await.unwrap(); + + let items = feed.get_items(pool, 1, 0).await.unwrap(); + assert_eq!(items[0].key(), item.key()); + } +} diff --git a/koucha/src/score.rs b/koucha/src/score.rs index e289c6e..754ca17 100644 --- a/koucha/src/score.rs +++ b/koucha/src/score.rs @@ -61,6 +61,41 @@ impl Gravity { ) } } +pub struct UnparsedTimedScore { + pub value: i64, + pub last_updated: DateTime, + pub last_boosted: Option>, +} + +impl UnparsedTimedScore { + pub fn parse(self) -> TimedScore { + match self.last_boosted { + None => TimedScore::Decaying(DecayingScore { + value: Score(self.value), + last_updated: self.last_updated, + }), + Some(last_boosted) => TimedScore::Boosted(BoostedScore { + value: Score(self.value), + boosted_at: last_boosted, + }), + } + } + + pub fn unparse(ts: TimedScore) -> Self { + match ts { + TimedScore::Decaying(ds) => UnparsedTimedScore { + value: ds.value.into(), + last_updated: ds.last_updated, + last_boosted: None, + }, + TimedScore::Boosted(bs) => UnparsedTimedScore { + value: bs.value.into(), + last_updated: bs.boosted_at, + last_boosted: Some(bs.boosted_at), + }, + } + } +} #[derive(Clone)] pub enum TimedScore { @@ -211,6 +246,29 @@ mod tests { // "Score" Tests + #[test] + fn parse_decaying() { + let ups = UnparsedTimedScore { + value: 10, + last_updated: get_datetime(), + last_boosted: None, + }; + + ups.parse().get_decaying().unwrap(); + } + + #[test] + fn parse_boosted() { + let dt = get_datetime(); + let ups = UnparsedTimedScore { + value: 10, + last_updated: dt, + last_boosted: Some(dt), + }; + + ups.parse().get_boosted().unwrap(); + } + #[test] fn new() { let score = TimedScore::new();