aboutsummaryrefslogtreecommitdiff
path: root/src/list.rs
diff options
context:
space:
mode:
authorMroik <mroik@delayed.space>2026-04-01 00:49:31 +0200
committerMroik <mroik@delayed.space>2026-04-13 06:55:04 +0200
commit7294e5944c2e5620c47d1ab014e217c5ee05b3a6 (patch)
tree7cff945aef488565a18d7129e3af278a9332b596 /src/list.rs
parent8f8fd10dc2b185ca0a8e8908229c4d4bbefd70b7 (diff)
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 <mroik@delayed.space>
Diffstat (limited to 'src/list.rs')
-rw-r--r--src/list.rs230
1 files changed, 230 insertions, 0 deletions
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<String>,
+ 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<QueryResult<Self::T>> {
+ 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<QueryResult<User>> {
+ 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<QueryResult<User>> {
+ 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<QueryResult<User>> {
+ 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<QueryResult<User>> {
+ 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<Database> {
+ 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");
+ }
+}
XMR address: 854DmXNrxULU3ZFJVs4Wc8PFhbq29RhqHhY8W6cdWrtFN3qmooKyyeYPcDzZTNRxphhJ5UzASQfAdEMwSteVqymk28aLhqj