Clean up server.rs, add DB and Client aliases
This commit is contained in:
parent
7bb4cf4230
commit
f5fc83a471
5 changed files with 153 additions and 128 deletions
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue