omni_schema_core/
error.rs

1
2use std::fmt;
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum SchemaError {
7    #[error("Type '{type_name}' is not supported for format '{format}'")]
8    UnsupportedType {
9        type_name: String,
10        format: String,
11    },
12
13    #[error("Circular reference detected: {path}")]
14    CircularReference {
15        path: String,
16    },
17
18    #[error("Type '{type_name}' not found in registry")]
19    TypeNotFound {
20        type_name: String,
21    },
22
23    #[error("Invalid attribute: {message}")]
24    InvalidAttribute {
25        message: String,
26    },
27
28    #[error("Conflicting attributes: {attr1} and {attr2}")]
29    ConflictingAttributes {
30        attr1: String,
31        attr2: String,
32    },
33
34    #[error("Invalid constraint: {message}")]
35    InvalidConstraint {
36        message: String,
37    },
38
39    #[error("I/O error: {0}")]
40    Io(#[from] std::io::Error),
41
42    #[error("Serialization error: {0}")]
43    Serialization(String),
44
45    #[error("Deserialization error: {0}")]
46    Deserialization(String),
47
48    #[error("Multiple errors occurred:\n{}", format_errors(.0))]
49    Multiple(Vec<SchemaError>),
50
51    #[error("{0}")]
52    Custom(String),
53}
54
55fn format_errors(errors: &[SchemaError]) -> String {
56    errors
57        .iter()
58        .enumerate()
59        .map(|(i, e)| format!("  {}. {}", i + 1, e))
60        .collect::<Vec<_>>()
61        .join("\n")
62}
63
64impl SchemaError {
65    pub fn unsupported_type(type_name: impl Into<String>, format: impl Into<String>) -> Self {
66        SchemaError::UnsupportedType {
67            type_name: type_name.into(),
68            format: format.into(),
69        }
70    }
71
72    pub fn circular_reference(path: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
73        let path_str = path
74            .into_iter()
75            .map(|s| s.as_ref().to_string())
76            .collect::<Vec<_>>()
77            .join(" -> ");
78        SchemaError::CircularReference { path: path_str }
79    }
80
81    pub fn type_not_found(type_name: impl Into<String>) -> Self {
82        SchemaError::TypeNotFound {
83            type_name: type_name.into(),
84        }
85    }
86
87    pub fn invalid_attribute(message: impl Into<String>) -> Self {
88        SchemaError::InvalidAttribute {
89            message: message.into(),
90        }
91    }
92
93    pub fn conflicting_attributes(attr1: impl Into<String>, attr2: impl Into<String>) -> Self {
94        SchemaError::ConflictingAttributes {
95            attr1: attr1.into(),
96            attr2: attr2.into(),
97        }
98    }
99
100    pub fn invalid_constraint(message: impl Into<String>) -> Self {
101        SchemaError::InvalidConstraint {
102            message: message.into(),
103        }
104    }
105
106    pub fn custom(message: impl Into<String>) -> Self {
107        SchemaError::Custom(message.into())
108    }
109
110    pub fn multiple(errors: Vec<SchemaError>) -> Self {
111        if errors.len() == 1 {
112            errors.into_iter().next().unwrap()
113        } else {
114            SchemaError::Multiple(errors)
115        }
116    }
117
118    pub fn is_unsupported_type(&self) -> bool {
119        matches!(self, SchemaError::UnsupportedType { .. })
120    }
121
122    pub fn is_type_not_found(&self) -> bool {
123        matches!(self, SchemaError::TypeNotFound { .. })
124    }
125
126    pub fn is_multiple(&self) -> bool {
127        matches!(self, SchemaError::Multiple(_))
128    }
129
130    pub fn inner_errors(&self) -> Option<&[SchemaError]> {
131        match self {
132            SchemaError::Multiple(errors) => Some(errors),
133            _ => None,
134        }
135    }
136}
137
138impl From<serde_json::Error> for SchemaError {
139    fn from(err: serde_json::Error) -> Self {
140        SchemaError::Serialization(err.to_string())
141    }
142}
143
144pub type SchemaResult<T> = Result<T, SchemaError>;
145
146#[derive(Debug, Default)]
147pub struct ErrorCollector {
148    errors: Vec<SchemaError>,
149}
150
151impl ErrorCollector {
152    pub fn new() -> Self {
153        Self::default()
154    }
155
156    pub fn push(&mut self, error: SchemaError) {
157        self.errors.push(error);
158    }
159
160    pub fn collect<T>(&mut self, result: SchemaResult<T>) -> Option<T> {
161        match result {
162            Ok(value) => Some(value),
163            Err(error) => {
164                self.push(error);
165                None
166            }
167        }
168    }
169
170    pub fn has_errors(&self) -> bool {
171        !self.errors.is_empty()
172    }
173
174    pub fn len(&self) -> usize {
175        self.errors.len()
176    }
177
178    pub fn is_empty(&self) -> bool {
179        self.errors.is_empty()
180    }
181
182    pub fn finish(self) -> SchemaResult<()> {
183        if self.errors.is_empty() {
184            Ok(())
185        } else {
186            Err(SchemaError::multiple(self.errors))
187        }
188    }
189
190    pub fn finish_with<T>(self, value: T) -> SchemaResult<T> {
191        if self.errors.is_empty() {
192            Ok(value)
193        } else {
194            Err(SchemaError::multiple(self.errors))
195        }
196    }
197
198    pub fn into_errors(self) -> Vec<SchemaError> {
199        self.errors
200    }
201}
202
203pub trait ResultExt<T> {
204    fn with_context(self, context: impl FnOnce() -> String) -> SchemaResult<T>;
205
206    fn unsupported_for(self, format: &str) -> SchemaResult<T>;
207}
208
209impl<T> ResultExt<T> for SchemaResult<T> {
210    fn with_context(self, context: impl FnOnce() -> String) -> SchemaResult<T> {
211        self.map_err(|e| SchemaError::Custom(format!("{}: {}", context(), e)))
212    }
213
214    fn unsupported_for(self, format: &str) -> SchemaResult<T> {
215        self.map_err(|e| match e {
216            SchemaError::UnsupportedType { type_name, .. } => {
217                SchemaError::unsupported_type(type_name, format)
218            }
219            other => other,
220        })
221    }
222}
223
224#[derive(Debug, Clone, PartialEq)]
225pub struct SchemaWarning {
226    pub code: WarningCode,
227    pub message: String,
228    pub location: Option<String>,
229}
230
231impl SchemaWarning {
232    pub fn new(code: WarningCode, message: impl Into<String>) -> Self {
233        Self {
234            code,
235            message: message.into(),
236            location: None,
237        }
238    }
239
240    pub fn at(mut self, location: impl Into<String>) -> Self {
241        self.location = Some(location.into());
242        self
243    }
244}
245
246impl fmt::Display for SchemaWarning {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        if let Some(ref location) = self.location {
249            write!(f, "[{}] at {}: {}", self.code, location, self.message)
250        } else {
251            write!(f, "[{}] {}", self.code, self.message)
252        }
253    }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
257pub enum WarningCode {
258    DeprecatedUsage,
259    PrecisionLoss,
260    PartialSupport,
261    ConstraintNotExpressible,
262    NonPortableDefault,
263    IgnoredAttribute,
264    RecursiveType,
265}
266
267impl fmt::Display for WarningCode {
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        let code = match self {
270            WarningCode::DeprecatedUsage => "W001",
271            WarningCode::PrecisionLoss => "W002",
272            WarningCode::PartialSupport => "W003",
273            WarningCode::ConstraintNotExpressible => "W004",
274            WarningCode::NonPortableDefault => "W005",
275            WarningCode::IgnoredAttribute => "W006",
276            WarningCode::RecursiveType => "W007",
277        };
278        write!(f, "{}", code)
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_unsupported_type_error() {
288        let err = SchemaError::unsupported_type("HashMap<i32, String>", "protobuf");
289        assert!(err.is_unsupported_type());
290        assert!(err.to_string().contains("HashMap<i32, String>"));
291        assert!(err.to_string().contains("protobuf"));
292    }
293
294    #[test]
295    fn test_circular_reference_error() {
296        let err = SchemaError::circular_reference(["User", "Post", "User"]);
297        assert!(err.to_string().contains("User -> Post -> User"));
298    }
299
300    #[test]
301    fn test_multiple_errors() {
302        let errors = vec![
303            SchemaError::type_not_found("Foo"),
304            SchemaError::type_not_found("Bar"),
305        ];
306        let err = SchemaError::multiple(errors);
307        assert!(err.is_multiple());
308        assert_eq!(err.inner_errors().unwrap().len(), 2);
309    }
310
311    #[test]
312    fn test_single_error_not_wrapped() {
313        let errors = vec![SchemaError::type_not_found("Foo")];
314        let err = SchemaError::multiple(errors);
315        assert!(!err.is_multiple());
316        assert!(err.is_type_not_found());
317    }
318
319    #[test]
320    fn test_error_collector() {
321        let mut collector = ErrorCollector::new();
322        assert!(!collector.has_errors());
323
324        collector.push(SchemaError::type_not_found("Foo"));
325        assert!(collector.has_errors());
326        assert_eq!(collector.len(), 1);
327
328        let result = collector.finish();
329        assert!(result.is_err());
330    }
331
332    #[test]
333    fn test_error_collector_empty() {
334        let collector = ErrorCollector::new();
335        let result = collector.finish();
336        assert!(result.is_ok());
337    }
338
339    #[test]
340    fn test_error_collector_with_value() {
341        let collector = ErrorCollector::new();
342        let result = collector.finish_with(42);
343        assert_eq!(result.unwrap(), 42);
344    }
345
346    #[test]
347    fn test_warning_display() {
348        let warning = SchemaWarning::new(WarningCode::DeprecatedUsage, "Field 'foo' is deprecated")
349            .at("User.foo");
350        let display = warning.to_string();
351        assert!(display.contains("W001"));
352        assert!(display.contains("User.foo"));
353        assert!(display.contains("deprecated"));
354    }
355}