use miette::SourceOffset;
use mit_commit::CommitMessage;
use crate::model::{Code, Problem};
pub struct ProblemBuilder {
error: String,
tip: String,
code: Code,
commit_message: String,
labels: Vec<(String, usize, usize)>,
url: Option<String>,
}
impl ProblemBuilder {
pub fn new(
error: impl Into<String>,
tip: impl Into<String>,
code: Code,
commit_message: &CommitMessage<'_>,
) -> Self {
Self {
error: error.into(),
tip: tip.into(),
code,
commit_message: String::from(commit_message.clone()),
labels: Vec::new(),
url: None,
}
}
pub fn with_url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
pub fn with_label(mut self, text: impl Into<String>, position: usize, length: usize) -> Self {
self.labels.push((text.into(), position, length));
self
}
pub fn with_label_for_line(
self,
commit_text: &str,
line_index: usize,
line: &str,
limit: usize,
label_text: impl Into<String>,
) -> Self {
if line.chars().count() <= limit {
return self;
}
let position = SourceOffset::from_location(
commit_text,
line_index + 1,
line.chars().take(limit).map(char::len_utf8).sum::<usize>() + 1,
)
.offset();
let length = line.chars().count().saturating_sub(limit);
self.with_label(label_text, position, length)
}
pub fn with_label_at_last_line(self, label_text: impl Into<String>) -> Self {
let original = &self.commit_message;
let trimmed = original.trim_end();
let trimmed_len = trimmed.len();
let last_line_start = trimmed.rfind('\n').map_or(0, |pos| pos + 1);
let last_line_length = trimmed_len - last_line_start;
self.with_label(label_text, last_line_start, last_line_length)
}
pub fn build(self) -> Problem {
let labels = if self.labels.is_empty() {
None
} else {
Some(self.labels)
};
Problem::new(
self.error,
self.tip,
self.code,
&self.commit_message.clone().into(),
labels,
self.url,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use miette::Diagnostic;
use mit_commit::CommitMessage;
#[test]
fn test_builder_creates_problem_with_basic_fields() {
let commit = CommitMessage::from("Test commit");
let problem = ProblemBuilder::new(
"Error message",
"Fix advice",
Code::BodyWiderThan72Characters,
&commit,
)
.build();
assert_eq!(problem.error(), "Error message");
assert_eq!(problem.tip(), "Fix advice");
assert_eq!(problem.code(), &Code::BodyWiderThan72Characters);
assert_eq!(problem.commit_message(), commit);
}
#[test]
fn test_builder_adds_url() {
let commit = CommitMessage::from("Test commit");
let problem = ProblemBuilder::new(
"Error message",
"Fix advice",
Code::BodyWiderThan72Characters,
&commit,
)
.with_url("https://example.com")
.build();
let diagnostic_output = format!("{problem:?}");
assert!(diagnostic_output.contains("https://example.com"));
}
#[test]
fn test_builder_adds_labels() {
let commit = CommitMessage::from("Test commit");
let problem = ProblemBuilder::new(
"Error message",
"Fix advice",
Code::BodyWiderThan72Characters,
&commit,
)
.with_label("Label 1", 0, 4)
.with_label("Label 2", 5, 6)
.build();
let diagnostic_output = format!("{problem:?}");
assert!(diagnostic_output.contains("Label 1"));
assert!(diagnostic_output.contains("Label 2"));
}
#[test]
fn test_with_label_for_line() {
let commit_text = "Subject\n\nThis is a very long line that exceeds the character limit";
let commit = CommitMessage::from(commit_text);
let problem = ProblemBuilder::new(
"Error message",
"Fix advice",
Code::BodyWiderThan72Characters,
&commit,
)
.with_label_for_line(
commit_text,
2,
"This is a very long line that exceeds the character limit",
10,
"Too long",
)
.build();
let diagnostic_output = format!("{problem:?}");
assert!(diagnostic_output.contains("Too long"));
}
#[test]
fn test_with_label_for_line_does_not_add_label_if_within_limit() {
let commit_text = "Subject\n\nShort line";
let commit = CommitMessage::from(commit_text);
let problem = ProblemBuilder::new(
"Error message",
"Fix advice",
Code::BodyWiderThan72Characters,
&commit,
)
.with_label_for_line(commit_text, 2, "Short line", 72, "Too long")
.build();
let diagnostic_output = format!("{problem:?}");
assert!(!diagnostic_output.contains("Too long"));
}
#[test]
fn test_builder_with_multiple_methods_chained() {
let commit_text = "Subject\n\nThis is a very long line that exceeds the character limit";
let commit = CommitMessage::from(commit_text);
let problem = ProblemBuilder::new(
"Error message",
"Fix advice",
Code::BodyWiderThan72Characters,
&commit,
)
.with_url("https://example.com")
.with_label("Manual label", 0, 7)
.with_label_for_line(
commit_text,
2,
"This is a very long line that exceeds the character limit",
10,
"Too long",
)
.build();
let diagnostic_output = format!("{problem:?}");
assert!(diagnostic_output.contains("https://example.com"));
assert!(diagnostic_output.contains("Manual label"));
assert!(diagnostic_output.contains("Too long"));
}
#[test]
fn test_with_label_for_line_position_calculation() {
let commit_text = "Subject\nSecond line\nThis is a line that exceeds the limit";
let commit = CommitMessage::from(commit_text);
let line_index = 2;
let line = "This is a line that exceeds the limit";
let limit = 10;
let problem = ProblemBuilder::new(
"Error message",
"Fix advice",
Code::BodyWiderThan72Characters,
&commit,
)
.with_label_for_line(commit_text, line_index, line, limit, "Too long")
.build();
let third_line_offset = commit_text.find("This is a line").unwrap();
let first_ten_chars_length = line.chars().take(limit).map(char::len_utf8).sum::<usize>();
let expected_position = third_line_offset + first_ten_chars_length;
let labels = problem.labels().unwrap().collect::<Vec<_>>();
assert_eq!(
labels[0].offset(),
expected_position,
"Label should be positioned at character {} (after the first {} characters of line {})",
expected_position,
limit,
line_index + 1
);
}
}