#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Ladder<S> {
levels: Vec<S>,
}
impl<S: PartialEq + Clone> Ladder<S> {
#[must_use]
pub fn new(most_to_least_severe: Vec<S>) -> Self {
Self {
levels: most_to_least_severe,
}
}
#[must_use]
pub fn position(&self, level: &S) -> Option<usize> {
self.levels.iter().position(|l| l == level)
}
#[must_use]
pub fn levels(&self) -> &[S] {
&self.levels
}
#[must_use]
pub fn step(&self, from: &S, delta: i32) -> S {
let Some(i) = self.position(from) else {
return from.clone();
};
let last = self.levels.len() - 1; let mag = usize::try_from(delta.unsigned_abs()).unwrap_or(usize::MAX);
let idx = if delta >= 0 {
i.saturating_add(mag).min(last)
} else {
i.saturating_sub(mag)
};
self.levels[idx].clone()
}
}
#[cfg(test)]
mod tests {
use super::Ladder;
fn severity() -> Ladder<&'static str> {
Ladder::new(vec!["error", "warning", "info"])
}
#[test]
fn steps_down_saturate() {
assert_eq!(severity().step(&"error", 2), "info");
}
#[test]
fn over_step_down_clamps() {
assert_eq!(severity().step(&"info", 5), "info");
}
#[test]
fn over_step_up_clamps() {
assert_eq!(severity().step(&"error", -3), "error");
}
#[test]
fn escalate_then_deescalate_is_noop() {
let l = severity();
let total = -1 + 1;
assert_eq!(l.step(&"warning", total), "warning");
}
#[test]
fn markup_ladder() {
let l = Ladder::new(vec!["draft", "review", "final"]);
assert_eq!(l.step(&"draft", 1), "review");
assert_eq!(l.step(&"final", -2), "draft");
}
#[test]
fn unknown_level_unchanged() {
let l = severity();
assert_eq!(l.position(&"bogus"), None);
assert_eq!(l.step(&"bogus", 1), "bogus");
}
}