1use std::fmt;
7
8use crate::{Atom, LineCol, Path, Schema, SyntaxError};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum Problem {
14 Invalid,
16 Syntax,
18 Expected,
20 NotAReference,
22 WrongKind,
24 UnknownField,
26 MissingField,
28 OutOfRange,
30 BadTyped,
32 BadBinary,
34 BadId,
37 Unresolved,
39}
40
41impl Problem {
42 pub const ALL: [Problem; 12] = [
44 Problem::Invalid,
45 Problem::Syntax,
46 Problem::Expected,
47 Problem::NotAReference,
48 Problem::WrongKind,
49 Problem::UnknownField,
50 Problem::MissingField,
51 Problem::OutOfRange,
52 Problem::BadTyped,
53 Problem::BadBinary,
54 Problem::BadId,
55 Problem::Unresolved,
56 ];
57
58 #[must_use]
60 pub const fn code(self) -> &'static str {
61 match self {
62 Problem::Invalid => "SD0000",
63 Problem::Syntax => "SD0001",
64 Problem::Expected => "SD0002",
65 Problem::NotAReference => "SD0003",
66 Problem::WrongKind => "SD0004",
67 Problem::UnknownField => "SD0005",
68 Problem::MissingField => "SD0006",
69 Problem::OutOfRange => "SD0007",
70 Problem::BadTyped => "SD0008",
71 Problem::BadBinary => "SD0009",
72 Problem::BadId => "SD0010",
73 Problem::Unresolved => "SD0011",
74 }
75 }
76
77 #[must_use]
79 pub fn of_code(code: &str) -> Option<Problem> {
80 Problem::ALL.into_iter().find(|p| p.code() == code)
81 }
82}
83
84impl fmt::Display for Problem {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.write_str(self.code())
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum Code {
95 Data(Problem),
97 Tool(Atom),
99}
100
101impl Code {
102 pub fn tool(code: Atom) -> Code {
104 Problem::of_code(code.as_str()).map_or(Code::Tool(code), Code::Data)
105 }
106
107 #[must_use]
109 pub fn as_str(self) -> &'static str {
110 match self {
111 Code::Data(p) => p.code(),
112 Code::Tool(a) => a.as_str(),
113 }
114 }
115
116 fn atom(self) -> Atom {
118 match self {
119 Code::Data(p) => Atom::new(p.code()),
120 Code::Tool(a) => a,
121 }
122 }
123}
124
125impl From<Problem> for Code {
126 fn from(p: Problem) -> Code {
127 Code::Data(p)
128 }
129}
130
131impl From<Atom> for Code {
132 fn from(a: Atom) -> Code {
133 Code::tool(a)
134 }
135}
136
137impl fmt::Display for Code {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 f.write_str(self.as_str())
140 }
141}
142
143impl PartialEq<Problem> for Code {
144 fn eq(&self, p: &Problem) -> bool {
145 *self == Code::Data(*p)
146 }
147}
148
149impl Schema for Code {
151 fn shape() -> crate::Shape {
152 Atom::shape()
153 }
154 fn read(v: &crate::ValueRef<'_>, cx: &mut crate::Reader<'_>) -> Option<Self> {
155 Atom::read(v, cx).map(Code::tool)
156 }
157 fn write(&self) -> crate::Value {
158 self.atom().write()
159 }
160 fn encode(&self, w: &mut crate::binary::Writer) {
161 self.atom().encode(w);
162 }
163 fn decode(d: &mut crate::binary::Decoder<'_>) -> Option<Self> {
164 Atom::decode(d).map(Code::tool)
165 }
166 fn pack(&self, p: &mut crate::frame::Packer) {
167 self.atom().pack(p);
168 }
169 fn unpack(u: &mut crate::frame::Unpacker<'_>) -> Option<Self> {
170 Atom::unpack(u).map(Code::tool)
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Schema)]
176pub enum Severity {
177 #[default]
179 Error,
180 Warning,
182 Note,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Default, Schema)]
188#[data(rename_all = "camelCase")]
189pub struct Diagnostic {
190 pub severity: Severity,
192 pub code: Option<Code>,
195 pub message: String,
197 pub file: Option<Atom>,
199 #[data(default)]
201 pub path: Path,
202 #[data(default)]
204 pub on_key: bool,
205 pub line: Option<u32>,
207 pub col: Option<u32>,
209 pub end_line: Option<u32>,
211 pub end_col: Option<u32>,
213 pub tool: Option<Atom>,
215}
216
217impl Diagnostic {
218 pub fn error(code: impl Into<Code>, message: impl fmt::Display) -> Self {
220 Self::of(Severity::Error, code, message)
221 }
222
223 pub fn warning(code: impl Into<Code>, message: impl fmt::Display) -> Self {
225 Self::of(Severity::Warning, code, message)
226 }
227
228 pub fn of(severity: Severity, code: impl Into<Code>, message: impl fmt::Display) -> Self {
230 Diagnostic {
231 severity,
232 code: Some(code.into()),
233 message: message.to_string(),
234 ..Self::default()
235 }
236 }
237
238 #[must_use]
240 pub fn syntax(e: SyntaxError) -> Self {
241 Self::error(Problem::Syntax, e.message).at(e.at)
242 }
243
244 #[must_use]
246 pub fn in_file(mut self, file: impl fmt::Display) -> Self {
247 self.file = Some(Atom::new(&file.to_string()));
248 self
249 }
250
251 #[must_use]
253 pub fn at_line(mut self, line: usize) -> Self {
254 self.line = u32::try_from(line).ok();
255 self
256 }
257
258 #[must_use]
260 pub fn at(mut self, at: LineCol) -> Self {
261 self.line = Some(at.line);
262 self.col = Some(at.col);
263 self
264 }
265
266 #[must_use]
268 pub fn at_path(mut self, path: Path) -> Self {
269 self.path = path;
270 self
271 }
272
273 #[must_use]
275 pub fn by(mut self, tool: Atom) -> Self {
276 self.tool = Some(tool);
277 self
278 }
279
280 #[must_use]
282 pub fn or_code(mut self, code: impl Into<Code>) -> Self {
283 self.code.get_or_insert(code.into());
284 self
285 }
286
287 #[must_use]
289 pub fn map(mut self, wrap: impl FnOnce(String) -> String) -> Self {
290 self.message = wrap(self.message);
291 self
292 }
293
294 #[must_use]
296 pub fn is_error(&self) -> bool {
297 self.severity == Severity::Error
298 }
299}
300
301impl fmt::Display for Diagnostic {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 match (&self.file, self.line, self.col) {
305 (Some(file), Some(line), Some(col)) => write!(f, "{file}:{line}:{col}: ")?,
306 (Some(file), Some(line), None) => write!(f, "{file}:{line}: ")?,
307 (Some(file), None, _) => write!(f, "{file}: ")?,
308 (None, Some(line), Some(col)) => write!(f, "{line}:{col}: ")?,
309 (None, Some(line), None) => write!(f, "line {line}: ")?,
310 (None, None, _) => {}
311 }
312 if !self.path.is_root() {
313 write!(f, "{}: ", self.path)?;
314 }
315 f.write_str(&self.message)
316 }
317}
318
319impl std::error::Error for Diagnostic {}
320
321impl From<String> for Diagnostic {
323 fn from(message: String) -> Self {
324 Diagnostic {
325 message,
326 ..Self::default()
327 }
328 }
329}
330
331impl From<&str> for Diagnostic {
332 fn from(message: &str) -> Self {
333 message.to_owned().into()
334 }
335}
336
337impl From<Diagnostic> for String {
339 fn from(d: Diagnostic) -> Self {
340 d.to_string()
341 }
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct Errors(pub Vec<Diagnostic>);
347
348impl fmt::Display for Errors {
349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350 for (i, e) in self.0.iter().enumerate() {
351 if i > 0 {
352 f.write_str("\n")?;
353 }
354 write!(f, "{e}")?;
355 }
356 Ok(())
357 }
358}
359
360impl std::error::Error for Errors {}
361
362impl From<SyntaxError> for Errors {
363 fn from(e: SyntaxError) -> Self {
364 Errors(vec![Diagnostic::syntax(e)])
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn said_with_what_is_known_and_a_frame_reads_back() {
374 let d = Diagnostic::error(Atom::new("XY0002"), "no owner")
375 .in_file("docs/intro.doc")
376 .at_line(3)
377 .at_path(Path::root().key("owner"));
378 assert_eq!(d.to_string(), "docs/intro.doc:3: owner: no owner");
379 let mut bytes = Vec::new();
380 crate::io::write_frame(&mut bytes, &d).unwrap();
381 let back: Diagnostic = crate::io::read_frame(&mut bytes.as_slice())
382 .unwrap()
383 .unwrap();
384 assert_eq!(back, d);
385 assert_eq!(
386 Diagnostic::from("bad")
387 .or_code(Atom::new("XY0004"))
388 .code
389 .map(Code::as_str),
390 Some("XY0004")
391 );
392 let missing = Diagnostic::error(Problem::MissingField, "missing `id`");
394 let mut framed = Vec::new();
395 crate::io::write_frame(&mut framed, &missing).unwrap();
396 let read_back: Diagnostic = crate::io::read_frame(&mut framed.as_slice())
397 .unwrap()
398 .unwrap();
399 assert_eq!(read_back.code, Some(Code::Data(Problem::MissingField)));
400 assert_eq!(Code::tool(Atom::new("SD0006")), Problem::MissingField);
401 assert!(
402 Problem::ALL
403 .iter()
404 .all(|p| Problem::of_code(p.code()) == Some(*p))
405 );
406 }
407}