Skip to main content

ara_core/
report.rs

1//! Diagnostics produced by parsing/validation.
2//!
3//! A [`Diagnostic`] carries a **logical** path (e.g. `nodes[N07].evidence[0]`),
4//! not a source `line:column` — `serde-saphyr` does not expose reliable spans
5//! through serde, so line numbers are intentionally not promised.
6
7use serde::Serialize;
8
9/// Severity of a diagnostic.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "lowercase")]
12pub enum Severity {
13    Error,
14    Warning,
15}
16
17impl std::fmt::Display for Severity {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        match self {
20            Severity::Error => f.write_str("error"),
21            Severity::Warning => f.write_str("warning"),
22        }
23    }
24}
25
26/// A single diagnostic: severity, logical path, and message.
27#[derive(Debug, Clone, PartialEq, Serialize)]
28pub struct Diagnostic {
29    pub severity: Severity,
30    pub path: String,
31    pub message: String,
32}
33
34impl std::fmt::Display for Diagnostic {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        write!(f, "{}: {}: {}", self.severity, self.path, self.message)
37    }
38}
39
40/// The outcome of a parse: separated errors and warnings.
41///
42/// A parse "succeeds" (`is_ok`) when there are no errors — warnings do not
43/// block success but **must** still be surfaced by callers.
44#[derive(Debug, Clone, Default, PartialEq, Serialize)]
45pub struct ParseReport {
46    errors: Vec<Diagnostic>,
47    warnings: Vec<Diagnostic>,
48}
49
50impl ParseReport {
51    /// Records an error.
52    pub(crate) fn error(&mut self, path: impl Into<String>, message: impl Into<String>) {
53        self.errors.push(Diagnostic {
54            severity: Severity::Error,
55            path: path.into(),
56            message: message.into(),
57        });
58    }
59
60    /// Records a warning.
61    pub(crate) fn warn(&mut self, path: impl Into<String>, message: impl Into<String>) {
62        self.warnings.push(Diagnostic {
63            severity: Severity::Warning,
64            path: path.into(),
65            message: message.into(),
66        });
67    }
68
69    /// All errors, in the order they were recorded.
70    pub fn errors(&self) -> &[Diagnostic] {
71        &self.errors
72    }
73
74    /// All warnings, in the order they were recorded.
75    pub fn warnings(&self) -> &[Diagnostic] {
76        &self.warnings
77    }
78
79    /// True when there are no errors (warnings are allowed).
80    pub fn is_ok(&self) -> bool {
81        self.errors.is_empty()
82    }
83}
84
85impl std::fmt::Display for ParseReport {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        for d in &self.errors {
88            writeln!(f, "{d}")?;
89        }
90        for d in &self.warnings {
91            writeln!(f, "{d}")?;
92        }
93        write!(
94            f,
95            "{} error(s), {} warning(s)",
96            self.errors.len(),
97            self.warnings.len()
98        )
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn ok_and_accessors() {
108        let mut r = ParseReport::default();
109        assert!(r.is_ok());
110        r.warn("document", "empty");
111        assert!(r.is_ok()); // warnings don't block
112        assert_eq!(r.warnings().len(), 1);
113        r.error("nodes[N01]", "duplicate node id");
114        assert!(!r.is_ok());
115        assert_eq!(r.errors().len(), 1);
116    }
117
118    #[test]
119    fn diagnostic_display() {
120        let d = Diagnostic {
121            severity: Severity::Error,
122            path: "nodes[N07].evidence[0]".into(),
123            message: "unknown claim".into(),
124        };
125        assert_eq!(
126            d.to_string(),
127            "error: nodes[N07].evidence[0]: unknown claim"
128        );
129    }
130}