Skip to main content

aven_protocol/tail/
parent.rs

1/// How an accepted change touches a task's deleted field.
2#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3pub enum ParentAction {
4    Create = 0,
5    Set = 1,
6    ForceResolve = 2,
7}
8
9impl ParentAction {
10    pub fn from_byte(byte: u8) -> Option<Self> {
11        match byte {
12            0 => Some(Self::Create),
13            1 => Some(Self::Set),
14            2 => Some(Self::ForceResolve),
15            _ => None,
16        }
17    }
18}
19
20/// Accepted-order conservative retention, independent of decrypted task storage.
21#[derive(Default)]
22pub struct ParentState {
23    pub version: Option<String>,
24    pub deleted: bool,
25    pub protected: bool,
26}
27impl ParentState {
28    /// A reference hint can add protection but cannot establish deletion evidence.
29    pub fn protect_hint(&mut self, deleted: bool, version: Option<&str>) {
30        self.protected |=
31            self.version.is_none() || self.version.as_deref() != version || self.deleted != deleted;
32    }
33
34    pub fn apply(&mut self, action: ParentAction, id: &str, deleted: bool, version: Option<&str>) {
35        if action == ParentAction::Create {
36            if self.version.is_none() {
37                self.version = version.map(str::to_owned);
38            }
39        } else if self.version.is_none()
40            || (action == ParentAction::Set && version != self.version.as_deref())
41        {
42            self.protected = true;
43        } else {
44            self.deleted = deleted;
45            self.version = Some(id.to_owned());
46        }
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    /// Applies accepted history as (id, action, deleted, version) where action
55    /// 0 creates, 1 sets and 2 force-resolves the deleted field; returns
56    /// whether the parent is unreferenced.
57    fn project(history: &[(&str, u8, bool, Option<&str>)]) -> bool {
58        let mut state = ParentState::default();
59        for (id, action, deleted, version) in history {
60            state.apply(
61                ParentAction::from_byte(*action).unwrap(),
62                id,
63                *deleted,
64                *version,
65            );
66        }
67        state.deleted && state.version.is_some() && !state.protected
68    }
69
70    #[test]
71    fn ordered_deletion_restore_and_custom_seed() {
72        let mut history = vec![
73            ("create", 0, false, Some("seed")),
74            ("delete", 1, true, Some("seed")),
75        ];
76        assert!(project(&history));
77        history.push(("restore", 1, false, Some("delete")));
78        assert!(!project(&history));
79        history.push(("delete2", 1, true, Some("restore")));
80        assert!(project(&history));
81    }
82
83    #[test]
84    fn stale_delete_and_force_resolution_keep_protection() {
85        let mut history = vec![
86            ("create", 0, false, Some("seed")),
87            ("delete", 1, true, Some("seed")),
88            ("restore", 1, false, Some("delete")),
89            ("stale", 1, true, Some("seed")),
90        ];
91        assert!(!project(&history));
92        history.push(("resolve", 2, true, Some("restore")));
93        assert!(!project(&history));
94    }
95
96    #[test]
97    fn equal_value_conflicts_and_missing_creation_are_conservative() {
98        assert!(!project(&[
99            ("create", 0, false, Some("seed")),
100            ("delete", 1, true, Some("seed")),
101            ("other", 1, true, Some("seed")),
102        ]));
103        assert!(!project(&[("delete", 1, true, None)]));
104        assert!(!project(&[]));
105    }
106}