Skip to main content

nifty_download/
follow.rs

1//! Keeping a download's bindings on its item when the list holding it changes.
2//!
3//! A binding names a list item by its position, and positions move when items before it
4//! are removed or the list is rebuilt. A download bound with `id:` writes its id into the
5//! item, so before each report the app looks for the item that holds the id, and moves
6//! each binding that shares the item's path with it to wherever that item is now.
7
8use nifty_runtime::{DownloadField, Step};
9
10/// A place in an app's state, as its runner names one.
11pub trait StatePlace: Clone + PartialEq {
12    /// The fields and items from its state down to it.
13    fn steps(&self) -> &[Step];
14    /// The same state, with `steps` instead.
15    #[must_use]
16    fn with_steps(&self, steps: Vec<Step>) -> Self;
17    /// Whether `other` is in the same state.
18    fn same_root(&self, other: &Self) -> bool;
19}
20
21/// Reading an app's state, for finding where a download's id went.
22pub trait StateReader {
23    type Place: StatePlace;
24    /// How many items the list at `place` has; `None` when there's no such place.
25    fn list_len(&self, place: &Self::Place) -> Option<usize>;
26    /// Whether the text at `place` is `text`; `false` when there's no such place.
27    fn holds_text(&self, place: &Self::Place, text: &str) -> bool;
28}
29
30/// Moves `bindings` to the item that holds `id` now. `false` when no item holds it any
31/// more, so nothing shows the download. Bindings without an `id:` stay where they are.
32pub fn follow_id<R: StateReader>(
33    reader: &R,
34    bindings: &mut [(DownloadField, R::Place)],
35    id: &str,
36) -> bool {
37    let Some(at) = bindings
38        .iter()
39        .find(|(field, _)| *field == DownloadField::Id)
40        .map(|(_, place)| place.clone())
41    else {
42        return true;
43    };
44    let Some(now) = find(reader, &at, id) else {
45        return false;
46    };
47    if now != at {
48        for (_, place) in bindings.iter_mut() {
49            *place = moved(place, &at, &now);
50        }
51    }
52    true
53}
54
55/// Where the text `id` is now, among the places shaped like `at`: `at` itself while it
56/// still holds it, or else the first such place that does, trying every item of each list.
57fn find<R: StateReader>(reader: &R, at: &R::Place, id: &str) -> Option<R::Place> {
58    if reader.holds_text(at, id) {
59        return Some(at.clone());
60    }
61    search(reader, at, &mut Vec::new(), at.steps(), id)
62}
63
64fn search<R: StateReader>(
65    reader: &R,
66    at: &R::Place,
67    walked: &mut Vec<Step>,
68    rest: &[Step],
69    id: &str,
70) -> Option<R::Place> {
71    let Some((step, rest)) = rest.split_first() else {
72        let place = at.with_steps(walked.clone());
73        return reader.holds_text(&place, id).then_some(place);
74    };
75    let choices = match *step {
76        Step::Field(_) => vec![*step],
77        Step::Index(_) => {
78            let len = reader.list_len(&at.with_steps(walked.clone()))?;
79            (0..len).map(Step::Index).collect()
80        }
81    };
82    choices.into_iter().find_map(|choice| {
83        walked.push(choice);
84        let found = search(reader, at, walked, rest, id);
85        walked.pop();
86        found
87    })
88}
89
90/// `place`, moved the way the id moved from `from` to `to`: the steps it shares with
91/// `from` follow `to`, and the rest stay. A place in another state shares no steps, since
92/// `from` and `to` only differ in their list positions.
93fn moved<P: StatePlace>(place: &P, from: &P, to: &P) -> P {
94    if !place.same_root(from) {
95        return place.clone();
96    }
97    let shared = place
98        .steps()
99        .iter()
100        .zip(from.steps())
101        .take_while(|(a, b)| a == b)
102        .count();
103    let mut steps = to.steps()[..shared].to_vec();
104    steps.extend_from_slice(&place.steps()[shared..]);
105    place.with_steps(steps)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    /// A place in a test's state: which state, and the steps into it.
113    #[derive(Clone, Debug, PartialEq)]
114    struct Place(usize, Vec<Step>);
115
116    impl StatePlace for Place {
117        fn steps(&self) -> &[Step] {
118            &self.1
119        }
120
121        fn with_steps(&self, steps: Vec<Step>) -> Self {
122            Self(self.0, steps)
123        }
124
125        fn same_root(&self, other: &Self) -> bool {
126            self.0 == other.0
127        }
128    }
129
130    /// State 0 is a list of rows, each with its id in field 0 and its percentage in field
131    /// 1; state 1 is a separate number.
132    struct Rows(Vec<&'static str>);
133
134    impl StateReader for Rows {
135        type Place = Place;
136
137        fn list_len(&self, place: &Place) -> Option<usize> {
138            (place.0 == 0 && place.1.is_empty()).then_some(self.0.len())
139        }
140
141        fn holds_text(&self, place: &Place, text: &str) -> bool {
142            match place.1[..] {
143                [Step::Index(row), Step::Field(0)] if place.0 == 0 => {
144                    self.0.get(row) == Some(&text)
145                }
146                _ => false,
147            }
148        }
149    }
150
151    fn row(index: usize, field: usize) -> Place {
152        Place(0, vec![Step::Index(index), Step::Field(field)])
153    }
154
155    fn bindings_at(index: usize) -> Vec<(DownloadField, Place)> {
156        vec![
157            (DownloadField::Id, row(index, 0)),
158            (DownloadField::Progress, row(index, 1)),
159            (DownloadField::Status, Place(1, Vec::new())),
160        ]
161    }
162
163    #[test]
164    fn bindings_stay_while_the_item_stays() {
165        let mut bindings = bindings_at(1);
166        assert!(follow_id(&Rows(vec!["a", "b"]), &mut bindings, "b"));
167        assert_eq!(bindings, bindings_at(1));
168    }
169
170    #[test]
171    fn bindings_follow_their_item_to_its_new_position() {
172        let mut bindings = bindings_at(2);
173        assert!(follow_id(&Rows(vec!["c", "x"]), &mut bindings, "c"));
174        assert_eq!(
175            bindings,
176            [
177                (DownloadField::Id, row(0, 0)),
178                (DownloadField::Progress, row(0, 1)),
179                (DownloadField::Status, Place(1, Vec::new())),
180            ],
181            "the binding outside the list stays"
182        );
183    }
184
185    #[test]
186    fn a_removed_item_lets_go_of_the_download() {
187        let mut bindings = bindings_at(0);
188        assert!(!follow_id(&Rows(vec!["x"]), &mut bindings, "a"));
189        assert!(!follow_id(&Rows(Vec::new()), &mut bindings, "a"));
190    }
191
192    #[test]
193    fn bindings_without_an_id_stay_put() {
194        let mut bindings = vec![(DownloadField::Progress, row(3, 1))];
195        assert!(follow_id(&Rows(Vec::new()), &mut bindings, "a"));
196        assert_eq!(bindings, [(DownloadField::Progress, row(3, 1))]);
197    }
198}