1use std::collections::HashMap;
2use std::sync::RwLock;
3use std::sync::OnceLock;
4use std::fmt;
5use crate::error::ForgeError;
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(&self, code: String, description: String,
36 documentation_url: Option<String>, retryable: bool) -> Result<(), String> {
37 let mut codes = match self.codes.write() {
38 Ok(codes) => codes,
39 Err(_) => return Err("Failed to acquire write lock on error registry".to_string()),
40 };
41
42 if codes.contains_key(&code) {
43 return Err(format!("Error code '{code}' is already registered"));
44 }
45
46 codes.insert(code.clone(), ErrorCodeInfo {
47 code,
48 description,
49 documentation_url,
50 retryable,
51 });
52
53 Ok(())
54 }
55
56 pub fn get_code_info(&self, code: &str) -> Option<ErrorCodeInfo> {
58 match self.codes.read() {
59 Ok(codes) => codes.get(code).cloned(),
60 Err(_) => None,
61 }
62 }
63
64 pub fn is_registered(&self, code: &str) -> bool {
66 match self.codes.read() {
67 Ok(codes) => codes.contains_key(code),
68 Err(_) => false,
69 }
70 }
71
72 pub fn global() -> &'static ErrorRegistry {
74 static REGISTRY: OnceLock<ErrorRegistry> = OnceLock::new();
75 REGISTRY.get_or_init(ErrorRegistry::new)
76 }
77}
78
79#[derive(Debug)]
81pub struct CodedError<E> {
82 pub error: E,
84 pub code: String,
86 pub fatal: bool,
88}
89
90impl<E> CodedError<E> {
91 pub fn new(error: E, code: impl Into<String>) -> Self {
93 let code = code.into();
94
95 if !ErrorRegistry::global().is_registered(&code) {
97 let _ = register_error_code(
98 code.clone(),
99 format!("Error code {code}"),
100 None as Option<String>,
101 false
102 );
103 }
104
105 Self {
106 error,
107 code,
108 fatal: false
109 }
110 }
111
112 pub fn code_info(&self) -> Option<ErrorCodeInfo> {
114 ErrorRegistry::global().get_code_info(&self.code)
115 }
116
117 pub fn with_retryable(self, _retryable: bool) -> Self {
119 self
121 }
122
123 pub fn with_fatal(mut self, fatal: bool) -> Self {
125 self.fatal = fatal;
126 self
127 }
128
129 pub fn with_status(self, _status: u16) -> Self {
131 self
134 }
135}
136
137impl<E: fmt::Display> fmt::Display for CodedError<E> {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 write!(f, "[{}] {}", self.code, self.error)
140 }
141}
142
143impl<E: std::error::Error + 'static> std::error::Error for CodedError<E> {
144 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
145 Some(&self.error)
146 }
147}
148
149impl<E: ForgeError> ForgeError for CodedError<E> {
151 fn kind(&self) -> &'static str {
152 self.error.kind()
153 }
154
155 fn caption(&self) -> &'static str {
156 self.error.caption()
157 }
158
159 fn is_retryable(&self) -> bool {
160 self.code_info().map_or_else(
162 || self.error.is_retryable(),
163 |info| info.retryable
164 )
165 }
166
167 fn is_fatal(&self) -> bool {
168 self.fatal || self.error.is_fatal()
169 }
170
171 fn status_code(&self) -> u16 {
172 self.error.status_code()
173 }
174
175 fn exit_code(&self) -> i32 {
176 self.error.exit_code()
177 }
178
179 fn user_message(&self) -> String {
180 format!("[{}] {}", self.code, self.error.user_message())
181 }
182
183 fn dev_message(&self) -> String {
184 if let Some(info) = self.code_info() {
185 if let Some(url) = info.documentation_url {
186 return format!("[{}] {} ({})", self.code, self.error.dev_message(), url);
187 }
188 }
189 format!("[{}] {}", self.code, self.error.dev_message())
190 }
191
192 fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
193 self.error.backtrace()
194 }
195}
196
197pub trait WithErrorCode<E> {
199 fn with_code(self, code: impl Into<String>) -> CodedError<E>;
201}
202
203impl<E> WithErrorCode<E> for E {
204 fn with_code(self, code: impl Into<String>) -> CodedError<E> {
205 CodedError::new(self, code)
206 }
207}
208
209pub fn register_error_code(
211 code: impl Into<String>,
212 description: impl Into<String>,
213 documentation_url: Option<impl Into<String>>,
214 retryable: bool
215) -> Result<(), String> {
216 ErrorRegistry::global().register_code(
217 code.into(),
218 description.into(),
219 documentation_url.map(|url| url.into()),
220 retryable
221 )
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::AppError;
228
229 #[test]
230 fn test_error_with_code() {
231 let error = AppError::config("Invalid config").with_code("CONFIG-001");
232
233 assert_eq!(error.to_string(), "[CONFIG-001] ⚙️ Configuration Error: Invalid config");
234 }
235
236 #[test]
237 fn test_register_error_code() {
238 let _ = register_error_code(
239 "AUTH-001",
240 "Authentication failed due to invalid credentials",
241 Some("https://docs.example.com/errors/auth-001"),
242 true
243 );
244
245 let info = ErrorRegistry::global().get_code_info("AUTH-001");
246 assert!(info.is_some());
247 let info = info.unwrap();
248 assert_eq!(info.code, "AUTH-001");
249 assert_eq!(info.description, "Authentication failed due to invalid credentials");
250 assert_eq!(info.documentation_url, Some("https://docs.example.com/errors/auth-001".to_string()));
251 assert!(info.retryable);
252 }
253}