use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use std::convert::AsRef; use std::fs; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use std::str::FromStr; use structopt::StructOpt; use twox_hash::XxHash64; type Account = String; type Category = String; #[derive(Debug)] #[derive(Hash)] #[derive(Deserialize, Serialize)] #[derive(StructOpt)] pub enum TransactionKind { Expense, Income, //TODO Transfer, } impl FromStr for TransactionKind { type Err = String; fn from_str(s: &str) -> Result { match s { "expense" => Ok(TransactionKind::Expense), "income" => Ok(TransactionKind::Income), _ => Err(format!("Unknown transaction kind: {:?}", s)), } } } #[derive(Debug)] #[derive(Hash)] #[derive(Deserialize, Serialize)] pub struct Transaction { pub description: String, pub category: Category, pub amount: Decimal, pub kind: TransactionKind, pub from: Account, pub to: Account, } #[derive(Debug)] #[derive(Hash)] #[derive(Deserialize, Serialize)] pub struct Post { transaction: Transaction, removed: bool, } #[derive(Debug)] pub struct Store { root: PathBuf, posts: Vec, } impl Store { //TODO Result pub fn open(root: PathBuf) -> Option { Some(Self { posts: Self::open_dir(&root)?, root, }) } //TODO check if hash matches //TODO Result //TODO overkill? maybe we can use subfolders later on fn open_dir(dir: &Path) -> Option> { let mut res = Vec::new(); for entry in std::fs::read_dir(dir).ok()? { let entry = entry.ok()?; if entry.file_type().ok()?.is_dir() { let mut posts = Self::open_dir(&entry.path())?; res.append(&mut posts); } else { res.push(Post::open(&entry.path())?); } } Some(res) } pub fn write(&self) -> std::io::Result<()> { for post in &self.posts { let mut path = self.root.clone(); path.push(format!("{}", post.id())); post.write(&path)?; } Ok(()) } } impl Post { pub fn new(transaction: Transaction) -> Self { Self { transaction, removed: false, } } fn write>(&self, p: &P) -> std::io::Result<()> { fs::write(p, serde_json::to_string_pretty(self).unwrap()) //TODO control pretty or not } //TODO Result fn open>(p: &P) -> Option { fs::read_to_string(p).ok().as_ref().and_then(|s| serde_json::from_str(s).ok()) } fn id(&self) -> u64 { let mut h = XxHash64::default(); self.hash(&mut h); h.finish() } }