rust_expect/dialog/
definition.rs

1//! Dialog definitions for scripted interactions.
2
3use std::collections::HashMap;
4use std::time::Duration;
5
6use crate::types::ControlChar;
7
8/// A dialog step definition.
9#[derive(Debug, Clone, Default)]
10pub struct DialogStep {
11    /// Name of this step (optional for simple dialogs).
12    pub name: String,
13    /// Pattern to expect.
14    pub expect: Option<String>,
15    /// Response to send.
16    pub send: Option<String>,
17    /// Control character to send (alternative to text).
18    pub send_control: Option<ControlChar>,
19    /// Timeout for this step.
20    pub timeout: Option<Duration>,
21    /// Whether to continue on timeout.
22    pub continue_on_timeout: bool,
23    /// Next step name (for branching).
24    pub next: Option<String>,
25    /// Conditional branches.
26    pub branches: HashMap<String, String>,
27}
28
29impl DialogStep {
30    /// Create a new step with a name.
31    #[must_use]
32    pub fn new(name: impl Into<String>) -> Self {
33        Self {
34            name: name.into(),
35            ..Default::default()
36        }
37    }
38
39    /// Create a step that expects a pattern (simple unnamed step).
40    #[must_use]
41    pub fn expect(pattern: impl Into<String>) -> Self {
42        Self {
43            expect: Some(pattern.into()),
44            ..Default::default()
45        }
46    }
47
48    /// Create a step that sends text (simple unnamed step).
49    #[must_use]
50    pub fn send(text: impl Into<String>) -> Self {
51        Self {
52            send: Some(text.into()),
53            ..Default::default()
54        }
55    }
56
57    /// Chain: set the pattern to expect (builder pattern).
58    #[must_use]
59    pub fn with_expect(mut self, pattern: impl Into<String>) -> Self {
60        self.expect = Some(pattern.into());
61        self
62    }
63
64    /// Chain: set the text to send (builder pattern).
65    #[must_use]
66    pub fn with_send(mut self, text: impl Into<String>) -> Self {
67        self.send = Some(text.into());
68        self
69    }
70
71    /// Chain: set a control character to send (e.g., Ctrl+C).
72    #[must_use]
73    pub const fn with_send_control(mut self, ctrl: ControlChar) -> Self {
74        self.send_control = Some(ctrl);
75        self
76    }
77
78    /// Chain: set the text to send after expecting.
79    /// Alias for `with_send`, for fluent API.
80    #[must_use]
81    pub fn then_send(mut self, text: impl Into<String>) -> Self {
82        self.send = Some(text.into());
83        self
84    }
85
86    /// Chain: set a control character to send after expecting.
87    /// Alias for `with_send_control`, for fluent API.
88    #[must_use]
89    pub const fn then_send_control(mut self, ctrl: ControlChar) -> Self {
90        self.send_control = Some(ctrl);
91        self
92    }
93
94    /// Set the timeout for this step.
95    #[must_use]
96    pub const fn timeout(mut self, timeout: Duration) -> Self {
97        self.timeout = Some(timeout);
98        self
99    }
100
101    /// Set the next step name.
102    #[must_use]
103    pub fn then(mut self, next: impl Into<String>) -> Self {
104        self.next = Some(next.into());
105        self
106    }
107
108    /// Add a conditional branch.
109    #[must_use]
110    pub fn branch(mut self, pattern: impl Into<String>, step: impl Into<String>) -> Self {
111        self.branches.insert(pattern.into(), step.into());
112        self
113    }
114
115    /// Set whether to continue on timeout.
116    #[must_use]
117    pub const fn continue_on_timeout(mut self, cont: bool) -> Self {
118        self.continue_on_timeout = cont;
119        self
120    }
121
122    /// Get the expect pattern.
123    #[must_use]
124    pub fn expect_pattern(&self) -> Option<&str> {
125        self.expect.as_deref()
126    }
127
128    /// Get the send text.
129    #[must_use]
130    pub fn send_text(&self) -> Option<&str> {
131        self.send.as_deref()
132    }
133
134    /// Get the control character to send.
135    #[must_use]
136    pub const fn send_control(&self) -> Option<ControlChar> {
137        self.send_control
138    }
139
140    /// Get the timeout.
141    #[must_use]
142    pub const fn get_timeout(&self) -> Option<Duration> {
143        self.timeout
144    }
145
146    /// Check if should continue on timeout.
147    #[must_use]
148    pub const fn continues_on_timeout(&self) -> bool {
149        self.continue_on_timeout
150    }
151}
152
153/// A complete dialog definition.
154#[derive(Debug, Clone, Default)]
155pub struct Dialog {
156    /// Name of the dialog.
157    pub name: String,
158    /// Description.
159    pub description: String,
160    /// Steps in the dialog.
161    pub steps: Vec<DialogStep>,
162    /// Entry point step name.
163    pub entry: Option<String>,
164    /// Variables for substitution.
165    pub variables: HashMap<String, String>,
166}
167
168impl Dialog {
169    /// Create a new empty dialog.
170    #[must_use]
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// Create a new dialog with a name.
176    #[must_use]
177    pub fn named(name: impl Into<String>) -> Self {
178        Self {
179            name: name.into(),
180            ..Default::default()
181        }
182    }
183
184    /// Set the description.
185    #[must_use]
186    pub fn description(mut self, desc: impl Into<String>) -> Self {
187        self.description = desc.into();
188        self
189    }
190
191    /// Add a step.
192    #[must_use]
193    pub fn step(mut self, step: DialogStep) -> Self {
194        if self.entry.is_none() && !step.name.is_empty() {
195            self.entry = Some(step.name.clone());
196        }
197        self.steps.push(step);
198        self
199    }
200
201    /// Set a variable.
202    #[must_use]
203    pub fn variable(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
204        self.variables.insert(name.into(), value.into());
205        self
206    }
207
208    /// Set the entry point.
209    #[must_use]
210    pub fn entry_point(mut self, step: impl Into<String>) -> Self {
211        self.entry = Some(step.into());
212        self
213    }
214
215    /// Get the number of steps.
216    #[must_use]
217    pub const fn len(&self) -> usize {
218        self.steps.len()
219    }
220
221    /// Check if the dialog is empty.
222    #[must_use]
223    pub const fn is_empty(&self) -> bool {
224        self.steps.is_empty()
225    }
226
227    /// Get the steps.
228    #[must_use]
229    pub fn steps(&self) -> &[DialogStep] {
230        &self.steps
231    }
232
233    /// Get the variables.
234    #[must_use]
235    pub const fn variables(&self) -> &HashMap<String, String> {
236        &self.variables
237    }
238
239    /// Get a step by name.
240    #[must_use]
241    pub fn get_step(&self, name: &str) -> Option<&DialogStep> {
242        self.steps.iter().find(|s| s.name == name)
243    }
244
245    /// Substitute variables in a string.
246    #[must_use]
247    pub fn substitute(&self, s: &str) -> String {
248        let mut result = s.to_string();
249        for (name, value) in &self.variables {
250            result = result.replace(&format!("${{{name}}}"), value);
251            result = result.replace(&format!("${name}"), value);
252        }
253        result
254    }
255}
256
257/// A builder for creating dialogs.
258#[derive(Debug, Clone, Default)]
259pub struct DialogBuilder {
260    dialog: Dialog,
261}
262
263impl DialogBuilder {
264    /// Create a new builder (unnamed dialog).
265    #[must_use]
266    pub fn new() -> Self {
267        Self::default()
268    }
269
270    /// Create a new builder with a named dialog.
271    #[must_use]
272    pub fn named(name: impl Into<String>) -> Self {
273        Self {
274            dialog: Dialog::named(name),
275        }
276    }
277
278    /// Add a step.
279    #[must_use]
280    pub fn step(mut self, step: DialogStep) -> Self {
281        self.dialog = self.dialog.step(step);
282        self
283    }
284
285    /// Add a variable (alias for var).
286    #[must_use]
287    pub fn variable(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
288        self.dialog = self.dialog.variable(name, value);
289        self
290    }
291
292    /// Add a variable.
293    #[must_use]
294    pub fn var(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
295        self.dialog = self.dialog.variable(name, value);
296        self
297    }
298
299    /// Add a simple expect-send step.
300    #[must_use]
301    pub fn expect_send(
302        mut self,
303        name: impl Into<String>,
304        expect: impl Into<String>,
305        send: impl Into<String>,
306    ) -> Self {
307        self.dialog = self
308            .dialog
309            .step(DialogStep::new(name).with_expect(expect).with_send(send));
310        self
311    }
312
313    /// Build the dialog.
314    #[must_use]
315    pub fn build(self) -> Dialog {
316        self.dialog
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn dialog_basic() {
326        let dialog = DialogBuilder::new()
327            .step(DialogStep::expect("login:").then_send("admin"))
328            .step(DialogStep::expect("password:").then_send("secret"))
329            .var("USER", "admin")
330            .build();
331
332        assert_eq!(dialog.len(), 2);
333        assert_eq!(dialog.substitute("${USER}"), "admin");
334    }
335
336    #[test]
337    fn dialog_empty() {
338        let dialog = Dialog::new();
339        assert!(dialog.is_empty());
340        assert_eq!(dialog.len(), 0);
341    }
342
343    #[test]
344    fn dialog_named_steps() {
345        let dialog = Dialog::named("login")
346            .step(
347                DialogStep::new("username")
348                    .with_expect("login:")
349                    .with_send("admin\n"),
350            )
351            .step(
352                DialogStep::new("password")
353                    .with_expect("password:")
354                    .with_send("secret\n"),
355            )
356            .variable("USER", "admin");
357
358        assert_eq!(dialog.name, "login");
359        assert_eq!(dialog.steps.len(), 2);
360        assert_eq!(dialog.substitute("${USER}"), "admin");
361    }
362
363    #[test]
364    fn dialog_step_accessors() {
365        let step = DialogStep::expect("prompt")
366            .then_send("response")
367            .timeout(Duration::from_secs(10));
368
369        assert_eq!(step.expect_pattern(), Some("prompt"));
370        assert_eq!(step.send_text(), Some("response"));
371        assert_eq!(step.get_timeout(), Some(Duration::from_secs(10)));
372    }
373
374    #[test]
375    fn dialog_variable_substitution() {
376        let dialog = DialogBuilder::new()
377            .var("name", "Alice")
378            .var("greeting", "Hello")
379            .build();
380
381        assert_eq!(dialog.substitute("${greeting}, ${name}!"), "Hello, Alice!");
382    }
383
384    #[test]
385    fn dialog_builder_named() {
386        let dialog = DialogBuilder::named("test")
387            .expect_send("step1", "prompt>", "command\n")
388            .variable("VAR", "value")
389            .build();
390
391        assert_eq!(dialog.name, "test");
392        assert_eq!(dialog.steps.len(), 1);
393    }
394}