Skip to main content

rust_expect/expect/
before_after.rs

1//! Before/after pattern handlers for expect operations.
2//!
3//! This module provides persistent pattern handlers that are automatically
4//! checked during every expect operation. These are useful for handling
5//! common patterns like error messages or prompts that can appear at any time.
6
7use std::collections::HashMap;
8
9use super::pattern::{Pattern, PatternSet};
10
11/// Handler function type for before/after patterns.
12pub type PatternHandler = Box<dyn Fn(&str) -> HandlerAction + Send + Sync>;
13
14/// Action to take after a pattern handler executes.
15#[derive(Debug, Clone, Default)]
16pub enum HandlerAction {
17    /// Continue with the expect operation.
18    #[default]
19    Continue,
20    /// Stop the expect operation and return success with this match.
21    Return(String),
22    /// Stop the expect operation and return an error.
23    Abort(String),
24    /// Send a response and continue.
25    Respond(String),
26}
27
28/// A persistent pattern with its handler.
29pub struct PersistentPattern {
30    /// The pattern to match.
31    pub pattern: Pattern,
32    /// The handler to execute on match.
33    pub handler: PatternHandler,
34    /// Whether this pattern is currently enabled.
35    pub enabled: bool,
36    /// Priority (lower = higher priority).
37    pub priority: i32,
38}
39
40impl PersistentPattern {
41    /// Create a new persistent pattern.
42    #[must_use]
43    pub fn new(pattern: Pattern, handler: PatternHandler) -> Self {
44        Self {
45            pattern,
46            handler,
47            enabled: true,
48            priority: 0,
49        }
50    }
51
52    /// Create a pattern with a simple response.
53    pub fn with_response(pattern: Pattern, response: impl Into<String>) -> Self {
54        let response = response.into();
55        Self::new(
56            pattern,
57            Box::new(move |_| HandlerAction::Respond(response.clone())),
58        )
59    }
60
61    /// Create a pattern that aborts on match.
62    pub fn with_abort(pattern: Pattern, message: impl Into<String>) -> Self {
63        let message = message.into();
64        Self::new(
65            pattern,
66            Box::new(move |_| HandlerAction::Abort(message.clone())),
67        )
68    }
69
70    /// Set the priority for this pattern.
71    #[must_use]
72    pub const fn with_priority(mut self, priority: i32) -> Self {
73        self.priority = priority;
74        self
75    }
76
77    /// Disable this pattern.
78    pub const fn disable(&mut self) {
79        self.enabled = false;
80    }
81
82    /// Enable this pattern.
83    pub const fn enable(&mut self) {
84        self.enabled = true;
85    }
86}
87
88impl std::fmt::Debug for PersistentPattern {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct("PersistentPattern")
91            .field("pattern", &self.pattern)
92            .field("enabled", &self.enabled)
93            .field("priority", &self.priority)
94            .finish_non_exhaustive()
95    }
96}
97
98/// Manager for before/after patterns.
99///
100/// On each poll of an expect operation, before patterns are checked *before*
101/// the explicit patterns (highest priority), and after patterns are checked
102/// *after* the explicit patterns fail to match (a fallback). This mirrors
103/// Tcl expect's `expect_before`/`expect_after` semantics; after patterns do
104/// **not** run once an explicit pattern has matched.
105#[derive(Default)]
106pub struct PatternManager {
107    /// Patterns checked before the explicit patterns on each poll.
108    before_patterns: HashMap<String, PersistentPattern>,
109    /// Patterns checked as a fallback after the explicit patterns fail.
110    after_patterns: HashMap<String, PersistentPattern>,
111    /// Counter for generating unique IDs.
112    next_id: usize,
113}
114
115impl PatternManager {
116    /// Create a new pattern manager.
117    #[must_use]
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Add a before pattern and return its ID.
123    pub fn add_before(&mut self, pattern: PersistentPattern) -> String {
124        let id = self.generate_id("before");
125        self.before_patterns.insert(id.clone(), pattern);
126        id
127    }
128
129    /// Add an after pattern and return its ID.
130    pub fn add_after(&mut self, pattern: PersistentPattern) -> String {
131        let id = self.generate_id("after");
132        self.after_patterns.insert(id.clone(), pattern);
133        id
134    }
135
136    /// Remove a before pattern by ID.
137    pub fn remove_before(&mut self, id: &str) -> Option<PersistentPattern> {
138        self.before_patterns.remove(id)
139    }
140
141    /// Remove an after pattern by ID.
142    pub fn remove_after(&mut self, id: &str) -> Option<PersistentPattern> {
143        self.after_patterns.remove(id)
144    }
145
146    /// Get a before pattern by ID.
147    #[must_use]
148    pub fn get_before(&self, id: &str) -> Option<&PersistentPattern> {
149        self.before_patterns.get(id)
150    }
151
152    /// Get a mutable before pattern by ID.
153    pub fn get_before_mut(&mut self, id: &str) -> Option<&mut PersistentPattern> {
154        self.before_patterns.get_mut(id)
155    }
156
157    /// Get an after pattern by ID.
158    #[must_use]
159    pub fn get_after(&self, id: &str) -> Option<&PersistentPattern> {
160        self.after_patterns.get(id)
161    }
162
163    /// Get a mutable after pattern by ID.
164    pub fn get_after_mut(&mut self, id: &str) -> Option<&mut PersistentPattern> {
165        self.after_patterns.get_mut(id)
166    }
167
168    /// Check before patterns against the buffer.
169    ///
170    /// Returns `(id, action, matched_pattern)` for the first matching pattern,
171    /// or `None` if none match. The matched pattern is returned so the caller
172    /// can consume the triggering match from its buffer (via `Matcher`) and
173    /// avoid re-firing on the next poll.
174    #[must_use]
175    pub fn check_before(&self, buffer: &str) -> Option<(String, HandlerAction, Pattern)> {
176        self.check_patterns(&self.before_patterns, buffer)
177    }
178
179    /// Check after patterns against the buffer.
180    ///
181    /// Returns `(id, action, matched_pattern)` for the first matching pattern,
182    /// or `None` if none match. See [`Self::check_before`] for why the pattern
183    /// is returned.
184    #[must_use]
185    pub fn check_after(&self, buffer: &str) -> Option<(String, HandlerAction, Pattern)> {
186        self.check_patterns(&self.after_patterns, buffer)
187    }
188
189    /// Get all before patterns as a `PatternSet` for matching.
190    #[must_use]
191    pub fn before_pattern_set(&self) -> PatternSet {
192        self.patterns_to_set(&self.before_patterns)
193    }
194
195    /// Get all after patterns as a `PatternSet` for matching.
196    #[must_use]
197    pub fn after_pattern_set(&self) -> PatternSet {
198        self.patterns_to_set(&self.after_patterns)
199    }
200
201    /// Clear all before patterns.
202    pub fn clear_before(&mut self) {
203        self.before_patterns.clear();
204    }
205
206    /// Clear all after patterns.
207    pub fn clear_after(&mut self) {
208        self.after_patterns.clear();
209    }
210
211    /// Clear all patterns.
212    pub fn clear_all(&mut self) {
213        self.before_patterns.clear();
214        self.after_patterns.clear();
215    }
216
217    /// Get the number of before patterns.
218    #[must_use]
219    pub fn before_count(&self) -> usize {
220        self.before_patterns.len()
221    }
222
223    /// Get the number of after patterns.
224    #[must_use]
225    pub fn after_count(&self) -> usize {
226        self.after_patterns.len()
227    }
228
229    fn generate_id(&mut self, prefix: &str) -> String {
230        let id = format!("{prefix}_{}", self.next_id);
231        self.next_id += 1;
232        id
233    }
234
235    #[allow(clippy::unused_self)]
236    fn check_patterns(
237        &self,
238        patterns: &HashMap<String, PersistentPattern>,
239        buffer: &str,
240    ) -> Option<(String, HandlerAction, Pattern)> {
241        // Collect enabled patterns sorted by priority
242        let mut sorted: Vec<_> = patterns.iter().filter(|(_, p)| p.enabled).collect();
243        sorted.sort_by_key(|(_, p)| p.priority);
244
245        for (id, persistent) in sorted {
246            if persistent.pattern.matches(buffer).is_some() {
247                let action = (persistent.handler)(buffer);
248                if !matches!(action, HandlerAction::Continue) {
249                    return Some((id.clone(), action, persistent.pattern.clone()));
250                }
251            }
252        }
253        None
254    }
255
256    #[allow(clippy::unused_self)]
257    fn patterns_to_set(&self, patterns: &HashMap<String, PersistentPattern>) -> PatternSet {
258        let mut sorted: Vec<_> = patterns.iter().filter(|(_, p)| p.enabled).collect();
259        sorted.sort_by_key(|(_, p)| p.priority);
260
261        PatternSet::from_patterns(sorted.into_iter().map(|(_, p)| p.pattern.clone()).collect())
262    }
263}
264
265impl std::fmt::Debug for PatternManager {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        f.debug_struct("PatternManager")
268            .field("before_count", &self.before_patterns.len())
269            .field("after_count", &self.after_patterns.len())
270            .finish()
271    }
272}
273
274/// Builder for common before/after pattern configurations.
275pub struct PatternBuilder {
276    manager: PatternManager,
277}
278
279impl PatternBuilder {
280    /// Create a new pattern builder.
281    #[must_use]
282    pub fn new() -> Self {
283        Self {
284            manager: PatternManager::new(),
285        }
286    }
287
288    /// Add a password prompt handler.
289    #[must_use]
290    pub fn with_password_handler(mut self, password: impl Into<String>) -> Self {
291        let password = password.into();
292        let pattern = PersistentPattern::with_response(
293            Pattern::regex(r"[Pp]assword:?\s*$").unwrap_or_else(|_| Pattern::literal("Password:")),
294            format!("{password}\n"),
295        );
296        self.manager.add_before(pattern);
297        self
298    }
299
300    /// Add a sudo password handler.
301    #[must_use]
302    pub fn with_sudo_handler(mut self, password: impl Into<String>) -> Self {
303        let password = password.into();
304        let pattern = PersistentPattern::with_response(
305            Pattern::regex(r"\[sudo\] password")
306                .unwrap_or_else(|_| Pattern::literal("[sudo] password")),
307            format!("{password}\n"),
308        );
309        self.manager.add_before(pattern);
310        self
311    }
312
313    /// Add an error pattern that aborts.
314    #[must_use]
315    pub fn with_error_pattern(mut self, pattern: Pattern, message: impl Into<String>) -> Self {
316        let persistent = PersistentPattern::with_abort(pattern, message);
317        self.manager.add_before(persistent);
318        self
319    }
320
321    /// Add a yes/no prompt handler that responds with yes.
322    #[must_use]
323    pub fn with_yes_handler(mut self) -> Self {
324        let pattern = PersistentPattern::with_response(
325            Pattern::regex(r"\(yes/no\)\??\s*$").unwrap_or_else(|_| Pattern::literal("(yes/no)")),
326            "yes\n",
327        );
328        self.manager.add_before(pattern);
329        self
330    }
331
332    /// Add a y/n prompt handler that responds with y.
333    #[must_use]
334    pub fn with_yn_handler(mut self) -> Self {
335        let pattern = PersistentPattern::with_response(
336            Pattern::regex(r"\[y/n\]\??\s*$").unwrap_or_else(|_| Pattern::literal("[y/n]")),
337            "y\n",
338        );
339        self.manager.add_before(pattern);
340        self
341    }
342
343    /// Add a continue prompt handler.
344    #[must_use]
345    pub fn with_continue_handler(mut self) -> Self {
346        let pattern = PersistentPattern::with_response(
347            Pattern::regex(r"Press (?:Enter|any key) to continue")
348                .unwrap_or_else(|_| Pattern::literal("Press Enter")),
349            "\n",
350        );
351        self.manager.add_before(pattern);
352        self
353    }
354
355    /// Build the pattern manager.
356    #[must_use]
357    pub fn build(self) -> PatternManager {
358        self.manager
359    }
360}
361
362impl Default for PatternBuilder {
363    fn default() -> Self {
364        Self::new()
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn pattern_manager_before() {
374        let mut manager = PatternManager::new();
375
376        let pattern = PersistentPattern::with_response(Pattern::literal("password:"), "secret\n");
377        let id = manager.add_before(pattern);
378
379        let result = manager.check_before("Enter password: ");
380        assert!(result.is_some());
381
382        let (matched_id, action, _pattern) = result.unwrap();
383        assert_eq!(matched_id, id);
384        assert!(matches!(action, HandlerAction::Respond(_)));
385    }
386
387    #[test]
388    fn pattern_manager_priority() {
389        let mut manager = PatternManager::new();
390
391        let low = PersistentPattern::new(
392            Pattern::literal("test"),
393            Box::new(|_| HandlerAction::Respond("low".into())),
394        )
395        .with_priority(10);
396
397        let high = PersistentPattern::new(
398            Pattern::literal("test"),
399            Box::new(|_| HandlerAction::Respond("high".into())),
400        )
401        .with_priority(1);
402
403        manager.add_before(low);
404        manager.add_before(high);
405
406        let result = manager.check_before("test");
407        assert!(result.is_some());
408
409        if let Some((_, HandlerAction::Respond(s), _pattern)) = result {
410            assert_eq!(s, "high");
411        } else {
412            panic!("Expected Respond action");
413        }
414    }
415
416    #[test]
417    fn pattern_manager_disable() {
418        let mut manager = PatternManager::new();
419
420        let pattern = PersistentPattern::with_response(Pattern::literal("test"), "response");
421        let id = manager.add_before(pattern);
422
423        // Should match when enabled
424        assert!(manager.check_before("test").is_some());
425
426        // Disable and check again
427        manager.get_before_mut(&id).unwrap().disable();
428        assert!(manager.check_before("test").is_none());
429    }
430
431    #[test]
432    fn pattern_manager_after() {
433        let mut manager = PatternManager::new();
434
435        let pattern = PersistentPattern::with_response(Pattern::literal("more? "), "yes\n");
436        let id = manager.add_after(pattern);
437
438        let result = manager.check_after("Show more? ");
439        assert!(result.is_some(), "after pattern should match");
440
441        let (matched_id, action, _pattern) = result.unwrap();
442        assert_eq!(matched_id, id);
443        assert!(matches!(action, HandlerAction::Respond(_)));
444
445        // After patterns are independent of before patterns.
446        assert!(manager.check_before("Show more? ").is_none());
447        assert_eq!(manager.after_count(), 1);
448        assert_eq!(manager.before_count(), 0);
449    }
450
451    #[test]
452    fn pattern_builder() {
453        let manager = PatternBuilder::new()
454            .with_password_handler("secret")
455            .with_yes_handler()
456            .build();
457
458        assert_eq!(manager.before_count(), 2);
459    }
460}