agentmux_srv\backend/
utilds.rs

1#![allow(dead_code)]
2// Copyright 2025-2026, AgentMux Corp.
3// SPDX-License-Identifier: Apache-2.0
4
5//! Utility data structures.
6//! Port of Go's pkg/utilds/.
7//!
8//! Provides a line-buffered reader with circular buffer and callbacks.
9
10
11use std::io::{self, BufRead, BufReader, Read};
12use std::sync::Mutex;
13
14// ---- Constants ----
15
16/// Default maximum number of lines in the buffer.
17const DEFAULT_MAX_LINES: usize = 1000;
18
19/// Callback type for line notifications.
20type LineCallback = Box<dyn Fn(&str) + Send>;
21
22// ---- ReaderLineBuffer ----
23
24/// A line-buffered reader backed by a circular buffer.
25///
26/// Reads lines from an `io::Read` source, stores them in a fixed-size
27/// circular buffer, and optionally calls a callback for each line.
28pub struct ReaderLineBuffer<R: Read> {
29    inner: Mutex<ReaderLineBufferInner<R>>,
30}
31
32struct ReaderLineBufferInner<R: Read> {
33    reader: BufReader<R>,
34    lines: Vec<String>,
35    max_lines: usize,
36    total_line_count: usize,
37    done: bool,
38    line_callback: Option<LineCallback>,
39}
40
41impl<R: Read> ReaderLineBuffer<R> {
42    /// Create a new ReaderLineBuffer wrapping the given reader.
43    ///
44    /// `max_lines` specifies the circular buffer capacity.
45    /// If 0, defaults to 1000.
46    pub fn new(reader: R, max_lines: usize) -> Self {
47        let max = if max_lines == 0 {
48            DEFAULT_MAX_LINES
49        } else {
50            max_lines
51        };
52        Self {
53            inner: Mutex::new(ReaderLineBufferInner {
54                reader: BufReader::new(reader),
55                lines: Vec::with_capacity(max),
56                max_lines: max,
57                total_line_count: 0,
58                done: false,
59                line_callback: None,
60            }),
61        }
62    }
63
64    /// Set a callback that is invoked for each line read.
65    pub fn set_line_callback<F>(&self, callback: F)
66    where
67        F: Fn(&str) + Send + 'static,
68    {
69        self.inner.lock().unwrap().line_callback = Some(Box::new(callback));
70    }
71
72    /// Read the next line from the underlying reader.
73    ///
74    /// Returns `Ok(line)` for each line, or `Err(io::ErrorKind::UnexpectedEof)`
75    /// when the reader is exhausted.
76    pub fn read_line(&self) -> io::Result<String> {
77        let mut inner = self.inner.lock().unwrap();
78        if inner.done {
79            return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "reader done"));
80        }
81
82        let mut line = String::new();
83        match inner.reader.read_line(&mut line) {
84            Ok(0) => {
85                inner.done = true;
86                Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF"))
87            }
88            Ok(_) => {
89                // Strip trailing newline
90                let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
91                let owned = trimmed.to_string();
92                inner.add_line(&owned);
93                Ok(owned)
94            }
95            Err(e) => {
96                inner.done = true;
97                Err(e)
98            }
99        }
100    }
101
102    /// Read all remaining lines until EOF.
103    pub fn read_all(&self) {
104        loop {
105            if self.read_line().is_err() {
106                break;
107            }
108        }
109    }
110
111    /// Check if the reader has been fully consumed.
112    pub fn is_done(&self) -> bool {
113        self.inner.lock().unwrap().done
114    }
115
116    /// Get a copy of all buffered lines.
117    pub fn get_lines(&self) -> Vec<String> {
118        self.inner.lock().unwrap().lines.clone()
119    }
120
121    /// Get the number of lines currently in the buffer.
122    pub fn get_line_count(&self) -> usize {
123        self.inner.lock().unwrap().lines.len()
124    }
125
126    /// Get the total number of lines read (may exceed buffer capacity).
127    pub fn get_total_line_count(&self) -> usize {
128        self.inner.lock().unwrap().total_line_count
129    }
130}
131
132impl<R: Read> ReaderLineBufferInner<R> {
133    fn add_line(&mut self, line: &str) {
134        if self.lines.len() >= self.max_lines {
135            self.lines.remove(0);
136        }
137        self.lines.push(line.to_string());
138        self.total_line_count += 1;
139
140        if let Some(ref cb) = self.line_callback {
141            cb(line);
142        }
143    }
144}
145
146// ---- Tests ----
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use std::io::Cursor;
152    use std::sync::Arc;
153
154    #[test]
155    fn test_read_single_line() {
156        let data = Cursor::new("hello\n");
157        let buf = ReaderLineBuffer::new(data, 10);
158
159        let line = buf.read_line().unwrap();
160        assert_eq!(line, "hello");
161        assert_eq!(buf.get_line_count(), 1);
162        assert_eq!(buf.get_total_line_count(), 1);
163    }
164
165    #[test]
166    fn test_read_multiple_lines() {
167        let data = Cursor::new("line1\nline2\nline3\n");
168        let buf = ReaderLineBuffer::new(data, 10);
169
170        assert_eq!(buf.read_line().unwrap(), "line1");
171        assert_eq!(buf.read_line().unwrap(), "line2");
172        assert_eq!(buf.read_line().unwrap(), "line3");
173        assert!(buf.read_line().is_err());
174        assert!(buf.is_done());
175    }
176
177    #[test]
178    fn test_read_all() {
179        let data = Cursor::new("a\nb\nc\n");
180        let buf = ReaderLineBuffer::new(data, 10);
181
182        buf.read_all();
183
184        assert!(buf.is_done());
185        let lines = buf.get_lines();
186        assert_eq!(lines, vec!["a", "b", "c"]);
187        assert_eq!(buf.get_total_line_count(), 3);
188    }
189
190    #[test]
191    fn test_circular_buffer() {
192        let data = Cursor::new("1\n2\n3\n4\n5\n");
193        let buf = ReaderLineBuffer::new(data, 3);
194
195        buf.read_all();
196
197        let lines = buf.get_lines();
198        assert_eq!(lines, vec!["3", "4", "5"]); // Only last 3
199        assert_eq!(buf.get_line_count(), 3);
200        assert_eq!(buf.get_total_line_count(), 5);
201    }
202
203    #[test]
204    fn test_default_max_lines() {
205        let data = Cursor::new("test\n");
206        let buf = ReaderLineBuffer::new(data, 0); // 0 = default
207
208        buf.read_all();
209        // Should use DEFAULT_MAX_LINES (1000), not crash
210        assert_eq!(buf.get_total_line_count(), 1);
211    }
212
213    #[test]
214    fn test_line_callback() {
215        let data = Cursor::new("hello\nworld\n");
216        let buf = ReaderLineBuffer::new(data, 10);
217
218        let collected = Arc::new(Mutex::new(Vec::<String>::new()));
219        let collected_clone = collected.clone();
220        buf.set_line_callback(move |line| {
221            collected_clone.lock().unwrap().push(line.to_string());
222        });
223
224        buf.read_all();
225
226        let lines = collected.lock().unwrap();
227        assert_eq!(*lines, vec!["hello", "world"]);
228    }
229
230    #[test]
231    fn test_empty_input() {
232        let data = Cursor::new("");
233        let buf = ReaderLineBuffer::new(data, 10);
234
235        assert!(buf.read_line().is_err());
236        assert!(buf.is_done());
237        assert_eq!(buf.get_line_count(), 0);
238    }
239
240    #[test]
241    fn test_no_trailing_newline() {
242        let data = Cursor::new("no newline at end");
243        let buf = ReaderLineBuffer::new(data, 10);
244
245        let line = buf.read_line().unwrap();
246        assert_eq!(line, "no newline at end");
247    }
248
249    #[test]
250    fn test_windows_line_endings() {
251        let data = Cursor::new("line1\r\nline2\r\n");
252        let buf = ReaderLineBuffer::new(data, 10);
253
254        buf.read_all();
255        let lines = buf.get_lines();
256        assert_eq!(lines, vec!["line1", "line2"]);
257    }
258
259    #[test]
260    fn test_get_lines_returns_copy() {
261        let data = Cursor::new("a\nb\n");
262        let buf = ReaderLineBuffer::new(data, 10);
263
264        buf.read_all();
265        let lines1 = buf.get_lines();
266        let lines2 = buf.get_lines();
267        assert_eq!(lines1, lines2);
268    }
269}