//! Typed application error enum. //! //! Replaces `Result` across Tauri commands and backend modules. //! Tauri 2.x requires `serde::Serialize` on command error types. //! The custom `Serialize` impl produces `{ kind: "Database", detail: "..." }` //! so the frontend receives structured, classifiable errors. use serde::Serialize; /// Structured error type for all Tauri commands and backend operations. #[derive(Debug)] pub enum AppError { Database { detail: String }, Auth { detail: String }, Filesystem { detail: String }, Ipc { detail: String }, NotFound { detail: String }, Validation { detail: String }, Sidecar { detail: String }, Config { detail: String }, Network { detail: String }, Internal { detail: String }, } impl std::fmt::Display for AppError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { AppError::Database { detail } => write!(f, "Database: {detail}"), AppError::Auth { detail } => write!(f, "Auth: {detail}"), AppError::Filesystem { detail } => write!(f, "Filesystem: {detail}"), AppError::Ipc { detail } => write!(f, "IPC: {detail}"), AppError::NotFound { detail } => write!(f, "NotFound: {detail}"), AppError::Validation { detail } => write!(f, "Validation: {detail}"), AppError::Sidecar { detail } => write!(f, "Sidecar: {detail}"), AppError::Config { detail } => write!(f, "Config: {detail}"), AppError::Network { detail } => write!(f, "Network: {detail}"), AppError::Internal { detail } => write!(f, "Internal: {detail}"), } } } impl std::error::Error for AppError {} /// Helper struct for structured JSON serialization. /// Produces `{ "kind": "Database", "detail": "message" }`. #[derive(Serialize)] struct ErrorPayload<'a> { kind: &'a str, detail: &'a str, } impl Serialize for AppError { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { let (kind, detail) = match self { AppError::Database { detail } => ("Database", detail.as_str()), AppError::Auth { detail } => ("Auth", detail.as_str()), AppError::Filesystem { detail } => ("Filesystem", detail.as_str()), AppError::Ipc { detail } => ("IPC", detail.as_str()), AppError::NotFound { detail } => ("NotFound", detail.as_str()), AppError::Validation { detail } => ("Validation", detail.as_str()), AppError::Sidecar { detail } => ("Sidecar", detail.as_str()), AppError::Config { detail } => ("Config", detail.as_str()), AppError::Network { detail } => ("Network", detail.as_str()), AppError::Internal { detail } => ("Internal", detail.as_str()), }; ErrorPayload { kind, detail }.serialize(serializer) } } // --- Convenience conversions --- impl From for AppError { fn from(e: rusqlite::Error) -> Self { AppError::Database { detail: e.to_string(), } } } impl From for AppError { fn from(e: std::io::Error) -> Self { AppError::Filesystem { detail: e.to_string(), } } } impl From for AppError { fn from(e: serde_json::Error) -> Self { AppError::Validation { detail: e.to_string(), } } } /// Convert from legacy `String` errors (used at the boundary between /// agor-core functions that still return `Result` and /// Tauri commands that now return `Result`). impl From for AppError { fn from(s: String) -> Self { AppError::Internal { detail: s } } } impl From<&str> for AppError { fn from(s: &str) -> Self { AppError::Internal { detail: s.to_string(), } } } /// Convenience constructors for common error patterns. impl AppError { pub fn database(detail: impl Into) -> Self { AppError::Database { detail: detail.into(), } } pub fn auth(detail: impl Into) -> Self { AppError::Auth { detail: detail.into(), } } pub fn filesystem(detail: impl Into) -> Self { AppError::Filesystem { detail: detail.into(), } } pub fn not_found(detail: impl Into) -> Self { AppError::NotFound { detail: detail.into(), } } pub fn validation(detail: impl Into) -> Self { AppError::Validation { detail: detail.into(), } } pub fn sidecar(detail: impl Into) -> Self { AppError::Sidecar { detail: detail.into(), } } pub fn config(detail: impl Into) -> Self { AppError::Config { detail: detail.into(), } } pub fn network(detail: impl Into) -> Self { AppError::Network { detail: detail.into(), } } pub fn internal(detail: impl Into) -> Self { AppError::Internal { detail: detail.into(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_display() { let e = AppError::database("connection refused"); assert_eq!(e.to_string(), "Database: connection refused"); } #[test] fn test_serialize_produces_structured_json() { let e = AppError::Database { detail: "table not found".to_string(), }; let json = serde_json::to_value(&e).unwrap(); assert_eq!(json["kind"], "Database"); assert_eq!(json["detail"], "table not found"); } #[test] fn test_from_rusqlite_error() { let e: AppError = rusqlite::Error::SqliteFailure( rusqlite::ffi::Error::new(1), Some("test".to_string()), ) .into(); assert!(matches!(e, AppError::Database { .. })); } #[test] fn test_from_io_error() { let e: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "gone").into(); assert!(matches!(e, AppError::Filesystem { .. })); } #[test] fn test_from_string() { let e: AppError = "something went wrong".into(); assert!(matches!(e, AppError::Internal { .. })); } #[test] fn test_from_serde_json_error() { let e: AppError = serde_json::from_str::("not json") .unwrap_err() .into(); assert!(matches!(e, AppError::Validation { .. })); } }