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 std::error::Error;
use reqwest::Client;
use koucha::{
AdapterOptions,
};
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { 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(()) Ok(())
} }

View file

@ -1,8 +1,9 @@
use reqwest::{Url, Client}; use reqwest::Url;
use sqlx::SqlitePool;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use crate::{ use crate::{
Result, Result,
AdapterPool,
AdapterClient,
Item, Item,
channel::fetch::FetchedRSSChannel, channel::fetch::FetchedRSSChannel,
item::UnparsedItem, item::UnparsedItem,
@ -50,29 +51,29 @@ impl Channel {
pub fn link(&self) -> &Url { &self.link } pub fn link(&self) -> &Url { &self.link }
pub fn description(&self) -> Option<&str> { self.description.as_deref() } 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!( let channels: Result<Vec<Channel>> = sqlx::query_as!(
UnparsedChannel, UnparsedChannel,
"SELECT id, title, link, description, last_fetched FROM channels" "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 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!( let channel: Result<Self> = sqlx::query_as!(
UnparsedChannel, UnparsedChannel,
"SELECT id, title, link, description, last_fetched "SELECT id, title, link, description, last_fetched
FROM channels FROM channels
WHERE id = ?", WHERE id = ?",
id.0 id.0
).fetch_one(pool).await?.parse(); ).fetch_one(&pool.0).await?.parse();
channel channel
} }
pub async fn create( pub async fn create(
pool: &SqlitePool, link: Url pool: &AdapterPool, link: Url
) -> Result<Self> { ) -> Result<Self> {
let link_str = link.as_str(); let link_str = link.as_str();
@ -82,7 +83,7 @@ impl Channel {
FROM channels FROM channels
WHERE link = ?", WHERE link = ?",
link_str link_str
).fetch_one(pool).await { ).fetch_one(&pool.0).await {
return existing_channel.parse(); return existing_channel.parse();
} }
@ -92,7 +93,7 @@ impl Channel {
VALUES (?, ?) VALUES (?, ?)
RETURNING id, title, link, description, last_fetched", RETURNING id, title, link, description, last_fetched",
link_str, link_str link_str, link_str
).fetch_one(pool).await?.parse(); ).fetch_one(&pool.0).await?.parse();
new_channel new_channel
} }
@ -102,12 +103,12 @@ impl Channel {
// TODO implement conditional fetching // TODO implement conditional fetching
pub async fn fetch_rss( pub async fn fetch_rss(
&self, client: &Client &self, client: &AdapterClient
) -> Result<Option<FetchedRSSChannel>> { ) -> Result<Option<FetchedRSSChannel>> {
if self.should_skip_fetch() { if self.should_skip_fetch() {
return Ok(None); return Ok(None);
} }
let bytestream = client.get(self.link.clone()) let bytestream = client.0.get(self.link.clone())
.send().await? .send().await?
.bytes().await?; .bytes().await?;
@ -117,7 +118,7 @@ impl Channel {
} }
pub async fn update_metadata( pub async fn update_metadata(
&self, pool: &SqlitePool, fetched: FetchedRSSChannel &self, pool: &AdapterPool, fetched: FetchedRSSChannel
) -> Result<()> { ) -> Result<()> {
let title = fetched.title(); let title = fetched.title();
let description = fetched.description(); let description = fetched.description();
@ -130,13 +131,13 @@ impl Channel {
WHERE id = ?", WHERE id = ?",
title, link, description, fetched_at, title, link, description, fetched_at,
self.id.0 self.id.0
).execute(pool).await?; ).execute(&pool.0).await?;
Ok(()) Ok(())
} }
pub async fn update_items( pub async fn update_items(
&self, pool: &SqlitePool, fetched: FetchedRSSChannel &self, pool: &AdapterPool, fetched: FetchedRSSChannel
) -> Result<()> { ) -> Result<()> {
let fetched_at = fetched.fetched_at().to_rfc2822(); let fetched_at = fetched.fetched_at().to_rfc2822();
@ -151,19 +152,19 @@ impl Channel {
VALUES (?, ?, ?, ?, ?, ?)", VALUES (?, ?, ?, ?, ?, ?)",
self.id.0, guid, fetched_at, title, description, content self.id.0, guid, fetched_at, title, description, content
) )
.execute(pool) .execute(&pool.0)
.await?; .await?;
} }
Ok(()) 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!( let items: Result<Vec<Item>> = sqlx::query_as!(
UnparsedItem, UnparsedItem,
"SELECT id as `id!` FROM items WHERE channel_id = ?", "SELECT id as `id!` FROM items WHERE channel_id = ?",
self.id.0 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 items
} }
@ -172,6 +173,7 @@ impl Channel {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{Adapter, AdapterBuilder};
use rss::{ use rss::{
Guid as RSSGuid, Guid as RSSGuid,
Item as RSSItem, Item as RSSItem,
@ -179,7 +181,6 @@ mod tests {
Channel as RSSChannel, Channel as RSSChannel,
ChannelBuilder as RSSChannelBuilder, ChannelBuilder as RSSChannelBuilder,
}; };
use sqlx::SqlitePool;
const ITEM_TITLE: &str = "My Item"; const ITEM_TITLE: &str = "My Item";
const ITEM_GUID1: &str = "https://mycontent.com/blog/1"; const ITEM_GUID1: &str = "https://mycontent.com/blog/1";
@ -191,18 +192,19 @@ mod tests {
const FEED1: &str = "https://example.com/feed"; const FEED1: &str = "https://example.com/feed";
const FEED2: &str = "https://example2.com/feed"; const FEED2: &str = "https://example2.com/feed";
async fn setup_test_db() -> SqlitePool { async fn setup_adapter() -> Adapter {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap(); AdapterBuilder::new()
sqlx::migrate!().run(&pool).await.unwrap(); .database_url("sqlite::memory:")
pool .create().await.unwrap()
} }
#[tokio::test] #[tokio::test]
async fn create_channel() { 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 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!(channel.id().0 > 0);
assert_eq!(channel.link().as_str(), FEED1); assert_eq!(channel.link().as_str(), FEED1);
@ -211,11 +213,12 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn create_duplicate_returns_existing() { 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 url_feed = Url::parse(FEED1).unwrap();
let channel1 = Channel::create(&pool, url_feed.clone()).await.unwrap(); let channel1 = Channel::create(pool, url_feed.clone()).await.unwrap();
let channel2 = Channel::create(&pool, url_feed).await.unwrap(); let channel2 = Channel::create(pool, url_feed).await.unwrap();
assert_eq!( assert_eq!(
i64::from(channel1.id()), i64::from(channel1.id()),
@ -225,24 +228,26 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn get_all_channels() { 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_feed1 = Url::parse(FEED1).unwrap();
let url_feed2 = Url::parse(FEED2).unwrap(); let url_feed2 = Url::parse(FEED2).unwrap();
Channel::create(&pool, url_feed1).await.unwrap(); Channel::create(pool, url_feed1).await.unwrap();
Channel::create(&pool, url_feed2).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); assert_eq!(channels.len(), 2);
} }
#[tokio::test] #[tokio::test]
async fn update_metadata() { 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 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() let fake_rss: RSSChannel = RSSChannelBuilder::default()
.title(CHAN_TITLE) .title(CHAN_TITLE)
@ -252,9 +257,9 @@ mod tests {
let fetched = FetchedRSSChannel::parse(fake_rss).unwrap(); 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.title(), CHAN_TITLE);
assert_eq!(updated.link().as_str(), FEED2); assert_eq!(updated.link().as_str(), FEED2);
assert_eq!(updated.description(), Some(CHAN_DESC)); assert_eq!(updated.description(), Some(CHAN_DESC));
@ -262,10 +267,11 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn update_items() { 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 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() let item1: RSSItem = RSSItemBuilder::default()
.title(ITEM_TITLE.to_string()) .title(ITEM_TITLE.to_string())
@ -290,18 +296,19 @@ mod tests {
let fetched = FetchedRSSChannel::parse(fake_rss).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(), 2); assert_eq!(items.len(), 2);
} }
#[tokio::test] #[tokio::test]
async fn update_items_ignores_duplicates() { 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 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() let item1: RSSItem = RSSItemBuilder::default()
.title(ITEM_TITLE.to_string()) .title(ITEM_TITLE.to_string())
@ -319,12 +326,12 @@ mod tests {
let fetched = FetchedRSSChannel::parse(fake_rss.clone()).unwrap(); 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(); 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); assert_eq!(items.len(), 1);
} }
} }

View file

@ -1,5 +1,6 @@
use crate::{ use crate::{
Result, Result,
AdapterPool,
Item, Item,
item::UnparsedItem, item::UnparsedItem,
Channel, Channel,
@ -9,7 +10,6 @@ use crate::{
}, },
user::UserId, user::UserId,
}; };
use sqlx::SqlitePool;
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct FeedId(i64); pub struct FeedId(i64);
@ -38,19 +38,19 @@ impl Feed {
pub fn title(&self) -> &str { &self.title } pub fn title(&self) -> &str { &self.title }
pub async fn get( pub async fn get(
pool: &SqlitePool, id: FeedId pool: &AdapterPool, id: FeedId
) -> Result<Self> { ) -> Result<Self> {
let feed = sqlx::query_as!( let feed = sqlx::query_as!(
UnparsedFeed, UnparsedFeed,
"SELECT id, title FROM feeds WHERE id = ?", "SELECT id, title FROM feeds WHERE id = ?",
id.0 id.0
).fetch_one(pool).await?.parse(); ).fetch_one(&pool.0).await?.parse();
feed feed
} }
pub async fn create( pub async fn create(
pool: &SqlitePool, user_id: UserId, title: &str pool: &AdapterPool, user_id: UserId, title: &str
) -> Result<Self> { ) -> Result<Self> {
let int_id = i64::from(user_id); let int_id = i64::from(user_id);
let new_feed = sqlx::query_as!( let new_feed = sqlx::query_as!(
@ -59,37 +59,37 @@ impl Feed {
VALUES (?, ?) VALUES (?, ?)
RETURNING id as `id!`, title", RETURNING id as `id!`, title",
int_id, title int_id, title
).fetch_one(pool).await?.parse(); ).fetch_one(&pool.0).await?.parse();
new_feed new_feed
} }
pub async fn update_title( pub async fn update_title(
pool: &SqlitePool, id: FeedId, new_title: &str pool: &AdapterPool, id: FeedId, new_title: &str
) -> Result<()> { ) -> Result<()> {
sqlx::query!( sqlx::query!(
"UPDATE feeds SET title = ? WHERE id = ?", "UPDATE feeds SET title = ? WHERE id = ?",
new_title, id.0 new_title, id.0
).execute(pool).await?; ).execute(&pool.0).await?;
Ok(()) Ok(())
} }
pub async fn add_channel( pub async fn add_channel(
&self, pool: &SqlitePool, channel_id: ChannelId &self, pool: &AdapterPool, channel_id: ChannelId
) -> Result<()> { ) -> Result<()> {
let int_channel_id = i64::from(channel_id); let int_channel_id = i64::from(channel_id);
sqlx::query!( sqlx::query!(
"INSERT INTO feed_channels (feed_id, channel_id) "INSERT INTO feed_channels (feed_id, channel_id)
VALUES (?, ?)", VALUES (?, ?)",
self.id.0, int_channel_id self.id.0, int_channel_id
).execute(pool).await?; ).execute(&pool.0).await?;
Ok(()) Ok(())
} }
pub async fn get_items( pub async fn get_items(
&self, pool: &SqlitePool, limit: u8, offset: u32 &self, pool: &AdapterPool, limit: u8, offset: u32
) -> Result<Vec<Item>> { ) -> Result<Vec<Item>> {
let items: Result<Vec<Item>> = sqlx::query_as!( let items: Result<Vec<Item>> = sqlx::query_as!(
UnparsedItem, UnparsedItem,
@ -98,13 +98,13 @@ impl Feed {
ORDER BY score DESC ORDER BY score DESC
LIMIT ? OFFSET ?", LIMIT ? OFFSET ?",
self.id.0, 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 items
} }
pub async fn get_channels( pub async fn get_channels(
&self, pool: &SqlitePool &self, pool: &AdapterPool
) -> Result<Vec<Channel>> { ) -> Result<Vec<Channel>> {
let channels: Result<Vec<Channel>> = sqlx::query_as!( let channels: Result<Vec<Channel>> = sqlx::query_as!(
UnparsedChannel, UnparsedChannel,
@ -113,7 +113,7 @@ impl Feed {
JOIN feed_channels fc on c.id = fc.channel_id JOIN feed_channels fc on c.id = fc.channel_id
WHERE fc.feed_id = ?", WHERE fc.feed_id = ?",
self.id.0 self.id.0
).fetch_all(pool).await?.into_iter() ).fetch_all(&pool.0).await?.into_iter()
.map(UnparsedChannel::parse).collect(); .map(UnparsedChannel::parse).collect();
channels channels
@ -123,21 +123,21 @@ impl Feed {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::User; use crate::{User, Adapter, AdapterBuilder};
use sqlx::SqlitePool;
async fn setup_test_db() -> SqlitePool { async fn setup_adapter() -> Adapter {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap(); AdapterBuilder::new()
sqlx::migrate!().run(&pool).await.unwrap(); .database_url("sqlite::memory:")
pool .create().await.unwrap()
} }
#[tokio::test] #[tokio::test]
async fn create_feed() { async fn create_feed() {
let pool = setup_test_db().await; let adapter = setup_adapter().await;
let user = User::create(&pool, "Alice").await.unwrap(); 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_eq!(feed.title(), "Tech News");
assert!(feed.id().0 > 0); assert!(feed.id().0 > 0);
@ -145,13 +145,14 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_update_title() { async fn test_update_title() {
let pool = setup_test_db().await; let adapter = setup_adapter().await;
let user = User::create(&pool, "Alice").await.unwrap(); let pool = adapter.get_pool();
let feed = Feed::create(&pool, user.id(), "Tech News").await.unwrap(); 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"); 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>>; type Result<T> = std::result::Result<T, Box<dyn Error>>;
mod user; mod user;
pub use user::User; pub use user::{
User,
UserId
};
mod feed; mod feed;
pub use feed::Feed; pub use feed::{
Feed,
FeedId,
};
mod channel; mod channel;
pub use channel::Channel; pub use channel::{
Channel,
ChannelId,
fetch::FetchedRSSChannel,
};
mod item; 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, database_url: String,
} }
impl AdapterOptions { impl AdapterBuilder {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
database_url: "sqlite:test.db".to_string(), database_url: "sqlite:test.db".to_string(),
@ -30,18 +46,19 @@ impl AdapterOptions {
pub async fn create(self) -> Result<Adapter> { pub async fn create(self) -> Result<Adapter> {
let db = sqlx::sqlite::SqlitePoolOptions::new() let db = sqlx::sqlite::SqlitePoolOptions::new()
.connect(&self.database_url).await?; .connect(&self.database_url).await?;
sqlx::migrate!().run(&db).await?;
let client = reqwest::Client::new(); let client = reqwest::Client::new();
Ok(Adapter { db, client }) Ok(Adapter { db: AdapterPool(db), client: AdapterClient(client) })
} }
} }
pub struct Adapter { pub struct Adapter {
db: sqlx::SqlitePool, db: AdapterPool,
client: reqwest::Client, client: AdapterClient,
} }
impl Adapter { impl Adapter {
pub fn get_pool(&self) -> &sqlx::SqlitePool { &self.db } pub fn get_pool(&self) -> &AdapterPool { &self.db }
pub fn get_client(&self) -> &reqwest::Client { &self.client } pub fn get_client(&self) -> &AdapterClient { &self.client }
} }

View file

@ -1,6 +1,6 @@
use sqlx::SqlitePool;
use crate::{ use crate::{
Result, Result,
AdapterPool,
Feed, Feed,
feed::UnparsedFeed, feed::UnparsedFeed,
}; };
@ -31,31 +31,31 @@ impl User {
pub fn id(&self) -> UserId { self.id } pub fn id(&self) -> UserId { self.id }
pub fn name(&self) -> &str { &self.name } 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!( let user = sqlx::query_as!(
UnparsedUser, UnparsedUser,
"SELECT id, name FROM users WHERE id = ?", "SELECT id, name FROM users WHERE id = ?",
id.0 id.0
).fetch_one(pool).await?.parse(); ).fetch_one(&pool.0).await?.parse();
user 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!( let users: Result<Vec<Self>> = sqlx::query_as!(
UnparsedUser, UnparsedUser,
"SELECT id, name FROM users" "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 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!( let result = sqlx::query!(
"INSERT INTO users (name) "INSERT INTO users (name)
VALUES (?) VALUES (?)
RETURNING id, name", RETURNING id, name",
name name
).fetch_one(pool).await?; ).fetch_one(&pool.0).await?;
Ok(Self { Ok(Self {
id: UserId(result.id), id: UserId(result.id),
@ -64,22 +64,22 @@ impl User {
} }
pub async fn update_name( pub async fn update_name(
pool: &SqlitePool, id: UserId, new_name: &str pool: &AdapterPool, id: UserId, new_name: &str
) -> Result<()> { ) -> Result<()> {
sqlx::query!( sqlx::query!(
"UPDATE users SET name = ? WHERE id = ?", "UPDATE users SET name = ? WHERE id = ?",
new_name, id.0 new_name, id.0
).execute(pool).await?; ).execute(&pool.0).await?;
Ok(()) 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!( let feeds: Result<Vec<Feed>> = sqlx::query_as!(
UnparsedFeed, UnparsedFeed,
"SELECT id, title FROM feeds WHERE user_id = ?", "SELECT id, title FROM feeds WHERE user_id = ?",
self.id.0 self.id.0
).fetch_all(pool).await?.into_iter() ).fetch_all(&pool.0).await?.into_iter()
.map(UnparsedFeed::parse).collect(); .map(UnparsedFeed::parse).collect();
feeds feeds
@ -89,30 +89,32 @@ impl User {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use sqlx::SqlitePool; use crate::{AdapterBuilder, Adapter};
async fn setup_test_db() -> SqlitePool { async fn setup_adapter() -> Adapter {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap(); AdapterBuilder::new()
sqlx::migrate!().run(&pool).await.unwrap(); .database_url("sqlite::memory:")
pool .create().await.unwrap()
} }
#[tokio::test] #[tokio::test]
async fn get_user() { 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_eq!(fetched_user.name, "Alice");
assert!(fetched_user.id.0 > 0); assert!(fetched_user.id.0 > 0);
} }
#[tokio::test] #[tokio::test]
async fn create_user() { 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_eq!(user.name, "Alice");
assert!(user.id.0 > 0); assert!(user.id.0 > 0);
@ -120,22 +122,24 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn create_duplicate_user() { 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 _user = User::create(pool, "Alice").await.unwrap();
let duplicate_user = User::create(&pool, "Alice").await; let duplicate_user = User::create(pool, "Alice").await;
assert!(duplicate_user.is_err()); assert!(duplicate_user.is_err());
} }
#[tokio::test] #[tokio::test]
async fn get_all_users() { 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, "Alice").await.unwrap();
User::create(&pool, "Bob").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_eq!(users.len(), 2);
assert!(users.iter().any(|u| u.name == "Alice")); assert!(users.iter().any(|u| u.name == "Alice"));
@ -144,32 +148,35 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn update_name() { 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(); let user = User::create(pool, "Alice").await.unwrap();
User::update_name(&pool, user.id, "Alicia").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"); assert_eq!(updated.name, "Alicia");
} }
#[tokio::test] #[tokio::test]
async fn update_name_to_duplicate() { 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 alice = User::create(pool, "Alice").await.unwrap();
let _sam = User::create(&pool, "Sam").await.unwrap(); let _sam = User::create(pool, "Sam").await.unwrap();
let status = User::update_name(&pool, alice.id, "Sam").await; let status = User::update_name(pool, alice.id, "Sam").await;
assert!(status.is_err()); assert!(status.is_err());
} }
#[tokio::test] #[tokio::test]
async fn get_feeds_empty() { 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 user = User::create(pool, "Alice").await.unwrap();
let feeds = user.get_feeds(&pool).await.unwrap(); let feeds = user.get_feeds(pool).await.unwrap();
assert_eq!(feeds.len(), 0); assert_eq!(feeds.len(), 0);
} }