use std::io::Write;
use anyhow::Result;
use crate::markdown::{estimate_tokens, render, service_is_visible, visible_endpoints};
use crate::models::{ApiDocumentation, DocConfig};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ServiceStats {
pub name: String,
pub endpoints: usize,
pub tokens: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct Stats {
pub rows: Vec<ServiceStats>,
pub total_endpoints: usize,
pub total_tokens: usize,
}
fn estimate_render(doc: &ApiDocumentation, config: &DocConfig) -> Result<usize> {
let mut buffer = Vec::new();
let _rendered = render(&mut buffer, doc, config)?;
Ok(estimate_tokens(&buffer))
}
pub(crate) fn compute(doc: &ApiDocumentation, config: &DocConfig) -> Result<Stats> {
let endpoints = visible_endpoints(doc, config);
let mut rows = Vec::new();
for service in &doc.services {
if !service_is_visible(&service.name, config) {
continue;
}
let count = endpoints
.iter()
.filter(|endpoint| endpoint.services.contains(&service.name))
.count();
if count == 0 {
continue;
}
let mut service_config = config.clone();
service_config.service_filter = Some(vec![service.name.clone()]);
rows.push(ServiceStats {
name: service.name.clone(),
endpoints: count,
tokens: estimate_render(doc, &service_config)?,
});
}
let total_tokens = if endpoints.is_empty() {
0
} else {
estimate_render(doc, config)?
};
Ok(Stats {
rows,
total_endpoints: endpoints.len(),
total_tokens,
})
}
const SERVICE_HEADER: &str = "SERVICE";
const ENDPOINTS_HEADER: &str = "ENDPOINTS";
const TOKENS_HEADER: &str = "~TOKENS";
const TOTAL_LABEL: &str = "TOTAL";
const GAP: &str = " ";
fn digits(value: usize) -> usize {
value.to_string().len()
}
pub(crate) fn write_stats<W: Write>(writer: &mut W, stats: &Stats) -> Result<()> {
let name_width = stats
.rows
.iter()
.map(|row| row.name.chars().count())
.chain([SERVICE_HEADER.len(), TOTAL_LABEL.len()])
.max()
.unwrap_or(SERVICE_HEADER.len());
let endpoints_width = stats
.rows
.iter()
.map(|row| digits(row.endpoints))
.chain([ENDPOINTS_HEADER.len(), digits(stats.total_endpoints)])
.max()
.unwrap_or(ENDPOINTS_HEADER.len());
let tokens_width = stats
.rows
.iter()
.map(|row| digits(row.tokens))
.chain([TOKENS_HEADER.len(), digits(stats.total_tokens)])
.max()
.unwrap_or(TOKENS_HEADER.len());
writeln!(
writer,
"{SERVICE_HEADER:<name_width$}{GAP}{ENDPOINTS_HEADER:>endpoints_width$}{GAP}{TOKENS_HEADER:>tokens_width$}"
)?;
for row in &stats.rows {
writeln!(
writer,
"{:<name_width$}{GAP}{:>endpoints_width$}{GAP}{:>tokens_width$}",
row.name, row.endpoints, row.tokens
)?;
}
writeln!(
writer,
"{TOTAL_LABEL:<name_width$}{GAP}{:>endpoints_width$}{GAP}{:>tokens_width$}",
stats.total_endpoints, stats.total_tokens
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn render_to_string(stats: &Stats) -> String {
let mut buffer = Vec::new();
write_stats(&mut buffer, stats).unwrap();
String::from_utf8(buffer).unwrap()
}
#[test]
fn columns_align_to_widest_value() {
let stats = Stats {
rows: vec![
ServiceStats {
name: "Findings".into(),
endpoints: 42,
tokens: 6231,
},
ServiceStats {
name: "Long Service".into(),
endpoints: 7,
tokens: 12,
},
],
total_endpoints: 49,
total_tokens: 6300,
};
assert_eq!(
render_to_string(&stats),
"\
SERVICE ENDPOINTS ~TOKENS
Findings 42 6231
Long Service 7 12
TOTAL 49 6300
"
);
}
#[test]
fn empty_table_is_header_and_zero_total() {
assert_eq!(
render_to_string(&Stats::default()),
"\
SERVICE ENDPOINTS ~TOKENS
TOTAL 0 0
"
);
}
}