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}