1use std::cell::RefCell;
2use std::collections::VecDeque;
3use std::pin::Pin;
4use std::task::{Context, Poll};
5
6use serde::ser::SerializeMap as _;
7use serde::{Serialize, Serializer};
8
9use crate::show::{Fields, Shown};
10use crate::{Cast, Error, FieldError, Result, SqlValue};
11
12const REFUSED_MOVE: &str = "validation.move";
13
14pub type Moves<T> = fn(&T) -> Option<&'static Targets>;
16
17#[derive(Debug, PartialEq, Eq)]
19pub struct Targets {
20 pub of: &'static str,
21 pub states: &'static [&'static str],
22}
23
24impl Targets {
25 #[must_use]
26 pub fn contains(&self, name: &str) -> bool {
27 self.states.contains(&name)
28 }
29}
30
31#[must_use]
32pub fn refused(field: impl Into<String>, from: &str, to: &str) -> Error {
33 let error = FieldError::new(
34 field,
35 REFUSED_MOVE,
36 format!("cannot move from {from} to {to}"),
37 )
38 .with("from", from)
39 .with("to", to);
40 Error::Invalid(vec![error])
41}
42
43#[must_use]
44pub fn targets<T: Cast>(value: &T) -> Option<&'static Targets> {
45 T::MOVES.and_then(|moves| moves(value))
46}
47
48pub fn refuse_unlisted<T: Cast>(field: &str, stored: &T, current: &T) -> Result<()> {
49 let Some(targets) = targets(stored) else {
50 return Ok(());
51 };
52 let (mut old, mut new) = (Vec::new(), Vec::new());
53 stored.encode(&mut old);
54 current.encode(&mut new);
55 if new == old {
56 return Ok(());
57 }
58 match new.as_slice() {
59 [SqlValue::Text(target)]
61 if take(targets.of, state_name(&old), target) || targets.contains(target) =>
62 {
63 Ok(())
64 }
65 _ => Err(refused(field, state_name(&old), state_name(&new))),
66 }
67}
68
69fn state_name(columns: &[SqlValue]) -> &str {
70 match columns {
71 [SqlValue::Text(name)] => name,
72 _ => "nothing",
73 }
74}
75
76impl Fields for Targets {
77 fn name(&self, index: usize) -> Option<&str> {
78 self.states.get(index).copied()
79 }
80
81 fn get(&self, name: &str) -> Option<Shown<'_>> {
82 self.contains(name).then_some(Shown::Bool(true))
83 }
84}
85
86impl Serialize for Targets {
87 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
88 let mut map = serializer.serialize_map(Some(self.states.len()))?;
89 for name in self.states {
90 map.serialize_entry(name, &true)?;
91 }
92 map.end()
93 }
94}
95
96const UNSCOPED_LIMIT: usize = 64;
98
99thread_local! {
100 static JOURNAL: RefCell<Journal> = const {
101 RefCell::new(Journal {
102 moves: Vec::new(),
103 scoped: false,
104 })
105 };
106}
107
108struct Journal {
109 moves: Vec<Move>,
110 scoped: bool,
112}
113
114#[derive(Clone, Copy)]
115struct Move {
116 of: &'static str,
117 from: &'static str,
118 to: &'static str,
119}
120
121pub(crate) fn record(of: &'static str, from: &'static str, to: &'static str) {
122 JOURNAL.with_borrow_mut(|journal| {
123 if !journal.scoped && journal.moves.len() >= UNSCOPED_LIMIT {
124 journal.moves.remove(0);
125 }
126 journal.moves.push(Move { of, from, to });
127 });
128}
129
130#[must_use]
133pub fn take(of: &str, from: &str, to: &str) -> bool {
134 JOURNAL.with_borrow_mut(|journal| {
135 let Some(mut chain) = chain(&journal.moves, of, from, to) else {
136 return false;
137 };
138 chain.sort_unstable_by(|first, second| second.cmp(first));
139 for index in chain {
140 journal.moves.remove(index);
141 }
142 true
143 })
144}
145
146fn chain(moves: &[Move], of: &str, from: &str, to: &str) -> Option<Vec<usize>> {
147 let mut before: Vec<Option<usize>> = vec![None; moves.len()];
148 let mut seen: Vec<bool> = moves
149 .iter()
150 .map(|step| step.of == of && step.from == from)
151 .collect();
152 let mut queue: VecDeque<usize> = (0..moves.len()).filter(|index| seen[*index]).collect();
153 while let Some(index) = queue.pop_front() {
154 let step = moves[index];
155 if step.to == to {
156 let mut chain = vec![index];
157 while let Some(previous) = before[*chain.last()?] {
158 chain.push(previous);
159 }
160 return Some(chain);
161 }
162 for (next, candidate) in moves.iter().enumerate() {
163 if !seen[next] && candidate.of == of && candidate.from == step.to {
164 seen[next] = true;
165 before[next] = Some(index);
166 queue.push_back(next);
167 }
168 }
169 }
170 None
171}
172
173pub fn recorded<F: Future>(future: F) -> Recorded<F> {
175 Recorded {
176 future: Box::pin(future),
177 journal: Journal {
178 moves: Vec::new(),
179 scoped: true,
180 },
181 }
182}
183
184pub struct Recorded<F: Future> {
185 future: Pin<Box<F>>,
186 journal: Journal,
187}
188
189impl<F: Future> Future for Recorded<F> {
190 type Output = F::Output;
191
192 fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<F::Output> {
193 let this = &mut *self;
194 let _lent = Lent::new(&mut this.journal);
195 this.future.as_mut().poll(context)
196 }
197}
198
199struct Lent<'a>(&'a mut Journal);
202
203impl<'a> Lent<'a> {
204 fn new(journal: &'a mut Journal) -> Self {
205 swap_with_thread(journal);
206 Self(journal)
207 }
208}
209
210impl Drop for Lent<'_> {
211 fn drop(&mut self) {
212 swap_with_thread(self.0);
213 }
214}
215
216fn swap_with_thread(journal: &mut Journal) {
217 JOURNAL.with_borrow_mut(|thread| std::mem::swap(thread, journal));
218}
219
220#[cfg(test)]
221mod tests {
222 use std::task::Waker;
223
224 use super::*;
225
226 fn in_scope<T>(work: impl FnOnce() -> T) -> T {
227 let mut scoped = std::pin::pin!(recorded(async { work() }));
228 match scoped
229 .as_mut()
230 .poll(&mut Context::from_waker(Waker::noop()))
231 {
232 Poll::Ready(answer) => answer,
233 Poll::Pending => unreachable!("the work never waits"),
234 }
235 }
236
237 #[test]
238 fn targets_show_and_serialize_each_state_as_true() {
239 let targets = Targets {
240 of: "Light",
241 states: &["RED"],
242 };
243
244 assert!(targets.contains("RED") && !targets.contains("GREEN"));
245 assert_eq!((targets.name(0), targets.name(1)), (Some("RED"), None));
246 assert!(matches!(targets.get("RED"), Some(Shown::Bool(true))));
247 assert!(targets.get("GREEN").is_none());
248 assert_eq!(
249 serde_json::to_value(&targets).unwrap(),
250 serde_json::json!({ "RED": true })
251 );
252 }
253
254 #[test]
255 fn follows_recorded_steps_of_one_type() {
256 in_scope(|| {
257 record("Order", "OPEN", "PAID");
258 record("Order", "PAID", "SHIPPED");
259 record("Light", "SHIPPED", "GONE");
260
261 assert!(!take("Order", "OPEN", "GONE"));
262 assert!(!take("Order", "SHIPPED", "OPEN"));
263 assert!(take("Order", "OPEN", "SHIPPED"));
264 });
265 }
266
267 #[test]
268 fn a_move_of_another_type_explains_nothing() {
269 in_scope(|| {
270 record("Payment", "PENDING", "PAID");
271
272 assert!(!take("Order", "PENDING", "PAID"));
273 assert!(take("Payment", "PENDING", "PAID"));
274 });
275 }
276
277 #[test]
278 fn a_move_explains_one_record_s_save_only() {
279 in_scope(|| {
280 record("Order", "OPEN", "PAID");
281 record("Order", "PAID", "SHIPPED");
282
283 assert!(take("Order", "OPEN", "PAID"), "the first record's own move");
284 assert!(
285 take("Order", "PAID", "SHIPPED"),
286 "the second record's own move"
287 );
288 assert!(
289 !take("Order", "OPEN", "SHIPPED"),
290 "a third record jumping by assignment"
291 );
292 });
293 }
294
295 #[test]
296 fn a_state_is_not_reached_from_itself_without_a_round_trip() {
297 in_scope(|| {
298 record("Order", "OPEN", "PAID");
299
300 assert!(!take("Order", "OPEN", "OPEN"));
301 record("Order", "PAID", "OPEN");
302 assert!(take("Order", "OPEN", "OPEN"));
303 });
304 }
305
306 #[test]
307 fn a_scope_keeps_every_move_however_many() {
308 in_scope(|| {
309 record("Order", "OPEN", "PAID");
310 for _ in 0..UNSCOPED_LIMIT * 2 {
311 record("Light", "RED", "GREEN");
312 }
313
314 assert!(take("Order", "OPEN", "PAID"));
315 });
316 }
317
318 #[test]
319 fn a_recorded_future_keeps_its_moves_to_itself() {
320 let inside = in_scope(|| {
321 record("Order", "OPEN", "PAID");
322 take("Order", "OPEN", "PAID")
323 });
324
325 assert!(inside);
326 assert!(!take("Order", "OPEN", "PAID"));
327 }
328
329 #[test]
330 fn a_panicking_future_gives_the_thread_its_own_journal_back() {
331 let caught = std::panic::catch_unwind(|| {
332 in_scope(|| {
333 record("Order", "OPEN", "PAID");
334 panic!("the handler failed");
335 });
336 });
337
338 assert!(caught.is_err());
339 assert!(!take("Order", "OPEN", "PAID"));
340 }
341}