use anyhow::Result; use rusqlite::fallible_iterator::FallibleIterator; use crate::database::{Query, QueryResult}; #[derive(PartialEq, Debug)] struct User { name: Option, email: String, } impl User { fn new(email: &str) -> Self { Self { name: None, email: String::from(email), } } fn insert(&self) -> UserQuery<'_> { let name = self.name.as_deref(); UserQuery::Insert(name, self.email.as_str()) } fn delete(&self) -> UserQuery<'_> { UserQuery::Delete(&self.email) } fn query_by_email(&self) -> UserQuery<'_> { UserQuery::QueryByEmail(&self.email) } fn query_all<'a>() -> UserQuery<'a> { UserQuery::QueryAll } fn subscribe<'a>(&'a self, list: &'a List) -> SubscriptionQuery<'a> { SubscriptionQuery::Insert(&self.email, &list.name) } fn unsubscribe<'a>(&'a self, list: &'a List) -> SubscriptionQuery<'a> { SubscriptionQuery::Delete(&self.email, &list.name) } fn subscribed_to(&self) -> SubscriptionQuery<'_> { SubscriptionQuery::Lists(&self.email) } } enum UserQuery<'a> { Insert(Option<&'a str>, &'a str), Delete(&'a str), QueryByEmail(&'a str), QueryAll, } impl Query for UserQuery<'_> { type T = User; fn callback(&self, tx: &rusqlite::Transaction) -> Result> { match self { UserQuery::Insert(name, email) => self.db_insert(tx, name, email), UserQuery::Delete(email) => self.db_delete(tx, email), UserQuery::QueryByEmail(email) => self.db_query_by_email(tx, email), UserQuery::QueryAll => self.db_query_all(tx), } } } impl UserQuery<'_> { fn db_insert( &self, tx: &rusqlite::Transaction<'_>, name: &Option<&str>, email: &str, ) -> Result> { if let Some(name) = name { let q = "INSERT INTO user (name, email) VALUES (?, ?)"; tx.execute(q, [name, email])?; return Ok(QueryResult::Empty); } let q = "INSERT INTO user (email) VALUES (?)"; tx.execute(q, [email])?; Ok(QueryResult::Empty) } fn db_delete(&self, tx: &rusqlite::Transaction<'_>, email: &str) -> Result> { let q = "DELETE FROM user WHERE email = ?"; tx.execute(q, [email])?; Ok(QueryResult::Empty) } fn db_query_by_email( &self, tx: &rusqlite::Transaction<'_>, email: &str, ) -> Result> { let q = "SELECT * FROM user WHERE email LIKE ?"; let mut stmt = tx.prepare(q)?; let ris = stmt .query([email])? .map(|row| { Ok(User { name: row.get(1).unwrap(), email: row.get(2).unwrap(), }) }) .collect()?; Ok(QueryResult::Vec(ris)) } fn db_query_all(&self, tx: &rusqlite::Transaction<'_>) -> Result> { let q = "SELECT * FROM user"; let mut stmt = tx.prepare(q)?; let ris = stmt .query(())? .map(|row| { Ok(User { name: row.get(1).unwrap(), email: row.get(2).unwrap(), }) }) .collect()?; Ok(QueryResult::Vec(ris)) } } #[derive(Debug, PartialEq)] struct List { name: String, desc: Option, } impl List { fn new(name: &str) -> Self { List { name: String::from(name), desc: None, } } fn insert(&self) -> ListQuery<'_> { ListQuery::Insert(&self.name, self.desc.as_deref()) } fn delete(&self) -> ListQuery<'_> { ListQuery::Delete(&self.name) } fn query_all<'a>() -> ListQuery<'a> { ListQuery::QueryAll } fn subscribers(&self) -> SubscriptionQuery<'_> { SubscriptionQuery::Subscribers(&self.name) } } enum ListQuery<'a> { Insert(&'a str, Option<&'a str>), Delete(&'a str), QueryByName(&'a str), QueryAll, } impl Query for ListQuery<'_> { type T = List; fn callback(&self, tx: &rusqlite::Transaction) -> Result> { match self { ListQuery::Insert(name, desc) => self.db_insert(tx, name, desc), ListQuery::Delete(name) => self.db_delete(tx, name), ListQuery::QueryByName(name) => self.db_query_by_address(tx, name), ListQuery::QueryAll => self.db_query_all(tx), } } } impl ListQuery<'_> { fn db_insert( &self, tx: &rusqlite::Transaction, name: &str, desc: &Option<&str>, ) -> Result> { if let Some(desc) = desc { let q = "INSERT INTO list (name, description) VALUES (?, ?)"; tx.execute(q, [name, desc])?; } else { let q = "INSERT INTO list (name) VALUES (?)"; tx.execute(q, [name])?; } Ok(QueryResult::Empty) } fn db_delete(&self, tx: &rusqlite::Transaction, name: &str) -> Result> { let q = "DELETE FROM list WHERE name = ?"; tx.execute(q, [name])?; Ok(QueryResult::Empty) } fn db_query_by_address( &self, tx: &rusqlite::Transaction, name: &str, ) -> Result> { let q = "SELECT * FROM list WHERE name = ?"; let mut stmt = tx.prepare(q)?; let ris = stmt .query([name])? .map(|row| { Ok(List { name: row.get(1).unwrap(), desc: row.get(2).unwrap(), }) }) .collect()?; Ok(QueryResult::Vec(ris)) } fn db_query_all(&self, tx: &rusqlite::Transaction) -> Result> { let q = "SELECT * FROM list"; let mut stmt = tx.prepare(q)?; let ris = stmt .query(())? .map(|row| { Ok(List { name: row.get(1).unwrap(), desc: row.get(2).unwrap(), }) }) .collect()?; Ok(QueryResult::Vec(ris)) } } enum SubscriptionResult { List(List), Subscriber(User), Count(i64), } enum SubscriptionQuery<'a> { Insert(&'a str, &'a str), Delete(&'a str, &'a str), /// This is just for debug Count, Lists(&'a str), Subscribers(&'a str), } impl Query for SubscriptionQuery<'_> { type T = SubscriptionResult; fn callback(&self, tx: &rusqlite::Transaction) -> Result> { match self { SubscriptionQuery::Insert(email, list_name) => self.db_insert(tx, email, list_name), SubscriptionQuery::Count => self.db_query_all(tx), SubscriptionQuery::Delete(email, list_name) => self.db_delete(tx, email, list_name), SubscriptionQuery::Lists(email) => self.db_lists(tx, email), SubscriptionQuery::Subscribers(list_name) => self.db_subscribers(tx, list_name), } } } impl SubscriptionQuery<'_> { fn db_insert( &self, tx: &rusqlite::Transaction, email: &str, list_name: &str, ) -> Result> { let q = "INSERT INTO subscription SELECT user.id, list.id FROM user CROSS JOIN list WHERE user.email = ? AND list.name = ?"; tx.execute(q, [email, list_name])?; Ok(QueryResult::Empty) } fn db_query_all(&self, tx: &rusqlite::Transaction) -> Result> { let q = "SELECT COUNT(*) FROM subscription"; let ris = tx.query_one(q, (), |row| Ok(row.get(0).unwrap()))?; Ok(QueryResult::Single(SubscriptionResult::Count(ris))) } fn db_delete( &self, tx: &rusqlite::Transaction, email: &str, list_name: &str, ) -> Result> { let q = "WITH helper AS (SELECT user.id AS user_id, list.id AS list_id FROM user CROSS JOIN list WHERE user.email = ? AND list.name = ?) DELETE FROM subscription WHERE EXISTS ( SELECT 1 FROM helper WHERE helper.user_id = subscription.user_id AND helper.list_id = subscription.list_id)"; tx.execute(q, [email, list_name])?; Ok(QueryResult::Empty) } fn db_lists( &self, tx: &rusqlite::Transaction, email: &str, ) -> Result> { let q = "SELECT list.name, list.description FROM subscription JOIN list ON list.id = subscription.list_id JOIN user ON user.id = subscription.user_id WHERE user.email = ?"; let ris = tx .prepare(q)? .query([email])? .map(|row| { Ok(SubscriptionResult::List(List { name: row.get(0).unwrap(), desc: row.get(1).unwrap(), })) }) .collect()?; Ok(QueryResult::Vec(ris)) } fn db_subscribers( &self, tx: &rusqlite::Transaction, list_name: &str, ) -> Result> { let q = "SELECT user.name, user.email FROM subscription JOIN list ON list.id = subscription.list_id JOIN user ON user.id = subscription.user_id WHERE list.name = ?"; let ris = tx .prepare(q)? .query([list_name])? .map(|row| { Ok(SubscriptionResult::Subscriber(User { name: row.get(0).unwrap(), email: row.get(1).unwrap(), })) }) .collect()?; Ok(QueryResult::Vec(ris)) } } #[cfg(test)] mod tests { use anyhow::Result; use crate::{ database::{DB_NAME, Database, QueryResult}, list::{List, SubscriptionQuery, SubscriptionResult, User}, }; fn setup() -> Result { std::fs::create_dir("test")?; Database::new(&format!("test/{}", DB_NAME)) } fn cleanup() -> Result<()> { std::fs::remove_dir_all("test")?; Ok(()) } #[test] fn user_insert_wo_name() { let mut database = setup().expect("Failed setup"); let user = User { name: None, email: String::from("mroik@poul.org"), }; database.execute(user.insert()).expect("Failed insert"); let user2 = database .execute(user.query_by_email()) .expect("Failed query"); match user2 { QueryResult::Vec(items) => { assert_eq!(items.len(), 1); assert_eq!(items[0], user); } _ => assert!(false), } drop(database); cleanup().expect("Failed cleanup"); } #[test] fn user_insert_with_name() { let mut database = setup().expect("Failed setup"); let user = User { name: Some(String::from("Mirko Faina")), email: String::from("mroik@poul.org"), }; database.execute(user.insert()).expect("Failed insert"); let user2 = database .execute(user.query_by_email()) .expect("Failed query"); match &user2 { QueryResult::Vec(items) => { assert_eq!(items.len(), 1); assert_eq!(items[0], user); } _ => assert!(false), } drop(database); cleanup().expect("Failed cleanup"); } #[test] fn user_insert_twice() { let mut database = setup().expect("Failed setup"); let user = User { name: Some(String::from("Mirko Faina")), email: String::from("mroik@poul.org"), }; database.execute(user.insert()).expect("Failed insert"); let ris = database.execute(user.insert()); assert!(ris.is_err()); drop(database); cleanup().expect("Failed cleanup"); } #[test] fn user_delete() { let mut database = setup().expect("Failed setup"); let user = User { name: Some(String::from("Mirko Faina")), email: String::from("mroik@poul.org"), }; database.execute(user.insert()).expect("Failed insert"); database.execute(user.delete()).expect("Failed delete"); let ris = database .execute(User::query_all()) .expect("Failed query all"); match ris { QueryResult::Vec(items) => assert!(items.is_empty()), _ => assert!(false), } drop(database); cleanup().expect("Failed cleanup"); } #[test] fn list_insert_wo_desc() { let mut database = setup().expect("failed setup"); let list = List { name: String::from("poul"), desc: None, }; database.execute(list.insert()).expect("failed insert"); let ris = database.execute(List::query_all()).expect("failed query"); match ris { QueryResult::Vec(items) => { assert_eq!(items.len(), 1); assert_eq!(items[0], list); } _ => assert!(false), } cleanup().expect("failed cleanup"); } #[test] fn list_insert_with_desc() { let mut database = setup().expect("failed setup"); let list = List { name: String::from("poul"), desc: Some(String::from("The mailing list of the POuL")), }; database.execute(list.insert()).expect("failed insert"); let ris = database.execute(List::query_all()).expect("failed query"); match ris { QueryResult::Vec(items) => { assert_eq!(items.len(), 1); assert_eq!(items[0], list); } _ => assert!(false), } drop(database); cleanup().expect("failed cleanup"); } #[test] fn list_insert_twice() { let mut database = setup().expect("failed setup"); let list = List { name: String::from("poul"), desc: Some(String::from("The mailing list of the POuL")), }; database.execute(list.insert()).expect("failed insert"); assert!(database.execute(list.insert()).is_err()); drop(database); cleanup().expect("failed cleanup"); } #[test] fn list_delete() { let mut database = setup().expect("failed setup"); let list = List { name: String::from("poul"), desc: Some(String::from("The mailing list of the POuL")), }; database.execute(list.insert()).expect("failed insert"); database.execute(list.delete()).expect("failed delete"); let ris = database .execute(List::query_all()) .expect("Failed query all"); match ris { QueryResult::Vec(items) => assert!(items.is_empty()), _ => assert!(false), } drop(database); cleanup().expect("failed cleanup"); } #[test] fn subscription_insert() { let mut database = setup().expect("failed setup"); let list = List { name: String::from("poul"), desc: None, }; let list2 = List { name: String::from("dev"), desc: None, }; database.execute(list.insert()).expect("failed insert"); database.execute(list2.insert()).expect("failed insert"); let user = User { name: None, email: String::from("mroik@delayed.space"), }; let user2 = User { name: None, email: String::from("mroik@poul.org"), }; database.execute(user.insert()).expect("failed insert"); database.execute(user2.insert()).expect("failed insert"); database .execute(user.subscribe(&list)) .expect("failed insert"); let ris = database .execute(SubscriptionQuery::Count) .expect("failed query"); match ris { QueryResult::Single(SubscriptionResult::Count(c)) => { assert_eq!(c, 1) } _ => assert!(false), } drop(database); cleanup().expect("failed cleanup"); } #[test] fn subscription_delete() { let mut database = setup().expect("failed setup"); let list = List { name: String::from("poul"), desc: None, }; let list2 = List { name: String::from("dev"), desc: None, }; database.execute(list.insert()).expect("failed insert"); database.execute(list2.insert()).expect("failed insert"); let user = User { name: None, email: String::from("mroik@delayed.space"), }; let user2 = User { name: None, email: String::from("mroik@poul.org"), }; database.execute(user.insert()).expect("failed insert"); database.execute(user2.insert()).expect("failed insert"); database .execute(user.subscribe(&list)) .expect("failed insert"); database .execute(user.subscribe(&list2)) .expect("failed insert"); database .execute(user2.subscribe(&list)) .expect("failed insert"); database .execute(user2.subscribe(&list2)) .expect("failed insert"); let mut ris = database .execute(SubscriptionQuery::Count) .expect("failed query"); match ris { QueryResult::Single(SubscriptionResult::Count(c)) => { assert_eq!(c, 4) } _ => assert!(false), } database .execute(user.unsubscribe(&list)) .expect("failed delete"); ris = database .execute(SubscriptionQuery::Count) .expect("failed query"); match ris { QueryResult::Single(SubscriptionResult::Count(c)) => { assert_eq!(c, 3) } _ => assert!(false), } drop(database); cleanup().expect("failed cleanup"); } #[test] fn subscription_lists() { let mut database = setup().expect("failed setup"); let list = List { name: String::from("poul"), desc: None, }; let list2 = List { name: String::from("dev"), desc: None, }; database.execute(list.insert()).expect("failed insert"); database.execute(list2.insert()).expect("failed insert"); let user = User { name: None, email: String::from("mroik@delayed.space"), }; let user2 = User { name: None, email: String::from("mroik@poul.org"), }; database.execute(user.insert()).expect("failed insert"); database.execute(user2.insert()).expect("failed insert"); database .execute(user.subscribe(&list)) .expect("failed insert"); database .execute(user.subscribe(&list2)) .expect("failed insert"); database .execute(user2.subscribe(&list)) .expect("failed insert"); database .execute(user2.subscribe(&list2)) .expect("failed insert"); let mut ris = database .execute(SubscriptionQuery::Count) .expect("failed query"); match ris { QueryResult::Single(SubscriptionResult::Count(c)) => { assert_eq!(c, 4) } _ => assert!(false), } ris = database .execute(user.subscribed_to()) .expect("failed query"); match ris { QueryResult::Vec(items) => assert_eq!(items.len(), 2), _ => assert!(false), } drop(database); cleanup().expect("failed cleanup"); } #[test] fn subscription_subscribers() { let mut database = setup().expect("failed setup"); let list = List { name: String::from("poul"), desc: None, }; let list2 = List { name: String::from("dev"), desc: None, }; database.execute(list.insert()).expect("failed insert"); database.execute(list2.insert()).expect("failed insert"); let user = User { name: None, email: String::from("mroik@delayed.space"), }; let user2 = User { name: None, email: String::from("mroik@poul.org"), }; database.execute(user.insert()).expect("failed insert"); database.execute(user2.insert()).expect("failed insert"); database .execute(user.subscribe(&list)) .expect("failed insert"); database .execute(user.subscribe(&list2)) .expect("failed insert"); database .execute(user2.subscribe(&list)) .expect("failed insert"); database .execute(user2.subscribe(&list2)) .expect("failed insert"); let mut ris = database .execute(SubscriptionQuery::Count) .expect("failed query"); match ris { QueryResult::Single(SubscriptionResult::Count(c)) => { assert_eq!(c, 4) } _ => assert!(false), } ris = database.execute(list.subscribers()).expect("failed query"); match ris { QueryResult::Vec(items) => assert_eq!(items.len(), 2), _ => assert!(false), } drop(database); cleanup().expect("failed cleanup"); } }