use serde::{Deserialize, Serialize};
pub const R23_MAX_DISPLAY_TOTAL: usize = 999_999_999;
pub const R23_CLAMP_THRESHOLD: usize = 1_000_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Reason {
Cap,
Budget,
Depth,
Walk,
}
impl Reason {
pub const fn precedence(self) -> u8 {
match self {
Self::Walk => 4,
Self::Depth => 3,
Self::Budget => 2,
Self::Cap => 1,
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Walk => "walk",
Self::Depth => "depth",
Self::Budget => "budget",
Self::Cap => "cap",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Unit {
Sites,
Paths,
Files,
Results,
Rows,
Lines,
Items,
Directories,
Hops,
}
impl Unit {
pub const fn as_str(self) -> &'static str {
match self {
Self::Sites => "sites",
Self::Paths => "paths",
Self::Files => "files",
Self::Results => "results",
Self::Rows => "rows",
Self::Lines => "lines",
Self::Items => "items",
Self::Directories => "directories",
Self::Hops => "hops",
}
}
}
impl std::fmt::Display for Unit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum Total {
Exact(usize),
AtLeast(usize),
}
impl Total {
pub fn value(&self) -> usize {
match *self {
Total::Exact(v) | Total::AtLeast(v) => v,
}
}
pub fn is_exact(&self) -> bool {
matches!(self, Total::Exact(_))
}
pub fn is_at_least(&self) -> bool {
matches!(self, Total::AtLeast(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListEnvelope {
pub shown: usize,
pub total: Total,
pub unit: Unit,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<Reason>,
pub causes: Vec<Reason>,
pub narrow: Vec<String>,
}
impl ListEnvelope {
pub fn new(
shown: usize,
total: Total,
unit: Unit,
mut causes: Vec<Reason>,
narrow: &[&str],
) -> Self {
causes.sort_by(|a, b| b.precedence().cmp(&a.precedence()));
causes.dedup();
let reason = causes.first().copied();
Self {
shown,
total,
unit,
reason,
causes,
narrow: narrow.iter().map(|s| s.to_string()).collect(),
}
}
}
pub fn render_total(total: &Total) -> String {
if total.value() >= R23_CLAMP_THRESHOLD {
format!("≥{R23_MAX_DISPLAY_TOTAL}")
} else {
match *total {
Total::Exact(v) => format!("{v}"),
Total::AtLeast(v) => format!("≥{v}"),
}
}
}
pub fn render_trailer(envelope: &ListEnvelope) -> Option<String> {
let reason = envelope.reason?;
let shown = envelope.shown;
let total_str = render_total(&envelope.total);
let unit_str = envelope.unit.as_str();
let reason_str = reason.as_str();
let base = format!("shown {shown} of {total_str} {unit_str} ({reason_str})");
if envelope.narrow.is_empty() {
Some(base)
} else {
Some(format!("{base} · narrow: {}", envelope.narrow.join(", ")))
}
}
pub fn measure_trailer_len(envelope: &ListEnvelope) -> usize {
render_trailer(envelope).map(|s| s.len()).unwrap_or(0)
}
pub fn derive_wire_key(list_id: &str, is_text_surface: bool) -> String {
if is_text_surface {
format!("{}_list_envelope", list_id.replace('.', "_"))
} else {
let last_segment = list_id.rsplit('.').next().unwrap_or(list_id);
format!("{last_segment}_list_envelope")
}
}