Skip to main content

kernel/capabilities/
stop_matcher.rs

1//! Streaming stop-sequence detection. Text is emitted as it arrives, except for
2//! a trailing suffix that could be the start of a stop sequence, which is held
3//! back until the next chunk resolves it.
4
5use crate::capabilities::held_suffix_len;
6use crate::records::JsonValue;
7
8/// A stop-sequence matcher fed text incrementally.
9#[derive(Debug, Clone)]
10pub struct StopMatcher {
11    stops: Vec<String>,
12    buffer: String,
13    stopped: bool,
14}
15
16impl StopMatcher {
17    /// Create a matcher for the given stop sequences. Empty strings are ignored.
18    pub fn new(stops: Vec<String>) -> Self {
19        Self {
20            stops: stops.into_iter().filter(|stop| !stop.is_empty()).collect(),
21            buffer: String::new(),
22            stopped: false,
23        }
24    }
25
26    /// Whether any stop sequence is configured.
27    pub fn is_active(&self) -> bool {
28        !self.stops.is_empty()
29    }
30
31    /// Whether a stop sequence has been reached.
32    pub fn is_stopped(&self) -> bool {
33        self.stopped
34    }
35
36    /// Feed a chunk and return the text safe to emit now. When a stop sequence is
37    /// reached, the text up to it is emitted, the matcher latches `stopped`, and
38    /// further feeds return nothing. With no stops configured, the chunk passes
39    /// straight through.
40    pub fn feed(&mut self, chunk: &str) -> String {
41        if !self.is_active() {
42            return chunk.to_owned();
43        }
44        if self.stopped {
45            return String::new();
46        }
47        self.buffer.push_str(chunk);
48        if let Some(position) = self.earliest_stop() {
49            let emit = self.buffer[..position].to_owned();
50            self.buffer.clear();
51            self.stopped = true;
52            return emit;
53        }
54        let held = held_suffix_len(&self.buffer, &self.stops);
55        let split = self.buffer.len() - held;
56        let emit = self.buffer[..split].to_owned();
57        self.buffer.drain(..split);
58        emit
59    }
60
61    /// Emit any buffered text that was held back. Returns nothing once stopped.
62    pub fn flush(&mut self) -> String {
63        if self.stopped {
64            return String::new();
65        }
66        std::mem::take(&mut self.buffer)
67    }
68
69    fn earliest_stop(&self) -> Option<usize> {
70        self.stops
71            .iter()
72            .filter_map(|stop| self.buffer.find(stop.as_str()))
73            .min()
74    }
75}
76
77/// Extract stop sequences from a parameter value: a single string, or an array
78/// of strings. Anything else yields no stops.
79pub fn stop_strings(value: Option<&JsonValue>) -> Vec<String> {
80    match value {
81        Some(JsonValue::String(single)) => vec![single.clone()],
82        Some(JsonValue::Array(items)) => items
83            .iter()
84            .filter_map(|item| item.as_str().map(str::to_owned))
85            .collect(),
86        _ => Vec::new(),
87    }
88}