1use std::time::Duration;
2
3use serde::{
4 Deserialize,
5 Serialize,
6};
7use thiserror::Error;
8
9#[derive(Error, Debug, Clone)]
10pub enum DomainError {
11 #[error("Provider not found: {0}")]
12 ProviderNotFound(String),
13
14 #[error("Pipeline not found: {0}")]
15 PipelineNotFound(String),
16
17 #[error("Not found: {0}")]
18 NotFound(String),
19
20 #[error("Invalid configuration: {0}")]
21 InvalidConfig(String),
22
23 #[error("Authentication failed: {0}")]
24 AuthenticationFailed(String),
25
26 #[error("Authentication failed: {0}")]
27 ProviderAuthenticationFailed(String),
28
29 #[error("Permission denied: {0}")]
30 PermissionDenied(String),
31
32 #[error("API error: {0}")]
33 ApiError(String),
34
35 #[error("Database error: {0}")]
36 DatabaseError(String),
37
38 #[error("Invalid provider type: {0}")]
39 InvalidProviderType(String),
40
41 #[error("Provider error: {0}")]
42 ProviderError(String),
43
44 #[error("Not supported: {0}")]
45 NotSupported(String),
46
47 #[error("Internal error: {0}")]
48 InternalError(String),
49
50 #[error("Rate limited: {message}")]
51 RateLimited {
52 message: String,
53 retry_after: Option<Duration>,
54 },
55
56 #[error("Service unavailable: {message}")]
57 ServiceUnavailable {
58 message: String,
59 retry_after: Option<Duration>,
60 },
61
62 #[error("Timeout: {0}")]
63 Timeout(String),
64
65 #[error("TLS error: {0}")]
66 Tls(String),
67
68 #[error("Network error: {0}")]
69 NetworkError(String),
70
71 #[error("Lock error: {0}")]
72 LockError(String),
73
74 #[error("Concurrent modification: {0}")]
75 ConcurrentModification(String),
76
77 #[error("Configuration file not found - setup wizard required")]
78 ConfigNotFound,
79
80 #[error("Configuration file is invalid: {path} - {errors}")]
81 InvalidConfigFile { path: String, errors: String },
82
83 #[error("Application not initialized - run setup first")]
84 NotInitialized,
85
86 #[error(
87 "Migration failed at step '{step}': {reason}\n\nRECOVERY INSTRUCTIONS:\n{instructions}"
88 )]
89 MigrationFailed {
90 step: String,
91 reason: String,
92 instructions: String,
93 },
94
95 #[error("Data consistency error: {0}")]
96 DataConsistency(String),
97}
98
99pub type DomainResult<T> = Result<T, DomainError>;
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum FailureKind {
104 Network,
105 Timeout,
106 Tls,
107 RateLimited,
108 Unavailable,
109 Auth,
110 Permission,
111 Config,
112 NotFound,
113 Other,
114}
115
116impl FailureKind {
117 pub fn is_transient(self) -> bool {
118 matches!(
119 self,
120 Self::Network | Self::Timeout | Self::Unavailable | Self::RateLimited
121 )
122 }
123}
124
125impl DomainError {
126 pub fn failure_kind(&self) -> FailureKind {
127 match self {
128 Self::NetworkError(_) => FailureKind::Network,
129 Self::Timeout(_) => FailureKind::Timeout,
130 Self::Tls(_) => FailureKind::Tls,
131 Self::RateLimited { .. } => FailureKind::RateLimited,
132 Self::ServiceUnavailable { .. } => FailureKind::Unavailable,
133 Self::AuthenticationFailed(_) | Self::ProviderAuthenticationFailed(_) => {
134 FailureKind::Auth
135 }
136 Self::PermissionDenied(_) => FailureKind::Permission,
137 Self::InvalidConfig(_)
138 | Self::InvalidProviderType(_)
139 | Self::InvalidConfigFile { .. }
140 | Self::ConfigNotFound => FailureKind::Config,
141 Self::ProviderNotFound(_) | Self::PipelineNotFound(_) | Self::NotFound(_) => {
142 FailureKind::NotFound
143 }
144 _ => FailureKind::Other,
145 }
146 }
147
148 pub fn retry_after(&self) -> Option<Duration> {
149 match self {
150 Self::RateLimited { retry_after, .. }
151 | Self::ServiceUnavailable { retry_after, .. } => *retry_after,
152 _ => None,
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn failure_kind_classifies_transient_errors() {
163 let transient = [
164 DomainError::NetworkError("x".into()),
165 DomainError::Timeout("x".into()),
166 DomainError::ServiceUnavailable {
167 message: "x".into(),
168 retry_after: None,
169 },
170 DomainError::RateLimited {
171 message: "x".into(),
172 retry_after: Some(Duration::from_secs(5)),
173 },
174 ];
175 for err in transient {
176 assert!(err.failure_kind().is_transient(), "{err:?}");
177 }
178
179 let permanent = [
180 (DomainError::Tls("x".into()), FailureKind::Tls),
181 (
182 DomainError::ProviderAuthenticationFailed("x".into()),
183 FailureKind::Auth,
184 ),
185 (
186 DomainError::PermissionDenied("x".into()),
187 FailureKind::Permission,
188 ),
189 (DomainError::InvalidConfig("x".into()), FailureKind::Config),
190 (
191 DomainError::PipelineNotFound("x".into()),
192 FailureKind::NotFound,
193 ),
194 (DomainError::ApiError("x".into()), FailureKind::Other),
195 ];
196 for (err, kind) in permanent {
197 assert_eq!(err.failure_kind(), kind);
198 assert!(!kind.is_transient());
199 }
200 }
201}