1use std::fmt;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum DiagnosticLevel {
26 Fatal,
27 Warning,
28 Info,
29}
30
31impl fmt::Display for DiagnosticLevel {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 DiagnosticLevel::Fatal => write!(f, "error"),
35 DiagnosticLevel::Warning => write!(f, "warning"),
36 DiagnosticLevel::Info => write!(f, "info"),
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum DiagnosticCode {
43 EmptyFile,
44 MissingHeader,
45 InvalidUtf8,
46 TruncatedJson,
47
48 MissingHeaderField,
49 UnsupportedVersion,
50 InvalidTimestamp,
51 InvalidInitialState,
52
53 InvalidEventJson,
54 UnknownEventType,
55 WrongFieldCount,
56 WrongFieldType,
57
58 NonExistentObservationId,
59 DuplicateObservationId,
60
61 ChangeCountMismatch,
62 InvalidChangeCount,
63
64 InvalidPointerSyntax,
65 PathNotFound,
66 InvalidArrayIndex,
67 ArrayIndexOutOfBounds,
68 ParentPathNotFound,
69
70 TypeMismatch,
71 OldValueMismatch,
72
73 MoveOnNonArray,
74 MoveIndexOutOfBounds,
75 InvalidMoveIndex,
76
77 SnapshotStateMismatch,
78 SnapshotTimestampOrder,
79}
80
81impl DiagnosticCode {
82 pub fn as_str(&self) -> &'static str {
83 match self {
84 DiagnosticCode::EmptyFile => "E001",
85 DiagnosticCode::MissingHeader => "E002",
86 DiagnosticCode::InvalidUtf8 => "E003",
87 DiagnosticCode::TruncatedJson => "E004",
88
89 DiagnosticCode::MissingHeaderField => "E010",
90 DiagnosticCode::UnsupportedVersion => "E011",
91 DiagnosticCode::InvalidTimestamp => "W012",
92 DiagnosticCode::InvalidInitialState => "E013",
93
94 DiagnosticCode::InvalidEventJson => "E020",
95 DiagnosticCode::UnknownEventType => "W021",
96 DiagnosticCode::WrongFieldCount => "E022",
97 DiagnosticCode::WrongFieldType => "E023",
98
99 DiagnosticCode::NonExistentObservationId => "E030",
100 DiagnosticCode::DuplicateObservationId => "W031",
101
102 DiagnosticCode::ChangeCountMismatch => "W040",
103 DiagnosticCode::InvalidChangeCount => "E041",
104
105 DiagnosticCode::InvalidPointerSyntax => "E050",
106 DiagnosticCode::PathNotFound => "E051",
107 DiagnosticCode::InvalidArrayIndex => "E052",
108 DiagnosticCode::ArrayIndexOutOfBounds => "E053",
109 DiagnosticCode::ParentPathNotFound => "E054",
110
111 DiagnosticCode::TypeMismatch => "E060",
112 DiagnosticCode::OldValueMismatch => "W061",
113
114 DiagnosticCode::MoveOnNonArray => "E070",
115 DiagnosticCode::MoveIndexOutOfBounds => "E071",
116 DiagnosticCode::InvalidMoveIndex => "E072",
117
118 DiagnosticCode::SnapshotStateMismatch => "W080",
119 DiagnosticCode::SnapshotTimestampOrder => "W081",
120 }
121 }
122
123 pub fn title(&self) -> &'static str {
124 match self {
125 DiagnosticCode::EmptyFile => "Empty file",
126 DiagnosticCode::MissingHeader => "Missing header",
127 DiagnosticCode::InvalidUtf8 => "Invalid UTF-8 encoding",
128 DiagnosticCode::TruncatedJson => "Truncated JSON",
129
130 DiagnosticCode::MissingHeaderField => "Missing required header field",
131 DiagnosticCode::UnsupportedVersion => "Unsupported version",
132 DiagnosticCode::InvalidTimestamp => "Invalid timestamp",
133 DiagnosticCode::InvalidInitialState => "Invalid initial state",
134
135 DiagnosticCode::InvalidEventJson => "Invalid event JSON",
136 DiagnosticCode::UnknownEventType => "Unknown event type",
137 DiagnosticCode::WrongFieldCount => "Wrong field count",
138 DiagnosticCode::WrongFieldType => "Wrong field type",
139
140 DiagnosticCode::NonExistentObservationId => "Non-existent observation ID",
141 DiagnosticCode::DuplicateObservationId => "Duplicate observation ID",
142
143 DiagnosticCode::ChangeCountMismatch => "Change count mismatch",
144 DiagnosticCode::InvalidChangeCount => "Invalid change count",
145
146 DiagnosticCode::InvalidPointerSyntax => "Invalid JSON Pointer syntax",
147 DiagnosticCode::PathNotFound => "Path not found",
148 DiagnosticCode::InvalidArrayIndex => "Invalid array index",
149 DiagnosticCode::ArrayIndexOutOfBounds => "Array index out of bounds",
150 DiagnosticCode::ParentPathNotFound => "Parent path not found",
151
152 DiagnosticCode::TypeMismatch => "Type mismatch",
153 DiagnosticCode::OldValueMismatch => "Old value mismatch",
154
155 DiagnosticCode::MoveOnNonArray => "Move operation on non-array",
156 DiagnosticCode::MoveIndexOutOfBounds => "Move index out of bounds",
157 DiagnosticCode::InvalidMoveIndex => "Invalid move index",
158
159 DiagnosticCode::SnapshotStateMismatch => "Snapshot state mismatch",
160 DiagnosticCode::SnapshotTimestampOrder => "Snapshot timestamp out of order",
161 }
162 }
163}
164
165#[derive(Debug, Clone)]
166pub struct Diagnostic {
167 pub filename: Option<String>,
168 pub line_number: Option<usize>,
169 pub column: Option<usize>,
170 pub level: DiagnosticLevel,
171 pub code: DiagnosticCode,
172 pub description: String,
173 pub code_snippet: Option<String>,
174 pub advice: Option<String>,
175}
176
177impl Diagnostic {
178 pub fn new(level: DiagnosticLevel, code: DiagnosticCode, description: String) -> Self {
179 Self {
180 filename: None,
181 line_number: None,
182 column: None,
183 level,
184 code,
185 description,
186 code_snippet: None,
187 advice: None,
188 }
189 }
190
191 #[inline]
192 pub fn fatal(code: DiagnosticCode, description: String) -> Self {
193 Self::new(DiagnosticLevel::Fatal, code, description)
194 }
195
196 pub fn with_location(mut self, filename: String, line_number: usize) -> Self {
197 self.filename = Some(filename);
198 self.line_number = Some(line_number);
199 self
200 }
201
202 pub fn with_column(mut self, column: usize) -> Self {
203 self.column = Some(column);
204 self
205 }
206
207 pub fn with_snippet(mut self, snippet: String) -> Self {
208 self.code_snippet = Some(snippet);
209 self
210 }
211
212 pub fn with_advice(mut self, advice: String) -> Self {
213 self.advice = Some(advice);
214 self
215 }
216
217 pub fn is_fatal(&self) -> bool {
218 self.level == DiagnosticLevel::Fatal
219 }
220}
221
222impl From<Diagnostic> for Vec<Diagnostic> {
223 fn from(diagnostic: Diagnostic) -> Self {
224 vec![diagnostic]
225 }
226}
227
228impl fmt::Display for Diagnostic {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 if let (Some(filename), Some(line)) = (&self.filename, self.line_number) {
231 if let Some(col) = self.column {
232 write!(f, "{}:{}:{} - ", filename, line, col)?;
233 } else {
234 write!(f, "{}:{} - ", filename, line)?;
235 }
236 }
237
238 writeln!(
239 f,
240 "{} {}: {}",
241 self.level,
242 self.code.as_str(),
243 self.code.title()
244 )?;
245 writeln!(f)?;
246 writeln!(f, "{}", self.description)?;
247
248 if let Some(snippet) = &self.code_snippet {
249 writeln!(f)?;
250 writeln!(f, "{}", snippet)?;
251 }
252
253 if let Some(advice) = &self.advice {
254 writeln!(f)?;
255 writeln!(f, "{}", advice)?;
256 }
257
258 Ok(())
259 }
260}
261
262#[derive(Debug, Default)]
263pub struct DiagnosticCollector {
264 diagnostics: Vec<Diagnostic>,
265}
266
267impl DiagnosticCollector {
268 pub fn new() -> Self {
269 Self {
270 diagnostics: Vec::new(),
271 }
272 }
273
274 pub fn add(&mut self, diagnostic: Diagnostic) {
275 self.diagnostics.push(diagnostic);
276 }
277
278 pub fn has_fatal(&self) -> bool {
279 self.diagnostics.iter().any(|d| d.is_fatal())
280 }
281
282 pub fn diagnostics(&self) -> &[Diagnostic] {
283 &self.diagnostics
284 }
285
286 pub fn into_diagnostics(self) -> Vec<Diagnostic> {
287 self.diagnostics
288 }
289
290 pub fn is_empty(&self) -> bool {
291 self.diagnostics.is_empty()
292 }
293
294 pub fn len(&self) -> usize {
295 self.diagnostics.len()
296 }
297}