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}