From f5fc83a471589fc0fac9c8ce021714b6f8f4ffdb Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Thu, 22 Jan 2026 11:39:33 -0800 Subject: [PATCH] Clean up server.rs, add DB and Client aliases --- koucha/src/bin/server.rs | 7 --- koucha/src/channel.rs | 95 +++++++++++++++++++++------------------- koucha/src/feed.rs | 55 +++++++++++------------ koucha/src/lib.rs | 39 ++++++++++++----- koucha/src/user.rs | 85 ++++++++++++++++++----------------- 5 files changed, 153 insertions(+), 128 deletions(-) diff --git a/koucha/src/bin/server.rs b/koucha/src/bin/server.rs index 4a05eed..5ca7b99 100644 --- a/koucha/src/bin/server.rs +++ b/koucha/src/bin/server.rs @@ -1,14 +1,7 @@ use std::error::Error; -use reqwest::Client; -use koucha::{ - AdapterOptions, -}; #[tokio::main] async fn main() -> Result<(), Box> { - // let adapter = AdapterOptions::new().create().await?; - // - // let _channel = fetch_channel(&client, "https://lorem-rss.herokuapp.com/feed?unit=year").await?; Ok(()) } diff --git a/koucha/src/channel.rs b/koucha/src/channel.rs index b95b0b8..da61531 100644 --- a/koucha/src/channel.rs +++ b/koucha/src/channel.rs @@ -1,8 +1,9 @@ -use reqwest::{Url, Client}; -use sqlx::SqlitePool; +use reqwest::Url; use chrono::{DateTime, Utc}; use crate::{ Result, + AdapterPool, + AdapterClient, Item, channel::fetch::FetchedRSSChannel, item::UnparsedItem, @@ -50,29 +51,29 @@ impl Channel { pub fn link(&self) -> &Url { &self.link } pub fn description(&self) -> Option<&str> { self.description.as_deref() } - pub async fn get_all(pool: &SqlitePool) -> Result> { + pub async fn get_all(pool: &AdapterPool) -> Result> { let channels: Result> = sqlx::query_as!( UnparsedChannel, "SELECT id, title, link, description, last_fetched FROM channels" - ).fetch_all(pool).await?.into_iter().map(UnparsedChannel::parse).collect(); + ).fetch_all(&pool.0).await?.into_iter().map(UnparsedChannel::parse).collect(); channels } - pub async fn get(pool: &SqlitePool, id: ChannelId) -> Result { + pub async fn get(pool: &AdapterPool, id: ChannelId) -> Result { let channel: Result = sqlx::query_as!( UnparsedChannel, "SELECT id, title, link, description, last_fetched FROM channels WHERE id = ?", id.0 - ).fetch_one(pool).await?.parse(); + ).fetch_one(&pool.0).await?.parse(); channel } pub async fn create( - pool: &SqlitePool, link: Url + pool: &AdapterPool, link: Url ) -> Result { let link_str = link.as_str(); @@ -82,7 +83,7 @@ impl Channel { FROM channels WHERE link = ?", link_str - ).fetch_one(pool).await { + ).fetch_one(&pool.0).await { return existing_channel.parse(); } @@ -92,7 +93,7 @@ impl Channel { VALUES (?, ?) RETURNING id, title, link, description, last_fetched", link_str, link_str - ).fetch_one(pool).await?.parse(); + ).fetch_one(&pool.0).await?.parse(); new_channel } @@ -102,12 +103,12 @@ impl Channel { // TODO implement conditional fetching pub async fn fetch_rss( - &self, client: &Client + &self, client: &AdapterClient ) -> Result> { if self.should_skip_fetch() { return Ok(None); } - let bytestream = client.get(self.link.clone()) + let bytestream = client.0.get(self.link.clone()) .send().await? .bytes().await?; @@ -117,7 +118,7 @@ impl Channel { } pub async fn update_metadata( - &self, pool: &SqlitePool, fetched: FetchedRSSChannel + &self, pool: &AdapterPool, fetched: FetchedRSSChannel ) -> Result<()> { let title = fetched.title(); let description = fetched.description(); @@ -130,13 +131,13 @@ impl Channel { WHERE id = ?", title, link, description, fetched_at, self.id.0 - ).execute(pool).await?; + ).execute(&pool.0).await?; Ok(()) } pub async fn update_items( - &self, pool: &SqlitePool, fetched: FetchedRSSChannel + &self, pool: &AdapterPool, fetched: FetchedRSSChannel ) -> Result<()> { let fetched_at = fetched.fetched_at().to_rfc2822(); @@ -151,19 +152,19 @@ impl Channel { VALUES (?, ?, ?, ?, ?, ?)", self.id.0, guid, fetched_at, title, description, content ) - .execute(pool) + .execute(&pool.0) .await?; } Ok(()) } - pub async fn get_items(&self, pool: &SqlitePool) -> Result> { + pub async fn get_items(&self, pool: &AdapterPool) -> Result> { let items: Result> = sqlx::query_as!( UnparsedItem, "SELECT id as `id!` FROM items WHERE channel_id = ?", self.id.0 - ).fetch_all(pool).await?.into_iter().map(UnparsedItem::parse).collect(); + ).fetch_all(&pool.0).await?.into_iter().map(UnparsedItem::parse).collect(); items } @@ -172,6 +173,7 @@ impl Channel { #[cfg(test)] mod tests { use super::*; + use crate::{Adapter, AdapterBuilder}; use rss::{ Guid as RSSGuid, Item as RSSItem, @@ -179,7 +181,6 @@ mod tests { Channel as RSSChannel, ChannelBuilder as RSSChannelBuilder, }; - use sqlx::SqlitePool; const ITEM_TITLE: &str = "My Item"; const ITEM_GUID1: &str = "https://mycontent.com/blog/1"; @@ -191,18 +192,19 @@ mod tests { const FEED1: &str = "https://example.com/feed"; const FEED2: &str = "https://example2.com/feed"; - async fn setup_test_db() -> SqlitePool { - let pool = SqlitePool::connect("sqlite::memory:").await.unwrap(); - sqlx::migrate!().run(&pool).await.unwrap(); - pool + async fn setup_adapter() -> Adapter { + AdapterBuilder::new() + .database_url("sqlite::memory:") + .create().await.unwrap() } #[tokio::test] async fn create_channel() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); let url_feed = Url::parse(FEED1).unwrap(); - let channel = Channel::create(&pool, url_feed).await.unwrap(); + let channel = Channel::create(pool, url_feed).await.unwrap(); assert!(channel.id().0 > 0); assert_eq!(channel.link().as_str(), FEED1); @@ -211,11 +213,12 @@ mod tests { #[tokio::test] async fn create_duplicate_returns_existing() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); let url_feed = Url::parse(FEED1).unwrap(); - let channel1 = Channel::create(&pool, url_feed.clone()).await.unwrap(); - let channel2 = Channel::create(&pool, url_feed).await.unwrap(); + let channel1 = Channel::create(pool, url_feed.clone()).await.unwrap(); + let channel2 = Channel::create(pool, url_feed).await.unwrap(); assert_eq!( i64::from(channel1.id()), @@ -225,24 +228,26 @@ mod tests { #[tokio::test] async fn get_all_channels() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); let url_feed1 = Url::parse(FEED1).unwrap(); let url_feed2 = Url::parse(FEED2).unwrap(); - Channel::create(&pool, url_feed1).await.unwrap(); - Channel::create(&pool, url_feed2).await.unwrap(); + Channel::create(pool, url_feed1).await.unwrap(); + Channel::create(pool, url_feed2).await.unwrap(); - let channels = Channel::get_all(&pool).await.unwrap(); + let channels = Channel::get_all(pool).await.unwrap(); assert_eq!(channels.len(), 2); } #[tokio::test] async fn update_metadata() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); let url_feed = Url::parse(FEED1).unwrap(); - let channel = Channel::create(&pool, url_feed).await.unwrap(); + let channel = Channel::create(pool, url_feed).await.unwrap(); let fake_rss: RSSChannel = RSSChannelBuilder::default() .title(CHAN_TITLE) @@ -252,9 +257,9 @@ mod tests { let fetched = FetchedRSSChannel::parse(fake_rss).unwrap(); - channel.update_metadata(&pool, fetched).await.unwrap(); + channel.update_metadata(pool, fetched).await.unwrap(); - let updated = Channel::get(&pool, channel.id()).await.unwrap(); + let updated = Channel::get(pool, channel.id()).await.unwrap(); assert_eq!(updated.title(), CHAN_TITLE); assert_eq!(updated.link().as_str(), FEED2); assert_eq!(updated.description(), Some(CHAN_DESC)); @@ -262,10 +267,11 @@ mod tests { #[tokio::test] async fn update_items() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); let url_feed = Url::parse(FEED1).unwrap(); - let channel = Channel::create(&pool, url_feed).await.unwrap(); + let channel = Channel::create(pool, url_feed).await.unwrap(); let item1: RSSItem = RSSItemBuilder::default() .title(ITEM_TITLE.to_string()) @@ -290,18 +296,19 @@ mod tests { let fetched = FetchedRSSChannel::parse(fake_rss).unwrap(); - channel.update_items(&pool, fetched).await.unwrap(); + channel.update_items(pool, fetched).await.unwrap(); - let items = channel.get_items(&pool).await.unwrap(); + let items = channel.get_items(pool).await.unwrap(); assert_eq!(items.len(), 2); } #[tokio::test] async fn update_items_ignores_duplicates() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); let url_feed = Url::parse(FEED1).unwrap(); - let channel = Channel::create(&pool, url_feed).await.unwrap(); + let channel = Channel::create(pool, url_feed).await.unwrap(); let item1: RSSItem = RSSItemBuilder::default() .title(ITEM_TITLE.to_string()) @@ -319,12 +326,12 @@ mod tests { let fetched = FetchedRSSChannel::parse(fake_rss.clone()).unwrap(); - channel.update_items(&pool, fetched).await.unwrap(); + channel.update_items(pool, fetched).await.unwrap(); let fetched = FetchedRSSChannel::parse(fake_rss).unwrap(); - channel.update_items(&pool, fetched).await.unwrap(); + channel.update_items(pool, fetched).await.unwrap(); - let items = channel.get_items(&pool).await.unwrap(); + let items = channel.get_items(pool).await.unwrap(); assert_eq!(items.len(), 1); } } diff --git a/koucha/src/feed.rs b/koucha/src/feed.rs index 8e33b92..5e8092d 100644 --- a/koucha/src/feed.rs +++ b/koucha/src/feed.rs @@ -1,5 +1,6 @@ use crate::{ Result, + AdapterPool, Item, item::UnparsedItem, Channel, @@ -9,7 +10,6 @@ use crate::{ }, user::UserId, }; -use sqlx::SqlitePool; #[derive(Copy, Clone)] pub struct FeedId(i64); @@ -38,19 +38,19 @@ impl Feed { pub fn title(&self) -> &str { &self.title } pub async fn get( - pool: &SqlitePool, id: FeedId + pool: &AdapterPool, id: FeedId ) -> Result { let feed = sqlx::query_as!( UnparsedFeed, "SELECT id, title FROM feeds WHERE id = ?", id.0 - ).fetch_one(pool).await?.parse(); + ).fetch_one(&pool.0).await?.parse(); feed } pub async fn create( - pool: &SqlitePool, user_id: UserId, title: &str + pool: &AdapterPool, user_id: UserId, title: &str ) -> Result { let int_id = i64::from(user_id); let new_feed = sqlx::query_as!( @@ -59,37 +59,37 @@ impl Feed { VALUES (?, ?) RETURNING id as `id!`, title", int_id, title - ).fetch_one(pool).await?.parse(); + ).fetch_one(&pool.0).await?.parse(); new_feed } pub async fn update_title( - pool: &SqlitePool, id: FeedId, new_title: &str + pool: &AdapterPool, id: FeedId, new_title: &str ) -> Result<()> { sqlx::query!( "UPDATE feeds SET title = ? WHERE id = ?", new_title, id.0 - ).execute(pool).await?; + ).execute(&pool.0).await?; Ok(()) } pub async fn add_channel( - &self, pool: &SqlitePool, channel_id: ChannelId + &self, pool: &AdapterPool, channel_id: ChannelId ) -> Result<()> { let int_channel_id = i64::from(channel_id); sqlx::query!( "INSERT INTO feed_channels (feed_id, channel_id) VALUES (?, ?)", self.id.0, int_channel_id - ).execute(pool).await?; + ).execute(&pool.0).await?; Ok(()) } pub async fn get_items( - &self, pool: &SqlitePool, limit: u8, offset: u32 + &self, pool: &AdapterPool, limit: u8, offset: u32 ) -> Result> { let items: Result> = sqlx::query_as!( UnparsedItem, @@ -98,13 +98,13 @@ impl Feed { ORDER BY score DESC LIMIT ? OFFSET ?", self.id.0, limit, offset - ).fetch_all(pool).await?.into_iter().map(UnparsedItem::parse).collect(); + ).fetch_all(&pool.0).await?.into_iter().map(UnparsedItem::parse).collect(); items } pub async fn get_channels( - &self, pool: &SqlitePool + &self, pool: &AdapterPool ) -> Result> { let channels: Result> = sqlx::query_as!( UnparsedChannel, @@ -113,7 +113,7 @@ impl Feed { JOIN feed_channels fc on c.id = fc.channel_id WHERE fc.feed_id = ?", self.id.0 - ).fetch_all(pool).await?.into_iter() + ).fetch_all(&pool.0).await?.into_iter() .map(UnparsedChannel::parse).collect(); channels @@ -123,21 +123,21 @@ impl Feed { #[cfg(test)] mod tests { use super::*; - use crate::User; - use sqlx::SqlitePool; + use crate::{User, Adapter, AdapterBuilder}; - async fn setup_test_db() -> SqlitePool { - let pool = SqlitePool::connect("sqlite::memory:").await.unwrap(); - sqlx::migrate!().run(&pool).await.unwrap(); - pool + async fn setup_adapter() -> Adapter { + AdapterBuilder::new() + .database_url("sqlite::memory:") + .create().await.unwrap() } #[tokio::test] async fn create_feed() { - let pool = setup_test_db().await; - let user = User::create(&pool, "Alice").await.unwrap(); + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let user = User::create(pool, "Alice").await.unwrap(); - let feed = Feed::create(&pool, user.id(), "Tech News").await.unwrap(); + let feed = Feed::create(pool, user.id(), "Tech News").await.unwrap(); assert_eq!(feed.title(), "Tech News"); assert!(feed.id().0 > 0); @@ -145,13 +145,14 @@ mod tests { #[tokio::test] async fn test_update_title() { - let pool = setup_test_db().await; - let user = User::create(&pool, "Alice").await.unwrap(); - let feed = Feed::create(&pool, user.id(), "Tech News").await.unwrap(); + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let user = User::create(pool, "Alice").await.unwrap(); + let feed = Feed::create(pool, user.id(), "Tech News").await.unwrap(); - Feed::update_title(&pool, feed.id(), "Technology").await.unwrap(); + Feed::update_title(pool, feed.id(), "Technology").await.unwrap(); - let updated = Feed::get(&pool, feed.id()).await.unwrap(); + let updated = Feed::get(pool, feed.id()).await.unwrap(); assert_eq!(updated.title(), "Technology"); } } diff --git a/koucha/src/lib.rs b/koucha/src/lib.rs index d12c88e..227939a 100644 --- a/koucha/src/lib.rs +++ b/koucha/src/lib.rs @@ -3,19 +3,35 @@ use std::error::Error; type Result = std::result::Result>; mod user; -pub use user::User; +pub use user::{ + User, + UserId +}; mod feed; -pub use feed::Feed; +pub use feed::{ + Feed, + FeedId, +}; mod channel; -pub use channel::Channel; +pub use channel::{ + Channel, + ChannelId, + fetch::FetchedRSSChannel, +}; mod item; -pub use item::Item; +pub use item::{ + Item, + ItemId, +}; -pub struct AdapterOptions { +pub struct AdapterPool(sqlx::SqlitePool); +pub struct AdapterClient(reqwest::Client); + +pub struct AdapterBuilder { database_url: String, } -impl AdapterOptions { +impl AdapterBuilder { pub fn new() -> Self { Self { database_url: "sqlite:test.db".to_string(), @@ -30,18 +46,19 @@ impl AdapterOptions { pub async fn create(self) -> Result { let db = sqlx::sqlite::SqlitePoolOptions::new() .connect(&self.database_url).await?; + sqlx::migrate!().run(&db).await?; let client = reqwest::Client::new(); - Ok(Adapter { db, client }) + Ok(Adapter { db: AdapterPool(db), client: AdapterClient(client) }) } } pub struct Adapter { - db: sqlx::SqlitePool, - client: reqwest::Client, + db: AdapterPool, + client: AdapterClient, } impl Adapter { - pub fn get_pool(&self) -> &sqlx::SqlitePool { &self.db } - pub fn get_client(&self) -> &reqwest::Client { &self.client } + pub fn get_pool(&self) -> &AdapterPool { &self.db } + pub fn get_client(&self) -> &AdapterClient { &self.client } } diff --git a/koucha/src/user.rs b/koucha/src/user.rs index fc4e6ad..c8ea50b 100644 --- a/koucha/src/user.rs +++ b/koucha/src/user.rs @@ -1,6 +1,6 @@ -use sqlx::SqlitePool; use crate::{ Result, + AdapterPool, Feed, feed::UnparsedFeed, }; @@ -31,31 +31,31 @@ impl User { pub fn id(&self) -> UserId { self.id } pub fn name(&self) -> &str { &self.name } - pub async fn get(pool: &SqlitePool, id: UserId) -> Result { + pub async fn get(pool: &AdapterPool, id: UserId) -> Result { let user = sqlx::query_as!( UnparsedUser, "SELECT id, name FROM users WHERE id = ?", id.0 - ).fetch_one(pool).await?.parse(); + ).fetch_one(&pool.0).await?.parse(); user } - pub async fn get_all(pool: &SqlitePool) -> Result> { + pub async fn get_all(pool: &AdapterPool) -> Result> { let users: Result> = sqlx::query_as!( UnparsedUser, "SELECT id, name FROM users" - ).fetch_all(pool).await?.into_iter().map(UnparsedUser::parse).collect(); + ).fetch_all(&pool.0).await?.into_iter().map(UnparsedUser::parse).collect(); users } - pub async fn create(pool: &SqlitePool, name: &str) -> Result { + pub async fn create(pool: &AdapterPool, name: &str) -> Result { let result = sqlx::query!( "INSERT INTO users (name) VALUES (?) RETURNING id, name", name - ).fetch_one(pool).await?; + ).fetch_one(&pool.0).await?; Ok(Self { id: UserId(result.id), @@ -64,22 +64,22 @@ impl User { } pub async fn update_name( - pool: &SqlitePool, id: UserId, new_name: &str + pool: &AdapterPool, id: UserId, new_name: &str ) -> Result<()> { sqlx::query!( "UPDATE users SET name = ? WHERE id = ?", new_name, id.0 - ).execute(pool).await?; + ).execute(&pool.0).await?; Ok(()) } - pub async fn get_feeds(&self, pool: &SqlitePool) -> Result> { + pub async fn get_feeds(&self, pool: &AdapterPool) -> Result> { let feeds: Result> = sqlx::query_as!( UnparsedFeed, "SELECT id, title FROM feeds WHERE user_id = ?", self.id.0 - ).fetch_all(pool).await?.into_iter() + ).fetch_all(&pool.0).await?.into_iter() .map(UnparsedFeed::parse).collect(); feeds @@ -89,30 +89,32 @@ impl User { #[cfg(test)] mod tests { use super::*; - use sqlx::SqlitePool; + use crate::{AdapterBuilder, Adapter}; - async fn setup_test_db() -> SqlitePool { - let pool = SqlitePool::connect("sqlite::memory:").await.unwrap(); - sqlx::migrate!().run(&pool).await.unwrap(); - pool + async fn setup_adapter() -> Adapter { + AdapterBuilder::new() + .database_url("sqlite::memory:") + .create().await.unwrap() } #[tokio::test] async fn get_user() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); - let new_user = User::create(&pool, "Alice").await.unwrap(); + let new_user = User::create(pool, "Alice").await.unwrap(); - let fetched_user = User::get(&pool, new_user.id).await.unwrap(); + let fetched_user = User::get(pool, new_user.id).await.unwrap(); assert_eq!(fetched_user.name, "Alice"); assert!(fetched_user.id.0 > 0); } #[tokio::test] async fn create_user() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); - let user = User::create(&pool, "Alice").await.unwrap(); + let user = User::create(pool, "Alice").await.unwrap(); assert_eq!(user.name, "Alice"); assert!(user.id.0 > 0); @@ -120,22 +122,24 @@ mod tests { #[tokio::test] async fn create_duplicate_user() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); - let _user = User::create(&pool, "Alice").await.unwrap(); - let duplicate_user = User::create(&pool, "Alice").await; + let _user = User::create(pool, "Alice").await.unwrap(); + let duplicate_user = User::create(pool, "Alice").await; assert!(duplicate_user.is_err()); } #[tokio::test] async fn get_all_users() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); - User::create(&pool, "Alice").await.unwrap(); - User::create(&pool, "Bob").await.unwrap(); + User::create(pool, "Alice").await.unwrap(); + User::create(pool, "Bob").await.unwrap(); - let users = User::get_all(&pool).await.unwrap(); + let users = User::get_all(pool).await.unwrap(); assert_eq!(users.len(), 2); assert!(users.iter().any(|u| u.name == "Alice")); @@ -144,32 +148,35 @@ mod tests { #[tokio::test] async fn update_name() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); - let user = User::create(&pool, "Alice").await.unwrap(); - User::update_name(&pool, user.id, "Alicia").await.unwrap(); + let user = User::create(pool, "Alice").await.unwrap(); + User::update_name(pool, user.id, "Alicia").await.unwrap(); - let updated = User::get(&pool, user.id).await.unwrap(); + let updated = User::get(pool, user.id).await.unwrap(); assert_eq!(updated.name, "Alicia"); } #[tokio::test] async fn update_name_to_duplicate() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); - let alice = User::create(&pool, "Alice").await.unwrap(); - let _sam = User::create(&pool, "Sam").await.unwrap(); - let status = User::update_name(&pool, alice.id, "Sam").await; + let alice = User::create(pool, "Alice").await.unwrap(); + let _sam = User::create(pool, "Sam").await.unwrap(); + let status = User::update_name(pool, alice.id, "Sam").await; assert!(status.is_err()); } #[tokio::test] async fn get_feeds_empty() { - let pool = setup_test_db().await; + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); - let user = User::create(&pool, "Alice").await.unwrap(); - let feeds = user.get_feeds(&pool).await.unwrap(); + let user = User::create(pool, "Alice").await.unwrap(); + let feeds = user.get_feeds(pool).await.unwrap(); assert_eq!(feeds.len(), 0); }