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"), RuleSeverity::Warn => ("warn", "\x1b[1;33m"), 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}