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
13use crate::capabilities::CompiledFeature;
14
15/// Result alias for core-layer operations.
16pub type CoreResult<T> = Result<T, CoreError>;
17
18/// Canonical error type for the core orchestration layer.
19///
20/// Variants cover the major failure categories encountered by core use-cases.
21/// None of the variants carry presentation logic — that belongs in the adapter.
22#[derive(Debug, Error)]
23pub enum CoreError {
24 /// An error propagated from the domain layer.
25 #[error(transparent)]
26 Domain(#[from] DomainError),
27
28 /// Filesystem or other I/O failure.
29 #[error("{context}: {source}")]
30 Io {
31 /// Short description of the operation that failed.
32 context: String,
33 /// Underlying I/O error.
34 #[source]
35 source: io::Error,
36 },
37
38 /// Input validation failure (bad arguments, constraint violations).
39 #[error("{0}")]
40 Validation(String),
41
42 /// Parse failure (duration strings, JSON, YAML, etc.).
43 #[error("{0}")]
44 Parse(String),
45
46 /// Process execution failure (git, shell commands).
47 #[error("{0}")]
48 Process(String),
49
50 /// SQLite operation failure.
51 #[error("sqlite error: {0}")]
52 Sqlite(String),
53
54 /// An expected asset or resource was not found.
55 #[error("{0}")]
56 NotFound(String),
57
58 /// Serialization or deserialization failure.
59 #[error("{context}: {message}")]
60 Serde {
61 /// Short description of the operation.
62 context: String,
63 /// Error detail.
64 message: String,
65 },
66
67 /// Configuration or a compatibility command requested code not present in this build.
68 #[error(
69 "feature '{feature}' is unavailable (requested by {requested_by}). Recovery: {recovery}"
70 )]
71 FeatureUnavailable {
72 /// Stable Cargo feature identifier.
73 feature: CompiledFeature,
74 /// Configuration field or command that requested the feature.
75 requested_by: String,
76 /// Action the user can take without silent fallback.
77 recovery: String,
78 },
79}
80
81impl CoreError {
82 /// Build an I/O error with context.
83 pub fn io(context: impl Into<String>, source: io::Error) -> Self {
84 Self::Io {
85 context: context.into(),
86 source,
87 }
88 }
89
90 /// Build a validation error.
91 pub fn validation(msg: impl Into<String>) -> Self {
92 Self::Validation(msg.into())
93 }
94
95 /// Build a parse error.
96 pub fn parse(msg: impl Into<String>) -> Self {
97 Self::Parse(msg.into())
98 }
99
100 /// Build a process error.
101 pub fn process(msg: impl Into<String>) -> Self {
102 Self::Process(msg.into())
103 }
104
105 /// Build a not-found error.
106 pub fn not_found(msg: impl Into<String>) -> Self {
107 Self::NotFound(msg.into())
108 }
109
110 /// Create a `CoreError::Serde` containing a context and a message describing a
111 /// serialization or deserialization failure.
112 ///
113 /// # Examples
114 ///
115 /// ```
116 /// use ito_core::errors::CoreError;
117 ///
118 /// let err = CoreError::serde("load config", "missing field `name`");
119 /// match err {
120 /// CoreError::Serde { context, message } => {
121 /// assert_eq!(context, "load config");
122 /// assert_eq!(message, "missing field `name`");
123 /// }
124 /// _ => panic!("expected Serde variant"),
125 /// }
126 /// ```
127 pub fn serde(context: impl Into<String>, message: impl Into<String>) -> Self {
128 Self::Serde {
129 context: context.into(),
130 message: message.into(),
131 }
132 }
133
134 /// Wraps a human-readable SQLite error message into a `CoreError::Sqlite`.
135 ///
136 /// Returns a `CoreError::Sqlite` containing the provided message.
137 ///
138 /// # Examples
139 ///
140 /// ```
141 /// use ito_core::errors::CoreError;
142 ///
143 /// let err = CoreError::sqlite("database locked");
144 /// let CoreError::Sqlite(msg) = err else {
145 /// panic!("expected Sqlite variant");
146 /// };
147 /// assert_eq!(msg, "database locked");
148 /// ```
149 pub fn sqlite(msg: impl Into<String>) -> Self {
150 Self::Sqlite(msg.into())
151 }
152
153 /// Build a typed unavailable-feature error.
154 pub fn feature_unavailable(
155 feature: CompiledFeature,
156 requested_by: impl Into<String>,
157 recovery: impl Into<String>,
158 ) -> Self {
159 Self::FeatureUnavailable {
160 feature,
161 requested_by: requested_by.into(),
162 recovery: recovery.into(),
163 }
164 }
165}
166
167#[cfg(test)]
168#[path = "errors_tests.rs"]
169mod errors_tests;