axum_gate/repositories/
errors.rs

1//! Repository- and database-category native errors.
2//!
3//! This module defines category-native error types for repositories and databases,
4//! used directly in handlers, services, and repositories when dealing with
5//! repository or database failures.
6//!
7//! # Overview
8//!
9//! - `RepositoriesError`: repository contract/operation failures (by repository type)
10//! - `DatabaseError`: database operation failures (by database operation)
11//! - `RepositoryType`: identifies the repository domain
12//! - `RepositoryOperation`: CRUD-like repository operations
13//! - `DatabaseOperation`: common database operations
14//!
15//! # Examples
16//!
17//! ```rust
18//! use axum_gate::repositories::{RepositoriesError, RepositoryType, RepositoryOperation};
19//! use axum_gate::errors::UserFriendlyError;
20//!
21//! let err = RepositoriesError::operation_failed(
22//!     RepositoryType::Account,
23//!     RepositoryOperation::Insert,
24//!     "unique constraint violation on `user_id`",
25//!     Some("user-123".into()),
26//!     Some("insert_account".into()),
27//! );
28//!
29//! assert!(err.developer_message().contains("account repository"));
30//! assert!(err.support_code().starts_with("REPO-ACCOUNT-INSERT-"));
31//! ```
32//!
33//! ```rust
34//! use axum_gate::repositories::{DatabaseError, DatabaseOperation};
35//! use axum_gate::errors::UserFriendlyError;
36//!
37//! let err = DatabaseError::with_context(
38//!     DatabaseOperation::Query,
39//!     "connection refused",
40//!     Some("accounts".into()),
41//!     None
42//! );
43//! assert!(matches!(err.severity(), axum_gate::errors::ErrorSeverity::Error));
44//! assert!(err.user_message().contains("data services"));
45//! ```
46
47use crate::errors::{ErrorSeverity, UserFriendlyError};
48use std::collections::hash_map::DefaultHasher;
49use std::fmt;
50use std::hash::{Hash, Hasher};
51use thiserror::Error;
52
53/// Repository type identifiers, aligned with this crate's DDD categories.
54#[derive(Debug, Clone)]
55pub enum RepositoryType {
56    /// Account repository
57    Account,
58    /// Secret repository
59    Secret,
60    /// Permission mapping repository
61    PermissionMapping,
62}
63
64impl fmt::Display for RepositoryType {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            RepositoryType::Account => write!(f, "account"),
68            RepositoryType::Secret => write!(f, "secret"),
69            RepositoryType::PermissionMapping => write!(f, "permission_mapping"),
70        }
71    }
72}
73
74/// Repository operation identifiers for structured reporting.
75#[derive(Debug, Clone)]
76pub enum RepositoryOperation {
77    /// Insert/create operation
78    Insert,
79    /// Fetch single record by key
80    Get,
81    /// Update/patch existing record
82    Update,
83    /// Delete/remove record
84    Delete,
85}
86
87impl fmt::Display for RepositoryOperation {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        match self {
90            RepositoryOperation::Insert => write!(f, "insert"),
91            RepositoryOperation::Get => write!(f, "get"),
92            RepositoryOperation::Update => write!(f, "update"),
93            RepositoryOperation::Delete => write!(f, "delete"),
94        }
95    }
96}
97
98/// Repository-category native errors.
99#[derive(Debug, Error)]
100#[non_exhaustive]
101pub enum RepositoriesError {
102    /// Repository operation failure (contract or adapter issue).
103    #[error("Repository error: {repository} {operation} - {message}")]
104    OperationFailed {
105        /// Type of repository involved.
106        repository: RepositoryType,
107        /// Operation that failed.
108        operation: RepositoryOperation,
109        /// Description of the failure (non-sensitive).
110        message: String,
111        /// Optional logical key or identifier (sanitized).
112        key: Option<String>,
113        /// Additional context (non-sensitive).
114        context: Option<String>,
115    },
116
117    /// A requested entity was not found in the repository.
118    #[error("Repository not found: {repository} - {key:?}")]
119    NotFound {
120        /// Type of repository involved.
121        repository: RepositoryType,
122        /// Optional logical key or identifier (sanitized).
123        key: Option<String>,
124    },
125
126    /// A constraint (uniqueness/foreign key) or precondition failed.
127    #[error("Repository constraint: {repository} - {message}")]
128    Constraint {
129        /// Type of repository involved.
130        repository: RepositoryType,
131        /// Description of the constraint failure (non-sensitive).
132        message: String,
133        /// Optional logical key or identifier (sanitized).
134        key: Option<String>,
135    },
136}
137
138impl RepositoriesError {
139    /// Construct an operation failure.
140    pub fn operation_failed(
141        repository: RepositoryType,
142        operation: RepositoryOperation,
143        message: impl Into<String>,
144        key: Option<String>,
145        context: Option<String>,
146    ) -> Self {
147        RepositoriesError::OperationFailed {
148            repository,
149            operation,
150            message: message.into(),
151            key,
152            context,
153        }
154    }
155
156    /// Construct a not found error.
157    pub fn not_found(repository: RepositoryType, key: Option<String>) -> Self {
158        RepositoriesError::NotFound { repository, key }
159    }
160
161    /// Construct a constraint/precondition failure.
162    pub fn constraint(
163        repository: RepositoryType,
164        message: impl Into<String>,
165        key: Option<String>,
166    ) -> Self {
167        RepositoriesError::Constraint {
168            repository,
169            message: message.into(),
170            key,
171        }
172    }
173
174    fn support_code_inner(&self) -> String {
175        let mut hasher = DefaultHasher::new();
176        match self {
177            RepositoriesError::OperationFailed {
178                repository,
179                operation,
180                key,
181                ..
182            } => {
183                format!(
184                    "REPO-{}-{}-{:X}",
185                    repository.to_string().to_uppercase(),
186                    operation.to_string().to_uppercase(),
187                    {
188                        format!("{:?}{:?}", repository, key).hash(&mut hasher);
189                        hasher.finish() % 10000
190                    }
191                )
192            }
193            RepositoriesError::NotFound { repository, key } => {
194                format!(
195                    "REPO-{}-NOTFOUND-{:X}",
196                    repository.to_string().to_uppercase(),
197                    {
198                        format!("{:?}{:?}", repository, key).hash(&mut hasher);
199                        hasher.finish() % 10000
200                    }
201                )
202            }
203            RepositoriesError::Constraint {
204                repository, key, ..
205            } => {
206                format!(
207                    "REPO-{}-CONSTRAINT-{:X}",
208                    repository.to_string().to_uppercase(),
209                    {
210                        format!("{:?}{:?}", repository, key).hash(&mut hasher);
211                        hasher.finish() % 10000
212                    }
213                )
214            }
215        }
216    }
217}
218
219impl UserFriendlyError for RepositoriesError {
220    fn user_message(&self) -> String {
221        match self {
222            RepositoriesError::OperationFailed { repository, .. } => match repository {
223                RepositoryType::Account => "We're having trouble accessing your account information. Please try refreshing the page or signing in again.".to_string(),
224                RepositoryType::Secret => "There's an issue with the security system. Please try again or contact support if the problem continues.".to_string(),
225                RepositoryType::PermissionMapping => "We're having trouble with the permission system. Your permissions are still active, but some features might not display correctly.".to_string(),
226            },
227            RepositoriesError::NotFound { repository, .. } => match repository {
228                RepositoryType::Account => "We couldn't find an account with the requested identifier.".to_string(),
229                RepositoryType::Secret => "We couldn't find the requested security information.".to_string(),
230                RepositoryType::PermissionMapping => "We couldn't find the requested permission mapping.".to_string(),
231            },
232            RepositoriesError::Constraint { repository, .. } => match repository {
233                RepositoryType::Account => "We couldn't complete this request due to an account constraint. Please review your input and try again.".to_string(),
234                RepositoryType::Secret => "We couldn't complete this request due to a security constraint. Please try again later.".to_string(),
235                RepositoryType::PermissionMapping => "We couldn't update the permission mapping due to a constraint. Your permissions remain unchanged.".to_string(),
236            },
237        }
238    }
239
240    fn developer_message(&self) -> String {
241        match self {
242            RepositoriesError::OperationFailed {
243                repository,
244                operation,
245                message,
246                key,
247                context,
248            } => {
249                let key_s = key
250                    .as_ref()
251                    .map(|k| format!(" [Key: {}]", k))
252                    .unwrap_or_default();
253                let ctx_s = context
254                    .as_ref()
255                    .map(|c| format!(" [Context: {}]", c))
256                    .unwrap_or_default();
257                format!(
258                    "Repository operation failed in {} repository ({}): {}{}{}",
259                    repository, operation, message, key_s, ctx_s
260                )
261            }
262            RepositoriesError::NotFound { repository, key } => {
263                let key_s = key
264                    .as_ref()
265                    .map(|k| format!(" [Key: {}]", k))
266                    .unwrap_or_default();
267                format!(
268                    "Repository entity not found in {} repository.{}",
269                    repository, key_s
270                )
271            }
272            RepositoriesError::Constraint {
273                repository,
274                message,
275                key,
276            } => {
277                let key_s = key
278                    .as_ref()
279                    .map(|k| format!(" [Key: {}]", k))
280                    .unwrap_or_default();
281                format!(
282                    "Repository constraint violation in {} repository: {}{}",
283                    repository, message, key_s
284                )
285            }
286        }
287    }
288
289    fn support_code(&self) -> String {
290        self.support_code_inner()
291    }
292
293    fn severity(&self) -> ErrorSeverity {
294        match self {
295            RepositoriesError::OperationFailed {
296                repository,
297                operation,
298                ..
299            } => match (repository, operation) {
300                (RepositoryType::Secret, _) => ErrorSeverity::Critical,
301                (RepositoryType::Account, RepositoryOperation::Delete) => ErrorSeverity::Critical,
302                _ => ErrorSeverity::Error,
303            },
304            RepositoriesError::NotFound { repository, .. } => match repository {
305                RepositoryType::Account => ErrorSeverity::Warning,
306                _ => ErrorSeverity::Info,
307            },
308            RepositoriesError::Constraint { repository, .. } => match repository {
309                RepositoryType::Account | RepositoryType::Secret => ErrorSeverity::Error,
310                _ => ErrorSeverity::Warning,
311            },
312        }
313    }
314
315    fn suggested_actions(&self) -> Vec<String> {
316        match self {
317            RepositoriesError::OperationFailed {
318                repository,
319                operation,
320                ..
321            } => match (repository, operation) {
322                (RepositoryType::Account, RepositoryOperation::Insert) => vec![
323                    "Ensure the account identifier is unique".to_string(),
324                    "Verify required fields are provided".to_string(),
325                    "Try your request again".to_string(),
326                    "Contact support if the problem persists".to_string(),
327                ],
328                (RepositoryType::Secret, _) => vec![
329                    "Do not retry password or secret operations repeatedly".to_string(),
330                    "Contact support if security operations continue to fail".to_string(),
331                ],
332                _ => vec![
333                    "Try your request again in a moment".to_string(),
334                    "Refresh the page and attempt the operation again".to_string(),
335                    "Contact support if issues persist".to_string(),
336                ],
337            },
338            RepositoriesError::NotFound { repository, .. } => match repository {
339                RepositoryType::Account => vec![
340                    "Verify the account identifier is correct".to_string(),
341                    "Ensure you are signed in with the correct account".to_string(),
342                    "Contact support if the account should exist".to_string(),
343                ],
344                _ => vec![
345                    "Verify the requested identifier is correct".to_string(),
346                    "Refresh the page and try again".to_string(),
347                ],
348            },
349            RepositoriesError::Constraint { repository, .. } => match repository {
350                RepositoryType::Account => vec![
351                    "Review the input for conflicting or duplicate values".to_string(),
352                    "Ensure unique identifiers are not reused".to_string(),
353                    "Try again after correcting the input".to_string(),
354                ],
355                _ => vec![
356                    "Review your input for constraint issues".to_string(),
357                    "Try your request again in a moment".to_string(),
358                    "Contact support if constraints are unclear".to_string(),
359                ],
360            },
361        }
362    }
363
364    fn is_retryable(&self) -> bool {
365        match self {
366            RepositoriesError::OperationFailed {
367                repository,
368                operation,
369                ..
370            } => {
371                match (repository, operation) {
372                    (RepositoryType::Secret, _) => false, // avoid repeated security ops
373                    _ => true,
374                }
375            }
376            RepositoriesError::NotFound { .. } => false,
377            RepositoriesError::Constraint { .. } => false,
378        }
379    }
380}
381
382/// Database operation types.
383#[derive(Debug, Clone)]
384pub enum DatabaseOperation {
385    /// Database connection
386    Connect,
387    /// Database query
388    Query,
389    /// Insert row/document
390    Insert,
391    /// Update row/document
392    Update,
393    /// Delete row/document
394    Delete,
395    /// Schema migration
396    Migration,
397    /// Backup/restore
398    Backup,
399    /// Transaction block
400    Transaction,
401}
402
403impl fmt::Display for DatabaseOperation {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        match self {
406            DatabaseOperation::Connect => write!(f, "connect"),
407            DatabaseOperation::Query => write!(f, "query"),
408            DatabaseOperation::Insert => write!(f, "insert"),
409            DatabaseOperation::Update => write!(f, "update"),
410            DatabaseOperation::Delete => write!(f, "delete"),
411            DatabaseOperation::Migration => write!(f, "migration"),
412            DatabaseOperation::Backup => write!(f, "backup"),
413            DatabaseOperation::Transaction => write!(f, "transaction"),
414        }
415    }
416}
417
418/// Database-category native errors.
419#[derive(Debug, Error)]
420#[non_exhaustive]
421pub enum DatabaseError {
422    /// Database operation failure (driver/engine-side).
423    #[error("Database error: {operation} - {message}")]
424    Operation {
425        /// The database operation that failed.
426        operation: DatabaseOperation,
427        /// Description of the failure (non-sensitive).
428        message: String,
429        /// The table/collection involved (if applicable).
430        table: Option<String>,
431        /// The record identifier involved (if applicable).
432        record_id: Option<String>,
433    },
434}
435
436impl DatabaseError {
437    /// Construct a database error without table/record context.
438    pub fn new(operation: DatabaseOperation, message: impl Into<String>) -> Self {
439        DatabaseError::Operation {
440            operation,
441            message: message.into(),
442            table: None,
443            record_id: None,
444        }
445    }
446
447    /// Construct a database error with table/record context.
448    pub fn with_context(
449        operation: DatabaseOperation,
450        message: impl Into<String>,
451        table: Option<String>,
452        record_id: Option<String>,
453    ) -> Self {
454        DatabaseError::Operation {
455            operation,
456            message: message.into(),
457            table,
458            record_id,
459        }
460    }
461
462    fn support_code_inner(&self) -> String {
463        let mut hasher = DefaultHasher::new();
464        match self {
465            DatabaseError::Operation {
466                operation, table, ..
467            } => {
468                format!("DB-{}-{:X}", operation.to_string().to_uppercase(), {
469                    format!("{:?}{:?}", operation, table).hash(&mut hasher);
470                    hasher.finish() % 10000
471                })
472            }
473        }
474    }
475}
476
477impl UserFriendlyError for DatabaseError {
478    fn user_message(&self) -> String {
479        match self {
480            DatabaseError::Operation { operation, .. } => match operation {
481                DatabaseOperation::Connect => {
482                    "We're having trouble connecting to our database. Please try again in a moment."
483                        .to_string()
484                }
485                DatabaseOperation::Query
486                | DatabaseOperation::Insert
487                | DatabaseOperation::Update
488                | DatabaseOperation::Delete => {
489                    "We're experiencing technical difficulties with our data services. Please try again shortly.".to_string()
490                }
491                DatabaseOperation::Migration | DatabaseOperation::Backup => {
492                    "Our system is currently undergoing maintenance. Please try again later."
493                        .to_string()
494                }
495                DatabaseOperation::Transaction => {
496                    "We couldn't complete your request due to a technical issue. Please try again."
497                        .to_string()
498                }
499            },
500        }
501    }
502
503    fn developer_message(&self) -> String {
504        match self {
505            DatabaseError::Operation {
506                operation,
507                message,
508                table,
509                record_id,
510            } => {
511                let table_context = table
512                    .as_ref()
513                    .map(|t| format!(" [Table: {}]", t))
514                    .unwrap_or_default();
515                let record_context = record_id
516                    .as_ref()
517                    .map(|r| format!(" [Record: {}]", r))
518                    .unwrap_or_default();
519                format!(
520                    "Database {} operation failed: {}{}{}",
521                    operation, message, table_context, record_context
522                )
523            }
524        }
525    }
526
527    fn support_code(&self) -> String {
528        self.support_code_inner()
529    }
530
531    fn severity(&self) -> ErrorSeverity {
532        match self {
533            DatabaseError::Operation { operation, .. } => match operation {
534                DatabaseOperation::Connect => ErrorSeverity::Critical,
535                DatabaseOperation::Migration | DatabaseOperation::Backup => ErrorSeverity::Critical,
536                _ => ErrorSeverity::Error,
537            },
538        }
539    }
540
541    fn suggested_actions(&self) -> Vec<String> {
542        match self {
543            DatabaseError::Operation { operation, .. } => match operation {
544                DatabaseOperation::Connect => vec![
545                    "Wait a few minutes and try again".to_string(),
546                    "Check our status page for any database maintenance notifications".to_string(),
547                    "Contact support if the issue persists for more than 15 minutes".to_string(),
548                ],
549                DatabaseOperation::Query
550                | DatabaseOperation::Insert
551                | DatabaseOperation::Update
552                | DatabaseOperation::Delete => vec![
553                    "Try your request again in a moment".to_string(),
554                    "Refresh the page and attempt the operation again".to_string(),
555                    "Save your work locally if possible and try again later".to_string(),
556                    "Contact support if you continue to experience issues".to_string(),
557                ],
558                DatabaseOperation::Migration | DatabaseOperation::Backup => vec![
559                    "This is a system maintenance issue that will be resolved automatically"
560                        .to_string(),
561                    "Check our status page for maintenance schedules".to_string(),
562                    "No action is required from you at this time".to_string(),
563                ],
564                DatabaseOperation::Transaction => vec![
565                    "Try completing your transaction again".to_string(),
566                    "Ensure all required information is provided".to_string(),
567                    "Contact support if the transaction continues to fail".to_string(),
568                ],
569            },
570        }
571    }
572
573    fn is_retryable(&self) -> bool {
574        match self {
575            DatabaseError::Operation { operation, .. } => match operation {
576                DatabaseOperation::Connect => true, // connection issues often resolve
577                DatabaseOperation::Migration | DatabaseOperation::Backup => false, // maintenance
578                _ => true,
579            },
580        }
581    }
582}