From 7294e5944c2e5620c47d1ab014e217c5ee05b3a6 Mon Sep 17 00:00:00 2001 From: Mroik Date: Wed, 1 Apr 2026 00:49:31 +0200 Subject: Implement User model The mailing list will need to save the data of the subscribers for them to receive the emails. Add User model with its DB interactions. Signed-off-by: Mroik --- src/database.rs | 20 +++-- src/list.rs | 230 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 3 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 src/list.rs (limited to 'src') diff --git a/src/database.rs b/src/database.rs index a1d0a18..de77f95 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,22 +1,22 @@ use anyhow::Result; use rusqlite::{Connection, Transaction, config::DbConfig}; -const DB_NAME: &str = "database.sqlite"; +pub const DB_NAME: &str = "database.sqlite"; const DB_VERSION: i64 = 0; -struct Database { +pub struct Database { conn: Connection, } impl Database { - fn new() -> Result { + pub fn new(db_file: &str) -> Result { let mut init = false; - if !std::fs::exists(DB_NAME)? { + if !std::fs::exists(db_file)? { init = true; } let mut db = Database { - conn: Connection::open(DB_NAME)?, + conn: Connection::open(db_file)?, }; if init { db.initialize()?; @@ -35,7 +35,7 @@ impl Database { tx.execute(q, ())?; q = "INSERT INTO version VALUES (?)"; - tx.execute(q, &[&DB_VERSION])?; + tx.execute(q, [&DB_VERSION])?; q = "CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -61,7 +61,10 @@ impl Database { Ok(()) } - fn execute(&mut self, q: Ex) -> Result> where Ex: DBExecutable { + pub fn execute(&mut self, q: Ex) -> Result> + where + Ex: DBExecutable, + { let tx = self.conn.transaction()?; let ris = q.execute(&tx)?; tx.commit()?; @@ -74,7 +77,8 @@ pub trait DBExecutable { fn execute(&self, tx: &Transaction) -> Result>; } +#[derive(Debug)] pub enum QueryResult { Empty, - Vec(Vec) + Vec(Vec), } diff --git a/src/list.rs b/src/list.rs new file mode 100644 index 0000000..36cd492 --- /dev/null +++ b/src/list.rs @@ -0,0 +1,230 @@ +use anyhow::Result; +use rusqlite::fallible_iterator::FallibleIterator; + +use crate::database::{DBExecutable, 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 + } +} + +enum UserQuery<'a> { + Insert(Option<&'a str>, &'a str), + Delete(&'a str), + QueryByEmail(&'a str), + QueryAll, +} + +impl DBExecutable for UserQuery<'_> { + type T = User; + fn execute(&self, tx: &rusqlite::Transaction) -> Result> { + match self { + UserQuery::Insert(_, _) => self.db_insert(tx), + UserQuery::Delete(_) => self.db_delete(tx), + UserQuery::QueryByEmail(_) => self.db_query_by_email(tx), + UserQuery::QueryAll => self.db_query_all(tx), + } + } +} + +impl UserQuery<'_> { + fn db_insert(&self, tx: &rusqlite::Transaction<'_>) -> Result> { + let (name, email) = if let UserQuery::Insert(name, email) = self { + (name, email) + } else { + unreachable!("this should only be called by a UserQuery::Insert") + }; + + 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<'_>) -> Result> { + let email = if let UserQuery::Delete(email) = self { + email + } else { + unreachable!("this should only be called by a UserQuery::Delete") + }; + + let q = "DELETE FROM user WHERE email = ?"; + tx.execute(q, [email])?; + Ok(QueryResult::Empty) + } + + fn db_query_by_email(&self, tx: &rusqlite::Transaction<'_>) -> Result> { + let email = if let UserQuery::QueryByEmail(email) = self { + email + } else { + unreachable!("this should only be called by a UserQuery::QueryByEmail") + }; + + 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)) + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use crate::{ + database::{DB_NAME, Database}, + list::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 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 { + crate::database::QueryResult::Empty => assert!(false), + crate::database::QueryResult::Vec(items) => { + assert_eq!(items.len(), 1); + assert_eq!(items[0], user); + } + } + + drop(database); + cleanup().expect("Failed cleanup"); + } + + #[test] + fn 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 { + crate::database::QueryResult::Empty => assert!(false), + crate::database::QueryResult::Vec(items) => { + assert_eq!(items.len(), 1); + assert_eq!(items[0], user); + } + } + + drop(database); + cleanup().expect("Failed cleanup"); + } + + #[test] + fn 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 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 { + crate::database::QueryResult::Empty => assert!(false), + crate::database::QueryResult::Vec(items) => assert!(items.is_empty()), + } + + drop(database); + cleanup().expect("Failed cleanup"); + } +} diff --git a/src/main.rs b/src/main.rs index 189b68b..60b75af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod database; +mod list; fn main() { println!("Hello, world!"); -- cgit v1.3