1use std::fmt;
7
8use crate::{Atom, LineCol, Path, Schema, SyntaxError};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Schema)]
12pub enum Severity {
13 #[default]
14 Error,
15 Warning,
16 Note,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Default, Schema)]
21#[data(rename_all = "camelCase")]
22pub struct Diagnostic {
23 pub severity: Severity,
24 pub code: Option<Atom>,
27 pub message: String,
28 pub file: Option<Atom>,
30 #[data(default)]
32 pub path: Path,
33 #[data(default)]
35 pub on_key: bool,
36 pub line: Option<u32>,
38 pub col: Option<u32>,
39 pub end_line: Option<u32>,
40 pub end_col: Option<u32>,
41 pub tool: Option<Atom>,
43}
44
45impl Diagnostic {
46 pub fn error(code: &str, message: impl fmt::Display) -> Self {
48 Self::of(Severity::Error, code, message)
49 }
50
51 pub fn warning(code: &str, message: impl fmt::Display) -> Self {
53 Self::of(Severity::Warning, code, message)
54 }
55
56 pub fn of(severity: Severity, code: &str, message: impl fmt::Display) -> Self {
57 Diagnostic {
58 severity,
59 code: Some(Atom::new(code)),
60 message: message.to_string(),
61 ..Self::default()
62 }
63 }
64
65 pub fn syntax(e: SyntaxError) -> Self {
67 Self::error(yog_ids::data_problem::SYNTAX, e.message).at(e.at)
68 }
69
70 #[must_use]
72 pub fn in_file(mut self, file: impl fmt::Display) -> Self {
73 self.file = Some(Atom::new(&file.to_string()));
74 self
75 }
76
77 #[must_use]
79 pub fn at_line(mut self, line: usize) -> Self {
80 self.line = u32::try_from(line).ok();
81 self
82 }
83
84 #[must_use]
86 pub fn at(mut self, at: LineCol) -> Self {
87 self.line = Some(at.line);
88 self.col = Some(at.col);
89 self
90 }
91
92 #[must_use]
94 pub fn at_path(mut self, path: Path) -> Self {
95 self.path = path;
96 self
97 }
98
99 #[must_use]
101 pub fn by(mut self, tool: &str) -> Self {
102 self.tool = Some(Atom::new(tool));
103 self
104 }
105
106 #[must_use]
108 pub fn or_code(mut self, code: &str) -> Self {
109 self.code.get_or_insert_with(|| Atom::new(code));
110 self
111 }
112
113 #[must_use]
115 pub fn map(mut self, wrap: impl FnOnce(String) -> String) -> Self {
116 self.message = wrap(self.message);
117 self
118 }
119
120 pub fn is_error(&self) -> bool {
121 self.severity == Severity::Error
122 }
123}
124
125impl fmt::Display for Diagnostic {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match (&self.file, self.line, self.col) {
129 (Some(file), Some(line), Some(col)) => write!(f, "{file}:{line}:{col}: ")?,
130 (Some(file), Some(line), None) => write!(f, "{file}:{line}: ")?,
131 (Some(file), None, _) => write!(f, "{file}: ")?,
132 (None, Some(line), Some(col)) => write!(f, "{line}:{col}: ")?,
133 (None, Some(line), None) => write!(f, "line {line}: ")?,
134 (None, None, _) => {}
135 }
136 if !self.path.is_root() {
137 write!(f, "{}: ", self.path)?;
138 }
139 f.write_str(&self.message)
140 }
141}
142
143impl std::error::Error for Diagnostic {}
144
145impl From<String> for Diagnostic {
147 fn from(message: String) -> Self {
148 Diagnostic {
149 message,
150 ..Self::default()
151 }
152 }
153}
154
155impl From<&str> for Diagnostic {
156 fn from(message: &str) -> Self {
157 message.to_owned().into()
158 }
159}
160
161impl From<Diagnostic> for String {
163 fn from(d: Diagnostic) -> Self {
164 d.to_string()
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct Errors(pub Vec<Diagnostic>);
171
172impl fmt::Display for Errors {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 for (i, e) in self.0.iter().enumerate() {
175 if i > 0 {
176 f.write_str("\n")?;
177 }
178 write!(f, "{e}")?;
179 }
180 Ok(())
181 }
182}
183
184impl std::error::Error for Errors {}
185
186impl From<SyntaxError> for Errors {
187 fn from(e: SyntaxError) -> Self {
188 Errors(vec![Diagnostic::syntax(e)])
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn said_with_what_is_known_and_a_frame_reads_back() {
198 let d = Diagnostic::error("YB0002", "no model")
199 .in_file("content/ruby.yblock")
200 .at_line(3)
201 .at_path(Path::root().key("model"));
202 assert_eq!(d.to_string(), "content/ruby.yblock:3: model: no model");
203 let mut bytes = Vec::new();
204 crate::io::write_frame(&mut bytes, &d).unwrap();
205 let back: Diagnostic = crate::io::read_frame(&mut bytes.as_slice())
206 .unwrap()
207 .unwrap();
208 assert_eq!(back, d);
209 assert_eq!(
210 Diagnostic::from("bad").or_code("YB0004").code.as_deref(),
211 Some("YB0004")
212 );
213 }
214}