1use http::StatusCode;
15
16use crate::{MaybeSend, MaybeSync};
17use serde::{Deserialize, Serialize};
18use std::fmt::{Debug, Display};
19use thiserror::Error;
20
21#[cfg(feature = "utoipa")]
22use utoipa::ToSchema;
23
24pub trait CqrsErrorCode: Debug + Display + Clone + MaybeSend + MaybeSync + 'static {
44 fn domain() -> &'static str;
46
47 fn domain_prefix() -> u16;
50
51 fn error_index(&self) -> u16;
53
54 fn http_status(&self) -> StatusCode;
56
57 fn internal_code(&self) -> u16 {
60 Self::domain_prefix() * 1000 + self.error_index()
61 }
62
63 fn code_string(&self) -> String {
66 format!("{}_{}", Self::domain().to_uppercase(), self)
67 }
68
69 fn error(&self, message: impl Into<String>) -> CqrsError
71 where
72 Self: Sized,
73 {
74 CqrsError::from_code(self, message)
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
84#[cfg_attr(feature = "utoipa", derive(ToSchema))]
85#[serde(rename_all = "camelCase")]
86pub struct CqrsErrorData {
87 pub domain: String,
89
90 pub code: String,
92
93 pub internal_code: u16,
95
96 #[serde(skip)]
98 pub status: u16,
99
100 pub message: String,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub details: Option<serde_json::Value>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub request_id: Option<String>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(transparent)]
131pub struct CqrsError(Box<CqrsErrorData>);
132
133impl std::ops::Deref for CqrsError {
134 type Target = CqrsErrorData;
135 fn deref(&self) -> &CqrsErrorData {
136 &self.0
137 }
138}
139
140impl std::ops::DerefMut for CqrsError {
141 fn deref_mut(&mut self) -> &mut CqrsErrorData {
142 &mut self.0
143 }
144}
145
146#[cfg(feature = "utoipa")]
147impl utoipa::PartialSchema for CqrsError {
148 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
149 CqrsErrorData::schema()
150 }
151}
152
153#[cfg(feature = "utoipa")]
154impl utoipa::ToSchema for CqrsError {
155 fn name() -> std::borrow::Cow<'static, str> {
156 std::borrow::Cow::Borrowed("CqrsError")
157 }
158}
159
160impl CqrsError {
161 pub fn from_code<C: CqrsErrorCode>(code: &C, message: impl Into<String>) -> Self {
163 Self(Box::new(CqrsErrorData {
164 domain: C::domain().to_string(),
165 code: code.code_string(),
166 internal_code: code.internal_code(),
167 status: code.http_status().as_u16(),
168 message: message.into(),
169 details: None,
170 request_id: None,
171 }))
172 }
173
174 pub fn with_details(mut self, details: serde_json::Value) -> Self {
176 self.details = Some(details);
177 self
178 }
179
180 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
182 self.request_id = Some(request_id.into());
183 self
184 }
185
186 pub fn http_status(&self) -> StatusCode {
188 StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
189 }
190
191 pub fn not_found(message: impl Into<String>) -> Self {
197 GenericErrorCode::NotFound.error(message)
198 }
199
200 pub fn validation(message: impl Into<String>) -> Self {
202 GenericErrorCode::ValidationFailed.error(message)
203 }
204
205 pub fn internal(message: impl Into<String>) -> Self {
207 GenericErrorCode::InternalError.error(message)
208 }
209
210 pub fn conflict(message: impl Into<String>) -> Self {
212 GenericErrorCode::Conflict.error(message)
213 }
214
215 pub fn unauthorized(message: impl Into<String>) -> Self {
217 GenericErrorCode::Unauthorized.error(message)
218 }
219
220 pub fn forbidden(message: impl Into<String>) -> Self {
222 GenericErrorCode::Forbidden.error(message)
223 }
224
225 pub fn user_error(e: impl std::fmt::Display) -> Self {
231 InfrastructureErrorCode::DomainError.error(e.to_string())
232 }
233
234 pub fn database_error(e: impl std::fmt::Display) -> Self {
236 InfrastructureErrorCode::DatabaseError.error(e.to_string())
237 }
238
239 pub fn serialization_error(e: impl std::fmt::Display) -> Self {
241 InfrastructureErrorCode::SerializationError.error(e.to_string())
242 }
243
244 pub fn concurrency_error() -> Self {
246 InfrastructureErrorCode::ConcurrencyError.error("Version conflict")
247 }
248
249 pub fn aggregate_not_found(id: &str) -> Self {
251 InfrastructureErrorCode::AggregateNotFound.error(format!("Aggregate '{}' not found", id))
252 }
253
254 pub fn aggregate_already_exists(id: &str) -> Self {
256 InfrastructureErrorCode::Conflict.error(format!("Aggregate '{}' already exists", id))
257 }
258
259 pub fn from_status(status: StatusCode, message: impl Into<String>) -> Self {
261 GenericErrorCode::from(status).error(message)
262 }
263}
264
265impl Display for CqrsError {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 write!(
268 f,
269 "[{}] {}: {}",
270 self.internal_code, self.code, self.message
271 )
272 }
273}
274
275impl std::error::Error for CqrsError {}
276
277impl From<std::io::Error> for CqrsError {
278 fn from(e: std::io::Error) -> Self {
279 CqrsError::user_error(e)
280 }
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
292pub enum InfrastructureErrorCode {
293 #[error("INTERNAL_ERROR")]
294 InternalError,
295 #[error("VALIDATION_FAILED")]
296 ValidationFailed,
297 #[error("NOT_FOUND")]
298 NotFound,
299 #[error("CONFLICT")]
300 Conflict,
301 #[error("UNAUTHORIZED")]
302 Unauthorized,
303 #[error("FORBIDDEN")]
304 Forbidden,
305 #[error("GONE")]
306 Gone,
307 #[error("DATABASE_ERROR")]
308 DatabaseError,
309 #[error("SERIALIZATION_ERROR")]
310 SerializationError,
311 #[error("AGGREGATE_NOT_FOUND")]
312 AggregateNotFound,
313 #[error("CONCURRENCY_ERROR")]
314 ConcurrencyError,
315 #[error("DOMAIN_ERROR")]
316 DomainError,
317 #[error("CQRS_ERROR")]
318 CqrsInternalError,
319 #[error("CONFIGURATION_ERROR")]
320 ConfigurationError,
321 #[error("UNKNOWN")]
322 Unknown,
323}
324
325impl CqrsErrorCode for InfrastructureErrorCode {
326 fn domain() -> &'static str {
327 "infrastructure"
328 }
329 fn domain_prefix() -> u16 {
330 0
331 }
332
333 fn error_index(&self) -> u16 {
334 match self {
335 Self::InternalError => 0,
336 Self::ValidationFailed => 1,
337 Self::NotFound => 2,
338 Self::Conflict => 3,
339 Self::Unauthorized => 4,
340 Self::Forbidden => 5,
341 Self::Gone => 6,
342 Self::DatabaseError => 10,
343 Self::SerializationError => 11,
344 Self::AggregateNotFound => 12,
345 Self::ConcurrencyError => 13,
346 Self::DomainError => 14,
347 Self::CqrsInternalError => 15,
348 Self::ConfigurationError => 16,
349 Self::Unknown => 99,
350 }
351 }
352
353 fn http_status(&self) -> StatusCode {
354 match self {
355 Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
356 Self::ValidationFailed => StatusCode::BAD_REQUEST,
357 Self::NotFound | Self::AggregateNotFound => StatusCode::NOT_FOUND,
358 Self::Conflict | Self::ConcurrencyError => StatusCode::CONFLICT,
359 Self::Unauthorized => StatusCode::UNAUTHORIZED,
360 Self::Forbidden => StatusCode::FORBIDDEN,
361 Self::Gone => StatusCode::GONE,
362 Self::DatabaseError
363 | Self::SerializationError
364 | Self::CqrsInternalError
365 | Self::ConfigurationError
366 | Self::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
367 Self::DomainError => StatusCode::BAD_REQUEST,
368 }
369 }
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
380pub enum GenericErrorCode {
381 #[error("INTERNAL_ERROR")]
382 InternalError,
383 #[error("VALIDATION_FAILED")]
384 ValidationFailed,
385 #[error("NOT_FOUND")]
386 NotFound,
387 #[error("CONFLICT")]
388 Conflict,
389 #[error("UNAUTHORIZED")]
390 Unauthorized,
391 #[error("FORBIDDEN")]
392 Forbidden,
393 #[error("GONE")]
394 Gone,
395}
396
397impl CqrsErrorCode for GenericErrorCode {
398 fn domain() -> &'static str {
399 "generic"
400 }
401 fn domain_prefix() -> u16 {
402 1
403 }
404
405 fn error_index(&self) -> u16 {
406 match self {
407 Self::InternalError => 0,
408 Self::ValidationFailed => 1,
409 Self::NotFound => 2,
410 Self::Conflict => 3,
411 Self::Unauthorized => 4,
412 Self::Forbidden => 5,
413 Self::Gone => 6,
414 }
415 }
416
417 fn http_status(&self) -> StatusCode {
418 match self {
419 Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
420 Self::ValidationFailed => StatusCode::BAD_REQUEST,
421 Self::NotFound => StatusCode::NOT_FOUND,
422 Self::Conflict => StatusCode::CONFLICT,
423 Self::Unauthorized => StatusCode::UNAUTHORIZED,
424 Self::Forbidden => StatusCode::FORBIDDEN,
425 Self::Gone => StatusCode::GONE,
426 }
427 }
428}
429
430impl From<StatusCode> for GenericErrorCode {
431 fn from(status: StatusCode) -> Self {
432 match status.as_u16() {
433 400 => GenericErrorCode::ValidationFailed,
434 401 => GenericErrorCode::Unauthorized,
435 403 => GenericErrorCode::Forbidden,
436 404 => GenericErrorCode::NotFound,
437 409 => GenericErrorCode::Conflict,
438 410 => GenericErrorCode::Gone,
439 _ => GenericErrorCode::InternalError,
440 }
441 }
442}
443
444#[deprecated(since = "0.2.0", note = "Use CqrsError instead")]
449pub type AggregateError = CqrsError;
450
451#[macro_export]
479macro_rules! define_domain_errors {
480 (
481 domain: $domain:literal,
482 prefix: $prefix:expr,
483 errors: {
484 $( $variant:ident => ($index:expr, $status:expr, $display:literal) ),* $(,)?
485 }
486 ) => {
487 #[derive(Debug, Clone, Copy, PartialEq, Eq, ::thiserror::Error)]
489 pub enum ErrorCode {
490 $(
491 #[error($display)]
492 $variant,
493 )*
494 }
495
496 impl $crate::CqrsErrorCode for ErrorCode {
497 fn domain() -> &'static str { $domain }
498 fn domain_prefix() -> u16 { $prefix }
499
500 fn error_index(&self) -> u16 {
501 match self {
502 $( Self::$variant => $index, )*
503 }
504 }
505
506 fn http_status(&self) -> ::http::StatusCode {
507 match self {
508 $( Self::$variant => $status, )*
509 }
510 }
511 }
512 };
513}
514
515#[cfg(test)]
520mod tests {
521 use super::*;
522
523 #[test]
524 fn test_generic_error_code() {
525 let err = GenericErrorCode::NotFound.error("Resource not found");
526 assert_eq!(err.domain, "generic");
527 assert_eq!(err.code, "GENERIC_NOT_FOUND");
528 assert_eq!(err.internal_code, 1002);
529 assert_eq!(err.status, 404);
530 }
531
532 #[test]
533 fn test_infrastructure_error_code() {
534 let err = InfrastructureErrorCode::DatabaseError.error("Connection failed");
535 assert_eq!(err.domain, "infrastructure");
536 assert_eq!(err.code, "INFRASTRUCTURE_DATABASE_ERROR");
537 assert_eq!(err.internal_code, 10); assert_eq!(err.status, 500);
539 }
540
541 #[test]
542 fn test_convenience_constructors() {
543 let err = CqrsError::not_found("User not found");
544 assert_eq!(err.code, "GENERIC_NOT_FOUND");
545
546 let err = CqrsError::validation("Invalid email");
547 assert_eq!(err.code, "GENERIC_VALIDATION_FAILED");
548 }
549
550 #[test]
551 fn test_migration_constructors() {
552 let err = CqrsError::user_error("bad input");
553 assert_eq!(err.code, "INFRASTRUCTURE_DOMAIN_ERROR");
554 assert_eq!(err.status, 400);
555
556 let err = CqrsError::database_error("connection lost");
557 assert_eq!(err.code, "INFRASTRUCTURE_DATABASE_ERROR");
558 assert_eq!(err.status, 500);
559
560 let err = CqrsError::serialization_error("invalid json");
561 assert_eq!(err.code, "INFRASTRUCTURE_SERIALIZATION_ERROR");
562 assert_eq!(err.status, 500);
563
564 let err = CqrsError::concurrency_error();
565 assert_eq!(err.code, "INFRASTRUCTURE_CONCURRENCY_ERROR");
566 assert_eq!(err.status, 409);
567
568 let err = CqrsError::aggregate_not_found("abc");
569 assert_eq!(err.code, "INFRASTRUCTURE_AGGREGATE_NOT_FOUND");
570 assert_eq!(err.status, 404);
571 assert!(err.message.contains("abc"));
572
573 let err = CqrsError::aggregate_already_exists("xyz");
574 assert_eq!(err.code, "INFRASTRUCTURE_CONFLICT");
575 assert_eq!(err.status, 409);
576 assert!(err.message.contains("xyz"));
577 }
578
579 #[test]
580 fn test_with_details() {
581 let err = GenericErrorCode::NotFound
582 .error("User not found")
583 .with_details(serde_json::json!({"user_id": "123"}));
584
585 assert!(err.details.is_some());
586 assert_eq!(err.details.as_ref().unwrap()["user_id"], "123");
587 }
588
589 #[test]
590 fn test_serialization() {
591 let err = GenericErrorCode::Conflict.error("Already exists");
592 let json = serde_json::to_string(&err).unwrap();
593
594 assert!(json.contains("\"domain\":\"generic\""));
595 assert!(json.contains("\"code\":\"GENERIC_CONFLICT\""));
596 assert!(json.contains("\"internalCode\":1003"));
597 assert!(json.contains("\"message\":\"Already exists\""));
598 assert!(!json.contains("\"status\""));
600 }
601}