Skip to main content

error_forge/
registry.rs

1use crate::error::ForgeError;
2use std::collections::HashMap;
3use std::fmt;
4use std::sync::OnceLock;
5use std::sync::RwLock;
6
7/// A central registry for error codes and metadata
8pub struct ErrorRegistry {
9    /// Maps error codes to their descriptions
10    codes: RwLock<HashMap<String, ErrorCodeInfo>>,
11}
12
13/// Metadata for a registered error code
14#[derive(Clone, Debug)]
15pub struct ErrorCodeInfo {
16    /// The error code (e.g. "AUTH-001")
17    pub code: String,
18    /// A human-readable description of this error type
19    pub description: String,
20    /// A URL to documentation about this error, if available
21    pub documentation_url: Option<String>,
22    /// Whether this error is expected to be retryable
23    pub retryable: bool,
24}
25
26impl ErrorRegistry {
27    /// Create a new empty error registry
28    fn new() -> Self {
29        Self {
30            codes: RwLock::new(HashMap::new()),
31        }
32    }
33
34    /// Register an error code with metadata
35    pub fn register_code(
36        &self,
37        code: String,
38        description: String,
39        documentation_url: Option<String>,
40        retryable: bool,
41    ) -> Result<(), String> {
42        let mut codes = match self.codes.write() {
43            Ok(codes) => codes,
44            Err(_) => return Err("Failed to acquire write lock on error registry".to_string()),
45        };
46
47        if codes.contains_key(&code) {
48            return Err(format!("Error code '{code}' is already registered"));
49        }
50
51        codes.insert(
52            code.clone(),
53            ErrorCodeInfo {
54                code,
55                description,
56                documentation_url,
57                retryable,
58            },
59        );
60
61        Ok(())
62    }
63
64    /// Get info about a registered error code
65    pub fn get_code_info(&self, code: &str) -> Option<ErrorCodeInfo> {
66        match self.codes.read() {
67            Ok(codes) => codes.get(code).cloned(),
68            Err(_) => None,
69        }
70    }
71
72    /// Check if an error code is registered
73    pub fn is_registered(&self, code: &str) -> bool {
74        match self.codes.read() {
75            Ok(codes) => codes.contains_key(code),
76            Err(_) => false,
77        }
78    }
79
80    /// Get the global error registry instance
81    pub fn global() -> &'static ErrorRegistry {
82        static REGISTRY: OnceLock<ErrorRegistry> = OnceLock::new();
83        REGISTRY.get_or_init(ErrorRegistry::new)
84    }
85}
86
87/// An error with an associated error code
88#[derive(Debug)]
89pub struct CodedError<E> {
90    /// The original error
91    pub error: E,
92    /// The error code
93    pub code: String,
94    /// Per-instance override for retryability
95    pub retryable: Option<bool>,
96    /// Whether this error is fatal
97    pub fatal: bool,
98    /// Per-instance override for status code
99    pub status: Option<u16>,
100}
101
102impl<E> CodedError<E> {
103    /// Create a new coded error
104    pub fn new(error: E, code: impl Into<String>) -> Self {
105        let code = code.into();
106
107        // Register the code if it's not already registered
108        if !ErrorRegistry::global().is_registered(&code) {
109            let _ = register_error_code(
110                code.clone(),
111                format!("Error code {code}"),
112                None as Option<String>,
113                false,
114            );
115        }
116
117        Self {
118            error,
119            code,
120            retryable: None,
121            fatal: false,
122            status: None,
123        }
124    }
125
126    /// Get information about this error code from the registry
127    pub fn code_info(&self) -> Option<ErrorCodeInfo> {
128        ErrorRegistry::global().get_code_info(&self.code)
129    }
130
131    /// Set whether this error is retryable
132    pub fn with_retryable(mut self, retryable: bool) -> Self {
133        self.retryable = Some(retryable);
134        self
135    }
136
137    /// Set whether this error is fatal
138    pub fn with_fatal(mut self, fatal: bool) -> Self {
139        self.fatal = fatal;
140        self
141    }
142
143    /// Set the HTTP status code for this error
144    pub fn with_status(mut self, status: u16) -> Self {
145        self.status = Some(status);
146        self
147    }
148}
149
150impl<E: fmt::Display> fmt::Display for CodedError<E> {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(f, "[{}] {}", self.code, self.error)
153    }
154}
155
156impl<E: std::error::Error + 'static> std::error::Error for CodedError<E> {
157    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
158        Some(&self.error)
159    }
160}
161
162// Implement ForgeError for CodedError when the inner error implements ForgeError
163impl<E: ForgeError> ForgeError for CodedError<E> {
164    fn kind(&self) -> &'static str {
165        self.error.kind()
166    }
167
168    fn caption(&self) -> &'static str {
169        self.error.caption()
170    }
171
172    fn is_retryable(&self) -> bool {
173        self.retryable.unwrap_or_else(|| {
174            self.code_info()
175                .map_or_else(|| self.error.is_retryable(), |info| info.retryable)
176        })
177    }
178
179    fn is_fatal(&self) -> bool {
180        self.fatal || self.error.is_fatal()
181    }
182
183    fn status_code(&self) -> u16 {
184        self.status.unwrap_or_else(|| self.error.status_code())
185    }
186
187    fn exit_code(&self) -> i32 {
188        self.error.exit_code()
189    }
190
191    fn user_message(&self) -> String {
192        format!("[{}] {}", self.code, self.error.user_message())
193    }
194
195    fn dev_message(&self) -> String {
196        if let Some(info) = self.code_info() {
197            if let Some(url) = info.documentation_url {
198                return format!("[{}] {} ({})", self.code, self.error.dev_message(), url);
199            }
200        }
201        format!("[{}] {}", self.code, self.error.dev_message())
202    }
203
204    fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
205        self.error.backtrace()
206    }
207}
208
209/// Extension trait for adding error codes
210pub trait WithErrorCode<E> {
211    /// Attach an error code to an error
212    fn with_code(self, code: impl Into<String>) -> CodedError<E>;
213}
214
215impl<E> WithErrorCode<E> for E {
216    fn with_code(self, code: impl Into<String>) -> CodedError<E> {
217        CodedError::new(self, code)
218    }
219}
220
221/// Register an error code in the global registry
222pub fn register_error_code(
223    code: impl Into<String>,
224    description: impl Into<String>,
225    documentation_url: Option<impl Into<String>>,
226    retryable: bool,
227) -> Result<(), String> {
228    ErrorRegistry::global().register_code(
229        code.into(),
230        description.into(),
231        documentation_url.map(|url| url.into()),
232        retryable,
233    )
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::AppError;
240
241    #[test]
242    fn test_error_with_code() {
243        let error = AppError::config("Invalid config").with_code("CONFIG-001");
244
245        assert_eq!(
246            error.to_string(),
247            "[CONFIG-001] ⚙️ Configuration Error: Invalid config"
248        );
249    }
250
251    #[test]
252    fn test_register_error_code() {
253        let _ = register_error_code(
254            "AUTH-001",
255            "Authentication failed due to invalid credentials",
256            Some("https://docs.example.com/errors/auth-001"),
257            true,
258        );
259
260        let info = ErrorRegistry::global().get_code_info("AUTH-001");
261        assert!(info.is_some());
262        let info = info.unwrap();
263        assert_eq!(info.code, "AUTH-001");
264        assert_eq!(
265            info.description,
266            "Authentication failed due to invalid credentials"
267        );
268        assert_eq!(
269            info.documentation_url,
270            Some("https://docs.example.com/errors/auth-001".to_string())
271        );
272        assert!(info.retryable);
273    }
274}