1use std::collections::HashMap;
7use std::fmt;
8use thiserror::Error;
9
10#[derive(Error, Debug)]
27pub enum FoundationError {
28 #[error("Configuration error: {message}")]
30 Config { message: String },
31
32 #[error("IO error: {0}")]
34 Io(#[from] std::io::Error),
35
36 #[error("Serialization error: {0}")]
38 Serialization(#[from] serde_json::Error),
39
40 #[error("Error: {0}")]
42 Other(#[from] anyhow::Error),
43
44 #[error("Invalid input: {0}")]
46 InvalidInput(String),
47
48 #[error("Parse error: {0}")]
50 Parse(String),
51
52 #[error("HTTP error: {0}")]
54 Http(String),
55
56 #[error("URL parse error: {0}")]
58 UrlParse(String),
59
60 #[error("Authentication error: {0}")]
62 Authentication(String),
63}
64
65impl FoundationError {
66 pub fn with_context(self, context: &str) -> Self {
68 FoundationError::Other(anyhow::Error::new(self).context(context.to_string()))
69 }
70}
71
72pub type Result<T> = std::result::Result<T, FoundationError>;
74
75pub fn io_error_with_path(
94 err: std::io::Error,
95 path: &std::path::Path,
96 action: &str,
97) -> FoundationError {
98 FoundationError::Io(std::io::Error::new(
99 err.kind(),
100 format!("Failed to {action} {}: {err}", path.display()),
101 ))
102}
103
104pub trait ErrorContext<T, E> {
120 fn context(self, context: impl fmt::Display) -> Result<T>;
122
123 fn with_context<F>(self, f: F) -> Result<T>
125 where
126 F: FnOnce() -> String;
127}
128
129impl<T, E> ErrorContext<T, E> for std::result::Result<T, E>
130where
131 E: std::error::Error + Send + Sync + 'static,
132{
133 fn context(self, context: impl fmt::Display) -> Result<T> {
134 self.map_err(|e| FoundationError::Other(anyhow::Error::new(e).context(context.to_string())))
135 }
136
137 fn with_context<F>(self, f: F) -> Result<T>
138 where
139 F: FnOnce() -> String,
140 {
141 self.map_err(|e| FoundationError::Other(anyhow::Error::new(e).context(f())))
142 }
143}
144
145#[derive(Debug)]
151pub struct ErrorWithMetadata {
152 pub error: Box<dyn std::error::Error + Send + Sync>,
154 pub metadata: HashMap<String, String>,
156}
157
158impl ErrorWithMetadata {
159 pub fn new(error: impl std::error::Error + Send + Sync + 'static) -> Self {
161 Self {
162 error: Box::new(error),
163 metadata: HashMap::new(),
164 }
165 }
166
167 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
169 self.metadata.insert(key.into(), value.into());
170 self
171 }
172
173 pub fn get_metadata(&self, key: &str) -> Option<&str> {
175 self.metadata.get(key).map(|s| s.as_str())
176 }
177}
178
179impl fmt::Display for ErrorWithMetadata {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 write!(f, "{}", self.error)?;
182 if !self.metadata.is_empty() {
183 write!(f, " [")?;
184 let mut first = true;
185 for (key, value) in &self.metadata {
186 if !first {
187 write!(f, ", ")?;
188 }
189 write!(f, "{key}={value}")?;
190 first = false;
191 }
192 write!(f, "]")?;
193 }
194 Ok(())
195 }
196}
197
198impl std::error::Error for ErrorWithMetadata {
199 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
200 Some(self.error.as_ref())
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_error_with_metadata() {
210 let err = ErrorWithMetadata::new(FoundationError::InvalidInput("test".to_string()))
211 .with_metadata("file", "config.json")
212 .with_metadata("line", "42");
213
214 assert_eq!(err.get_metadata("file"), Some("config.json"));
215 assert_eq!(err.get_metadata("line"), Some("42"));
216 assert_eq!(err.get_metadata("missing"), None);
217
218 let display = format!("{err}");
219 assert!(display.contains("Invalid input"));
220 assert!(display.contains("file=config.json"));
221 assert!(display.contains("line=42"));
222 }
223
224 #[test]
225 fn test_error_context() {
226 let result: std::result::Result<(), std::io::Error> = Err(std::io::Error::new(
227 std::io::ErrorKind::NotFound,
228 "file not found",
229 ));
230
231 let with_context = result.context("Reading configuration");
232 assert!(with_context.is_err());
233
234 let err_msg = format!("{}", with_context.unwrap_err());
235 assert!(err_msg.contains("Reading configuration"));
236 }
237}