use super::StatsFormatter;
use crate::{
config,
formatters::{
get_response_formatter,
host_stats::DetailedHostStats,
stats::{OutputStats, ResponseStats},
},
};
use anyhow::Result;
use lychee_lib::InputSource;
use std::{
collections::HashSet,
fmt::{self, Display},
};
const WIDTH: usize = 20;
fn write_stat(f: &mut fmt::Formatter, title: &str, stat: usize, newline: bool) -> fmt::Result {
f.write_str(title)?;
let spacing = WIDTH.saturating_sub(title.chars().count());
f.write_str(format!("{stat:.>spacing$}").as_str())?;
if newline {
f.write_str("\n")?;
}
Ok(())
}
struct DetailedResponseStats {
stats: ResponseStats,
mode: config::OutputMode,
}
impl Display for DetailedResponseStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let stats = &self.stats;
let separator = "-".repeat(WIDTH + 1);
writeln!(f, "📝 Summary")?;
writeln!(f, "{separator}")?;
write_stat(f, "🔍 Total", stats.total, true)?;
write_stat(f, "🔗 Unique", stats.unique, true)?;
write_stat(f, "✅ Successful", stats.successful, true)?;
write_stat(f, "⏳ Timeouts", stats.timeouts, true)?;
write_stat(f, "🔀 Redirected", stats.redirects, true)?;
write_stat(f, "👻 Excluded", stats.excludes, true)?;
write_stat(f, "❓ Unknown", stats.unknown, true)?;
write_stat(f, "🚫 Errors", stats.errors, true)?;
write_stat(f, "⛔ Unsupported", stats.errors, false)?;
let response_formatter = get_response_formatter(&self.mode);
for (source, responses) in
super::sort_stats_iter(stats.error_map.iter().chain(stats.timeout_map.iter()))
{
write!(f, "\n\nErrors in {source}")?;
for response in responses {
write!(f, "\n{}", response_formatter.format_response(response))?;
}
write_stats(f, "Suggestions", source, stats.suggestion_map.get(source))?;
write_stats(f, "Redirects", source, stats.redirect_map.get(source))?;
}
Ok(())
}
}
fn write_stats<T: Display>(
f: &mut fmt::Formatter<'_>,
title: &str,
source: &InputSource,
set: Option<&HashSet<T>>,
) -> Result<(), fmt::Error> {
if let Some(items) = set {
let mut sorted: Vec<_> = items.iter().collect();
sorted.sort_by(|a, b| {
let (a, b) = (a.to_string().to_lowercase(), b.to_string().to_lowercase());
numeric_sort::cmp(&a, &b)
});
writeln!(f, "\n\n{title} in {source}")?;
for item in sorted {
writeln!(f, "{item}")?;
}
}
Ok(())
}
pub(crate) struct Detailed {
mode: config::OutputMode,
}
impl Detailed {
pub(crate) const fn new(mode: config::OutputMode) -> Self {
Self { mode }
}
}
impl StatsFormatter for Detailed {
fn format(&self, stats: OutputStats) -> Result<String> {
let response_stats = DetailedResponseStats {
stats: stats.response_stats,
mode: self.mode.clone(),
};
let host_stats = DetailedHostStats {
host_stats: stats.host_stats,
};
Ok(format!("{response_stats}\n{host_stats}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{config::OutputMode, formatters::stats::get_dummy_stats};
use pretty_assertions::assert_eq;
#[test]
fn test_detailed_formatter() {
let formatter = Detailed::new(OutputMode::Plain);
let result = formatter.format(get_dummy_stats()).unwrap();
assert_eq!(
result,
"📝 Summary
---------------------
🔍 Total............2
🔗 Unique...........2
✅ Successful.......0
⏳ Timeouts.........1
🔀 Redirected.......1
👻 Excluded.........0
❓ Unknown..........0
🚫 Errors...........1
⛔ Unsupported......1
Errors in https://example.com/
[404] https://github.com/mre/idiomatic-rust-doesnt-exist-man (at 1:1) | 404 Not Found
[TIMEOUT] https://httpbin.org/delay/2 (at 1:1) | Request timed out
Suggestions in https://example.com/
https://original.dev/ --> https://suggestion.dev/
Redirects in https://example.com/
https://1.dev/ --[308]--> https://2.dev/ --[308]--> http://redirected.dev/
📊 Per-host Statistics
---------------------
Host: example.com
Total requests: 5
Successful: 3 (60.0%)
Rate limited: 1 (429 Too Many Requests)
Server errors (5xx): 1
Cache hit rate: 20.0%
Cache hits: 1, misses: 4
"
);
}
}