use chrono::Utc; use serde::{Deserialize, Serialize}; use tauri::AppHandle; use tauri_plugin_store::StoreExt; use uuid::Uuid; const DRAFTS_STORE_FILE: &str = "hikari-drafts.json"; const DRAFTS_STORE_KEY: &str = "drafts"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Draft { pub id: String, pub content: String, pub saved_at: String, } fn load_all_drafts(app: &AppHandle) -> Result, String> { let store = app .store(DRAFTS_STORE_FILE) .map_err(|e| e.to_string())?; match store.get(DRAFTS_STORE_KEY) { Some(value) => serde_json::from_value(value.clone()).map_err(|e| e.to_string()), None => Ok(vec![]), } } fn save_all_drafts(app: &AppHandle, drafts: &[Draft]) -> Result<(), String> { let store = app .store(DRAFTS_STORE_FILE) .map_err(|e| e.to_string())?; let value = serde_json::to_value(drafts).map_err(|e| e.to_string())?; store.set(DRAFTS_STORE_KEY, value); store.save().map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub async fn list_drafts(app: AppHandle) -> Result, String> { let mut drafts = load_all_drafts(&app)?; // Sort newest first — ISO 8601 timestamps sort lexicographically drafts.sort_by(|a, b| b.saved_at.cmp(&a.saved_at)); Ok(drafts) } #[tauri::command] pub async fn save_draft(app: AppHandle, content: String) -> Result { let mut drafts = load_all_drafts(&app)?; let draft = Draft { id: Uuid::new_v4().to_string(), content, saved_at: Utc::now().to_rfc3339(), }; drafts.push(draft.clone()); save_all_drafts(&app, &drafts)?; Ok(draft) } #[tauri::command] pub async fn delete_draft(app: AppHandle, draft_id: String) -> Result<(), String> { let mut drafts = load_all_drafts(&app)?; drafts.retain(|d| d.id != draft_id); save_all_drafts(&app, &drafts) } #[tauri::command] pub async fn delete_all_drafts(app: AppHandle) -> Result<(), String> { save_all_drafts(&app, &[]) } #[cfg(test)] mod tests { use super::*; fn make_draft(id: &str, content: &str, saved_at: &str) -> Draft { Draft { id: id.to_string(), content: content.to_string(), saved_at: saved_at.to_string(), } } #[test] fn test_draft_serialization() { let draft = make_draft("test-id", "Hello world", "2026-01-01T00:00:00+00:00"); let json = serde_json::to_string(&draft).expect("Failed to serialize"); let parsed: Draft = serde_json::from_str(&json).expect("Failed to deserialize"); assert_eq!(parsed.id, draft.id); assert_eq!(parsed.content, draft.content); assert_eq!(parsed.saved_at, draft.saved_at); } #[test] fn test_draft_clone() { let original = make_draft("clone-id", "Clone me", "2026-01-01T00:00:00+00:00"); let cloned = original.clone(); assert_eq!(original.id, cloned.id); assert_eq!(original.content, cloned.content); assert_eq!(original.saved_at, cloned.saved_at); } #[test] fn test_sort_newest_first() { let mut drafts = [ make_draft("a", "First", "2026-01-01T00:00:00+00:00"), make_draft("b", "Third", "2026-01-03T00:00:00+00:00"), make_draft("c", "Second", "2026-01-02T00:00:00+00:00"), ]; drafts.sort_by(|a, b| b.saved_at.cmp(&a.saved_at)); assert_eq!(drafts[0].id, "b"); assert_eq!(drafts[1].id, "c"); assert_eq!(drafts[2].id, "a"); } #[test] fn test_retain_excludes_deleted() { let mut drafts = vec![ make_draft("keep-1", "Keep me", "2026-01-01T00:00:00+00:00"), make_draft("delete-me", "Delete me", "2026-01-02T00:00:00+00:00"), make_draft("keep-2", "Keep me too", "2026-01-03T00:00:00+00:00"), ]; let target_id = "delete-me".to_string(); drafts.retain(|d| d.id != target_id); assert_eq!(drafts.len(), 2); assert!(drafts.iter().all(|d| d.id != "delete-me")); } #[test] fn test_find_by_id() { let drafts = [ make_draft("draft-1", "First draft", "2026-01-01T00:00:00+00:00"), make_draft("draft-2", "Second draft", "2026-01-02T00:00:00+00:00"), make_draft("draft-3", "Third draft", "2026-01-03T00:00:00+00:00"), ]; let found = drafts.iter().find(|d| d.id == "draft-2"); assert!(found.is_some()); assert_eq!(found.unwrap().content, "Second draft"); let not_found = drafts.iter().find(|d| d.id == "draft-999"); assert!(not_found.is_none()); } #[test] fn test_multiline_content() { let content = "Line 1\nLine 2\nLine 3"; let draft = make_draft("multi", content, "2026-01-01T00:00:00+00:00"); assert!(draft.content.contains('\n')); assert_eq!(draft.content.split('\n').count(), 3); } #[test] fn test_empty_after_delete_all() { let mut drafts = vec![ make_draft("a", "A", "2026-01-01T00:00:00+00:00"), make_draft("b", "B", "2026-01-02T00:00:00+00:00"), ]; drafts.clear(); assert!(drafts.is_empty()); } #[test] fn test_uuid_format() { // UUIDs should be non-empty and contain hyphens let id = Uuid::new_v4().to_string(); assert!(!id.is_empty()); assert!(id.contains('-')); assert_eq!(id.len(), 36); } #[test] fn test_timestamp_is_rfc3339() { let ts = Utc::now().to_rfc3339(); // RFC 3339 timestamps contain T and + or Z assert!(ts.contains('T')); assert!(ts.ends_with("+00:00") || ts.ends_with('Z')); } }