use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reference {
pub filename: String,
pub line: Option<u32>,
pub column: Option<u32>,
}
impl Reference {
pub fn new(filename: impl Into<String>) -> Self {
Self {
filename: filename.into(),
line: None,
column: None,
}
}
}
impl fmt::Display for Reference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.filename)?;
if let Some(l) = self.line {
write!(f, ":{}", l)?;
if let Some(c) = self.column {
write!(f, ":{}", c)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnnotatedRef {
pub reference: Reference,
pub label: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum MessageLevel {
Info,
Warning,
Error,
}
impl fmt::Display for MessageLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Error => write!(f, "error"),
Self::Warning => write!(f, "warning"),
Self::Info => write!(f, "info"),
}
}
}
#[derive(Debug, Clone)]
pub struct Message {
pub level: MessageLevel,
pub text: String,
pub reference: Option<Reference>,
pub related: Vec<AnnotatedRef>,
}
impl Message {
pub fn error(text: impl Into<String>) -> Self {
Self {
level: MessageLevel::Error,
text: text.into(),
reference: None,
related: vec![],
}
}
pub fn error_at(text: impl Into<String>, reference: impl Into<String>) -> Self {
Self {
level: MessageLevel::Error,
text: text.into(),
reference: Some(Reference::new(reference)),
related: vec![],
}
}
pub fn warning_at(text: impl Into<String>, reference: impl Into<String>) -> Self {
Self {
level: MessageLevel::Warning,
text: text.into(),
reference: Some(Reference::new(reference)),
related: vec![],
}
}
pub fn with_related(mut self, reference: impl Into<String>, label: impl Into<String>) -> Self {
self.related.push(AnnotatedRef {
reference: Reference::new(reference),
label: label.into(),
});
self
}
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.level, self.text)?;
if let Some(r) = &self.reference {
write!(f, "\n --> {}", r)?;
}
for ann in &self.related {
write!(f, "\n = note: {}: {}", ann.label, ann.reference)?;
}
Ok(())
}
}
pub fn has_errors(msgs: &[Message]) -> bool {
msgs.iter().any(|m| m.level == MessageLevel::Error)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_display_error_with_reference() {
let msg = Message::error_at("something went wrong", "file.yaml:matchers[0].id");
let s = msg.to_string();
assert!(s.contains("error: something went wrong"));
assert!(s.contains("file.yaml:matchers[0].id"));
}
#[test]
fn message_display_with_related() {
let msg = Message::error_at("duplicate id: foo", "file.yaml:matchers[1].id")
.with_related("file.yaml:matchers[0].id", "first defined here");
let s = msg.to_string();
assert!(s.contains("first defined here"));
assert!(s.contains("file.yaml:matchers[0].id"));
}
#[test]
fn has_errors_detects_error_level() {
let msgs = vec![
Message::warning_at("a warning", "somewhere"),
Message::error_at("an error", "somewhere"),
];
assert!(has_errors(&msgs));
}
#[test]
fn has_errors_false_for_warnings_only() {
let msgs = vec![Message::warning_at("just a warning", "somewhere")];
assert!(!has_errors(&msgs));
}
#[test]
fn has_errors_false_for_empty() {
assert!(!has_errors(&[]));
}
}