aboutsummaryrefslogtreecommitdiff
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
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>
-rw-r--r--src/database.rs20
-rw-r--r--src/list.rs230
-rw-r--r--src/main.rs1
3 files changed, 243 insertions, 8 deletions
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<Self> {
+ pub fn new(db_file: &str) -> Result<Self> {
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<Ex>(&mut self, q: Ex) -> Result<QueryResult<Ex::T>> where Ex: DBExecutable {
+ pub fn execute<Ex>(&mut self, q: Ex) -> Result<QueryResult<Ex::T>>
+ 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<QueryResult<Self::T>>;
}
+#[derive(Debug)]
pub enum QueryResult<T> {
Empty,
- Vec(Vec<T>)
+ Vec(Vec<T>),
}
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");
+ }
+}
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!");
XMR address: 854DmXNrxULU3ZFJVs4Wc8PFhbq29RhqHhY8W6cdWrtFN3qmooKyyeYPcDzZTNRxphhJ5UzASQfAdEMwSteVqymk28aLhqj