Skip to main content

freeswitch_log_parser/
chain.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4/// Iterator that concatenates named segments and tracks which line number
5/// each segment starts at. Pair with [`SegmentTracker`] to look up which
6/// segment a given line belongs to.
7pub struct TrackedChain {
8    segments: Vec<Box<dyn Iterator<Item = String>>>,
9    current: usize,
10    lines_emitted: u64,
11    starts: Rc<RefCell<Vec<u64>>>,
12    emit_sentinel: bool,
13}
14
15/// Handle for looking up which segment a line number belongs to.
16///
17/// Created alongside a [`TrackedChain`] — keep this while the chain is
18/// consumed by [`LogStream`](crate::LogStream).
19pub struct SegmentTracker {
20    filenames: Vec<String>,
21    starts: Rc<RefCell<Vec<u64>>>,
22}
23
24impl TrackedChain {
25    /// Build a tracked chain from named segments.
26    ///
27    /// Returns the iterator (feed to `LogStream::new()`) and a tracker handle
28    /// (use to look up segment boundaries after entries are yielded).
29    pub fn new(
30        segments: Vec<(String, Box<dyn Iterator<Item = String>>)>,
31    ) -> (Self, SegmentTracker) {
32        let (filenames, iters): (Vec<_>, Vec<_>) = segments.into_iter().unzip();
33        let starts = Rc::new(RefCell::new(if iters.is_empty() {
34            Vec::new()
35        } else {
36            vec![1u64]
37        }));
38        let tracker = SegmentTracker {
39            filenames: filenames.clone(),
40            starts: starts.clone(),
41        };
42        let chain = TrackedChain {
43            segments: iters,
44            current: 0,
45            lines_emitted: 0,
46            starts,
47            emit_sentinel: false,
48        };
49        (chain, tracker)
50    }
51}
52
53impl Iterator for TrackedChain {
54    type Item = String;
55
56    fn next(&mut self) -> Option<String> {
57        if self.emit_sentinel {
58            self.emit_sentinel = false;
59            return Some("\x00".to_string());
60        }
61        loop {
62            if self.current >= self.segments.len() {
63                return None;
64            }
65            if let Some(line) = self.segments[self.current].next() {
66                self.lines_emitted += 1;
67                return Some(line);
68            }
69            self.current += 1;
70            if self.current < self.segments.len() {
71                self.starts.borrow_mut().push(self.lines_emitted + 1);
72                self.emit_sentinel = true;
73                return self.next();
74            }
75        }
76    }
77}
78
79impl SegmentTracker {
80    /// Look up which segment a line number belongs to.
81    ///
82    /// Returns `(segment_index, filename)` or `None` for line number 0.
83    pub fn segment_for_line(&self, line_number: u64) -> Option<(usize, &str)> {
84        if line_number == 0 {
85            return None;
86        }
87        let starts = self.starts.borrow();
88        let idx = starts.partition_point(|&s| s <= line_number);
89        if idx == 0 {
90            return None;
91        }
92        let seg = idx - 1;
93        Some((seg, &self.filenames[seg]))
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    fn seg(name: &str, lines: Vec<&str>) -> (String, Box<dyn Iterator<Item = String>>) {
102        let owned: Vec<String> = lines.into_iter().map(String::from).collect();
103        (name.to_string(), Box::new(owned.into_iter()))
104    }
105
106    #[test]
107    fn single_segment() {
108        let (chain, tracker) = TrackedChain::new(vec![seg("a.log", vec!["x", "y", "z"])]);
109        let lines: Vec<_> = chain.collect();
110        assert_eq!(lines, ["x", "y", "z"]);
111        assert_eq!(tracker.segment_for_line(1), Some((0, "a.log")));
112        assert_eq!(tracker.segment_for_line(3), Some((0, "a.log")));
113    }
114
115    #[test]
116    fn two_segments() {
117        let (chain, tracker) = TrackedChain::new(vec![
118            seg("a.log", vec!["a1", "a2"]),
119            seg("b.log", vec!["b1"]),
120        ]);
121        let lines: Vec<_> = chain.collect();
122        assert_eq!(lines, ["a1", "a2", "\x00", "b1"]);
123        assert_eq!(tracker.segment_for_line(1), Some((0, "a.log")));
124        assert_eq!(tracker.segment_for_line(2), Some((0, "a.log")));
125        assert_eq!(tracker.segment_for_line(3), Some((1, "b.log")));
126    }
127
128    #[test]
129    fn empty_segment_skipped() {
130        let (chain, tracker) = TrackedChain::new(vec![
131            seg("a.log", vec!["a1"]),
132            seg("empty.log", vec![]),
133            seg("c.log", vec!["c1"]),
134        ]);
135        let lines: Vec<_> = chain.collect();
136        assert_eq!(lines, ["a1", "\x00", "\x00", "c1"]);
137        assert_eq!(tracker.segment_for_line(1), Some((0, "a.log")));
138        assert_eq!(tracker.segment_for_line(2), Some((2, "c.log")));
139    }
140
141    #[test]
142    fn line_zero_returns_none() {
143        let (_chain, tracker) = TrackedChain::new(vec![seg("a.log", vec!["x"])]);
144        assert_eq!(tracker.segment_for_line(0), None);
145    }
146
147    #[test]
148    fn empty_chain() {
149        let (chain, tracker) = TrackedChain::new(vec![]);
150        let lines: Vec<String> = chain.collect();
151        assert!(lines.is_empty());
152        assert_eq!(tracker.segment_for_line(1), None);
153    }
154}