Skip to main content

rust_ef/
error.rs

1//! Error types for Rust Entity Framework (rust-ef).
2//!
3//! Each variant carries an optional `#[source]` for preserving the original
4//! error chain, enabling programmatic inspection via `std::error::Error::source()`.
5//! Convenience constructors (`EFError::query(msg)`, etc.) keep call sites concise;
6//! `with_source` and `with_sql` builders attach context after construction.
7
8use thiserror::Error;
9
10/// Boxed error suitable for `EFError::source`.
11pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
12
13/// Represents all possible errors that can occur in rust-ef operations.
14#[derive(Error, Debug)]
15pub enum EFError {
16    /// Database connection error.
17    #[error("database connection error: {0}")]
18    Connection(String, #[source] Option<BoxError>),
19
20    /// Query execution error.
21    #[error("query error: {0}")]
22    Query(String, #[source] Option<BoxError>),
23
24    /// Entity not found error.
25    #[error("entity not found: {0}")]
26    NotFound(String, #[source] Option<BoxError>),
27
28    /// Model validation error.
29    #[error("model validation error: {0}")]
30    ModelValidation(String, #[source] Option<BoxError>),
31
32    /// Migration error.
33    #[error("migration error: {0}")]
34    Migration(String, #[source] Option<BoxError>),
35
36    /// Provider-specific error.
37    #[error("provider error: {0}")]
38    Provider(String, #[source] Option<BoxError>),
39
40    /// Configuration error.
41    #[error("configuration error: {0}")]
42    Configuration(String, #[source] Option<BoxError>),
43
44    /// Change tracking error.
45    #[error("change tracking error: {0}")]
46    ChangeTracking(String, #[source] Option<BoxError>),
47
48    /// Transaction error.
49    #[error("transaction error: {0}")]
50    Transaction(String, #[source] Option<BoxError>),
51
52    /// Concurrency conflict error.
53    #[error("concurrency conflict: {0}")]
54    ConcurrencyConflict(String, #[source] Option<BoxError>),
55
56    /// Type conversion error.
57    #[error("type conversion error: {0}")]
58    TypeConversion(String, #[source] Option<BoxError>),
59
60    /// General / unknown error.
61    #[error("{0}")]
62    Other(String, #[source] Option<BoxError>),
63}
64
65/// Stable, programmatic error code for `EFError` classification.
66///
67/// Unlike the `EFError` enum (which may gain variants over time), `EFErrorCode`
68/// is a stable, `Copy` taxonomy that callers can match on for retry logic,
69/// metrics, or user-facing routing. Sub-categories (e.g. `ConnectionTimeout`
70/// vs `ConnectionRefused`) are derived from message patterns within each
71/// `EFError` variant — see [`EFError::code`].
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub enum EFErrorCode {
74    ConnectionRefused,
75    ConnectionTimeout,
76    QuerySyntax,
77    QueryTimeout,
78    EntityNotFound,
79    ModelValidation,
80    MigrationConflict,
81    ProviderUnsupported,
82    ConfigurationInvalid,
83    ChangeTrackingCorrupted,
84    TransactionAborted,
85    TransactionDeadlock,
86    ConcurrencyConflict,
87    TypeConversionFailed,
88    Unknown,
89}
90
91impl EFError {
92    pub fn connection(msg: impl Into<String>) -> Self {
93        EFError::Connection(msg.into(), None)
94    }
95
96    pub fn query(msg: impl Into<String>) -> Self {
97        EFError::Query(msg.into(), None)
98    }
99
100    pub fn not_found(msg: impl Into<String>) -> Self {
101        EFError::NotFound(msg.into(), None)
102    }
103
104    pub fn model_validation(msg: impl Into<String>) -> Self {
105        EFError::ModelValidation(msg.into(), None)
106    }
107
108    pub fn migration(msg: impl Into<String>) -> Self {
109        EFError::Migration(msg.into(), None)
110    }
111
112    pub fn provider(msg: impl Into<String>) -> Self {
113        EFError::Provider(msg.into(), None)
114    }
115
116    pub fn configuration(msg: impl Into<String>) -> Self {
117        EFError::Configuration(msg.into(), None)
118    }
119
120    pub fn change_tracking(msg: impl Into<String>) -> Self {
121        EFError::ChangeTracking(msg.into(), None)
122    }
123
124    pub fn transaction(msg: impl Into<String>) -> Self {
125        EFError::Transaction(msg.into(), None)
126    }
127
128    pub fn concurrency_conflict(msg: impl Into<String>) -> Self {
129        EFError::ConcurrencyConflict(msg.into(), None)
130    }
131
132    pub fn type_conversion(msg: impl Into<String>) -> Self {
133        EFError::TypeConversion(msg.into(), None)
134    }
135
136    pub fn other(msg: impl Into<String>) -> Self {
137        EFError::Other(msg.into(), None)
138    }
139
140    /// Attaches a source error to this `EFError`, preserving the error chain.
141    pub fn with_source(self, source: BoxError) -> Self {
142        match self {
143            EFError::Connection(m, _) => EFError::Connection(m, Some(source)),
144            EFError::Query(m, _) => EFError::Query(m, Some(source)),
145            EFError::NotFound(m, _) => EFError::NotFound(m, Some(source)),
146            EFError::ModelValidation(m, _) => EFError::ModelValidation(m, Some(source)),
147            EFError::Migration(m, _) => EFError::Migration(m, Some(source)),
148            EFError::Provider(m, _) => EFError::Provider(m, Some(source)),
149            EFError::Configuration(m, _) => EFError::Configuration(m, Some(source)),
150            EFError::ChangeTracking(m, _) => EFError::ChangeTracking(m, Some(source)),
151            EFError::Transaction(m, _) => EFError::Transaction(m, Some(source)),
152            EFError::ConcurrencyConflict(m, _) => EFError::ConcurrencyConflict(m, Some(source)),
153            EFError::TypeConversion(m, _) => EFError::TypeConversion(m, Some(source)),
154            EFError::Other(m, _) => EFError::Other(m, Some(source)),
155        }
156    }
157
158    /// Extracts the human-readable message without the source chain.
159    pub fn message(&self) -> &str {
160        match self {
161            EFError::Connection(m, _) => m,
162            EFError::Query(m, _) => m,
163            EFError::NotFound(m, _) => m,
164            EFError::ModelValidation(m, _) => m,
165            EFError::Migration(m, _) => m,
166            EFError::Provider(m, _) => m,
167            EFError::Configuration(m, _) => m,
168            EFError::ChangeTracking(m, _) => m,
169            EFError::Transaction(m, _) => m,
170            EFError::ConcurrencyConflict(m, _) => m,
171            EFError::TypeConversion(m, _) => m,
172            EFError::Other(m, _) => m,
173        }
174    }
175
176    /// Returns a stable [`EFErrorCode`] for programmatic dispatch.
177    ///
178    /// Sub-categories within a variant are detected via case-insensitive
179    /// message patterns (e.g. `"timeout"` → `ConnectionTimeout`). Messages
180    /// are stable internal strings produced by rust-ef's own call sites;
181    /// user-supplied messages from [`EFError::other`] fall back to `Unknown`.
182    pub fn code(&self) -> EFErrorCode {
183        let msg = self.message().to_ascii_lowercase();
184        match self {
185            EFError::Connection(_, _) => {
186                if msg.contains("timeout") {
187                    EFErrorCode::ConnectionTimeout
188                } else {
189                    EFErrorCode::ConnectionRefused
190                }
191            }
192            EFError::Query(_, _) => {
193                if msg.contains("timeout") {
194                    EFErrorCode::QueryTimeout
195                } else {
196                    EFErrorCode::QuerySyntax
197                }
198            }
199            EFError::NotFound(_, _) => EFErrorCode::EntityNotFound,
200            EFError::ModelValidation(_, _) => EFErrorCode::ModelValidation,
201            EFError::Migration(_, _) => EFErrorCode::MigrationConflict,
202            EFError::Provider(_, _) => EFErrorCode::ProviderUnsupported,
203            EFError::Configuration(_, _) => EFErrorCode::ConfigurationInvalid,
204            EFError::ChangeTracking(_, _) => EFErrorCode::ChangeTrackingCorrupted,
205            EFError::Transaction(_, _) => {
206                if msg.contains("deadlock") {
207                    EFErrorCode::TransactionDeadlock
208                } else {
209                    EFErrorCode::TransactionAborted
210                }
211            }
212            EFError::ConcurrencyConflict(_, _) => EFErrorCode::ConcurrencyConflict,
213            EFError::TypeConversion(_, _) => EFErrorCode::TypeConversionFailed,
214            EFError::Other(_, _) => EFErrorCode::Unknown,
215        }
216    }
217}
218
219/// Result type alias for rust-ef operations.
220pub type EFResult<T> = Result<T, EFError>;