Clean up server.rs, add DB and Client aliases

This commit is contained in:
Julia Lange 2026-01-22 11:39:33 -08:00
parent 7bb4cf4230
commit f5fc83a471
Signed by: Julia
SSH key fingerprint: SHA256:5DJcfxa5/fKCYn57dcabJa2vN2e6eT0pBerYi5SUbto
5 changed files with 153 additions and 128 deletions

View file

@ -1,14 +1,7 @@
use std::error::Error;
use reqwest::Client;
use koucha::{
AdapterOptions,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// let adapter = AdapterOptions::new().create().await?;
//
// let _channel = fetch_channel(&client, "https://lorem-rss.herokuapp.com/feed?unit=year").await?;
Ok(())
}

View file

@ -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<Vec<Self>> {
pub async fn get_all(pool: &AdapterPool) -> Result<Vec<Self>> {
let channels: Result<Vec<Channel>> = 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<Self> {
pub async fn get(pool: &AdapterPool, id: ChannelId) -> Result<Self> {
let channel: Result<Self> = 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<Self> {
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<Option<FetchedRSSChannel>> {
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<Vec<Item>> {
pub async fn get_items(&self, pool: &AdapterPool) -> Result<Vec<Item>> {
let items: Result<Vec<Item>> = 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);
}
}

View file

@ -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<Self> {
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<Self> {
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<Vec<Item>> {
let items: Result<Vec<Item>> = 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<Vec<Channel>> {
let channels: Result<Vec<Channel>> = 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");
}
}

View file

@ -3,19 +3,35 @@ use std::error::Error;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
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<Adapter> {
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 }
}

View file

@ -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<Self> {
pub async fn get(pool: &AdapterPool, id: UserId) -> Result<Self> {
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<Vec<Self>> {
pub async fn get_all(pool: &AdapterPool) -> Result<Vec<Self>> {
let users: Result<Vec<Self>> = 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<Self> {
pub async fn create(pool: &AdapterPool, name: &str) -> Result<Self> {
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<Vec<Feed>> {
pub async fn get_feeds(&self, pool: &AdapterPool) -> Result<Vec<Feed>> {
let feeds: Result<Vec<Feed>> = 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);
}