evidentsource_core/domain/
error.rs1use thiserror::Error;
7
8#[derive(Error, Debug, Clone, PartialEq)]
10pub enum IdentifierError {
11 #[error("invalid database name '{0}'")]
12 DatabaseName(String),
13
14 #[error("invalid stream name: {0:?}")]
15 StreamName(Option<String>),
16
17 #[error("invalid event id: {0}")]
18 EventId(String),
19
20 #[error("invalid event type: {0:?}")]
21 EventType(Option<String>),
22
23 #[error("invalid event subject: {0}")]
24 EventSubject(String),
25
26 #[error("invalid state view name '{0}'")]
27 StateViewName(String),
28
29 #[error("invalid state change name '{0}'")]
30 StateChangeName(String),
31}
32
33#[derive(Error, Debug, Clone, PartialEq)]
35pub enum ConstraintError {
36 #[error("blank constraint")]
37 BlankConstraint,
38
39 #[error("invalid range: min ({min}) must be <= max ({max})")]
40 InvalidRange { min: u64, max: u64 },
41
42 #[error("constraint conflict: {left} vs {right}")]
43 Conflict { left: String, right: String },
44
45 #[error("cannot combine constraints with different selectors")]
46 IncompatibleSelectors,
47
48 #[error(transparent)]
49 Identifier(#[from] IdentifierError),
50}
51
52#[derive(Error, Debug, Clone, PartialEq)]
54pub enum EventError {
55 #[error("invalid event source: {0}")]
56 InvalidSource(String),
57
58 #[error("duplicate event id: {stream}/{event_id}")]
59 DuplicateEventId { stream: String, event_id: String },
60
61 #[error(transparent)]
62 Identifier(#[from] IdentifierError),
63}
64
65#[derive(Error, Debug, Clone)]
67pub enum TransactionError {
68 #[error("transaction must contain at least one event")]
69 Empty,
70
71 #[error("invalid transaction ID: {0}")]
72 InvalidId(String),
73
74 #[error("transaction too large: {actual} events, max {max}")]
75 TooLarge { actual: usize, max: usize },
76
77 #[error("duplicate transaction id: {0}")]
78 DuplicateId(String),
79
80 #[error("transaction contains {} invalid events", .0.len())]
81 InvalidEvents(Vec<EventError>),
82
83 #[error("transaction has {} invalid constraints", .0.len())]
84 InvalidConstraints(Vec<ConstraintError>),
85
86 #[error("transaction has {} constraint violations", .0.len())]
87 ConstraintViolations(Vec<String>),
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct OperationContext {
109 pub operation: String,
111 pub database: String,
113 pub details: Option<String>,
115}
116
117impl OperationContext {
118 pub fn new(operation: impl Into<String>, database: impl Into<String>) -> Self {
120 Self {
121 operation: operation.into(),
122 database: database.into(),
123 details: None,
124 }
125 }
126
127 pub fn with_details(mut self, details: impl Into<String>) -> Self {
129 self.details = Some(details.into());
130 self
131 }
132}
133
134impl std::fmt::Display for OperationContext {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 write!(f, "{}(db={})", self.operation, self.database)?;
137 if let Some(ref details) = self.details {
138 write!(f, " [{}]", details)?;
139 }
140 Ok(())
141 }
142}
143
144#[derive(Error, Debug, Clone)]
146pub enum DatabaseError {
147 #[error("database '{0}' not found")]
148 NotFound(String),
149
150 #[error("database '{0}' already exists")]
151 AlreadyExists(String),
152
153 #[error("concurrent write collision")]
154 ConcurrentWriteCollision,
155
156 #[error("operation timed out")]
157 Timeout,
158
159 #[error("server error: {0}")]
160 ServerError(String),
161
162 #[error("{context}: {source}")]
163 WithContext {
164 context: OperationContext,
165 #[source]
166 source: Box<DatabaseError>,
167 },
168
169 #[error(transparent)]
170 Identifier(#[from] IdentifierError),
171
172 #[error(transparent)]
173 Transaction(#[from] TransactionError),
174}
175
176impl DatabaseError {
177 pub fn with_context(self, context: OperationContext) -> Self {
193 DatabaseError::WithContext {
194 context,
195 source: Box::new(self),
196 }
197 }
198
199 pub fn inner(&self) -> &DatabaseError {
201 match self {
202 DatabaseError::WithContext { source, .. } => source.inner(),
203 _ => self,
204 }
205 }
206}
207
208#[derive(Error, Debug, Clone)]
210pub enum StateViewError {
211 #[error("state view {name}.v{version} not found")]
212 NotFound { name: String, version: u64 },
213
214 #[error("error evolving state view: {0}")]
215 EvolveError(String),
216
217 #[error("server error: {0}")]
218 ServerError(String),
219
220 #[error(transparent)]
221 Identifier(#[from] IdentifierError),
222}
223
224#[derive(Error, Debug, Clone)]
226pub enum StateChangeError {
227 #[error("state change {name}.v{version} not found")]
228 NotFound { name: String, version: u64 },
229
230 #[error("execution error: {0}")]
231 ExecutionError(String),
232
233 #[error("server error: {0}")]
234 ServerError(String),
235
236 #[error(transparent)]
237 Identifier(#[from] IdentifierError),
238
239 #[error(transparent)]
240 Database(#[from] DatabaseError),
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_identifier_error_display() {
249 let err = IdentifierError::DatabaseName("bad-name!".to_string());
250 assert_eq!(err.to_string(), "invalid database name 'bad-name!'");
251 }
252
253 #[test]
254 fn test_constraint_error_from_identifier() {
255 let id_err = IdentifierError::StreamName(Some("bad".to_string()));
256 let constraint_err: ConstraintError = id_err.into();
257 assert!(matches!(constraint_err, ConstraintError::Identifier(_)));
258 }
259
260 #[test]
261 fn test_database_error_from_transaction() {
262 let txn_err = TransactionError::Empty;
263 let db_err: DatabaseError = txn_err.into();
264 assert!(matches!(db_err, DatabaseError::Transaction(_)));
265 }
266
267 #[test]
268 fn test_operation_context_display() {
269 let ctx = OperationContext::new("transact", "my-db");
270 assert_eq!(ctx.to_string(), "transact(db=my-db)");
271
272 let ctx_with_details = OperationContext::new("execute_state_change", "my-db")
273 .with_details("create-account.v1");
274 assert_eq!(
275 ctx_with_details.to_string(),
276 "execute_state_change(db=my-db) [create-account.v1]"
277 );
278 }
279
280 #[test]
281 fn test_database_error_with_context() {
282 let err = DatabaseError::NotFound("my-db".to_string());
283 let ctx = OperationContext::new("transact", "my-db");
284 let with_ctx = err.with_context(ctx);
285
286 let msg = with_ctx.to_string();
287 assert!(msg.contains("transact"));
288 assert!(msg.contains("my-db"));
289 assert!(msg.contains("not found"));
290 }
291
292 #[test]
293 fn test_database_error_inner() {
294 let err = DatabaseError::NotFound("my-db".to_string());
295 let ctx = OperationContext::new("transact", "my-db");
296 let with_ctx = err.with_context(ctx);
297
298 let inner = with_ctx.inner();
300 assert!(matches!(inner, DatabaseError::NotFound(_)));
301 }
302
303 #[test]
304 fn test_database_error_nested_context() {
305 let err = DatabaseError::NotFound("my-db".to_string());
306 let ctx1 = OperationContext::new("inner_op", "my-db");
307 let ctx2 = OperationContext::new("outer_op", "my-db");
308 let nested = err.with_context(ctx1).with_context(ctx2);
309
310 let inner = nested.inner();
312 assert!(matches!(inner, DatabaseError::NotFound(_)));
313 }
314}