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)]
15pub struct ErrorCodeInfo {
16 pub code: String,
18 pub description: String,
20 pub documentation_url: Option<String>,
22 pub retryable: bool,
24}
25
26impl ErrorRegistry {
27 fn new() -> Self {
29 Self {
30 codes: RwLock::new(HashMap::new()),
31 }
32 }
33
34 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 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 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 pub fn global() -> &'static ErrorRegistry {
82 static REGISTRY: OnceLock<ErrorRegistry> = OnceLock::new();
83 REGISTRY.get_or_init(ErrorRegistry::new)
84 }
85}
86
87#[derive(Debug)]
89pub struct CodedError<E> {
90 pub error: E,
92 pub code: String,
94 pub retryable: Option<bool>,
96 pub fatal: bool,
98 pub status: Option<u16>,
100}
101
102impl<E> CodedError<E> {
103 pub fn new(error: E, code: impl Into<String>) -> Self {
105 let code = code.into();
106
107 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 pub fn code_info(&self) -> Option<ErrorCodeInfo> {
128 ErrorRegistry::global().get_code_info(&self.code)
129 }
130
131 pub fn with_retryable(mut self, retryable: bool) -> Self {
133 self.retryable = Some(retryable);
134 self
135 }
136
137 pub fn with_fatal(mut self, fatal: bool) -> Self {
139 self.fatal = fatal;
140 self
141 }
142
143 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
162impl<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
209pub trait WithErrorCode<E> {
211 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
221pub 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}