Skip to main content

tiller_sync/
error.rs

1//! Error types for the tiller application.
2//!
3//! This module provides:
4//! - `TillerError` / `Result<T>` - structured error type for lib, pub and MCP use
5//! - `Er` / `Res<T>` - anyhow-based types for internal use
6//! - Trait `IntoResult<T>` for converting the internal error type to a public `Result<T>`
7
8use serde::{Deserialize, Serialize};
9use std::fmt;
10
11/// This library's public result type.
12pub type Result<T> = std::result::Result<T, TillerError>;
13
14/// The type of error.
15///
16/// Errors are categorized into two groups:
17/// - **Protocol errors**: JSON-RPC level failures (convert to `ErrorData`)
18/// - **Tool errors**: Business logic failures (convert to `CallToolResult::error()`)
19#[derive(
20    Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
21)]
22#[serde(rename_all = "PascalCase")]
23pub enum ErrorType {
24    // === Protocol-level errors (become ErrorData) ===
25    /// Malformed or invalid MCP request
26    Request,
27
28    /// An error from the MCP server or service, unrelated to tiller
29    Service,
30
31    // === Tool-level errors (become CallToolResult::error()) ===
32    /// Unexpected internal failure, e.g. filesystem
33    Internal,
34
35    /// Sync operation failures (no backup, conflicts, formula issues)
36    Sync,
37
38    /// Authentication problems
39    Auth,
40
41    /// Configuration issues
42    Config,
43
44    /// SQLite database failures
45    Database,
46
47    /// An error whose precise type is not otherwise understood or known
48    #[default]
49    Other,
50}
51
52serde_plain::derive_display_from_serialize!(ErrorType);
53serde_plain::derive_fromstr_from_deserialize!(ErrorType);
54
55/// This library's public error type
56#[derive(Debug)]
57pub struct TillerError {
58    inner: anyhow::Error,
59    error_type: ErrorType,
60}
61
62impl TillerError {
63    /// The `ErrorType` of the `TillerError`
64    pub fn error_type(&self) -> ErrorType {
65        self.error_type
66    }
67
68    /// Returns true if this should be categorized as an MCP protocol error.
69    ///
70    /// Protocol errors should be converted to `ErrorData` in MCP responses.
71    pub fn is_protocol_error(&self) -> bool {
72        match self.error_type() {
73            ErrorType::Request => true,
74            ErrorType::Service => true,
75            ErrorType::Internal => false,
76            ErrorType::Sync => false,
77            ErrorType::Auth => false,
78            ErrorType::Config => false,
79            ErrorType::Database => false,
80            ErrorType::Other => false,
81        }
82    }
83
84    /// Returns true if this should be categorized as a tool error for MCP.
85    ///
86    /// Tool errors should be converted to `CallToolResult::error()` in MCP responses.
87    pub fn is_tool_error(&self) -> bool {
88        !self.is_protocol_error()
89    }
90}
91
92impl fmt::Display for TillerError {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "{} error: {:?}", self.error_type(), self.inner)
95    }
96}
97
98impl std::error::Error for TillerError {
99    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
100        self.inner.source()
101    }
102}
103
104/// A trait we can use to convert an `anyhow::Result<T>` into a public `Result<T>`
105pub(crate) trait IntoResult<T> {
106    fn pub_result(self, t: ErrorType) -> Result<T>;
107}
108
109/// Anyhow-based types for internal use. When constructing these, choose the name of the desired
110/// public error at the start of the error message. For example `InvalidRequest: blah blah`.
111pub(crate) type Er = anyhow::Error;
112pub(crate) type Res<T> = std::result::Result<T, Er>;
113
114// The implementation which makes an anyhow::Result convertible to a public Result
115impl<T> IntoResult<T> for Res<T> {
116    fn pub_result(self, t: ErrorType) -> Result<T> {
117        match self {
118            Ok(ok) => Ok(ok),
119            Err(e) => Err(TillerError {
120                inner: e,
121                error_type: t,
122            }),
123        }
124    }
125}
126
127#[test]
128fn pub_result_test() {
129    use anyhow::anyhow;
130    let anyhow_error = anyhow!("MY_ERROR_MESSAGE");
131    let anyhow_result: Res<()> = Err(anyhow_error);
132    let result = anyhow_result.pub_result(ErrorType::Sync);
133    let e = result.err().unwrap();
134    let message = e.to_string().lines().next().unwrap().to_string();
135    assert_eq!("Sync error: MY_ERROR_MESSAGE", message)
136}