use std::cmp::Ordering;
use std::fmt::Write;
use std::vec::IntoIter;
use conciliator::{
Conciliator,
Print,
Paint,
Inline,
Buffer,
Tree as PrintTree
};
use rusqlite::{
OptionalExtension,
Result as SqlResult
};
use crate::data::{
Activity,
Category,
category::Path
};
use super::Database;
#[derive(Debug, Eq, PartialEq, Default)]
pub struct MemTree {
activities: Vec<Option<Activity>>,
categories: Vec<Option<Category>>
}
pub struct PrintableTree<'a> {
description: &'a str,
tree: MemTree
}
pub trait Tree: Database{
fn get_activity(&self, id: u64) -> SqlResult<Activity> {
self.db().query_row(
"SELECT id, name, category_id \
FROM activity \
JOIN activity_belongs_to ON id = activity_id \
WHERE id = ?1",
[id],
|row| row.try_into()
)
}
fn get_category(&self, id: u64) -> SqlResult<Category> {
self.db().query_row(
"SELECT id, name, path \
FROM category \
JOIN category_belongs_to ON id = category_id \
WHERE id = ?1",
[id],
|row| row.try_into()
)
}
fn find_activity(&self, name: &str) -> SqlResult<Option<Activity>> {
self.db().query_row(
"SELECT id, name, category_id \
FROM activity \
JOIN activity_belongs_to ON id = activity_id \
WHERE name LIKE ?1",
[name],
|row| row.try_into()
).optional()
}
fn find_activities(&self, name: &str) -> SqlResult<Vec<Activity>> {
let mut stmt = self.db().prepare(
"SELECT id, name, category_id \
FROM activity \
JOIN activity_belongs_to ON id = activity_id \
WHERE name LIKE ?1 \
ORDER BY id"
)?;
let mut rows = stmt.query([name])?;
let mut acts = Vec::new();
while let Some(row) = rows.next()? {
acts.push(row.try_into()?);
}
Ok(acts)
}
fn find_activity_in(
&self,
cat: &Category,
name: &str)
-> SqlResult<Option<Activity>>
{
self.db().query_row(
"SELECT id, name, category_id \
FROM activity \
JOIN activity_belongs_to ON id = activity_id \
WHERE category_id = ?1 AND name LIKE ?2",
(cat.id, name),
|row| row.try_into()
).optional()
}
fn find_category(&self, name: &str) -> SqlResult<Option<Category>> {
self.db().query_row(
"SELECT id, name, path \
FROM category \
JOIN category_belongs_to ON id = category_id \
WHERE name LIKE ?1",
[name],
|row| row.try_into()
).optional()
}
fn find_categories(&self, name: &str) -> SqlResult<Vec<Category>> {
let mut stmt = self.db().prepare(
"SELECT id, name, path \
FROM category \
JOIN category_belongs_to ON id = category_id \
WHERE name LIKE ?1 \
ORDER BY path"
)?;
let mut rows = stmt.query([name])?;
let mut cats = Vec::new();
while let Some(row) = rows.next()? {
cats.push(row.try_into()?);
}
Ok(cats)
}
fn find_category_in(
&self,
cat: &Category,
name: &str)
-> SqlResult<Option<Category>>
{
let mut path = cat.path.serialize();
write!(path, "{}/", cat.id).unwrap();
self.db().query_row(
"SELECT id, name, path \
FROM category \
JOIN category_belongs_to ON id = category_id \
WHERE path = ?1 AND name LIKE ?2",
(path, name),
|row| row.try_into()
).optional()
}
fn find_category_at(&self, path: &str) -> SqlResult<Option<Category>> {
let path = match path.split_once('/') {
Some(("", path)) if !path.is_empty() => path,
Some(..) | None => return Ok(None)
};
let mut stmt = self.db().prepare(
"SELECT id, name, path \
FROM category \
JOIN category_belongs_to ON id = category_id \
WHERE path = ?1 AND name LIKE ?2"
)?;
let mut cur_path = String::from("/");
let mut maybe_cat: Option<Category> = None;
for next in path.split_terminator('/') {
let queried = stmt
.query_row((&cur_path, next), |row| row.try_into())
.optional()?;
maybe_cat = queried;
match &maybe_cat {
Some(cat) => write!(cur_path, "{}/", cat.id).unwrap(),
None => break
}
}
Ok(maybe_cat)
}
fn find_root_category(&self, name: &str) -> SqlResult<Option<Category>> {
self.db().query_row(
"SELECT id, name, path \
FROM category \
JOIN category_belongs_to ON id = category_id \
WHERE name LIKE ?1 AND path = '/'",
[name],
|row| row.try_into()
).optional()
}
fn get_activities(&self) -> SqlResult<Vec<Activity>> {
let mut stmt = self.db().prepare(
"SELECT id, name, category_id \
FROM activity \
JOIN activity_belongs_to ON id = activity_id \
ORDER BY id"
)?;
let mut rows = stmt.query([])?;
let mut acts = Vec::new();
while let Some(row) = rows.next()? {
acts.push(row.try_into()?);
}
Ok(acts)
}
fn get_categories(&self) -> SqlResult<Vec<Category>> {
let mut stmt = self.db().prepare(
"SELECT id, name, path \
FROM category \
JOIN category_belongs_to ON id = category_id \
ORDER BY id"
)?;
let mut rows = stmt.query([])?;
let mut cats = Vec::new();
while let Some(row) = rows.next()? {
cats.push(row.try_into()?);
}
Ok(cats)
}
fn get_activities_under(&self, cat: &Category) -> SqlResult<Vec<Activity>> {
let mut stmt = self.db().prepare(
"SELECT id, name, activity_belongs_to.category_id \
FROM activity \
JOIN activity_belongs_to ON id = activity_id \
JOIN category_belongs_to \
ON activity_belongs_to.category_id \
= category_belongs_to.category_id \
WHERE activity_belongs_to.category_id = ?1 \
OR path LIKE (?2 || ?1 || '/%') \
ORDER BY id"
)?;
let mut rows = stmt.query((cat.id, &cat.path))?;
let mut acts = Vec::new();
while let Some(row) = rows.next()? {
acts.push(row.try_into()?);
}
Ok(acts)
}
fn resolve_path_string(&self, path: &Path) -> SqlResult<String> {
let mut stmt = self.db()
.prepare("SELECT name FROM category WHERE id = ?1")?;
let folder = |mut s, id| -> SqlResult<String> {
stmt.query_row(
[id],
|row| {
let name = row.get_ref(0)?.as_str()?;
s += name;
Ok(())
}
)?;
s += "/";
Ok(s)
};
path.0
.iter()
.try_fold(String::from("/"), folder)
}
fn get_path_string(&self, category_id: u64) -> SqlResult<String> {
let cat = self.get_category(category_id)?;
let mut s = self.resolve_path_string(&cat.path)?;
s += &cat.name;
s += "/";
Ok(s)
}
fn create_root_category(&self, name: &str) -> SqlResult<u64> {
let tx = self.db().unchecked_transaction()?;
let id = self.db()
.prepare("INSERT INTO category (name) VALUES (?1)")?
.insert([name])?;
self.db().prepare(
"INSERT INTO category_belongs_to (category_id, path) \
VALUES (?1, '/')"
)?.insert([id])?;
tx.commit().map(|()| id as u64)
}
fn create_category(&self, cat: &Category, name: &str)
-> SqlResult<u64>
{
let tx = self.db().unchecked_transaction()?;
let id = self.db()
.prepare("INSERT INTO category (name) VALUES (?1)")?
.insert([name])?;
let mut path = cat.path.serialize();
path.push_str(&cat.id.to_string());
path.push('/');
self.db().prepare(
"INSERT INTO category_belongs_to (category_id, path) \
VALUES (?1, ?2)"
)?.insert((id, path))?;
tx.commit().map(|()| id as u64)
}
fn create_activity(&self, cat: &Category, name: &str)
-> SqlResult<u64>
{
let tx = self.db().unchecked_transaction()?;
let id = self.db()
.prepare("INSERT INTO activity (name) VALUES (?1)")?
.insert([name])?;
self.db().prepare(
"INSERT INTO activity_belongs_to (activity_id, category_id) \
VALUES (?1, ?2)"
)?.insert((id, cat.id))?;
tx.commit().map(|()| id as u64)
}
fn rename_activity(&self, act: &Activity) -> SqlResult<bool> {
self.db()
.prepare("UPDATE activity SET name = ?2 WHERE id = ?1")?
.execute((act.id, &act.name))
.map(|i| i == 1)
}
fn move_activity(&self, act: &Activity) -> SqlResult<bool> {
self.db()
.prepare(
"UPDATE activity_belongs_to \
SET category_id = ?2 \
WHERE activity_id = ?1"
)?
.execute((act.id, act.category_id))
.map(|i| i == 1)
}
fn delete_activity(&self, act: &Activity) -> SqlResult<bool> {
let tx = self.db().unchecked_transaction()?;
self.db()
.prepare(
"DELETE FROM activity_belongs_to \
WHERE activity_id == ?1"
)?
.execute([act.id])?;
let deleted = self.db()
.prepare("DELETE FROM activity WHERE id == ?1")?
.execute([act.id])
.map(|i| i == 1)?;
tx.commit().map(|()| deleted)
}
fn delete_category(&self, cat: &Category) -> SqlResult<bool> {
self.db()
.prepare("DELETE FROM category WHERE id == ?1")?
.execute([cat.id])
.map(|i| i == 1)
}
fn load_into_memory(&self) -> SqlResult<MemTree> {
let mut activities = Vec::new();
let mut categories = Vec::new();
let mut stmt = self.db().prepare(
"SELECT id, name, category_id \
FROM activity \
JOIN activity_belongs_to ON id = activity_id \
ORDER BY id"
)?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let activity: Activity = row.try_into()?;
match Ord::cmp(&(activity.id as usize), &activities.len()) {
Ordering::Equal => activities.push(Some(activity)),
Ordering::Greater => {
activities.resize_with(activity.id as usize, || None);
activities.push(Some(activity));
}
Ordering::Less => unreachable!("activities out of order")
}
}
let mut stmt = self.db().prepare(
"SELECT id, name, path \
FROM category \
JOIN category_belongs_to ON id = category_id \
ORDER BY id"
)?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let category: Category = row.try_into()?;
match Ord::cmp(&(category.id as usize), &categories.len()) {
Ordering::Equal => categories.push(Some(category)),
Ordering::Greater => {
categories.resize_with(category.id as usize, || None);
categories.push(Some(category));
}
Ordering::Less => unreachable!("categories out of order")
}
}
Ok(
MemTree {activities, categories}
)
}
fn print_tree<'a>(&self, desc: &'a str) -> SqlResult<PrintableTree<'a>> {
let mem_tree = self.load_into_memory()?;
Ok(PrintableTree{description: desc, tree: mem_tree})
}
}
impl<Db: Database + ?Sized> Tree for Db {}
impl<'a> Print for PrintableTree<'a> {
fn print<C: Conciliator + ?Sized>(mut self, con: &C) {
enum Node {
Category(Category),
Activity(Activity)
}
impl Inline for Node {
fn inline(&self, buffer: &mut Buffer) {
match self {
Self::Category(cat) => {buffer
.push_alpha_bold("[")
.push(cat)
.push_alpha_bold("]");
},
Self::Activity(act) => act.inline(buffer)
}
}
}
let root_cats = self.tree.categories
.iter_mut()
.filter(|sub_cat| match sub_cat {
Some(cat) => cat.path.0.is_empty(),
None => false
})
.filter_map(Option::take)
.map(Node::Category)
.collect::<Vec<_>>()
.into_iter();
let next = move |node: Node| -> Option<IntoIter<Node>> {
match node {
Node::Category(cat) => {
let mut path = cat.path.0.clone();
path.push(cat.id);
let mut refs: Vec<_> = self.tree.categories
.iter_mut()
.filter(|sub_cat| match sub_cat {
Some(c) => c.path.is_child_of(&cat),
None => false
})
.filter_map(Option::take)
.map(Node::Category)
.collect();
refs.extend(self.tree.activities
.iter_mut()
.filter(|opt_act| match opt_act {
Some(act) => act.category_id == cat.id,
None => false
})
.filter_map(Option::take)
.map(Node::Activity)
);
Some(refs.into_iter())
},
Node::Activity(..) => None
}
};
PrintTree::new(self.description, root_cats, next).print(con)
}
}
#[test]
fn empty() {
let db = crate::Nunc::testing().unwrap();
assert!(db.get_activities().unwrap().is_empty());
assert!(db.get_categories().unwrap().is_empty());
assert!(db.get_activity(0).is_err());
assert!(db.get_activity(1).is_err());
assert!(db.get_category(0).is_err());
assert!(db.get_category(1).is_err());
}
#[test]
fn create() -> SqlResult<()> {
let db = crate::Nunc::testing().unwrap();
let root_id = db.create_root_category("root")?;
let cats = db.get_categories()?;
assert_eq!(cats.len(), 1);
assert_eq!(cats[0], db.get_category(root_id)?);
assert_eq!(cats[0].id, root_id);
assert_eq!(cats[0].name, "root");
assert!(cats[0].path.0.is_empty());
let sub_id = db.create_category(&cats[0], "sub")?;
let cats = db.get_categories()?;
assert_eq!(cats.len(), 2);
assert_eq!(cats[0], db.get_category(root_id)?);
assert_eq!(cats[0].id, root_id);
assert_eq!(cats[0].name, "root");
assert!(cats[0].path.0.is_empty());
assert_eq!(cats[1], db.get_category(sub_id)?);
assert_eq!(cats[1].id, sub_id);
assert_eq!(cats[1].name, "sub");
assert_eq!(cats[1].path.0, vec![cats[0].id]);
let root_act_id = db.create_activity(&cats[0], "act_in_root")?;
let sub_act_id = db.create_activity(&cats[1], "act_in_sub")?;
let acts = db.get_activities()?;
assert_eq!(acts.len(), 2);
assert_eq!(acts[0], db.get_activity(root_act_id)?);
assert_eq!(acts[0].id, root_act_id);
assert_eq!(acts[0].name, "act_in_root");
assert_eq!(acts[1], db.get_activity(sub_act_id)?);
assert_eq!(acts[1].id, sub_act_id);
assert_eq!(acts[1].name, "act_in_sub");
Ok(())
}
#[test]
fn delete() -> SqlResult<()> {
let db = crate::Nunc::testing().unwrap();
let root = db.get_category(
db.create_root_category("root")?
)?;
db.delete_category(&root)?;
assert!(db.find_category("root")?.is_none());
let root = db.get_category(
db.create_root_category("root")?
)?;
db.create_activity(&root, "activity")?;
db.delete_category(&root).unwrap_err();
let act = db.find_activity("activity")?.unwrap();
db.delete_activity(&act)?;
db.delete_category(&root)?;
assert!(db.find_activity("activity")?.is_none());
assert!(db.find_category("root")?.is_none());
db.create_activity(&root, "activity").unwrap_err();
let root = db.get_category(
db.create_root_category("root")?
)?;
let sub = db.get_category(
db.create_category(&root, "sub")?
)?;
db.delete_category(&root).unwrap_err();
db.delete_category(&sub)?;
db.delete_category(&root)?;
assert!(db.find_category("sub")?.is_none());
assert!(db.find_category("root")?.is_none());
Ok(())
}
#[test]
fn tree_constraints() -> SqlResult<()> {
let db = crate::Nunc::testing().unwrap();
let root = db.get_category(
db.create_root_category("root")?
)?;
db.create_activity(&root, "activity")?;
db.create_activity(&root, "activity").unwrap_err();
db.create_activity(&root, "activity_2")?;
let mut act_2 = db.find_activity("activity_2")?.unwrap();
act_2.name = "activity".to_owned();
db.rename_activity(&act_2).unwrap_err();
let root_2 = db.get_category(
db.create_root_category("root_2")?
)?;
let mut act_2 = db.find_activity("activity_2")?.unwrap();
act_2.category_id = root_2.id;
db.move_activity(&act_2)?;
act_2.name = "activity".to_owned();
db.rename_activity(&act_2)?;
Ok(())
}
#[test]
fn activities_under() -> SqlResult<()> {
let nunc = crate::Nunc::testing().unwrap();
assert!(crate::tests::add_example_tree(&nunc).is_ok());
let alpha = nunc.find_category("Alpha")?.unwrap();
let beta = nunc.find_category("Beta")?.unwrap();
let gamma = nunc.find_category("Gamma")?.unwrap();
let hobby = nunc.find_category("Hobby")?.unwrap();
let language = nunc.find_category("Language")?.unwrap();
let check_act_names = |cat, expected| -> SqlResult<()> {
let mut act_names: Vec<_> = nunc.get_activities_under(cat)?
.drain(..)
.map(|act| act.name)
.collect();
act_names.sort();
assert_eq!(act_names, expected);
Ok(())
};
check_act_names(&alpha, vec!["Chess", "Run", "Russian"])?;
check_act_names(&beta, vec!["Fitness", "Programming"])?;
check_act_names(&gamma, vec!["Eating", "Sleep"])?;
check_act_names(&language, vec!["Russian"])?;
check_act_names(&hobby, vec!["Chess"])?;
nunc.create_root_category("empty")?;
let empty = nunc.find_category("empty")?.unwrap();
check_act_names(&empty, vec![])?;
let three = nunc.get_category(nunc.create_category(&hobby, "Three")?)?;
assert_eq!(nunc.resolve_path_string(&three.path)?, "/Alpha/Hobby/");
nunc.create_activity(&three, "Deep")?;
check_act_names(&three, vec!["Deep"])?;
check_act_names(&alpha, vec!["Chess", "Deep", "Run", "Russian"])?;
Ok(())
}