use anyhow::{Context, Result};
use parking_lot::Mutex;
use rusqlite::{Connection, params};
use rusqlite_migration::{M, Migrations};
use ustr::Ustr;
use crate::{error::ReviewListError, utils};
pub trait ReviewList {
fn add_to_review_list(&mut self, unit_id: Ustr) -> Result<(), ReviewListError>;
fn remove_from_review_list(&mut self, unit_id: Ustr) -> Result<(), ReviewListError>;
fn get_review_list_entries(&self) -> Result<Vec<Ustr>, ReviewListError>;
}
pub struct LocalReviewList {
connection: Mutex<Connection>,
}
impl LocalReviewList {
fn migrations() -> Migrations<'static> {
Migrations::new(vec![
M::up("CREATE TABLE review_list(unit_id TEXT NOT NULL UNIQUE);")
.down("DROP TABLE review_list"),
M::up("CREATE INDEX unit_id_index ON review_list (unit_id);")
.down("DROP INDEX unit_id_index"),
])
}
fn init(&mut self) -> Result<()> {
let migrations = Self::migrations();
let mut connection = self.connection.lock();
migrations
.to_latest(&mut connection)
.context("failed to initialize review list DB")
}
fn new(connection: Connection) -> Result<LocalReviewList> {
let mut review_list = LocalReviewList {
connection: Mutex::new(connection),
};
review_list.init()?;
Ok(review_list)
}
pub fn new_from_disk(db_path: &str) -> Result<LocalReviewList> {
Self::new(utils::new_connection(db_path)?)
}
fn add_to_review_list_helper(&mut self, unit_id: Ustr) -> Result<()> {
let connection = self.connection.lock();
let mut stmt =
connection.prepare_cached("INSERT OR IGNORE INTO review_list (unit_id) VALUES (?1)")?;
stmt.execute(params![unit_id.as_str()])?;
Ok(())
}
fn remove_from_review_list_helper(&mut self, unit_id: Ustr) -> Result<()> {
let connection = self.connection.lock();
let mut stmt = connection.prepare_cached("DELETE FROM review_list WHERE unit_id = $1")?;
stmt.execute(params![unit_id.as_str()])?;
Ok(())
}
fn get_review_list_entries_helper(&self) -> Result<Vec<Ustr>> {
let connection = self.connection.lock();
let mut stmt = connection.prepare_cached("SELECT unit_id from review_list;")?;
let mut rows = stmt.query(params![])?;
let mut entries = Vec::new();
while let Some(row) = rows.next()? {
let unit_id: String = row.get(0)?;
entries.push(Ustr::from(&unit_id));
}
Ok(entries)
}
}
impl ReviewList for LocalReviewList {
fn add_to_review_list(&mut self, unit_id: Ustr) -> Result<(), ReviewListError> {
self.add_to_review_list_helper(unit_id)
.map_err(|e| ReviewListError::AddUnit(unit_id, e))
}
fn remove_from_review_list(&mut self, unit_id: Ustr) -> Result<(), ReviewListError> {
self.remove_from_review_list_helper(unit_id)
.map_err(|e| ReviewListError::RemoveUnit(unit_id, e))
}
fn get_review_list_entries(&self) -> Result<Vec<Ustr>, ReviewListError> {
self.get_review_list_entries_helper()
.map_err(ReviewListError::GetEntries)
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod test {
use anyhow::Result;
use rusqlite::Connection;
use ustr::Ustr;
use crate::review_list::{LocalReviewList, ReviewList};
fn new_test_review_list() -> Result<Box<dyn ReviewList>> {
let review_list = LocalReviewList::new(Connection::open_in_memory()?)?;
Ok(Box::new(review_list))
}
#[test]
fn add_and_remove_from_review_list() -> Result<()> {
let mut review_list = new_test_review_list()?;
let unit_id = Ustr::from("unit_id");
let unit_id2 = Ustr::from("unit_id2");
review_list.add_to_review_list(unit_id)?;
review_list.add_to_review_list(unit_id)?;
review_list.add_to_review_list(unit_id2)?;
let entries = review_list.get_review_list_entries()?;
assert_eq!(entries.len(), 2);
assert!(entries.contains(&unit_id));
assert!(entries.contains(&unit_id2));
review_list.remove_from_review_list(unit_id)?;
let entries = review_list.get_review_list_entries()?;
assert_eq!(entries.len(), 1);
assert!(!entries.contains(&unit_id));
assert!(entries.contains(&unit_id2));
Ok(())
}
}