1use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum ErrorCode {
12 UnknownFormat,
13 Parse,
14 XmlParse,
15 CsvParse,
16 Io,
17 DataMismatch,
18 InvalidResource,
19 UnsupportedFormat,
20 Conversion,
21 Validation,
22 MissingLanguage,
23 AmbiguousMatch,
24 PolicyViolation,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
29pub struct ErrorContext {
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub path: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub format: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub key: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub language: Option<String>,
38 #[serde(skip_serializing_if = "Vec::is_empty", default)]
39 pub candidates: Vec<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct StructuredError {
45 pub code: ErrorCode,
46 pub message: String,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub context: Option<ErrorContext>,
49}
50
51#[derive(Error, Debug)]
52pub enum Error {
53 #[error("unknown format `{0}`")]
54 UnknownFormat(String),
55
56 #[error("parse error: {0}")]
57 Parse(#[from] serde_json::Error),
58
59 #[error("XML parse error: {0}")]
60 XmlParse(#[from] quick_xml::Error),
61
62 #[error("CSV parse error: {0}")]
63 CsvParse(#[from] csv::Error),
64
65 #[error("I/O error: {0}")]
66 Io(#[from] std::io::Error),
67
68 #[error("invalid data: {0}")]
69 DataMismatch(String),
70
71 #[error("invalid resource: {0}")]
72 InvalidResource(String),
73
74 #[error("unsupported format: {0}")]
75 UnsupportedFormat(String),
76
77 #[error("conversion error: {message}")]
78 Conversion {
79 message: String,
80 #[source]
81 source: Option<Box<dyn std::error::Error + Send + Sync>>,
82 },
83
84 #[error("validation error: {0}")]
85 Validation(String),
86
87 #[error("missing language for `{path}` ({format})")]
88 MissingLanguage { path: String, format: String },
89
90 #[error("ambiguous match for key `{key}` in language `{language}`: {candidates:?}")]
91 AmbiguousMatch {
92 key: String,
93 language: String,
94 candidates: Vec<String>,
95 },
96
97 #[error("policy violation: {0}")]
98 PolicyViolation(String),
99}
100
101impl Error {
102 pub fn conversion_error(
104 message: impl Into<String>,
105 source: Option<Box<dyn std::error::Error + Send + Sync>>,
106 ) -> Self {
107 Error::Conversion {
108 message: message.into(),
109 source,
110 }
111 }
112
113 pub fn validation_error(message: impl Into<String>) -> Self {
115 Error::Validation(message.into())
116 }
117
118 pub fn missing_language(path: impl Into<String>, format: impl Into<String>) -> Self {
120 Error::MissingLanguage {
121 path: path.into(),
122 format: format.into(),
123 }
124 }
125
126 pub fn policy_violation(message: impl Into<String>) -> Self {
128 Error::PolicyViolation(message.into())
129 }
130
131 pub fn error_code(&self) -> ErrorCode {
133 match self {
134 Error::UnknownFormat(_) => ErrorCode::UnknownFormat,
135 Error::Parse(_) => ErrorCode::Parse,
136 Error::XmlParse(_) => ErrorCode::XmlParse,
137 Error::CsvParse(_) => ErrorCode::CsvParse,
138 Error::Io(_) => ErrorCode::Io,
139 Error::DataMismatch(_) => ErrorCode::DataMismatch,
140 Error::InvalidResource(_) => ErrorCode::InvalidResource,
141 Error::UnsupportedFormat(_) => ErrorCode::UnsupportedFormat,
142 Error::Conversion { .. } => ErrorCode::Conversion,
143 Error::Validation(_) => ErrorCode::Validation,
144 Error::MissingLanguage { .. } => ErrorCode::MissingLanguage,
145 Error::AmbiguousMatch { .. } => ErrorCode::AmbiguousMatch,
146 Error::PolicyViolation(_) => ErrorCode::PolicyViolation,
147 }
148 }
149
150 pub fn context(&self) -> Option<ErrorContext> {
152 match self {
153 Error::MissingLanguage { path, format } => Some(ErrorContext {
154 path: Some(path.clone()),
155 format: Some(format.clone()),
156 ..ErrorContext::default()
157 }),
158 Error::AmbiguousMatch {
159 key,
160 language,
161 candidates,
162 } => Some(ErrorContext {
163 key: Some(key.clone()),
164 language: Some(language.clone()),
165 candidates: candidates.clone(),
166 ..ErrorContext::default()
167 }),
168 _ => None,
169 }
170 }
171
172 pub fn structured(&self) -> StructuredError {
174 StructuredError {
175 code: self.error_code(),
176 message: self.to_string(),
177 context: self.context(),
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::io;
186
187 #[test]
188 fn test_unknown_format_error() {
189 let error = Error::UnknownFormat("invalid_format".to_string());
190 assert_eq!(error.to_string(), "unknown format `invalid_format`");
191 }
192
193 #[test]
194 fn test_parse_error() {
195 let json_error = serde_json::from_str::<serde_json::Value>("{ invalid json }").unwrap_err();
196 let error = Error::Parse(json_error);
197 assert!(error.to_string().contains("parse error"));
198 }
199
200 #[test]
201 fn test_io_error() {
202 let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
203 let error = Error::Io(io_error);
204 assert!(error.to_string().contains("I/O error"));
205 }
206
207 #[test]
208 fn test_data_mismatch_error() {
209 let error = Error::DataMismatch("Invalid data format".to_string());
210 assert_eq!(error.to_string(), "invalid data: Invalid data format");
211 }
212
213 #[test]
214 fn test_invalid_resource_error() {
215 let error = Error::InvalidResource("Missing required field".to_string());
216 assert_eq!(
217 error.to_string(),
218 "invalid resource: Missing required field"
219 );
220 }
221
222 #[test]
223 fn test_unsupported_format_error() {
224 let error = Error::UnsupportedFormat("xyz".to_string());
225 assert_eq!(error.to_string(), "unsupported format: xyz");
226 }
227
228 #[test]
229 fn test_conversion_error_with_source() {
230 let source_error = Box::new(io::Error::new(io::ErrorKind::NotFound, "Source error"));
231 let error = Error::conversion_error("Conversion failed", Some(source_error));
232 assert!(
233 error
234 .to_string()
235 .contains("conversion error: Conversion failed")
236 );
237 }
238
239 #[test]
240 fn test_conversion_error_without_source() {
241 let error = Error::conversion_error("Conversion failed", None);
242 assert!(
243 error
244 .to_string()
245 .contains("conversion error: Conversion failed")
246 );
247 }
248
249 #[test]
250 fn test_validation_error() {
251 let error = Error::validation_error("Validation failed");
252 assert_eq!(error.to_string(), "validation error: Validation failed");
253 }
254
255 #[test]
256 fn test_error_display() {
257 let errors = vec![
258 Error::UnknownFormat("test".to_string()),
259 Error::DataMismatch("test".to_string()),
260 Error::InvalidResource("test".to_string()),
261 Error::UnsupportedFormat("test".to_string()),
262 Error::Validation("test".to_string()),
263 Error::PolicyViolation("test".to_string()),
264 ];
265
266 for error in errors {
267 let display = format!("{}", error);
268 assert!(!display.is_empty());
269 assert!(display.contains("test"));
270 }
271 }
272
273 #[test]
274 fn test_error_debug() {
275 let error = Error::UnknownFormat("test".to_string());
276 let debug = format!("{:?}", error);
277 assert!(debug.contains("UnknownFormat"));
278 assert!(debug.contains("test"));
279 }
280
281 #[test]
282 fn test_structured_error_for_missing_language() {
283 let error = Error::missing_language("/tmp/Localizable.strings", "strings");
284 let structured = error.structured();
285 assert_eq!(structured.code, ErrorCode::MissingLanguage);
286 assert_eq!(
287 structured.context.as_ref().and_then(|c| c.path.as_deref()),
288 Some("/tmp/Localizable.strings")
289 );
290 }
291
292 #[test]
293 fn test_structured_error_for_ambiguous_match() {
294 let error = Error::AmbiguousMatch {
295 key: "welcome".to_string(),
296 language: "fr".to_string(),
297 candidates: vec!["a".to_string(), "b".to_string()],
298 };
299 let structured = error.structured();
300 assert_eq!(structured.code, ErrorCode::AmbiguousMatch);
301 assert_eq!(
302 structured.context.as_ref().map(|c| c.candidates.clone()),
303 Some(vec!["a".to_string(), "b".to_string()])
304 );
305 }
306}