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}