Skip to main content

tui_dispatch_debug/
replay.rs

1//! Replay items for action replay with await markers.
2//!
3//! This module provides types for replay files that can include
4//! `_await` markers to pause replay until async effects complete.
5//!
6//! # Example JSON
7//! ```json
8//! [
9//!   {"SearchQuerySubmit": "Cabo Verde"},
10//!   {"_await": "SearchDidLoad"},
11//!   {"SearchSelect": 0},
12//!   "SearchConfirm",
13//!   {"_await_any": ["WeatherDidLoad", "WeatherDidError"]}
14//! ]
15//! ```
16
17use serde::{Deserialize, Serialize};
18
19#[cfg(feature = "json-schema")]
20use schemars::JsonSchema;
21
22/// A replay item: either an action or an await marker.
23///
24/// When deserializing, this uses untagged enum representation so that:
25/// - `{"_await": "pattern"}` becomes `AwaitOne`
26/// - `{"_await_any": [...]}` becomes `AwaitAny`
27/// - Any other JSON becomes `Action(A)`
28#[derive(Debug, Clone, Deserialize, Serialize)]
29#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
30#[serde(untagged)]
31pub enum ReplayItem<A> {
32    /// Pause replay until an action matching the glob pattern is dispatched.
33    ///
34    /// Supports `*` as wildcard (e.g., `"*DidLoad"` matches `"WeatherDidLoad"`).
35    #[cfg_attr(
36        feature = "json-schema",
37        schemars(description = "Pause until action matching pattern (supports * glob)")
38    )]
39    AwaitOne {
40        /// Glob pattern to match against action names
41        _await: String,
42    },
43
44    /// Pause replay until any of the patterns match.
45    #[cfg_attr(
46        feature = "json-schema",
47        schemars(description = "Pause until any action matching one of the patterns")
48    )]
49    AwaitAny {
50        /// List of glob patterns - replay continues when any matches
51        _await_any: Vec<String>,
52    },
53
54    /// A regular action to dispatch.
55    Action(A),
56}
57
58impl<A> ReplayItem<A> {
59    /// Returns true if this is an await marker (not an action).
60    pub fn is_await(&self) -> bool {
61        matches!(
62            self,
63            ReplayItem::AwaitOne { .. } | ReplayItem::AwaitAny { .. }
64        )
65    }
66
67    /// Returns the action if this is an Action variant.
68    pub fn into_action(self) -> Option<A> {
69        match self {
70            ReplayItem::Action(a) => Some(a),
71            _ => None,
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use serde::{Deserialize, Serialize};
80
81    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82    enum TestAction {
83        Fetch,
84        DidLoad(String),
85        Select(usize),
86    }
87
88    #[test]
89    fn test_deserialize_action() {
90        let json = r#""Fetch""#;
91        let item: ReplayItem<TestAction> = serde_json::from_str(json).unwrap();
92        assert!(matches!(item, ReplayItem::Action(TestAction::Fetch)));
93    }
94
95    #[test]
96    fn test_deserialize_action_with_data() {
97        let json = r#"{"DidLoad": "hello"}"#;
98        let item: ReplayItem<TestAction> = serde_json::from_str(json).unwrap();
99        assert!(matches!(
100            item,
101            ReplayItem::Action(TestAction::DidLoad(s)) if s == "hello"
102        ));
103    }
104
105    #[test]
106    fn test_deserialize_await_one() {
107        let json = r#"{"_await": "DidLoad"}"#;
108        let item: ReplayItem<TestAction> = serde_json::from_str(json).unwrap();
109        assert!(matches!(
110            item,
111            ReplayItem::AwaitOne { _await } if _await == "DidLoad"
112        ));
113    }
114
115    #[test]
116    fn test_deserialize_await_any() {
117        let json = r#"{"_await_any": ["DidLoad", "DidError"]}"#;
118        let item: ReplayItem<TestAction> = serde_json::from_str(json).unwrap();
119        match item {
120            ReplayItem::AwaitAny { _await_any } => {
121                assert_eq!(_await_any, vec!["DidLoad", "DidError"]);
122            }
123            _ => panic!("expected AwaitAny"),
124        }
125    }
126
127    #[test]
128    fn test_deserialize_mixed_array() {
129        let json = r#"[
130            "Fetch",
131            {"_await": "*DidLoad"},
132            {"Select": 0}
133        ]"#;
134        let items: Vec<ReplayItem<TestAction>> = serde_json::from_str(json).unwrap();
135        assert_eq!(items.len(), 3);
136        assert!(matches!(items[0], ReplayItem::Action(TestAction::Fetch)));
137        assert!(matches!(items[1], ReplayItem::AwaitOne { .. }));
138        assert!(matches!(
139            items[2],
140            ReplayItem::Action(TestAction::Select(0))
141        ));
142    }
143
144    #[test]
145    fn test_is_await() {
146        let action: ReplayItem<TestAction> = ReplayItem::Action(TestAction::Fetch);
147        let await_one: ReplayItem<TestAction> = ReplayItem::AwaitOne {
148            _await: "test".into(),
149        };
150        let await_any: ReplayItem<TestAction> = ReplayItem::AwaitAny {
151            _await_any: vec!["a".into()],
152        };
153
154        assert!(!action.is_await());
155        assert!(await_one.is_await());
156        assert!(await_any.is_await());
157    }
158}