Skip to main content

max / goingson

5.0 KB · 177 lines History Blame Raw
1 //! Core error types and structured error handling.
2
3 use thiserror::Error;
4
5 /// Core error type for the application.
6 ///
7 /// Provides structured error handling with context preservation and source chaining.
8 #[derive(Debug, Error)]
9 pub enum CoreError {
10 /// Database error with optional source for error chaining.
11 #[error("Database error: {message}")]
12 Database {
13 message: String,
14 #[source]
15 source: Option<Box<dyn std::error::Error + Send + Sync>>,
16 },
17
18 /// Resource not found with type and identifier context.
19 #[error("Not found: {resource} with id {id}")]
20 NotFound {
21 resource: &'static str,
22 id: String,
23 },
24
25 /// Validation error with field and message context.
26 #[error("Validation error: {field} - {message}")]
27 Validation {
28 field: &'static str,
29 message: String,
30 },
31
32 /// Parse error for malformed input.
33 #[error("Parse error: {0}")]
34 Parse(String),
35
36 /// Bad request (generic invalid input).
37 #[error("Bad request: {0}")]
38 BadRequest(String),
39
40 /// Internal server error.
41 #[error("Internal error: {0}")]
42 Internal(String),
43
44 /// Authentication error.
45 #[error("Authentication error: {0}")]
46 Auth(String),
47
48 /// Remote sync operation failed (push, pull, device registration).
49 #[error("Sync error: {0}")]
50 Sync(String),
51 }
52
53 impl CoreError {
54 /// Creates a database error from any error type that implements Error + Send + Sync.
55 pub fn database(err: impl std::error::Error + Send + Sync + 'static) -> Self {
56 CoreError::Database {
57 message: err.to_string(),
58 source: Some(Box::new(err)),
59 }
60 }
61
62 /// Creates a database error from a string message.
63 pub fn database_msg(msg: impl Into<String>) -> Self {
64 CoreError::Database {
65 message: msg.into(),
66 source: None,
67 }
68 }
69
70 /// Creates a not-found error with resource type and identifier.
71 pub fn not_found(resource: &'static str, id: impl ToString) -> Self {
72 CoreError::NotFound {
73 resource,
74 id: id.to_string(),
75 }
76 }
77
78 /// Creates a validation error for a specific field.
79 pub fn validation(field: &'static str, message: impl Into<String>) -> Self {
80 CoreError::Validation {
81 field,
82 message: message.into(),
83 }
84 }
85
86 /// Creates a parse error.
87 pub fn parse(msg: impl Into<String>) -> Self {
88 CoreError::Parse(msg.into())
89 }
90
91 /// Creates a bad request error.
92 pub fn bad_request(msg: impl Into<String>) -> Self {
93 CoreError::BadRequest(msg.into())
94 }
95
96 /// Creates an internal error.
97 pub fn internal(msg: impl Into<String>) -> Self {
98 CoreError::Internal(msg.into())
99 }
100
101 /// Creates an authentication error.
102 pub fn auth(msg: impl Into<String>) -> Self {
103 CoreError::Auth(msg.into())
104 }
105
106 /// Creates a sync error.
107 pub fn sync(msg: impl Into<String>) -> Self {
108 CoreError::Sync(msg.into())
109 }
110
111 /// Returns true if this is a not-found error.
112 pub fn is_not_found(&self) -> bool {
113 matches!(self, CoreError::NotFound { .. })
114 }
115
116 /// Returns true if this is a validation error.
117 pub fn is_validation(&self) -> bool {
118 matches!(self, CoreError::Validation { .. })
119 }
120
121 /// Returns true if this is a database error.
122 pub fn is_database(&self) -> bool {
123 matches!(self, CoreError::Database { .. })
124 }
125 }
126
127 #[cfg(test)]
128 mod tests {
129 use super::*;
130
131 #[test]
132 fn database_constructor_preserves_message() {
133 let err = CoreError::database_msg("connection refused");
134 assert!(err.to_string().contains("connection refused"));
135 assert!(err.is_database());
136 }
137
138 #[test]
139 fn not_found_constructor_formats_resource_and_id() {
140 let err = CoreError::not_found("Task", "abc-123");
141 let msg = err.to_string();
142 assert!(msg.contains("Task"));
143 assert!(msg.contains("abc-123"));
144 assert!(err.is_not_found());
145 }
146
147 #[test]
148 fn validation_constructor_includes_field_and_message() {
149 let err = CoreError::validation("name", "too long");
150 let msg = err.to_string();
151 assert!(msg.contains("name"));
152 assert!(msg.contains("too long"));
153 assert!(err.is_validation());
154 }
155
156 #[test]
157 fn is_not_found_false_for_other_variants() {
158 assert!(!CoreError::database_msg("fail").is_not_found());
159 assert!(!CoreError::parse("bad").is_not_found());
160 assert!(!CoreError::internal("oops").is_not_found());
161 }
162
163 #[test]
164 fn is_validation_false_for_other_variants() {
165 assert!(!CoreError::not_found("Task", "1").is_validation());
166 assert!(!CoreError::bad_request("nope").is_validation());
167 assert!(!CoreError::auth("denied").is_validation());
168 }
169
170 #[test]
171 fn is_database_false_for_other_variants() {
172 assert!(!CoreError::not_found("Task", "1").is_database());
173 assert!(!CoreError::validation("x", "y").is_database());
174 assert!(!CoreError::internal("fail").is_database());
175 }
176 }
177