Skip to main content

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}