Skip to main content

guinea_core/
guard.rs

1//! Asking a scope whether it may be torn down.
2//!
3//! A guard is a stage of navigation, not a redirect. Redirect-as-guard
4//! corrupts history - the place you were refused entry to is still in the back
5//! stack, so going back walks into the refusal again - and every ecosystem
6//! that tried it reached that conclusion separately.
7//!
8//! It lives here rather than in the router because the thing being asked is a
9//! [`Scope`](crate::scope::Scope): "may I be dropped" is a question about a
10//! scope's own state, and the router only decides when to ask it.
11
12use std::cell::{Cell, RefCell};
13use std::rc::Rc;
14
15/// What to put to the user when a guard wants an answer.
16///
17/// Plain data, no toolkit: every backend can draw three strings, and none of
18/// them can draw each other's dialog type. Built at the moment of asking
19/// rather than at install, so it is in whatever language is current then.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct Ask {
22    pub text: String,
23    pub confirm: String,
24    pub cancel: String,
25}
26
27impl Ask {
28    pub fn new(
29        text: impl Into<String>,
30        confirm: impl Into<String>,
31        cancel: impl Into<String>,
32    ) -> Self {
33        Self {
34            text: text.into(),
35            confirm: confirm.into(),
36            cancel: cancel.into(),
37        }
38    }
39}
40
41/// What a guard answers.
42///
43/// `Allow` allocates nothing, so the overwhelming majority of navigations stay
44/// exactly as synchronous as they were. "Optionally async" is the right to
45/// return the third variant, not a cost paid by the first.
46/// `Clone` because a backend may have to answer from a place that cannot reach
47/// the state the answer was computed from - it keeps the verdict beside the
48/// state and hands out copies.
49#[derive(Clone)]
50pub enum Verdict {
51    Allow,
52    Block,
53    /// Not yet. The question goes to the user; the [`Decision`] carries the
54    /// answer back.
55    Ask(Ask, Decision),
56}
57
58impl Verdict {
59    /// A question for the router to put, answered through `Router::answer`.
60    ///
61    /// A guard that means to answer from somewhere else - an actor finishing a
62    /// save, say - builds the [`Decision`] itself, keeps a clone, and returns
63    /// [`Verdict::Ask`] directly.
64    pub fn ask(ask: Ask) -> Self {
65        Verdict::Ask(ask, Decision::new())
66    }
67}
68
69/// The answer to a guard's question, before there is one.
70///
71/// A token rather than a future, deliberately. There is no `LocalSet` in the
72/// tree and the future would be `!Send`; this is an actor system rather than a
73/// combinator one, so the guard hands the token wherever the answer will come
74/// from and someone calls [`allow`](Self::allow) or [`block`](Self::block).
75///
76/// Settling twice does nothing: a superseded navigation may leave a token
77/// nobody will ever answer, and a dialog closed twice must not answer twice.
78#[derive(Clone)]
79pub struct Decision {
80    inner: Rc<Inner>,
81}
82
83/// What runs once the answer arrives.
84type Resume = Box<dyn FnOnce(bool)>;
85
86struct Inner {
87    settled: Cell<bool>,
88    answer: Cell<bool>,
89    /// Installed by whoever is waiting - the router, in practice.
90    resume: RefCell<Option<Resume>>,
91}
92
93impl Default for Decision {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl Decision {
100    pub fn new() -> Self {
101        Self {
102            inner: Rc::new(Inner {
103                settled: Cell::new(false),
104                answer: Cell::new(false),
105                resume: RefCell::new(None),
106            }),
107        }
108    }
109
110    /// Go ahead with the navigation that was parked.
111    pub fn allow(&self) {
112        self.settle(true);
113    }
114
115    /// Stay where we are.
116    pub fn block(&self) {
117        self.settle(false);
118    }
119
120    pub fn is_settled(&self) -> bool {
121        self.inner.settled.get()
122    }
123
124    /// What to run once the answer arrives. Replaces any previous one.
125    ///
126    /// If the answer already arrived, `resume` runs at once - a guard is free
127    /// to settle its own token before returning, and nothing should depend on
128    /// which came first.
129    pub fn on_answer(&self, resume: impl FnOnce(bool) + 'static) {
130        if self.inner.settled.get() {
131            resume(self.answered_with());
132            return;
133        }
134        *self.inner.resume.borrow_mut() = Some(Box::new(resume));
135    }
136
137    fn settle(&self, allowed: bool) {
138        if self.inner.settled.replace(true) {
139            return;
140        }
141        self.inner.answer.set(allowed);
142        // Taken out before running: `resume` is free to start another
143        // navigation, which would otherwise re-enter this borrow.
144        let resume = self.inner.resume.borrow_mut().take();
145        if let Some(resume) = resume {
146            resume(allowed);
147        }
148    }
149
150    fn answered_with(&self) -> bool {
151        self.inner.answer.get()
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn an_answer_reaches_whoever_was_waiting() {
161        let seen = Rc::new(Cell::new(None));
162        let decision = Decision::new();
163
164        let recorder = seen.clone();
165        decision.on_answer(move |allowed| recorder.set(Some(allowed)));
166
167        decision.allow();
168        assert_eq!(seen.get(), Some(true));
169    }
170
171    #[test]
172    fn an_answer_that_arrived_first_is_not_lost() {
173        let decision = Decision::new();
174        decision.block();
175
176        let seen = Rc::new(Cell::new(None));
177        let recorder = seen.clone();
178        decision.on_answer(move |allowed| recorder.set(Some(allowed)));
179
180        assert_eq!(
181            seen.get(),
182            Some(false),
183            "a guard may settle its own token before it returns"
184        );
185    }
186
187    #[test]
188    fn a_second_answer_changes_nothing() {
189        let count = Rc::new(Cell::new(0));
190        let decision = Decision::new();
191
192        let counter = count.clone();
193        decision.on_answer(move |_| counter.set(counter.get() + 1));
194
195        decision.allow();
196        decision.block();
197        assert_eq!(count.get(), 1, "a dialog closed twice must not answer twice");
198    }
199}