Skip to main content

ariel_rs/
error.rs

1//! Error types for ariel-rs diagram parsing and rendering.
2
3/// A single parse error with source location.
4#[derive(Debug, Clone, PartialEq)]
5pub struct ParseError {
6    /// 1-based line number where the error occurred, if known.
7    pub line: Option<usize>,
8    /// 1-based column number where the error occurred, if known.
9    pub column: Option<usize>,
10    /// Human-readable description of what went wrong.
11    pub message: String,
12}
13
14impl ParseError {
15    /// Create a parse error with a message only (no location).
16    pub fn new(message: impl Into<String>) -> Self {
17        Self {
18            line: None,
19            column: None,
20            message: message.into(),
21        }
22    }
23
24    /// Create a parse error with a line number.
25    pub fn at_line(line: usize, message: impl Into<String>) -> Self {
26        Self {
27            line: Some(line),
28            column: None,
29            message: message.into(),
30        }
31    }
32
33    /// Create a parse error with a full source location.
34    pub fn at(line: usize, column: usize, message: impl Into<String>) -> Self {
35        Self {
36            line: Some(line),
37            column: Some(column),
38            message: message.into(),
39        }
40    }
41}
42
43impl std::fmt::Display for ParseError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match (self.line, self.column) {
46            (Some(l), Some(c)) => write!(f, "line {l}, col {c}: {}", self.message),
47            (Some(l), None) => write!(f, "line {l}: {}", self.message),
48            _ => write!(f, "{}", self.message),
49        }
50    }
51}
52
53/// Error returned by [`crate::try_render`].
54#[derive(Debug, Clone)]
55pub struct RenderError {
56    /// The detected diagram type (e.g. `"flowchart"`, `"unknown"`).
57    pub diagram_type: String,
58    /// Human-readable description of the failure.
59    pub message: String,
60    /// Parse errors collected during parsing, if any.
61    pub parse_errors: Vec<ParseError>,
62}
63
64impl RenderError {
65    /// Create a render error for an unrecognised diagram type.
66    pub fn unknown_type() -> Self {
67        Self {
68            diagram_type: "unknown".into(),
69            message: "Unrecognized diagram type.".into(),
70            parse_errors: vec![],
71        }
72    }
73
74    /// Create a render error from a panic message.
75    pub fn from_panic(diagram_type: impl Into<String>, message: impl Into<String>) -> Self {
76        Self {
77            diagram_type: diagram_type.into(),
78            message: message.into(),
79            parse_errors: vec![],
80        }
81    }
82}
83
84impl std::fmt::Display for RenderError {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        write!(f, "{} render error: {}", self.diagram_type, self.message)?;
87        for e in &self.parse_errors {
88            write!(f, "\n  - {e}")?;
89        }
90        Ok(())
91    }
92}
93
94impl std::error::Error for RenderError {}
95
96/// Best-effort parse output: always contains a (possibly empty) diagram
97/// plus any errors collected during parsing.
98#[derive(Debug)]
99pub struct ParseResult<T> {
100    /// The parsed diagram (may be partial if errors occurred).
101    pub diagram: T,
102    /// Errors collected during parsing.
103    pub errors: Vec<ParseError>,
104}
105
106impl<T> ParseResult<T> {
107    /// Create a successful parse result with no errors.
108    pub fn ok(diagram: T) -> Self {
109        Self {
110            diagram,
111            errors: vec![],
112        }
113    }
114
115    /// Create a parse result with errors.
116    pub fn with_errors(diagram: T, errors: Vec<ParseError>) -> Self {
117        Self { diagram, errors }
118    }
119
120    /// Return true if there are no parse errors.
121    pub fn is_ok(&self) -> bool {
122        self.errors.is_empty()
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use std::error::Error;
130
131    // ── ParseError constructors ───────────────────────────────────────────────
132
133    #[test]
134    fn parse_error_new_no_location() {
135        let e = ParseError::new("oops");
136        assert_eq!(e.message, "oops");
137        assert_eq!(e.line, None);
138        assert_eq!(e.column, None);
139        assert_eq!(e.to_string(), "oops");
140    }
141
142    #[test]
143    fn parse_error_at_line() {
144        let e = ParseError::at_line(3, "bad token");
145        assert_eq!(e.line, Some(3));
146        assert_eq!(e.column, None);
147        assert_eq!(e.to_string(), "line 3: bad token");
148    }
149
150    #[test]
151    fn parse_error_at_full_location() {
152        let e = ParseError::at(5, 12, "unexpected '}'");
153        assert_eq!(e.line, Some(5));
154        assert_eq!(e.column, Some(12));
155        assert_eq!(e.to_string(), "line 5, col 12: unexpected '}'");
156    }
157
158    // ── RenderError constructors ──────────────────────────────────────────────
159
160    #[test]
161    fn render_error_unknown_type() {
162        let e = RenderError::unknown_type();
163        assert_eq!(e.diagram_type, "unknown");
164        assert!(e.message.contains("Unrecognized"));
165        assert!(e.parse_errors.is_empty());
166    }
167
168    #[test]
169    fn render_error_from_panic() {
170        let e = RenderError::from_panic("flowchart", "index out of bounds");
171        assert_eq!(e.diagram_type, "flowchart");
172        assert_eq!(e.message, "index out of bounds");
173        assert!(e.parse_errors.is_empty());
174    }
175
176    // ── RenderError Display ───────────────────────────────────────────────────
177
178    #[test]
179    fn render_error_display_no_parse_errors() {
180        let e = RenderError::from_panic("pie", "something failed");
181        let s = e.to_string();
182        assert!(s.contains("pie"));
183        assert!(s.contains("something failed"));
184    }
185
186    #[test]
187    fn render_error_display_with_parse_errors() {
188        let mut e = RenderError::unknown_type();
189        e.parse_errors.push(ParseError::at_line(2, "bad input"));
190        let s = e.to_string();
191        assert!(s.contains("line 2: bad input"));
192    }
193
194    // ── RenderError implements std::error::Error ──────────────────────────────
195
196    #[test]
197    fn render_error_source_is_none() {
198        let e = RenderError::unknown_type();
199        assert!(e.source().is_none());
200    }
201
202    // ── ParseResult ───────────────────────────────────────────────────────────
203
204    #[test]
205    fn parse_result_ok_is_ok() {
206        let r: ParseResult<i32> = ParseResult::ok(42);
207        assert!(r.is_ok());
208        assert_eq!(r.diagram, 42);
209        assert!(r.errors.is_empty());
210    }
211
212    #[test]
213    fn parse_result_with_errors_not_ok() {
214        let errs = vec![ParseError::new("err1"), ParseError::new("err2")];
215        let r: ParseResult<&str> = ParseResult::with_errors("partial", errs);
216        assert!(!r.is_ok());
217        assert_eq!(r.errors.len(), 2);
218        assert_eq!(r.diagram, "partial");
219    }
220}