Skip to main content

ito_core/
errors.rs

1//! Core-layer error types.
2//!
3//! [`CoreError`](crate::errors::CoreError) is the canonical error type for `ito-core`. All public
4//! functions in this crate return [`CoreResult`](crate::errors::CoreResult) rather than adapter-level
5//! error types. Adapter layers (CLI, web) convert `CoreError` into their own
6//! presentation types (e.g., miette `Report` for rich terminal output).
7
8use std::io;
9
10use ito_domain::errors::DomainError;
11use thiserror::Error;
12
13/// Result alias for core-layer operations.
14pub type CoreResult<T> = Result<T, CoreError>;
15
16/// Canonical error type for the core orchestration layer.
17///
18/// Variants cover the major failure categories encountered by core use-cases.
19/// None of the variants carry presentation logic — that belongs in the adapter.
20#[derive(Debug, Error)]
21pub enum CoreError {
22    /// An error propagated from the domain layer.
23    #[error(transparent)]
24    Domain(#[from] DomainError),
25
26    /// Filesystem or other I/O failure.
27    #[error("{context}: {source}")]
28    Io {
29        /// Short description of the operation that failed.
30        context: String,
31        /// Underlying I/O error.
32        #[source]
33        source: io::Error,
34    },
35
36    /// Input validation failure (bad arguments, constraint violations).
37    #[error("{0}")]
38    Validation(String),
39
40    /// Parse failure (duration strings, JSON, YAML, etc.).
41    #[error("{0}")]
42    Parse(String),
43
44    /// Process execution failure (git, shell commands).
45    #[error("{0}")]
46    Process(String),
47
48    /// SQLite operation failure.
49    #[error("sqlite error: {0}")]
50    Sqlite(String),
51
52    /// An expected asset or resource was not found.
53    #[error("{0}")]
54    NotFound(String),
55
56    /// Serialization or deserialization failure.
57    #[error("{context}: {message}")]
58    Serde {
59        /// Short description of the operation.
60        context: String,
61        /// Error detail.
62        message: String,
63    },
64}
65
66impl CoreError {
67    /// Build an I/O error with context.
68    pub fn io(context: impl Into<String>, source: io::Error) -> Self {
69        Self::Io {
70            context: context.into(),
71            source,
72        }
73    }
74
75    /// Build a validation error.
76    pub fn validation(msg: impl Into<String>) -> Self {
77        Self::Validation(msg.into())
78    }
79
80    /// Build a parse error.
81    pub fn parse(msg: impl Into<String>) -> Self {
82        Self::Parse(msg.into())
83    }
84
85    /// Build a process error.
86    pub fn process(msg: impl Into<String>) -> Self {
87        Self::Process(msg.into())
88    }
89
90    /// Build a not-found error.
91    pub fn not_found(msg: impl Into<String>) -> Self {
92        Self::NotFound(msg.into())
93    }
94
95    /// Create a `CoreError::Serde` containing a context and a message describing a
96    /// serialization or deserialization failure.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use ito_core::errors::CoreError;
102    ///
103    /// let err = CoreError::serde("load config", "missing field `name`");
104    /// match err {
105    ///     CoreError::Serde { context, message } => {
106    ///         assert_eq!(context, "load config");
107    ///         assert_eq!(message, "missing field `name`");
108    ///     }
109    ///     _ => panic!("expected Serde variant"),
110    /// }
111    /// ```
112    pub fn serde(context: impl Into<String>, message: impl Into<String>) -> Self {
113        Self::Serde {
114            context: context.into(),
115            message: message.into(),
116        }
117    }
118
119    /// Wraps a human-readable SQLite error message into a `CoreError::Sqlite`.
120    ///
121    /// Returns a `CoreError::Sqlite` containing the provided message.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use ito_core::errors::CoreError;
127    ///
128    /// let err = CoreError::sqlite("database locked");
129    /// let CoreError::Sqlite(msg) = err else {
130    ///     panic!("expected Sqlite variant");
131    /// };
132    /// assert_eq!(msg, "database locked");
133    /// ```
134    pub fn sqlite(msg: impl Into<String>) -> Self {
135        Self::Sqlite(msg.into())
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    /// Verifies that each `CoreError` helper constructor produces the expected enum variant and contains the correct data.
144    ///
145    /// Constructs every public `CoreError` variant via its respective helper (e.g., `io`, `validation`, `parse`, `process`,
146    /// `not_found`, `serde`, `sqlite`) and asserts both the variant and the values carried by that variant.
147    ///
148    /// # Examples
149    ///
150    ///
151    #[test]
152    fn core_error_helpers_construct_expected_variants() {
153        let io_err = CoreError::io("read config", io::Error::other("boom"));
154        let CoreError::Io { context, source } = io_err else {
155            panic!("expected io variant");
156        };
157        assert_eq!(context, "read config");
158        assert_eq!(source.to_string(), "boom");
159
160        let validation_err = CoreError::validation("bad");
161        let CoreError::Validation(validation_msg) = validation_err else {
162            panic!("expected validation variant");
163        };
164        assert_eq!(validation_msg, "bad");
165
166        let parse_err = CoreError::parse("bad");
167        let CoreError::Parse(parse_msg) = parse_err else {
168            panic!("expected parse variant");
169        };
170        assert_eq!(parse_msg, "bad");
171
172        let process_err = CoreError::process("bad");
173        let CoreError::Process(process_msg) = process_err else {
174            panic!("expected process variant");
175        };
176        assert_eq!(process_msg, "bad");
177
178        let not_found_err = CoreError::not_found("bad");
179        let CoreError::NotFound(not_found_msg) = not_found_err else {
180            panic!("expected not-found variant");
181        };
182        assert_eq!(not_found_msg, "bad");
183
184        let serde_err = CoreError::serde("load", "bad");
185        let CoreError::Serde { context, message } = serde_err else {
186            panic!("expected serde variant");
187        };
188        assert_eq!(context, "load");
189        assert_eq!(message, "bad");
190
191        let sqlite_err = CoreError::sqlite("bad");
192        let CoreError::Sqlite(sqlite_msg) = sqlite_err else {
193            panic!("expected sqlite variant");
194        };
195        assert_eq!(sqlite_msg, "bad");
196    }
197}