use crate::email::Email;
use crate::message::AlertItem;
use askama::Template;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SeverityCounts {
pub critical: u32,
pub warning: u32,
pub notice: u32,
}
impl SeverityCounts {
pub fn total(&self) -> u32 {
self.critical + self.warning + self.notice
}
pub fn summary(&self) -> Option<String> {
let parts: Vec<String> = [
(self.critical, "critical", "critical"),
(self.warning, "warning", "warnings"),
(self.notice, "notice", "notices"),
]
.into_iter()
.filter(|(n, _, _)| *n > 0)
.map(|(n, one, many)| format!("{n} {}", if n == 1 { one } else { many }))
.collect();
(!parts.is_empty()).then(|| parts.join(", "))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SiteDigest {
pub domain: String,
pub score: u8,
pub score_delta: Option<i32>,
pub checks_passed: u32,
pub checks_total: u32,
pub new_issues: Vec<(String, u32)>,
pub resolved_issues: Vec<(String, u32)>,
pub changes_by_severity: SeverityCounts,
pub top_changes: Vec<AlertItem>,
pub dashboard_url: String,
pub rankorg_url: Option<String>,
}
impl SiteDigest {
pub fn headline(&self) -> String {
let passed = format!(
"Your site passed {}/{} checks.",
self.checks_passed, self.checks_total
);
if self.score_delta.is_none() {
return passed;
}
match self.new_issues.len() {
0 => format!("{passed} No new issues."),
1 => format!("{passed} 1 new issue."),
n => format!("{passed} {n} new issues."),
}
}
pub fn score_label(&self) -> String {
match self.score_delta {
None => self.score.to_string(),
Some(0) => format!("{} (no change)", self.score),
Some(d) => format!("{} ({d:+})", self.score),
}
}
pub fn changes_line(&self) -> String {
match self.changes_by_severity.summary() {
Some(s) => format!("Changes this week: {s}."),
None => "No changes this week.".to_owned(),
}
}
pub fn more_changes(&self) -> u32 {
self.changes_by_severity
.total()
.saturating_sub(u32::try_from(self.top_changes.len()).unwrap_or(u32::MAX))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DigestView {
pub account_email: String,
pub week_label: String,
pub sites: Vec<SiteDigest>,
pub settings_url: String,
}
#[derive(Template)]
#[template(path = "digest.html")]
struct HtmlDigest<'a> {
view: &'a DigestView,
}
#[derive(Template)]
#[template(path = "digest.txt")]
struct TextDigest<'a> {
view: &'a DigestView,
}
impl DigestView {
pub fn subject(&self) -> String {
match self.sites.as_slice() {
[one] => format!("CodoSEO weekly: {} {}", one.domain, one.score_label()),
many => format!("CodoSEO weekly: {} sites", many.len()),
}
}
pub fn render(&self) -> Result<Email, askama::Error> {
Ok(Email {
to: self.account_email.clone(),
subject: self.subject(),
text: TextDigest { view: self }.render()?,
html: Some(HtmlDigest { view: self }.render()?),
})
}
}