//! API error types for Tauri commands use serde::Serialize; /// Typed error codes for API responses. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub enum ApiErrorCode { #[serde(rename = "BAD_REQUEST")] BadRequest, #[serde(rename = "NOT_FOUND")] NotFound, #[serde(rename = "DATABASE")] Database, #[serde(rename = "INTERNAL")] Internal, #[serde(rename = "PLUGIN")] Plugin, } impl std::fmt::Display for ApiErrorCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::BadRequest => write!(f, "BAD_REQUEST"), Self::NotFound => write!(f, "NOT_FOUND"), Self::Database => write!(f, "DATABASE"), Self::Internal => write!(f, "INTERNAL"), Self::Plugin => write!(f, "PLUGIN"), } } } /// API error type for Tauri commands #[derive(Debug, Clone, Serialize)] pub struct ApiError { pub code: ApiErrorCode, pub message: String, } impl ApiError { /// Create a `BAD_REQUEST` error for invalid input from the frontend. pub fn bad_request(message: impl Into) -> Self { Self { code: ApiErrorCode::BadRequest, message: message.into(), } } /// Create a `NOT_FOUND` error when a resource doesn't exist. pub fn not_found(message: impl Into) -> Self { Self { code: ApiErrorCode::NotFound, message: message.into(), } } /// Create a `DATABASE` error for SQL/connection failures. pub fn database(message: impl Into) -> Self { Self { code: ApiErrorCode::Database, message: message.into(), } } /// Create an `INTERNAL` error for unexpected server-side failures. pub fn internal(message: impl Into) -> Self { Self { code: ApiErrorCode::Internal, message: message.into(), } } /// Create a `PLUGIN` error for busser/plugin failures. pub fn plugin(message: impl Into) -> Self { Self { code: ApiErrorCode::Plugin, message: message.into(), } } } impl From for ApiError { fn from(e: sqlx::Error) -> Self { // Log the full error for debugging but send a generic message to the // frontend to avoid leaking file paths, SQL text, or schema details. tracing::error!(error = %e, "Database error"); Self::database("A database error occurred") } } impl From for ApiError { fn from(e: bb_feed::FeedError) -> Self { tracing::error!(error = %e, "Feed error"); Self::database("A feed query error occurred") } } impl From for ApiError { fn from(e: bb_core::orchestrator::OrchestratorError) -> Self { use bb_core::orchestrator::OrchestratorError; match e { OrchestratorError::Database(e) => { tracing::error!(error = %e, "Database error"); Self::database("A database error occurred") } OrchestratorError::Plugin(e) => { tracing::error!(error = %e, "Plugin error"); Self::plugin("A plugin error occurred") } OrchestratorError::Feed(e) => { tracing::error!(error = %e, "Feed error"); Self::database("A feed query error occurred") } OrchestratorError::Config(msg) => Self::internal(msg), } } } impl std::fmt::Display for ApiError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}: {}", self.code, self.message) } } #[cfg(test)] mod tests { use super::*; #[test] fn bad_request_has_correct_code() { let e = ApiError::bad_request("invalid"); assert_eq!(e.code, ApiErrorCode::BadRequest); assert_eq!(e.message, "invalid"); } #[test] fn not_found_has_correct_code() { let e = ApiError::not_found("missing"); assert_eq!(e.code, ApiErrorCode::NotFound); } #[test] fn constructor_codes() { assert_eq!(ApiError::database("err").code, ApiErrorCode::Database); assert_eq!(ApiError::internal("err").code, ApiErrorCode::Internal); assert_eq!(ApiError::plugin("err").code, ApiErrorCode::Plugin); } #[test] fn display_format() { let e = ApiError::bad_request("test msg"); assert_eq!(e.to_string(), "BAD_REQUEST: test msg"); } #[test] fn from_sqlx_error() { let sqlx_err = sqlx::Error::RowNotFound; let api_err = ApiError::from(sqlx_err); assert_eq!(api_err.code, ApiErrorCode::Database); assert!(!api_err.message.is_empty()); } #[test] fn from_orchestrator_config_error() { use bb_core::orchestrator::OrchestratorError; let err = OrchestratorError::Config("bad config".to_string()); let api_err = ApiError::from(err); assert_eq!(api_err.code, ApiErrorCode::Internal); assert_eq!(api_err.message, "bad config"); } #[test] fn serde_codes_match_wire_format() { let e = ApiError::bad_request("test"); let json = serde_json::to_string(&e).unwrap(); assert!(json.contains("\"BAD_REQUEST\"")); } }