use crate::prelude::{String, Vec};
use core::fmt;
#[cfg(feature = "miette")]
use crate::spanned::Spanned;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DiagnosticCode {
Parse,
Serialize,
Deserialize,
TypeMismatch,
MissingField,
UnknownField,
RecursionLimit,
RepetitionLimit,
Budget,
UnknownAnchor,
DuplicateKey,
KeyCollision,
IntegerOverflow,
NonScalarKey,
EndOfStream,
MoreThanOneDocument,
Io,
Other,
}
impl DiagnosticCode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Parse => "noyalib::parse",
Self::Serialize => "noyalib::serialize",
Self::Deserialize => "noyalib::deserialize",
Self::TypeMismatch => "noyalib::type_mismatch",
Self::MissingField => "noyalib::missing_field",
Self::UnknownField => "noyalib::unknown_field",
Self::RecursionLimit => "noyalib::recursion_limit",
Self::RepetitionLimit => "noyalib::repetition_limit",
Self::Budget => "noyalib::budget",
Self::UnknownAnchor => "noyalib::unknown_anchor",
Self::DuplicateKey => "noyalib::duplicate_key",
Self::KeyCollision => "noyalib::key_collision",
Self::IntegerOverflow => "noyalib::integer_overflow",
Self::NonScalarKey => "noyalib::non_scalar_key",
Self::EndOfStream => "noyalib::eof",
Self::MoreThanOneDocument => "noyalib::multi_document",
Self::Io => "noyalib::io",
Self::Other => "noyalib::error",
}
}
}
impl fmt::Display for DiagnosticCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DiagnosticSeverity {
Error,
Warning,
Information,
Hint,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SourceSpan {
offset: usize,
length: usize,
}
impl SourceSpan {
#[must_use]
pub const fn new(offset: usize, length: usize) -> Self {
Self { offset, length }
}
#[must_use]
pub const fn offset(self) -> usize {
self.offset
}
#[must_use]
pub const fn length(self) -> usize {
self.length
}
#[must_use]
pub const fn end(self) -> usize {
self.offset.saturating_add(self.length)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticLabel {
span: SourceSpan,
message: String,
primary: bool,
}
impl DiagnosticLabel {
#[must_use]
pub fn primary(span: SourceSpan, message: impl Into<String>) -> Self {
Self {
span,
message: message.into(),
primary: true,
}
}
#[must_use]
pub fn secondary(span: SourceSpan, message: impl Into<String>) -> Self {
Self {
span,
message: message.into(),
primary: false,
}
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
self.span
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub const fn is_primary(&self) -> bool {
self.primary
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
code: DiagnosticCode,
severity: DiagnosticSeverity,
message: String,
help: Option<String>,
labels: Vec<DiagnosticLabel>,
}
impl Diagnostic {
#[must_use]
pub fn new(
code: DiagnosticCode,
severity: DiagnosticSeverity,
message: impl Into<String>,
) -> Self {
Self {
code,
severity,
message: message.into(),
help: None,
labels: Vec::new(),
}
}
#[must_use]
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
#[must_use]
pub fn with_label(mut self, label: DiagnosticLabel) -> Self {
self.labels.push(label);
self
}
#[must_use]
pub const fn code(&self) -> DiagnosticCode {
self.code
}
#[must_use]
pub const fn severity(&self) -> DiagnosticSeverity {
self.severity
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub fn help(&self) -> Option<&str> {
self.help.as_deref()
}
#[must_use]
pub fn labels(&self) -> &[DiagnosticLabel] {
&self.labels
}
#[must_use]
pub fn primary_label(&self) -> Option<&DiagnosticLabel> {
self.labels.iter().find(|label| label.is_primary())
}
}
#[derive(Debug)]
#[cfg(feature = "miette")]
struct SpannedDiagnostic {
message: String,
labels: Vec<miette::LabeledSpan>,
source_code: String,
}
#[cfg(feature = "miette")]
impl fmt::Display for SpannedDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
#[cfg(feature = "miette")]
impl std::error::Error for SpannedDiagnostic {}
#[cfg(feature = "miette")]
impl miette::Diagnostic for SpannedDiagnostic {
fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
Some(Box::new("noyalib::validation"))
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.source_code)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
if self.labels.is_empty() {
None
} else {
Some(Box::new(self.labels.iter().cloned()))
}
}
}
#[cfg(feature = "miette")]
pub fn spanned_error<T, M: fmt::Display>(
source: &str,
span: &Spanned<T>,
message: M,
) -> miette::Report {
let msg = message.to_string();
let start = span.start.index();
let end = span.end.index();
let len = if end > start { end - start } else { 1 };
miette::Report::new(SpannedDiagnostic {
message: msg.clone(),
labels: vec![miette::LabeledSpan::new(Some(msg), start, len)],
source_code: source.to_owned(),
})
}
#[cfg(feature = "miette")]
pub fn spanned_error_with_context<T, U, M: fmt::Display, C: fmt::Display>(
source: &str,
primary_span: &Spanned<T>,
primary_message: M,
context_span: &Spanned<U>,
context_message: C,
) -> miette::Report {
let p_msg = primary_message.to_string();
let p_start = primary_span.start.index();
let p_end = primary_span.end.index();
let p_len = if p_end > p_start { p_end - p_start } else { 1 };
let c_msg = context_message.to_string();
let c_start = context_span.start.index();
let c_end = context_span.end.index();
let c_len = if c_end > c_start { c_end - c_start } else { 1 };
miette::Report::new(SpannedDiagnostic {
message: p_msg.clone(),
labels: vec![
miette::LabeledSpan::new(Some(p_msg), p_start, p_len),
miette::LabeledSpan::new(Some(c_msg), c_start, c_len),
],
source_code: source.to_owned(),
})
}
#[cfg(all(test, feature = "miette"))]
mod tests {
use super::*;
use crate::Spanned;
#[test]
fn spanned_error_creates_report() {
let yaml = "port: 80\n";
#[derive(serde::Deserialize)]
struct Cfg {
port: Spanned<u16>,
}
let cfg: Cfg = crate::from_str(yaml).unwrap();
let report = spanned_error(yaml, &cfg.port, "port must be >= 1024");
let msg = format!("{report}");
assert!(msg.contains("port must be >= 1024"));
}
#[test]
fn spanned_error_diagnostic_has_labels() {
use miette::Diagnostic;
let yaml = "value: 42\n";
#[derive(serde::Deserialize)]
struct Cfg {
value: Spanned<i32>,
}
let cfg: Cfg = crate::from_str(yaml).unwrap();
let report = spanned_error(yaml, &cfg.value, "too small");
let diag: &dyn Diagnostic = report.as_ref();
assert!(diag.labels().is_some());
assert!(diag.code().is_some());
assert!(diag.source_code().is_some());
}
}