use crate::diagnostic::{
DetailItem, DetailKind, DiagnosticKind, DiagnosticMessage, MessageContent,
};
#[derive(Debug, Clone)]
pub struct DiagnosticMessageBuilder {
kind: DiagnosticKind,
title: String,
code: Option<String>,
problem: Option<MessageContent>,
details: Vec<DetailItem>,
hints: Vec<MessageContent>,
location: Option<quarto_source_map::SourceInfo>,
}
impl DiagnosticMessageBuilder {
pub fn new(kind: DiagnosticKind, title: impl Into<String>) -> Self {
Self {
kind,
title: title.into(),
code: None,
problem: None,
details: Vec::new(),
hints: Vec::new(),
location: None,
}
}
pub fn error(title: impl Into<String>) -> Self {
Self::new(DiagnosticKind::Error, title)
}
pub fn generic_error(message: impl Into<String>, file: &str, line: u32) -> DiagnosticMessage {
let title = format!("{} (at {}:{})", message.into(), file, line);
Self::error(title).with_code("Q-0-99").build() }
pub fn generic_warning(message: impl Into<String>, file: &str, line: u32) -> DiagnosticMessage {
let title = format!("{} (at {}:{})", message.into(), file, line);
Self::warning(title).with_code("Q-0-99").build() }
pub fn warning(title: impl Into<String>) -> Self {
Self::new(DiagnosticKind::Warning, title)
}
pub fn info(title: impl Into<String>) -> Self {
Self::new(DiagnosticKind::Info, title)
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
pub fn with_location(mut self, location: quarto_source_map::SourceInfo) -> Self {
self.location = Some(location);
self
}
pub fn problem(mut self, stmt: impl Into<MessageContent>) -> Self {
self.problem = Some(stmt.into());
self
}
pub fn add_detail(mut self, detail: impl Into<MessageContent>) -> Self {
self.details.push(DetailItem {
kind: DetailKind::Error,
content: detail.into(),
location: None,
});
self
}
pub fn add_detail_at(
mut self,
detail: impl Into<MessageContent>,
location: quarto_source_map::SourceInfo,
) -> Self {
self.details.push(DetailItem {
kind: DetailKind::Error,
content: detail.into(),
location: Some(location),
});
self
}
pub fn add_info(mut self, info: impl Into<MessageContent>) -> Self {
self.details.push(DetailItem {
kind: DetailKind::Info,
content: info.into(),
location: None,
});
self
}
pub fn add_info_at(
mut self,
info: impl Into<MessageContent>,
location: quarto_source_map::SourceInfo,
) -> Self {
self.details.push(DetailItem {
kind: DetailKind::Info,
content: info.into(),
location: Some(location),
});
self
}
pub fn add_note(mut self, note: impl Into<MessageContent>) -> Self {
self.details.push(DetailItem {
kind: DetailKind::Note,
content: note.into(),
location: None,
});
self
}
pub fn add_note_at(
mut self,
note: impl Into<MessageContent>,
location: quarto_source_map::SourceInfo,
) -> Self {
self.details.push(DetailItem {
kind: DetailKind::Note,
content: note.into(),
location: Some(location),
});
self
}
pub fn add_faded_at(
mut self,
content: impl Into<MessageContent>,
location: quarto_source_map::SourceInfo,
) -> Self {
self.details.push(DetailItem {
kind: DetailKind::Faded,
content: content.into(),
location: Some(location),
});
self
}
pub fn add_hint(mut self, hint: impl Into<MessageContent>) -> Self {
self.hints.push(hint.into());
self
}
pub fn build(self) -> DiagnosticMessage {
DiagnosticMessage {
code: self.code,
title: self.title,
kind: self.kind,
problem: self.problem,
details: self.details,
hints: self.hints,
location: self.location,
}
}
pub fn build_with_validation(self) -> (DiagnosticMessage, Vec<String>) {
let mut warnings = Vec::new();
if self.problem.is_none() {
warnings.push(
"Error message missing problem statement. \
Consider adding .problem() to explain what went wrong."
.to_string(),
);
}
if self.details.len() > 5 {
warnings.push(format!(
"Error message has {} details. Tidyverse guidelines recommend max 5 to avoid \
overwhelming users.",
self.details.len()
));
}
(self.build(), warnings)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_error() {
let msg = DiagnosticMessageBuilder::error("Test error").build();
assert_eq!(msg.title, "Test error");
assert_eq!(msg.kind, DiagnosticKind::Error);
}
#[test]
fn test_builder_warning() {
let msg = DiagnosticMessageBuilder::warning("Test warning").build();
assert_eq!(msg.kind, DiagnosticKind::Warning);
}
#[test]
fn test_builder_info() {
let msg = DiagnosticMessageBuilder::info("Test info").build();
assert_eq!(msg.kind, DiagnosticKind::Info);
}
#[test]
fn test_builder_with_code() {
let msg = DiagnosticMessageBuilder::error("Test")
.with_code("Q-1-1")
.build();
assert_eq!(msg.code, Some("Q-1-1".to_string()));
}
#[test]
fn test_builder_problem() {
let msg = DiagnosticMessageBuilder::error("Test")
.problem("Something went wrong")
.build();
assert!(msg.problem.is_some());
assert_eq!(msg.problem.unwrap().as_str(), "Something went wrong");
}
#[test]
fn test_builder_details() {
let msg = DiagnosticMessageBuilder::error("Test")
.add_detail("Detail 1")
.add_info("Info 1")
.add_note("Note 1")
.build();
assert_eq!(msg.details.len(), 3);
assert_eq!(msg.details[0].kind, DetailKind::Error);
assert_eq!(msg.details[1].kind, DetailKind::Info);
assert_eq!(msg.details[2].kind, DetailKind::Note);
}
#[test]
fn test_builder_hints() {
let msg = DiagnosticMessageBuilder::error("Test")
.add_hint("Did you mean X?")
.add_hint("Try Y instead")
.build();
assert_eq!(msg.hints.len(), 2);
}
#[test]
fn test_builder_complete_message() {
let msg = DiagnosticMessageBuilder::error("Incompatible types")
.with_code("Q-1-2") .problem("Cannot combine date and datetime types")
.add_detail("`x` has type `date`")
.add_detail("`y` has type `datetime`")
.add_hint("Convert both to the same type?")
.build();
assert_eq!(msg.title, "Incompatible types");
assert_eq!(msg.code, Some("Q-1-2".to_string())); assert!(msg.problem.is_some());
assert_eq!(msg.details.len(), 2);
assert_eq!(msg.hints.len(), 1);
}
#[test]
fn test_builder_validation_no_problem() {
let (msg, warnings) = DiagnosticMessageBuilder::error("Test").build_with_validation();
assert_eq!(msg.title, "Test");
assert!(!warnings.is_empty());
assert!(warnings[0].contains("missing problem statement"));
}
#[test]
fn test_builder_validation_too_many_details() {
let (_msg, warnings) = DiagnosticMessageBuilder::error("Test")
.problem("Something wrong")
.add_detail("1")
.add_detail("2")
.add_detail("3")
.add_detail("4")
.add_detail("5")
.add_detail("6")
.build_with_validation();
assert!(!warnings.is_empty());
assert!(warnings[0].contains("6 details"));
assert!(warnings[0].contains("max 5"));
}
#[test]
fn test_builder_validation_passes() {
let (_msg, warnings) = DiagnosticMessageBuilder::error("Test")
.problem("Something wrong")
.add_detail("Detail")
.build_with_validation();
assert!(warnings.is_empty());
}
}