nunc 0.2.0

WIP: time tracking application
Documentation
use std::error::Error;
use std::fmt::{
	self,
	Write
};
use std::num::ParseIntError;
use std::str::FromStr;

use conciliator::{
	Buffer,
	Inline,
	Paint
};

use rusqlite::{
	Row,
	Result as SqlResult,
	Error as SqlError
};
use rusqlite::types::{
	FromSql,
	ToSql,
	ValueRef,
	FromSqlResult,
	FromSqlError,
	ToSqlOutput
};


#[derive(Debug, Eq, PartialEq)]
pub struct Category {
	pub id: u64,
	pub path: Path,
	pub name: String
}

#[derive(Debug, Eq, PartialEq)]
pub struct Resolved {
	pub id: u64,
	pub id_path: Path,
	pub string_path: String,
	pub name: String
}

#[derive(Debug, Eq, PartialEq)]
pub struct Path(pub(crate) Vec<u64>);

#[derive(Debug, Eq, PartialEq)]
pub enum ParsePathError {
	ParseInt(ParseIntError),
	Empty,
	MissingSeparator
}


/*
 *	CATEGORY
 */

impl Category {
	pub fn resolve(self, string_path: String) -> Resolved {
		let Self {id, name, path} = self;
		Resolved {id, id_path: path, string_path, name}
	}
}

impl<'r> TryFrom<&Row<'r>> for Category {
	type Error = SqlError;

	fn try_from(value: &Row<'r>) -> Result<Self, Self::Error> {
		let (id, name, path_str): (_, _, Box<str>) = value.try_into()?;
		let path = path_str.parse().unwrap();

		Ok(Self {id, name, path})
	}
}

impl Inline for Category {
	fn inline(&self, buf: &mut Buffer) {
		buf.push_alpha_bold(&self.name);
	}
}

/*
 *	RESOLVED
 */

impl Resolved {
	pub fn revert(self) -> Category {
		let Self {id, id_path, name, ..} = self;
		Category {id, path: id_path, name}
	}
}

impl Inline for Resolved {
	fn inline(&self, buffer: &mut Buffer) {
		buffer.push_plain(&self.string_path).push_beta(&self.name);
	}
}

/*
 *	CATEGORY PATH
 */

impl Path {
	pub fn serialize(&self) -> String {
		self.0.iter().fold(String::from("/"), |mut s, id| {
			write!(s, "{id}/").unwrap();
			s
		})
	}
	pub fn is_child_of(&self, category: &Category) -> bool {
		#[allow(clippy::needless_return)]
		return self.0.len() == (category.path.0.len() + 1)
			&& self.0.starts_with(&category.path.0)
			&& self.0.ends_with(&[category.id]);
	}
}
impl FromStr for Path {
	type Err = ParsePathError;
	fn from_str(s: &str) -> Result<Self, Self::Err> {
		let mut split = s.split('/');
		match split.next() {
			Some("") => match split.next_back() {
				Some("") => split
					.map(str::parse)
					.collect::<Result<_, _>>()
					.map_err(ParsePathError::ParseInt)
					.map(Self),
				_ => Err(ParsePathError::MissingSeparator)
			},
			Some(_) => Err(ParsePathError::MissingSeparator),
			None => Err(ParsePathError::Empty)
		}
	}
}


impl FromSql for Path {
	fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
		String::column_result(value)?
			.parse()
			.map_err(Into::into)
			.map_err(FromSqlError::Other)
	}
}
impl ToSql for Path {
	fn to_sql(&self) -> SqlResult<ToSqlOutput<'_>> {
		Ok(self.serialize().into())
	}
}

/*
 *	PARSE PATH ERROR
 */

impl Error for ParsePathError {}

impl fmt::Display for ParsePathError {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		fmt::Debug::fmt(self, f)
	}
}


/*
 *	TESTS
 */

#[test]
fn path_parse_and_serialize() {
	let valid = [
		("/", vec![]),
		("/0/", vec![0]),
		("/1/", vec![1]),
		("/0/1/2/3/", vec![0, 1, 2, 3])
	];

	let invalid = [
		"",
		"//",
		"0",
		"/0",
		"0/",
		"/0/1",
		"0/1",
		"a",
		"/a/",
		"/0/x/1/"
	];


	for (to_parse, expected) in valid {
		println!("Parsing '{to_parse}', expecting {expected:?}");
		let parsed = to_parse.parse();
		assert_eq!(
			parsed,
			Ok(Path(expected)),
			"failed to parse {to_parse}"
		);
		assert_eq!(parsed.unwrap().serialize(), to_parse);
	}

	for to_parse in invalid {
		println!("Parsing '{to_parse}', expecting error");
		let err = to_parse.parse::<Path>().unwrap_err();
		println!("Err: '{err:?}'!)");
	}
}