1use crate::prelude::{String, Vec};
23use core::fmt;
24
25#[cfg(feature = "miette")]
26use crate::spanned::Spanned;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33#[non_exhaustive]
34pub enum DiagnosticCode {
35 Parse,
37 Serialize,
39 Deserialize,
41 TypeMismatch,
43 MissingField,
45 UnknownField,
47 RecursionLimit,
49 RepetitionLimit,
51 Budget,
53 UnknownAnchor,
55 DuplicateKey,
57 KeyCollision,
59 IntegerOverflow,
61 NonScalarKey,
63 EndOfStream,
65 MoreThanOneDocument,
67 Io,
69 Other,
71}
72
73impl DiagnosticCode {
74 #[must_use]
76 pub const fn as_str(self) -> &'static str {
77 match self {
78 Self::Parse => "noyalib::parse",
79 Self::Serialize => "noyalib::serialize",
80 Self::Deserialize => "noyalib::deserialize",
81 Self::TypeMismatch => "noyalib::type_mismatch",
82 Self::MissingField => "noyalib::missing_field",
83 Self::UnknownField => "noyalib::unknown_field",
84 Self::RecursionLimit => "noyalib::recursion_limit",
85 Self::RepetitionLimit => "noyalib::repetition_limit",
86 Self::Budget => "noyalib::budget",
87 Self::UnknownAnchor => "noyalib::unknown_anchor",
88 Self::DuplicateKey => "noyalib::duplicate_key",
89 Self::KeyCollision => "noyalib::key_collision",
90 Self::IntegerOverflow => "noyalib::integer_overflow",
91 Self::NonScalarKey => "noyalib::non_scalar_key",
92 Self::EndOfStream => "noyalib::eof",
93 Self::MoreThanOneDocument => "noyalib::multi_document",
94 Self::Io => "noyalib::io",
95 Self::Other => "noyalib::error",
96 }
97 }
98}
99
100impl fmt::Display for DiagnosticCode {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.write_str(self.as_str())
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108#[non_exhaustive]
109pub enum DiagnosticSeverity {
110 Error,
112 Warning,
114 Information,
116 Hint,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
122pub struct SourceSpan {
123 offset: usize,
124 length: usize,
125}
126
127impl SourceSpan {
128 #[must_use]
130 pub const fn new(offset: usize, length: usize) -> Self {
131 Self { offset, length }
132 }
133
134 #[must_use]
136 pub const fn offset(self) -> usize {
137 self.offset
138 }
139
140 #[must_use]
142 pub const fn length(self) -> usize {
143 self.length
144 }
145
146 #[must_use]
148 pub const fn end(self) -> usize {
149 self.offset.saturating_add(self.length)
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct DiagnosticLabel {
156 span: SourceSpan,
157 message: String,
158 primary: bool,
159}
160
161impl DiagnosticLabel {
162 #[must_use]
164 pub fn primary(span: SourceSpan, message: impl Into<String>) -> Self {
165 Self {
166 span,
167 message: message.into(),
168 primary: true,
169 }
170 }
171
172 #[must_use]
174 pub fn secondary(span: SourceSpan, message: impl Into<String>) -> Self {
175 Self {
176 span,
177 message: message.into(),
178 primary: false,
179 }
180 }
181
182 #[must_use]
184 pub const fn span(&self) -> SourceSpan {
185 self.span
186 }
187
188 #[must_use]
190 pub fn message(&self) -> &str {
191 &self.message
192 }
193
194 #[must_use]
196 pub const fn is_primary(&self) -> bool {
197 self.primary
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct Diagnostic {
204 code: DiagnosticCode,
205 severity: DiagnosticSeverity,
206 message: String,
207 help: Option<String>,
208 labels: Vec<DiagnosticLabel>,
209}
210
211impl Diagnostic {
212 #[must_use]
214 pub fn new(
215 code: DiagnosticCode,
216 severity: DiagnosticSeverity,
217 message: impl Into<String>,
218 ) -> Self {
219 Self {
220 code,
221 severity,
222 message: message.into(),
223 help: None,
224 labels: Vec::new(),
225 }
226 }
227
228 #[must_use]
230 pub fn with_help(mut self, help: impl Into<String>) -> Self {
231 self.help = Some(help.into());
232 self
233 }
234
235 #[must_use]
237 pub fn with_label(mut self, label: DiagnosticLabel) -> Self {
238 self.labels.push(label);
239 self
240 }
241
242 #[must_use]
244 pub const fn code(&self) -> DiagnosticCode {
245 self.code
246 }
247
248 #[must_use]
250 pub const fn severity(&self) -> DiagnosticSeverity {
251 self.severity
252 }
253
254 #[must_use]
256 pub fn message(&self) -> &str {
257 &self.message
258 }
259
260 #[must_use]
262 pub fn help(&self) -> Option<&str> {
263 self.help.as_deref()
264 }
265
266 #[must_use]
268 pub fn labels(&self) -> &[DiagnosticLabel] {
269 &self.labels
270 }
271
272 #[must_use]
274 pub fn primary_label(&self) -> Option<&DiagnosticLabel> {
275 self.labels.iter().find(|label| label.is_primary())
276 }
277}
278
279#[derive(Debug)]
284#[cfg(feature = "miette")]
285struct SpannedDiagnostic {
286 message: String,
287 labels: Vec<miette::LabeledSpan>,
288 source_code: String,
289}
290
291#[cfg(feature = "miette")]
292impl fmt::Display for SpannedDiagnostic {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 f.write_str(&self.message)
295 }
296}
297
298#[cfg(feature = "miette")]
299impl std::error::Error for SpannedDiagnostic {}
300
301#[cfg(feature = "miette")]
302impl miette::Diagnostic for SpannedDiagnostic {
303 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
304 Some(Box::new("noyalib::validation"))
305 }
306
307 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
308 Some(&self.source_code)
309 }
310
311 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
312 if self.labels.is_empty() {
313 None
314 } else {
315 Some(Box::new(self.labels.iter().cloned()))
316 }
317 }
318}
319
320#[cfg(feature = "miette")]
340pub fn spanned_error<T, M: fmt::Display>(
341 source: &str,
342 span: &Spanned<T>,
343 message: M,
344) -> miette::Report {
345 let msg = message.to_string();
346 let start = span.start.index();
347 let end = span.end.index();
348 let len = if end > start { end - start } else { 1 };
349
350 miette::Report::new(SpannedDiagnostic {
351 message: msg.clone(),
352 labels: vec![miette::LabeledSpan::new(Some(msg), start, len)],
353 source_code: source.to_owned(),
354 })
355}
356
357#[cfg(feature = "miette")]
384pub fn spanned_error_with_context<T, U, M: fmt::Display, C: fmt::Display>(
385 source: &str,
386 primary_span: &Spanned<T>,
387 primary_message: M,
388 context_span: &Spanned<U>,
389 context_message: C,
390) -> miette::Report {
391 let p_msg = primary_message.to_string();
392 let p_start = primary_span.start.index();
393 let p_end = primary_span.end.index();
394 let p_len = if p_end > p_start { p_end - p_start } else { 1 };
395
396 let c_msg = context_message.to_string();
397 let c_start = context_span.start.index();
398 let c_end = context_span.end.index();
399 let c_len = if c_end > c_start { c_end - c_start } else { 1 };
400
401 miette::Report::new(SpannedDiagnostic {
402 message: p_msg.clone(),
403 labels: vec![
404 miette::LabeledSpan::new(Some(p_msg), p_start, p_len),
405 miette::LabeledSpan::new(Some(c_msg), c_start, c_len),
406 ],
407 source_code: source.to_owned(),
408 })
409}
410
411#[cfg(all(test, feature = "miette"))]
412mod tests {
413 use super::*;
414 use crate::Spanned;
415
416 #[test]
417 fn spanned_error_creates_report() {
418 let yaml = "port: 80\n";
419 #[derive(serde::Deserialize)]
420 struct Cfg {
421 port: Spanned<u16>,
422 }
423 let cfg: Cfg = crate::from_str(yaml).unwrap();
424 let report = spanned_error(yaml, &cfg.port, "port must be >= 1024");
425 let msg = format!("{report}");
426 assert!(msg.contains("port must be >= 1024"));
427 }
428
429 #[test]
430 fn spanned_error_diagnostic_has_labels() {
431 use miette::Diagnostic;
432
433 let yaml = "value: 42\n";
434 #[derive(serde::Deserialize)]
435 struct Cfg {
436 value: Spanned<i32>,
437 }
438 let cfg: Cfg = crate::from_str(yaml).unwrap();
439 let report = spanned_error(yaml, &cfg.value, "too small");
440
441 let diag: &dyn Diagnostic = report.as_ref();
443 assert!(diag.labels().is_some());
444 assert!(diag.code().is_some());
445 assert!(diag.source_code().is_some());
446 }
447}