Skip to main content

drft/
diagnostic.rs

1use crate::config::RuleSeverity;
2use serde::Serialize;
3
4#[derive(Debug, Clone, Serialize)]
5pub struct Diagnostic {
6    pub rule: String,
7    pub severity: RuleSeverity,
8    pub message: String,
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub source: Option<String>,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub target: Option<String>,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub node: Option<String>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub via: Option<String>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub path: Option<Vec<String>>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub fix: Option<String>,
21}
22
23impl Default for Diagnostic {
24    fn default() -> Self {
25        Diagnostic {
26            rule: String::new(),
27            severity: RuleSeverity::Warn,
28            message: String::new(),
29            source: None,
30            target: None,
31            node: None,
32            via: None,
33            path: None,
34            fix: None,
35        }
36    }
37}
38
39impl Diagnostic {
40    pub fn format_text(&self) -> String {
41        let severity = match self.severity {
42            RuleSeverity::Error => "error",
43            RuleSeverity::Warn => "warn",
44            RuleSeverity::Off => "off",
45        };
46
47        match (&self.source, &self.target) {
48            (Some(source), Some(target)) => {
49                let via_suffix = self
50                    .via
51                    .as_ref()
52                    .map(|v| format!(" via {v}"))
53                    .unwrap_or_default();
54                format!(
55                    "{severity}[{}]: {source} \u{2192} {target} ({}{})",
56                    self.rule, self.message, via_suffix
57                )
58            }
59            _ if self.path.is_some() => {
60                let path = self.path.as_ref().unwrap();
61                let cycle = path.join(" \u{2192} ");
62                format!("{severity}[{}]: {}: {cycle}", self.rule, self.message)
63            }
64            _ if self.node.is_some() => {
65                let node = self.node.as_ref().unwrap();
66                match &self.via {
67                    Some(via) => {
68                        format!("{severity}[{}]: {node} ({} {via})", self.rule, self.message)
69                    }
70                    None => format!("{severity}[{}]: {node} ({})", self.rule, self.message),
71                }
72            }
73            _ => format!("{severity}[{}]: {}", self.rule, self.message),
74        }
75    }
76
77    pub fn format_text_color(&self) -> String {
78        let (severity_str, color) = match self.severity {
79            RuleSeverity::Error => ("error", "\x1b[1;31m"), // bold red
80            RuleSeverity::Warn => ("warn", "\x1b[1;33m"),   // bold yellow
81            RuleSeverity::Off => ("off", "\x1b[0m"),
82        };
83        let reset = "\x1b[0m";
84        let bold = "\x1b[1m";
85        let cyan = "\x1b[36m";
86
87        match (&self.source, &self.target) {
88            (Some(source), Some(target)) => {
89                let via_suffix = self
90                    .via
91                    .as_ref()
92                    .map(|v| format!(" via {cyan}{v}{reset}"))
93                    .unwrap_or_default();
94                format!(
95                    "{color}{severity_str}{reset}[{bold}{}{reset}]: {cyan}{source}{reset} \u{2192} {cyan}{target}{reset} ({}{})",
96                    self.rule, self.message, via_suffix
97                )
98            }
99            _ if self.path.is_some() => {
100                let path = self.path.as_ref().unwrap();
101                let cycle = path
102                    .iter()
103                    .map(|p| format!("{cyan}{p}{reset}"))
104                    .collect::<Vec<_>>()
105                    .join(" \u{2192} ");
106                format!(
107                    "{color}{severity_str}{reset}[{bold}{}{reset}]: {}: {cycle}",
108                    self.rule, self.message
109                )
110            }
111            _ if self.node.is_some() => {
112                let node = self.node.as_ref().unwrap();
113                match &self.via {
114                    Some(via) => format!(
115                        "{color}{severity_str}{reset}[{bold}{}{reset}]: {cyan}{node}{reset} ({} {cyan}{via}{reset})",
116                        self.rule, self.message
117                    ),
118                    None => format!(
119                        "{color}{severity_str}{reset}[{bold}{}{reset}]: {cyan}{node}{reset} ({})",
120                        self.rule, self.message
121                    ),
122                }
123            }
124            _ => format!(
125                "{color}{severity_str}{reset}[{bold}{}{reset}]: {}",
126                self.rule, self.message
127            ),
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn text_format_edge_rule() {
138        let d = Diagnostic {
139            rule: "unresolved-edge".into(),
140            severity: RuleSeverity::Warn,
141            message: "file not found".into(),
142            source: Some("index.md".into()),
143            target: Some("gone.md".into()),
144            ..Default::default()
145        };
146        assert_eq!(
147            d.format_text(),
148            "warn[unresolved-edge]: index.md \u{2192} gone.md (file not found)"
149        );
150    }
151
152    #[test]
153    fn json_serialization() {
154        let d = Diagnostic {
155            rule: "unresolved-edge".into(),
156            severity: RuleSeverity::Warn,
157            message: "file not found".into(),
158            source: Some("index.md".into()),
159            target: Some("gone.md".into()),
160            ..Default::default()
161        };
162        let json = serde_json::to_string(&d).unwrap();
163        assert!(json.contains("\"rule\":\"unresolved-edge\""));
164        assert!(json.contains("\"severity\":\"warn\""));
165        assert!(json.contains("\"source\":\"index.md\""));
166        assert!(json.contains("\"target\":\"gone.md\""));
167        assert!(!json.contains("\"node\""));
168    }
169}