1use crate::error::ForgeError;
2use std::collections::HashMap;
3use std::fmt;
4use std::sync::OnceLock;
5use std::sync::RwLock;
6
7pub struct ErrorRegistry {
9 codes: RwLock<HashMap<String, ErrorCodeInfo>>,
11}
12
13#[derive(Clone, Debug)]
18#[non_exhaustive]
19pub struct ErrorCodeInfo {
20 pub code: String,
22 pub description: String,
24 pub documentation_url: Option<String>,
26 pub retryable: bool,
28}
29
30impl ErrorRegistry {
31 fn new() -> Self {
33 Self {
34 codes: RwLock::new(HashMap::new()),
35 }
36 }
37
38 pub fn register_code(
40 &self,
41 code: String,
42 description: String,
43 documentation_url: Option<String>,
44 retryable: bool,
45 ) -> Result<(), String> {
46 let mut codes = match self.codes.write() {
47 Ok(codes) => codes,
48 Err(_) => return Err("Failed to acquire write lock on error registry".to_string()),
49 };
50
51 if codes.contains_key(&code) {
52 return Err(format!("Error code '{code}' is already registered"));
53 }
54
55 codes.insert(
56 code.clone(),
57 ErrorCodeInfo {
58 code,
59 description,
60 documentation_url,
61 retryable,
62 },
63 );
64
65 Ok(())
66 }
67
68 pub fn get_code_info(&self, code: &str) -> Option<ErrorCodeInfo> {
70 match self.codes.read() {
71 Ok(codes) => codes.get(code).cloned(),
72 Err(_) => None,
73 }
74 }
75
76 pub fn is_registered(&self, code: &str) -> bool {
78 match self.codes.read() {
79 Ok(codes) => codes.contains_key(code),
80 Err(_) => false,
81 }
82 }
83
84 pub fn global() -> &'static ErrorRegistry {
86 static REGISTRY: OnceLock<ErrorRegistry> = OnceLock::new();
87 REGISTRY.get_or_init(ErrorRegistry::new)
88 }
89}
90
91#[derive(Debug)]
99#[non_exhaustive]
100pub struct CodedError<E> {
101 pub error: E,
103 pub code: String,
105 pub retryable: Option<bool>,
107 pub fatal: bool,
109 pub status: Option<u16>,
111}
112
113impl<E> CodedError<E> {
114 pub fn new(error: E, code: impl Into<String>) -> Self {
133 Self {
134 error,
135 code: code.into(),
136 retryable: None,
137 fatal: false,
138 status: None,
139 }
140 }
141
142 pub fn code_info(&self) -> Option<ErrorCodeInfo> {
144 ErrorRegistry::global().get_code_info(&self.code)
145 }
146
147 pub fn with_retryable(mut self, retryable: bool) -> Self {
149 self.retryable = Some(retryable);
150 self
151 }
152
153 pub fn with_fatal(mut self, fatal: bool) -> Self {
155 self.fatal = fatal;
156 self
157 }
158
159 pub fn with_status(mut self, status: u16) -> Self {
161 self.status = Some(status);
162 self
163 }
164}
165
166impl<E: fmt::Display> fmt::Display for CodedError<E> {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 write!(f, "[{}] {}", self.code, self.error)
169 }
170}
171
172impl<E: std::error::Error + 'static> std::error::Error for CodedError<E> {
173 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
174 Some(&self.error)
175 }
176}
177
178impl<E: ForgeError> ForgeError for CodedError<E> {
180 fn kind(&self) -> &'static str {
181 self.error.kind()
182 }
183
184 fn caption(&self) -> &'static str {
185 self.error.caption()
186 }
187
188 fn is_retryable(&self) -> bool {
189 self.retryable.unwrap_or_else(|| {
190 self.code_info()
191 .map_or_else(|| self.error.is_retryable(), |info| info.retryable)
192 })
193 }
194
195 fn is_fatal(&self) -> bool {
196 self.fatal || self.error.is_fatal()
197 }
198
199 fn status_code(&self) -> u16 {
200 self.status.unwrap_or_else(|| self.error.status_code())
201 }
202
203 fn exit_code(&self) -> i32 {
204 self.error.exit_code()
205 }
206
207 fn user_message(&self) -> String {
208 format!("[{}] {}", self.code, self.error.user_message())
209 }
210
211 fn dev_message(&self) -> String {
212 if let Some(info) = self.code_info() {
213 if let Some(url) = info.documentation_url {
214 return format!("[{}] {} ({})", self.code, self.error.dev_message(), url);
215 }
216 }
217 format!("[{}] {}", self.code, self.error.dev_message())
218 }
219
220 fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
221 self.error.backtrace()
222 }
223}
224
225pub trait WithErrorCode<E> {
227 fn with_code(self, code: impl Into<String>) -> CodedError<E>;
229}
230
231impl<E> WithErrorCode<E> for E {
232 fn with_code(self, code: impl Into<String>) -> CodedError<E> {
233 CodedError::new(self, code)
234 }
235}
236
237pub fn register_error_code(
239 code: impl Into<String>,
240 description: impl Into<String>,
241 documentation_url: Option<impl Into<String>>,
242 retryable: bool,
243) -> Result<(), String> {
244 ErrorRegistry::global().register_code(
245 code.into(),
246 description.into(),
247 documentation_url.map(|url| url.into()),
248 retryable,
249 )
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::AppError;
256
257 #[test]
258 fn test_error_with_code() {
259 let error = AppError::config("Invalid config").with_code("CONFIG-001");
260
261 assert_eq!(
262 error.to_string(),
263 "[CONFIG-001] ⚙️ Configuration Error: Invalid config"
264 );
265 }
266
267 #[test]
268 fn test_register_error_code() {
269 let _ = register_error_code(
270 "AUTH-001",
271 "Authentication failed due to invalid credentials",
272 Some("https://docs.example.com/errors/auth-001"),
273 true,
274 );
275
276 let info = ErrorRegistry::global().get_code_info("AUTH-001");
277 assert!(info.is_some());
278 let info = info.unwrap();
279 assert_eq!(info.code, "AUTH-001");
280 assert_eq!(
281 info.description,
282 "Authentication failed due to invalid credentials"
283 );
284 assert_eq!(
285 info.documentation_url,
286 Some("https://docs.example.com/errors/auth-001".to_string())
287 );
288 assert!(info.retryable);
289 }
290}