timed_fsm/response.rs
1use std::time::Duration;
2
3/// The result of a state machine transition.
4///
5/// A `Response` is a **declarative description** of what should happen:
6/// which actions to emit, which timers to set or kill, and whether the
7/// original event was consumed.
8///
9/// The state machine never executes side effects directly. The runtime
10/// reads the `Response` and performs the actual timer operations and
11/// action execution via [`dispatch`](crate::dispatch::dispatch).
12///
13/// # Three-field contract
14///
15/// | Field | Type | Meaning |
16/// |-------|------|---------|
17/// | `consumed` | `bool` | `true` → caller must **not** forward the event further; `false` → caller should pass it to the next handler |
18/// | `actions` | `Vec<A>` | Ordered list of output actions to execute; may be empty |
19/// | `timers` | `Vec<TimerCommand<T>>` | Ordered timer instructions (set/kill); processed before `actions` by [`dispatch`](crate::dispatch::dispatch) |
20///
21/// Build responses with the constructor methods ([`emit_one`](Self::emit_one),
22/// [`consume`](Self::consume), [`pass_through`](Self::pass_through)) and the
23/// chaining methods ([`with_timer`](Self::with_timer),
24/// [`with_kill_timer`](Self::with_kill_timer)).
25#[derive(Debug)]
26pub struct Response<A, T> {
27 /// Whether the original event was consumed by the state machine.
28 ///
29 /// - `true`: the event was handled; the caller should **not** propagate it.
30 /// - `false`: the event was not handled; the caller should propagate it
31 /// (e.g., pass through to the next hook in a chain).
32 pub consumed: bool,
33
34 /// Actions to emit, in order.
35 ///
36 /// May be empty if the transition only affects internal state or timers.
37 pub actions: Vec<A>,
38
39 /// Timer commands to execute, in order.
40 ///
41 /// The runtime must process these commands after the actions.
42 /// A [`Set`](TimerCommand::Set) with an ID that already has an active
43 /// timer should reset (overwrite) that timer.
44 pub timers: Vec<TimerCommand<T>>,
45}
46
47/// A command to set or kill a timer.
48///
49/// Timer commands are part of the [`Response`] returned by state machine
50/// transitions. The runtime is responsible for translating these into
51/// actual platform timer operations.
52///
53/// # `Set` vs `Kill`
54///
55/// | Variant | When to use |
56/// |---------|-------------|
57/// | [`Set`](Self::Set) | Start a new timer **or** reset an existing one (e.g., restart the debounce window on every incoming event) |
58/// | [`Kill`](Self::Kill) | Cancel a pending timer that is no longer needed (e.g., a key was released before the chord window closed) |
59///
60/// `Set` with an ID that already has an active timer **resets** that
61/// timer — it does not create a second concurrent timer for the same ID.
62/// `Kill` for an ID with no active timer is a silent no-op.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum TimerCommand<T> {
65 /// Start (or restart) a timer with the given ID and duration.
66 ///
67 /// If a timer with the same ID is already active, it is reset
68 /// to the new duration. Use this to implement "restart the window
69 /// on every new event" patterns such as debounce or inactivity
70 /// timeouts.
71 Set {
72 /// Timer identifier.
73 id: T,
74 /// Duration after which [`TimedStateMachine::on_timeout`](crate::TimedStateMachine::on_timeout) should be called.
75 duration: Duration,
76 },
77
78 /// Stop an active timer.
79 ///
80 /// If no timer with this ID is active, this is a no-op. Use this
81 /// when a subsequent event makes the pending timeout irrelevant
82 /// (e.g., the other chord key was released, so the window is moot).
83 Kill {
84 /// Timer identifier to stop.
85 id: T,
86 },
87}
88
89// ── Builder methods ──────────────────────────────────────────
90
91impl<A, T> Response<A, T> {
92 /// Create a response that consumes the event and emits multiple actions.
93 ///
94 /// Use this when a single transition produces two or more output actions
95 /// that need to be dispatched together (e.g., a chord that generates a
96 /// modifier keydown followed by a character keydown).
97 #[must_use]
98 pub const fn emit(actions: Vec<A>) -> Self {
99 Self {
100 consumed: true,
101 actions,
102 timers: Vec::new(),
103 }
104 }
105
106 /// Create a response that consumes the event and emits a single action.
107 ///
108 /// Use this for the common case where exactly one output action is
109 /// produced (e.g., a confirmed debounce level, a recognized key stroke).
110 #[must_use]
111 pub fn emit_one(action: A) -> Self {
112 Self {
113 consumed: true,
114 actions: vec![action],
115 timers: Vec::new(),
116 }
117 }
118
119 /// Create a response that consumes the event but emits no actions yet.
120 ///
121 /// Use this when the state machine enters a pending (buffering) state
122 /// and will emit actions only on a later event or timeout — the
123 /// **timer-pending pattern**. Typically followed by
124 /// [`.with_timer(…)`](Self::with_timer) to start the decision window.
125 ///
126 /// ```
127 /// # use std::time::Duration;
128 /// # use timed_fsm::Response;
129 /// // Absorb the event and start a 50 ms window.
130 /// let r: Response<u8, ()> = Response::consume()
131 /// .with_timer((), Duration::from_millis(50));
132 /// r.assert_consumed();
133 /// r.assert_timer_set(());
134 /// ```
135 #[must_use]
136 pub const fn consume() -> Self {
137 Self {
138 consumed: true,
139 actions: Vec::new(),
140 timers: Vec::new(),
141 }
142 }
143
144 /// Create a response that does **not** consume the event.
145 ///
146 /// Use this when this state machine does not handle the current event
147 /// and the caller should pass it to the next handler in the chain
148 /// (e.g., a key combination that is not part of this machine's grammar).
149 #[must_use]
150 pub const fn pass_through() -> Self {
151 Self {
152 consumed: false,
153 actions: Vec::new(),
154 timers: Vec::new(),
155 }
156 }
157
158 /// Add a timer set command to this response.
159 ///
160 /// This is a chainable builder method. It appends a
161 /// [`TimerCommand::Set`] with the given `id` and `duration`.
162 /// If a timer with the same ID is already running, the runtime
163 /// must reset it to the new duration.
164 #[must_use]
165 pub fn with_timer(mut self, id: T, duration: Duration) -> Self {
166 self.timers.push(TimerCommand::Set { id, duration });
167 self
168 }
169
170 /// Add a timer kill command to this response.
171 ///
172 /// This is a chainable builder method. It appends a
173 /// [`TimerCommand::Kill`] for the given `id`. If no timer with
174 /// that ID is active, the runtime treats this as a no-op.
175 #[must_use]
176 pub fn with_kill_timer(mut self, id: T) -> Self {
177 self.timers.push(TimerCommand::Kill { id });
178 self
179 }
180}
181
182// ── Trait implementations ────────────────────────────────────
183
184impl<A, T> Default for Response<A, T> {
185 fn default() -> Self {
186 Self::pass_through()
187 }
188}
189
190impl<A: Clone, T: Clone> Clone for Response<A, T> {
191 fn clone(&self) -> Self {
192 Self {
193 consumed: self.consumed,
194 actions: self.actions.clone(),
195 timers: self.timers.clone(),
196 }
197 }
198}
199
200impl<A: PartialEq, T: PartialEq> PartialEq for Response<A, T> {
201 fn eq(&self, other: &Self) -> bool {
202 self.consumed == other.consumed
203 && self.actions == other.actions
204 && self.timers == other.timers
205 }
206}
207
208impl<A: Eq, T: Eq> Eq for Response<A, T> {}
209
210// ── Assertion helpers ────────────────────────────────────────
211
212impl<A: core::fmt::Debug, T: Copy + Eq + core::fmt::Debug> Response<A, T> {
213 /// Assert that the event was consumed.
214 ///
215 /// # Panics
216 ///
217 /// Panics if `consumed` is `false`.
218 #[track_caller]
219 pub fn assert_consumed(&self) {
220 assert!(self.consumed, "expected consumed, got pass-through");
221 }
222
223 /// Assert that the event was not consumed (pass-through).
224 ///
225 /// # Panics
226 ///
227 /// Panics if `consumed` is `true`.
228 #[track_caller]
229 pub fn assert_pass_through(&self) {
230 assert!(!self.consumed, "expected pass-through, got consumed");
231 }
232
233 /// Assert that a timer set command exists for the given ID.
234 ///
235 /// # Panics
236 ///
237 /// Panics if no `Set` command with the given ID is found.
238 #[track_caller]
239 pub fn assert_timer_set(&self, id: T) {
240 assert!(
241 self.timers
242 .iter()
243 .any(|t| matches!(t, TimerCommand::Set { id: i, .. } if *i == id)),
244 "expected TimerCommand::Set with id {id:?}, found {:?}",
245 self.timers
246 );
247 }
248
249 /// Assert that a timer kill command exists for the given ID.
250 ///
251 /// # Panics
252 ///
253 /// Panics if no `Kill` command with the given ID is found.
254 #[track_caller]
255 pub fn assert_timer_kill(&self, id: T) {
256 assert!(
257 self.timers
258 .iter()
259 .any(|t| matches!(t, TimerCommand::Kill { id: i } if *i == id)),
260 "expected TimerCommand::Kill with id {id:?}, found {:?}",
261 self.timers
262 );
263 }
264
265 /// Assert the number of actions in the response.
266 ///
267 /// # Panics
268 ///
269 /// Panics if the action count does not match.
270 #[track_caller]
271 pub fn assert_action_count(&self, expected: usize) {
272 assert_eq!(
273 self.actions.len(),
274 expected,
275 "expected {expected} actions, got {}: {:?}",
276 self.actions.len(),
277 self.actions
278 );
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn emit_one_is_consumed_with_one_action() {
288 let r: Response<&str, ()> = Response::emit_one("hello");
289 assert!(r.consumed);
290 assert_eq!(r.actions, vec!["hello"]);
291 assert!(r.timers.is_empty());
292 }
293
294 #[test]
295 fn pass_through_is_not_consumed() {
296 let r: Response<&str, ()> = Response::pass_through();
297 assert!(!r.consumed);
298 assert!(r.actions.is_empty());
299 assert!(r.timers.is_empty());
300 }
301
302 #[test]
303 fn consume_is_consumed_with_no_actions() {
304 let r: Response<&str, ()> = Response::consume();
305 assert!(r.consumed);
306 assert!(r.actions.is_empty());
307 }
308
309 #[test]
310 fn builder_chain() {
311 let r: Response<i32, u8> = Response::emit_one(42)
312 .with_timer(1, Duration::from_millis(100))
313 .with_kill_timer(2);
314
315 assert!(r.consumed);
316 assert_eq!(r.actions, vec![42]);
317 assert_eq!(r.timers.len(), 2);
318 assert_eq!(
319 r.timers[0],
320 TimerCommand::Set {
321 id: 1,
322 duration: Duration::from_millis(100)
323 }
324 );
325 assert_eq!(r.timers[1], TimerCommand::Kill { id: 2 });
326 }
327
328 #[test]
329 fn default_is_pass_through() {
330 let r: Response<(), ()> = Response::default();
331 assert!(!r.consumed);
332 }
333
334 #[test]
335 fn assert_helpers_pass() {
336 let r: Response<i32, u8> = Response::emit_one(1)
337 .with_timer(0, Duration::from_millis(50))
338 .with_kill_timer(1);
339
340 r.assert_consumed();
341 r.assert_action_count(1);
342 r.assert_timer_set(0);
343 r.assert_timer_kill(1);
344 }
345
346 #[test]
347 #[should_panic(expected = "expected consumed")]
348 fn assert_consumed_panics_on_pass_through() {
349 Response::<(), ()>::pass_through().assert_consumed();
350 }
351
352 #[test]
353 #[should_panic(expected = "expected pass-through")]
354 fn assert_pass_through_panics_on_consumed() {
355 Response::<(), ()>::consume().assert_pass_through();
356 }
357
358 #[test]
359 fn clone_preserves_all_fields() {
360 let r = Response::emit(vec![1, 2])
361 .with_timer(0u8, Duration::from_millis(50))
362 .with_kill_timer(1);
363 let c = r.clone();
364 assert_eq!(r, c);
365 }
366
367 #[test]
368 fn partial_eq_detects_differences() {
369 let a: Response<i32, u8> = Response::emit_one(1);
370 let b: Response<i32, u8> = Response::emit_one(2);
371 assert_ne!(a, b);
372
373 let c: Response<i32, u8> = Response::consume();
374 let d: Response<i32, u8> = Response::pass_through();
375 assert_ne!(c, d);
376 }
377
378 #[test]
379 #[should_panic(expected = "expected TimerCommand::Set")]
380 fn assert_timer_set_panics_when_missing() {
381 Response::<(), u8>::consume().assert_timer_set(0);
382 }
383
384 #[test]
385 #[should_panic(expected = "expected TimerCommand::Kill")]
386 fn assert_timer_kill_panics_when_missing() {
387 Response::<(), u8>::consume().assert_timer_kill(0);
388 }
389
390 #[test]
391 #[should_panic(expected = "expected 3 actions")]
392 fn assert_action_count_panics_on_mismatch() {
393 Response::<i32, u8>::emit_one(1).assert_action_count(3);
394 }
395
396 #[test]
397 fn emit_with_multiple_actions() {
398 let r: Response<&str, ()> = Response::emit(vec!["a", "b", "c"]);
399 assert!(r.consumed);
400 assert_eq!(r.actions.len(), 3);
401 r.assert_action_count(3);
402 }
403}