Skip to main content

merman_core/
error.rs

1use crate::{SourceSpan, detect::DetectTypeError};
2
3pub type Result<T> = std::result::Result<T, Error>;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ParseDiagnosticSpanKind {
7    Exact,
8    InsertionPoint,
9    Fallback,
10}
11
12/// Structured parser diagnostic carried by [`Error::DiagramParse`].
13///
14/// `message()` is the human-readable text that older callers previously matched through a
15/// top-level `Error::DiagramParse { message, .. }` field. Prefer the structured accessors when an
16/// integration can use spans or stable diagnostic codes.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ParseDiagnostic {
19    message: String,
20    span: Option<SourceSpan>,
21    span_kind: ParseDiagnosticSpanKind,
22    code: Option<String>,
23}
24
25#[cfg(any(
26    feature = "diagram-class",
27    feature = "diagram-er",
28    feature = "diagram-flowchart",
29    feature = "diagram-swimlane",
30    feature = "diagram-sequence",
31    feature = "diagram-state"
32))]
33pub(crate) trait ParseErrorSourceSpan {
34    fn source_span(&self) -> Option<SourceSpan>;
35}
36
37#[cfg(any(
38    feature = "diagram-class",
39    feature = "diagram-er",
40    feature = "diagram-flowchart",
41    feature = "diagram-swimlane",
42    feature = "diagram-sequence",
43    feature = "diagram-state"
44))]
45impl ParseErrorSourceSpan for String {
46    fn source_span(&self) -> Option<SourceSpan> {
47        None
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::{Error, ParseDiagnostic, ParseDiagnosticSpanKind};
54    use crate::SourceSpan;
55
56    #[test]
57    fn diagram_parse_fallback_builds_structured_fallback_diagnostic() {
58        let error = Error::diagram_parse_fallback("flowchart", "bad syntax");
59        let display = error.to_string();
60
61        let Error::DiagramParse {
62            diagram_type,
63            diagnostic,
64        } = error
65        else {
66            panic!("expected diagram parse error");
67        };
68
69        assert_eq!(diagram_type, "flowchart");
70        assert_eq!(diagnostic.message(), "bad syntax");
71        assert_eq!(diagnostic.span(), None);
72        assert_eq!(diagnostic.span_kind(), ParseDiagnosticSpanKind::Fallback);
73        assert_eq!(diagnostic.code(), None);
74        assert_eq!(display, "Diagram parse error (flowchart): bad syntax");
75    }
76
77    #[test]
78    fn diagram_parse_diagnostic_preserves_structured_metadata() {
79        let span = SourceSpan::new(2, 5);
80        let error = Error::diagram_parse_diagnostic(
81            "state",
82            ParseDiagnostic::new("unexpected token")
83                .with_span(span, ParseDiagnosticSpanKind::Exact)
84                .with_code("merman.test"),
85        );
86
87        let Error::DiagramParse {
88            diagram_type,
89            diagnostic,
90        } = error
91        else {
92            panic!("expected diagram parse error");
93        };
94
95        assert_eq!(diagram_type, "state");
96        assert_eq!(diagnostic.message(), "unexpected token");
97        assert_eq!(diagnostic.span(), Some(span));
98        assert_eq!(diagnostic.span_kind(), ParseDiagnosticSpanKind::Exact);
99        assert_eq!(diagnostic.code(), Some("merman.test"));
100    }
101}
102
103impl ParseDiagnostic {
104    pub fn new(message: impl Into<String>) -> Self {
105        Self {
106            message: message.into(),
107            span: None,
108            span_kind: ParseDiagnosticSpanKind::Fallback,
109            code: None,
110        }
111    }
112
113    pub fn with_span(mut self, span: SourceSpan, span_kind: ParseDiagnosticSpanKind) -> Self {
114        self.span = Some(span);
115        self.span_kind = span_kind;
116        self
117    }
118
119    pub fn with_code(mut self, code: impl Into<String>) -> Self {
120        self.code = Some(code.into());
121        self
122    }
123
124    pub fn map_span(mut self, map: impl FnOnce(SourceSpan) -> SourceSpan) -> Self {
125        self.span = self.span.map(map);
126        self
127    }
128
129    pub(crate) fn without_span(mut self) -> Self {
130        self.span = None;
131        self.span_kind = ParseDiagnosticSpanKind::Fallback;
132        self
133    }
134
135    pub fn message(&self) -> &str {
136        &self.message
137    }
138
139    pub fn span(&self) -> Option<SourceSpan> {
140        self.span
141    }
142
143    pub fn span_kind(&self) -> ParseDiagnosticSpanKind {
144        self.span_kind
145    }
146
147    pub fn code(&self) -> Option<&str> {
148        self.code.as_deref()
149    }
150}
151
152#[derive(Debug, thiserror::Error)]
153pub enum Error {
154    #[error(transparent)]
155    OperationCancelled(#[from] crate::OperationCancelled),
156
157    #[error(transparent)]
158    ThemeColor(#[from] crate::theme_color::ColorError),
159
160    #[error(transparent)]
161    RuntimePolicy(#[from] crate::runtime::RuntimePolicyError),
162
163    #[error(transparent)]
164    DetectType(#[from] DetectTypeError),
165
166    #[error("Unsupported diagram type: {diagram_type}")]
167    UnsupportedDiagram { diagram_type: String },
168
169    /// Parser failure for a detected diagram.
170    ///
171    /// This API now carries `diagnostic: ParseDiagnostic` instead of a top-level `message`
172    /// field. Call `diagnostic.message()` for the display text and `diagnostic.span()` /
173    /// `diagnostic.span_kind()` when projecting editor or CLI diagnostics.
174    #[error("Diagram parse error ({diagram_type}): {}", diagnostic.message())]
175    DiagramParse {
176        diagram_type: String,
177        diagnostic: ParseDiagnostic,
178    },
179
180    #[error(
181        "Malformed YAML front-matter. If you were trying to use a YAML front-matter, please ensure that you've correctly opened and closed the YAML front-matter with un-indented `---` blocks"
182    )]
183    MalformedFrontMatter,
184
185    #[error("Invalid directive JSON: {message}")]
186    InvalidDirectiveJson { message: String },
187
188    #[error("Invalid YAML front-matter: {message}")]
189    InvalidFrontMatterYaml { message: String },
190}
191
192impl Error {
193    #[cfg(any(
194        feature = "diagram-git-graph",
195        feature = "diagram-radar",
196        feature = "diagram-mindmap"
197    ))]
198    pub(crate) fn with_exact_span_if_missing(self, span: SourceSpan) -> Self {
199        match self {
200            Self::DiagramParse {
201                diagram_type,
202                diagnostic,
203            } if diagnostic.span().is_none() => Self::DiagramParse {
204                diagram_type,
205                diagnostic: diagnostic.with_span(span, ParseDiagnosticSpanKind::Exact),
206            },
207            other => other,
208        }
209    }
210
211    pub fn diagram_parse_fallback(
212        diagram_type: impl Into<String>,
213        message: impl Into<String>,
214    ) -> Self {
215        Self::diagram_parse_diagnostic(diagram_type, ParseDiagnostic::new(message))
216    }
217
218    pub fn diagram_parse_exact(
219        diagram_type: impl Into<String>,
220        message: impl Into<String>,
221        span: SourceSpan,
222    ) -> Self {
223        Self::diagram_parse_diagnostic(
224            diagram_type,
225            ParseDiagnostic::new(message).with_span(span, ParseDiagnosticSpanKind::Exact),
226        )
227    }
228
229    pub fn diagram_parse_insertion_point(
230        diagram_type: impl Into<String>,
231        message: impl Into<String>,
232        offset: usize,
233    ) -> Self {
234        Self::diagram_parse_diagnostic(
235            diagram_type,
236            ParseDiagnostic::new(message).with_span(
237                SourceSpan::new(offset, offset),
238                ParseDiagnosticSpanKind::InsertionPoint,
239            ),
240        )
241    }
242
243    pub fn diagram_parse_fallback_at(
244        diagram_type: impl Into<String>,
245        message: impl Into<String>,
246        span: SourceSpan,
247    ) -> Self {
248        Self::diagram_parse_diagnostic(
249            diagram_type,
250            ParseDiagnostic::new(message).with_span(span, ParseDiagnosticSpanKind::Fallback),
251        )
252    }
253
254    pub fn diagram_parse_diagnostic(
255        diagram_type: impl Into<String>,
256        diagnostic: ParseDiagnostic,
257    ) -> Self {
258        Self::DiagramParse {
259            diagram_type: diagram_type.into(),
260            diagnostic,
261        }
262    }
263}