Skip to main content

verbs/
marker_plan.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Pure marker delete/list planning (no refs/oplog I/O).
3
4/// How `thread marker delete` selects targets.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum MarkerDeleteSelector {
7    /// Exact marker name.
8    Name(String),
9    /// Non-empty name prefix.
10    Prefix(String),
11}
12
13/// Invalid delete selector combinations.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum MarkerDeleteSelectorError {
16    /// Both name and prefix supplied.
17    Conflict,
18    /// Neither name nor prefix supplied.
19    Required,
20}
21
22impl MarkerDeleteSelectorError {
23    pub fn kind(self) -> &'static str {
24        match self {
25            Self::Conflict => "marker_delete_selector_conflict",
26            Self::Required => "marker_delete_selector_required",
27        }
28    }
29}
30
31/// Plan delete selector from CLI flags.
32pub fn plan_marker_delete_selector(
33    name: Option<String>,
34    prefix: Option<String>,
35) -> Result<MarkerDeleteSelector, MarkerDeleteSelectorError> {
36    match (name, prefix) {
37        (Some(name), None) => Ok(MarkerDeleteSelector::Name(name)),
38        (None, Some(prefix)) => {
39            if prefix.is_empty() {
40                // Empty prefix is refused separately at apply time with a
41                // dedicated empty-prefix kind; treat as Required here only
42                // when prefix is absent. Empty string is still Prefix so
43                // callers can map to empty-prefix advice.
44                Ok(MarkerDeleteSelector::Prefix(prefix))
45            } else {
46                Ok(MarkerDeleteSelector::Prefix(prefix))
47            }
48        }
49        (Some(_), Some(_)) => Err(MarkerDeleteSelectorError::Conflict),
50        (None, None) => Err(MarkerDeleteSelectorError::Required),
51    }
52}
53
54/// Whether a bulk prefix delete should refuse (empty prefix).
55pub fn marker_prefix_is_valid(prefix: &str) -> bool {
56    !prefix.is_empty()
57}
58
59/// List filter: empty / missing filter matches all; non-empty is prefix match.
60pub fn marker_list_filter_matches(name: &str, filter: Option<&str>) -> bool {
61    match filter {
62        Some(prefix) if !prefix.is_empty() => name.starts_with(prefix),
63        _ => true,
64    }
65}
66
67/// Human message for bulk prefix delete outcomes.
68pub fn marker_bulk_delete_message(prefix: &str, count: usize) -> String {
69    match count {
70        0 => format!("No markers matched prefix '{prefix}'"),
71        1 => format!("Deleted 1 marker matching prefix '{prefix}'"),
72        n => format!("Deleted {n} markers matching prefix '{prefix}'"),
73    }
74}
75
76/// Create / delete single-marker success messages.
77pub fn marker_create_message(name: &str, state_id_short: &str) -> String {
78    format!("Created marker '{name}' at {state_id_short}")
79}
80
81pub fn marker_delete_message(name: &str) -> String {
82    format!("Deleted marker '{name}'")
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn delete_selector_plans() {
91        assert_eq!(
92            plan_marker_delete_selector(Some("m".into()), None).unwrap(),
93            MarkerDeleteSelector::Name("m".into())
94        );
95        assert_eq!(
96            plan_marker_delete_selector(None, Some("pre".into())).unwrap(),
97            MarkerDeleteSelector::Prefix("pre".into())
98        );
99        assert_eq!(
100            plan_marker_delete_selector(Some("a".into()), Some("b".into())),
101            Err(MarkerDeleteSelectorError::Conflict)
102        );
103        assert_eq!(
104            plan_marker_delete_selector(None, None),
105            Err(MarkerDeleteSelectorError::Required)
106        );
107    }
108
109    #[test]
110    fn prefix_and_filter() {
111        assert!(marker_prefix_is_valid("x"));
112        assert!(!marker_prefix_is_valid(""));
113        assert!(marker_list_filter_matches("foo", None));
114        assert!(marker_list_filter_matches("foo", Some("")));
115        assert!(marker_list_filter_matches("foobar", Some("foo")));
116        assert!(!marker_list_filter_matches("bar", Some("foo")));
117    }
118
119    #[test]
120    fn bulk_messages() {
121        assert!(marker_bulk_delete_message("p", 0).contains("No markers"));
122        assert!(marker_bulk_delete_message("p", 1).contains("1 marker"));
123        assert!(marker_bulk_delete_message("p", 3).contains("3 markers"));
124    }
125}