1use thiserror::Error;
10
11#[derive(Debug, Error, Clone, PartialEq, Eq)]
20#[error("{path}: {message}")]
21pub struct DocumentError {
22 pub path: String,
24 pub code: Option<String>,
31 pub message: String,
33}
34
35impl DocumentError {
36 pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
39 Self {
40 path: path.into(),
41 code: None,
42 message: message.into(),
43 }
44 }
45
46 pub fn with_code(
48 path: impl Into<String>,
49 code: impl Into<String>,
50 message: impl Into<String>,
51 ) -> Self {
52 Self {
53 path: path.into(),
54 code: Some(code.into()),
55 message: message.into(),
56 }
57 }
58}
59
60#[derive(Debug, Error, Clone, PartialEq, Eq)]
68#[error("{message}")]
69pub struct SchemaError {
70 pub path: String,
72 pub code: String,
74 pub message: String,
76}
77
78impl SchemaError {
79 pub fn new(
81 path: impl Into<String>,
82 code: impl Into<String>,
83 message: impl Into<String>,
84 ) -> Self {
85 Self {
86 path: path.into(),
87 code: code.into(),
88 message: message.into(),
89 }
90 }
91}
92
93#[derive(Debug, Error, Clone, PartialEq, Eq)]
98#[error("line {line}, col {col}: {message}")]
99pub struct ParseError {
100 pub line: usize,
102 pub col: usize,
104 pub code: String,
107 pub message: String,
109}
110
111impl ParseError {
112 pub fn new(
115 line: usize,
116 col: usize,
117 code: impl Into<String>,
118 message: impl Into<String>,
119 ) -> Self {
120 Self {
121 line,
122 col,
123 code: code.into(),
124 message: message.into(),
125 }
126 }
127
128 pub fn codec_syntax(line: usize, col: usize, message: impl Into<String>) -> Self {
133 Self::new(line, col, "parse.codec-syntax", message)
134 }
135
136 pub fn position(&self) -> String {
139 format!("{}:{}", self.line, self.col)
140 }
141}
142
143#[derive(Debug, Error, Clone, PartialEq, Eq)]
153#[error("{0}")]
154pub struct FormatError(pub String);
155
156impl FormatError {
157 pub fn new(message: impl Into<String>) -> Self {
159 Self(message.into())
160 }
161}
162
163#[derive(Debug, Error, Clone, PartialEq, Eq)]
173#[error("{message}")]
174pub struct WriteError {
175 pub message: String,
177 pub report: Option<crate::report::WriteReport>,
179 pub path: Option<String>,
183 pub code: Option<String>,
187}
188
189impl WriteError {
190 pub fn new(message: impl Into<String>) -> Self {
192 Self {
193 message: message.into(),
194 report: None,
195 path: None,
196 code: None,
197 }
198 }
199
200 pub fn with_diagnostic(
203 path: impl Into<String>,
204 code: impl Into<String>,
205 message: impl Into<String>,
206 ) -> Self {
207 Self {
208 message: message.into(),
209 report: None,
210 path: Some(path.into()),
211 code: Some(code.into()),
212 }
213 }
214
215 pub fn with_report(message: impl Into<String>, report: crate::report::WriteReport) -> Self {
218 Self {
219 message: message.into(),
220 report: Some(report),
221 path: None,
222 code: None,
223 }
224 }
225
226 pub fn report(&self) -> Option<&crate::report::WriteReport> {
229 self.report.as_ref()
230 }
231}
232
233impl From<DocumentError> for WriteError {
234 fn from(e: DocumentError) -> Self {
235 WriteError::new(e.message)
236 }
237}
238
239#[derive(Debug, Error, Clone, PartialEq, Eq)]
248#[error("{0}")]
249pub struct MaterializeError(pub crate::schema::ValidationResult);
250
251impl MaterializeError {
252 pub fn new(result: crate::schema::ValidationResult) -> Self {
254 Self(result)
255 }
256
257 pub fn result(&self) -> &crate::schema::ValidationResult {
259 &self.0
260 }
261
262 pub fn errors(&self) -> &[crate::schema::ValidationError] {
264 self.0.errors()
265 }
266}
267
268#[derive(Debug, Error, Clone, PartialEq, Eq)]
270pub enum OmnistError {
271 #[error(transparent)]
273 Document(#[from] DocumentError),
274 #[error(transparent)]
276 Schema(#[from] SchemaError),
277 #[error(transparent)]
279 Materialize(#[from] MaterializeError),
280 #[error(transparent)]
282 Parse(#[from] ParseError),
283 #[error(transparent)]
285 Write(#[from] WriteError),
286 #[error(transparent)]
288 Format(#[from] FormatError),
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn document_error_display_includes_path_and_message() {
297 let e = DocumentError::new("$.foo", "not a Document value");
298 assert_eq!(e.to_string(), "$.foo: not a Document value");
299 }
300
301 #[test]
302 fn omnist_error_wraps_document_error_transparently() {
303 let doc_err = DocumentError::new("$.foo", "boom");
304 let wrapped: OmnistError = doc_err.clone().into();
305 assert_eq!(wrapped.to_string(), doc_err.to_string());
306 assert!(matches!(wrapped, OmnistError::Document(ref inner) if *inner == doc_err));
307 }
308
309 #[test]
310 fn document_error_clone_and_eq() {
311 let a = DocumentError::new("$", "x");
312 let b = a.clone();
313 assert_eq!(a, b);
314 }
315
316 #[test]
317 fn schema_error_display_and_eq() {
318 let e = SchemaError::new("R.a", "schema.unknown-type", "unknown type 'Missing'");
319 assert_eq!(e.path, "R.a");
320 assert_eq!(e.code, "schema.unknown-type");
321 assert_eq!(e.message, "unknown type 'Missing'");
322 assert_eq!(e.to_string(), "unknown type 'Missing'");
323 assert_eq!(e.clone(), e);
324 }
325
326 #[test]
327 fn omnist_error_wraps_schema_error_transparently() {
328 let schema_err = SchemaError::new("$", "schema.syntax", "boom");
329 let wrapped: OmnistError = schema_err.clone().into();
330 assert_eq!(wrapped.to_string(), schema_err.to_string());
331 assert!(matches!(wrapped, OmnistError::Schema(ref inner) if *inner == schema_err));
332 }
333
334 #[test]
335 fn parse_error_display_includes_line_col_and_message() {
336 let e = ParseError::new(3, 7, "parse.unexpected-token", "stray character '@'");
337 assert_eq!(e.to_string(), "line 3, col 7: stray character '@'");
338 }
339
340 #[test]
341 fn parse_error_carries_a_code_and_a_text_position_path() {
342 let e = ParseError::new(3, 7, "parse.trailing-content", "x");
343 assert_eq!(e.code, "parse.trailing-content");
344 assert_eq!(e.position(), "3:7");
345 let c = ParseError::codec_syntax(1, 1, "bad");
346 assert_eq!(c.code, "parse.codec-syntax");
347 assert_eq!(c.position(), "1:1");
348 }
349
350 #[test]
351 fn document_error_code_is_none_unless_the_taxonomy_names_it() {
352 assert_eq!(DocumentError::new("$", "misuse").code, None);
353 let e = DocumentError::with_code("$.a", "document.unlabeled-element", "x");
354 assert_eq!(e.code.as_deref(), Some("document.unlabeled-element"));
355 assert_eq!(e.path, "$.a");
356 assert_eq!(e.to_string(), "$.a: x");
357 }
358
359 #[test]
360 fn write_error_diagnostic_fields_are_set_only_by_with_diagnostic() {
361 let plain = WriteError::new("boom");
362 assert_eq!((plain.path, plain.code), (None, None));
363 let d = WriteError::with_diagnostic("$.n", "write.unsupported-value", "nope");
364 assert_eq!(d.path.as_deref(), Some("$.n"));
365 assert_eq!(d.code.as_deref(), Some("write.unsupported-value"));
366 let r = WriteError::with_report("strict", crate::report::WriteReport::new());
367 assert_eq!((r.path, r.code), (None, None));
368 }
369
370 #[test]
371 fn omnist_error_wraps_parse_error_transparently() {
372 let e = ParseError::new(1, 1, "parse.unexpected-token", "boom");
373 let wrapped: OmnistError = e.clone().into();
374 assert_eq!(wrapped.to_string(), e.to_string());
375 assert!(matches!(wrapped, OmnistError::Parse(ref inner) if *inner == e));
376 }
377
378 #[test]
379 fn write_error_display_and_from_document_error() {
380 let e = WriteError::new("nesting exceeds the maximum depth (200)");
381 assert_eq!(e.to_string(), "nesting exceeds the maximum depth (200)");
382 let doc_err = DocumentError::new("$", "nesting exceeds the maximum depth (200)");
383 let from_doc: WriteError = doc_err.into();
384 assert_eq!(from_doc, e);
385 }
386
387 #[test]
388 fn omnist_error_wraps_write_error_transparently() {
389 let e = WriteError::new("boom");
390 let wrapped: OmnistError = e.clone().into();
391 assert_eq!(wrapped.to_string(), e.to_string());
392 assert!(matches!(wrapped, OmnistError::Write(ref inner) if *inner == e));
393 }
394
395 #[test]
396 fn materialize_error_new_result_and_errors_accessors() {
397 let fields = vec![crate::schema::Field::required("x", crate::schema::STRING).unwrap()];
398 let rec = crate::schema::Record::new(fields).unwrap();
399 let mut env: indexmap::IndexMap<String, crate::schema::Record> = indexmap::IndexMap::new();
400 env.insert("Root".to_string(), rec);
401 let schema = crate::schema::Schema::new(crate::schema::Ref::new("Root"), env).unwrap();
402 let node = crate::document::RawNode::Edges(vec![]);
406 let res = crate::materialize::materialize(&node, Some(&schema))
407 .unwrap_err()
408 .0;
409 assert!(!res.ok());
410
411 let e = MaterializeError::new(res.clone());
412 assert_eq!(e.result(), &res);
413 assert_eq!(e.errors(), res.errors());
414 assert_eq!(e.to_string(), res.to_string());
415 }
416
417 #[test]
418 fn omnist_error_wraps_materialize_error_transparently() {
419 let res = crate::schema::ValidationResult::new();
420 let e = MaterializeError::new(res);
421 let wrapped: OmnistError = e.clone().into();
422 assert_eq!(wrapped.to_string(), e.to_string());
423 assert!(matches!(wrapped, OmnistError::Materialize(ref inner) if *inner == e));
424 }
425}