use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::RwLock;
use crate::error::EnvelopeError;
use crate::models::{Category, CategoryGroup, CategoryGroupId, CategoryId};
use super::file_io::{read_json, write_json_atomic};
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct CategoryData {
pub groups: Vec<CategoryGroup>,
pub categories: Vec<Category>,
}
pub struct CategoryRepository {
path: PathBuf,
groups: RwLock<HashMap<CategoryGroupId, CategoryGroup>>,
categories: RwLock<HashMap<CategoryId, Category>>,
}
impl CategoryRepository {
pub fn new(path: PathBuf) -> Self {
Self {
path,
groups: RwLock::new(HashMap::new()),
categories: RwLock::new(HashMap::new()),
}
}
pub fn load(&self) -> Result<(), EnvelopeError> {
let file_data: CategoryData = read_json(&self.path)?;
let mut groups = self
.groups
.write()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
let mut categories = self
.categories
.write()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
groups.clear();
categories.clear();
for group in file_data.groups {
groups.insert(group.id, group);
}
for category in file_data.categories {
categories.insert(category.id, category);
}
Ok(())
}
pub fn save(&self) -> Result<(), EnvelopeError> {
let groups = self
.groups
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
let categories = self
.categories
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
let mut group_list: Vec<_> = groups.values().cloned().collect();
group_list.sort_by_key(|g| g.sort_order);
let mut category_list: Vec<_> = categories.values().cloned().collect();
category_list.sort_by_key(|c| (c.sort_order, c.name.clone()));
let file_data = CategoryData {
groups: group_list,
categories: category_list,
};
write_json_atomic(&self.path, &file_data)
}
pub fn get_group(&self, id: CategoryGroupId) -> Result<Option<CategoryGroup>, EnvelopeError> {
let groups = self
.groups
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
Ok(groups.get(&id).cloned())
}
pub fn get_all_groups(&self) -> Result<Vec<CategoryGroup>, EnvelopeError> {
let groups = self
.groups
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
let mut list: Vec<_> = groups.values().cloned().collect();
list.sort_by_key(|g| g.sort_order);
Ok(list)
}
pub fn get_group_by_name(&self, name: &str) -> Result<Option<CategoryGroup>, EnvelopeError> {
let groups = self
.groups
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
let name_lower = name.to_lowercase();
Ok(groups
.values()
.find(|g| g.name.to_lowercase() == name_lower)
.cloned())
}
pub fn upsert_group(&self, group: CategoryGroup) -> Result<(), EnvelopeError> {
let mut groups = self
.groups
.write()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
groups.insert(group.id, group);
Ok(())
}
pub fn delete_group(
&self,
id: CategoryGroupId,
delete_categories: bool,
) -> Result<bool, EnvelopeError> {
let mut groups = self
.groups
.write()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
if delete_categories {
let mut categories = self.categories.write().map_err(|e| {
EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e))
})?;
categories.retain(|_, c| c.group_id != id);
}
Ok(groups.remove(&id).is_some())
}
pub fn get_category(&self, id: CategoryId) -> Result<Option<Category>, EnvelopeError> {
let categories = self
.categories
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
Ok(categories.get(&id).cloned())
}
pub fn get_all_categories(&self) -> Result<Vec<Category>, EnvelopeError> {
let categories = self
.categories
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
let mut list: Vec<_> = categories.values().cloned().collect();
list.sort_by_key(|c| (c.sort_order, c.name.clone()));
Ok(list)
}
pub fn get_categories_in_group(
&self,
group_id: CategoryGroupId,
) -> Result<Vec<Category>, EnvelopeError> {
let categories = self
.categories
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
let mut list: Vec<_> = categories
.values()
.filter(|c| c.group_id == group_id)
.cloned()
.collect();
list.sort_by_key(|c| (c.sort_order, c.name.clone()));
Ok(list)
}
pub fn get_category_by_name(&self, name: &str) -> Result<Option<Category>, EnvelopeError> {
let categories = self
.categories
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
let name_lower = name.to_lowercase();
Ok(categories
.values()
.find(|c| c.name.to_lowercase() == name_lower)
.cloned())
}
pub fn upsert_category(&self, category: Category) -> Result<(), EnvelopeError> {
let mut categories = self
.categories
.write()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
categories.insert(category.id, category);
Ok(())
}
pub fn delete_category(&self, id: CategoryId) -> Result<bool, EnvelopeError> {
let mut categories = self
.categories
.write()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire write lock: {}", e)))?;
Ok(categories.remove(&id).is_some())
}
pub fn group_count(&self) -> Result<usize, EnvelopeError> {
let groups = self
.groups
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
Ok(groups.len())
}
pub fn category_count(&self) -> Result<usize, EnvelopeError> {
let categories = self
.categories
.read()
.map_err(|e| EnvelopeError::Storage(format!("Failed to acquire read lock: {}", e)))?;
Ok(categories.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_repo() -> (TempDir, CategoryRepository) {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("budget.json");
let repo = CategoryRepository::new(path);
(temp_dir, repo)
}
#[test]
fn test_empty_load() {
let (_temp_dir, repo) = create_test_repo();
repo.load().unwrap();
assert_eq!(repo.group_count().unwrap(), 0);
assert_eq!(repo.category_count().unwrap(), 0);
}
#[test]
fn test_group_operations() {
let (_temp_dir, repo) = create_test_repo();
repo.load().unwrap();
let group = CategoryGroup::new("Bills");
let id = group.id;
repo.upsert_group(group).unwrap();
assert_eq!(repo.group_count().unwrap(), 1);
let retrieved = repo.get_group(id).unwrap().unwrap();
assert_eq!(retrieved.name, "Bills");
repo.delete_group(id, false).unwrap();
assert_eq!(repo.group_count().unwrap(), 0);
}
#[test]
fn test_category_operations() {
let (_temp_dir, repo) = create_test_repo();
repo.load().unwrap();
let group = CategoryGroup::new("Bills");
repo.upsert_group(group.clone()).unwrap();
let category = Category::new("Rent", group.id);
let cat_id = category.id;
repo.upsert_category(category).unwrap();
assert_eq!(repo.category_count().unwrap(), 1);
let retrieved = repo.get_category(cat_id).unwrap().unwrap();
assert_eq!(retrieved.name, "Rent");
let in_group = repo.get_categories_in_group(group.id).unwrap();
assert_eq!(in_group.len(), 1);
}
#[test]
fn test_save_and_reload() {
let (temp_dir, repo) = create_test_repo();
repo.load().unwrap();
let group = CategoryGroup::new("Bills");
let category = Category::new("Rent", group.id);
let cat_id = category.id;
repo.upsert_group(group).unwrap();
repo.upsert_category(category).unwrap();
repo.save().unwrap();
let path = temp_dir.path().join("budget.json");
let repo2 = CategoryRepository::new(path);
repo2.load().unwrap();
assert_eq!(repo2.group_count().unwrap(), 1);
assert_eq!(repo2.category_count().unwrap(), 1);
let retrieved = repo2.get_category(cat_id).unwrap().unwrap();
assert_eq!(retrieved.name, "Rent");
}
#[test]
fn test_get_by_name() {
let (_temp_dir, repo) = create_test_repo();
repo.load().unwrap();
let group = CategoryGroup::new("My Bills");
repo.upsert_group(group.clone()).unwrap();
let category = Category::new("Monthly Rent", group.id);
repo.upsert_category(category).unwrap();
let found_group = repo.get_group_by_name("my bills").unwrap();
assert!(found_group.is_some());
let found_cat = repo.get_category_by_name("MONTHLY RENT").unwrap();
assert!(found_cat.is_some());
}
#[test]
fn test_delete_group_with_categories() {
let (_temp_dir, repo) = create_test_repo();
repo.load().unwrap();
let group = CategoryGroup::new("Bills");
let group_id = group.id;
repo.upsert_group(group.clone()).unwrap();
let cat1 = Category::new("Rent", group.id);
let cat2 = Category::new("Utilities", group.id);
repo.upsert_category(cat1).unwrap();
repo.upsert_category(cat2).unwrap();
assert_eq!(repo.category_count().unwrap(), 2);
repo.delete_group(group_id, true).unwrap();
assert_eq!(repo.group_count().unwrap(), 0);
assert_eq!(repo.category_count().unwrap(), 0);
}
}