Skip to main content

ed_journals/modules/io/models/
log_iter.rs

1use crate::logs::LogEvent;
2use crate::modules::io::error::LogIOError;
3use serde::de::DeserializeOwned;
4use std::io::Read;
5use std::marker::PhantomData;
6
7/// Standard iterator for iterating over some [Read] and returning entries. By default, creating
8/// an iterator using [LogIter::new] will return [LogEvent]s and can use [LogIter::new_raw] to
9/// return raw [serde_json::Value]s. If you want to parse any other type, you can use the [From]
10/// trait instead.
11///
12/// ```rust
13/// use std::fs::File;
14/// use std::io::BufReader;
15/// use ed_journals::io::LogIter;
16///
17/// let file = File::open("../test-files/journals/Journal.2022-10-11T214552.01.log")
18///     .unwrap();
19/// let buf_reader = BufReader::new(file);
20///
21/// let mut iterator = LogIter::new(buf_reader);
22///
23/// assert!(iterator.next().is_some());
24/// ```
25#[derive(Debug)]
26pub struct LogIter<T, R = LogEvent>
27where
28    T: Read,
29    R: DeserializeOwned,
30{
31    inner: T,
32    _p: PhantomData<R>,
33}
34
35impl<T> LogIter<T>
36where
37    T: Read,
38{
39    /// Creates a new iterator over the given [Read] which will return
40    /// [LogEvents](crate::logs::LogEvent).
41    pub fn new(inner: T) -> LogIter<T, LogEvent> {
42        LogIter::new_typed(inner)
43    }
44
45    /// Creates a new iterator over the given [Read] which will return raw
46    /// [Values](serde_json::Value).
47    pub fn new_raw(inner: T) -> LogIter<T, serde_json::Value> {
48        LogIter::new_typed(inner)
49    }
50
51    /// Creates a new iterator over the given [Read] which will return entries of the given type.
52    pub fn new_typed<R>(inner: T) -> LogIter<T, R>
53    where
54        R: DeserializeOwned,
55    {
56        LogIter {
57            inner,
58            _p: PhantomData,
59        }
60    }
61}
62
63impl<T, R> From<T> for LogIter<T, R>
64where
65    T: Read,
66    R: DeserializeOwned,
67{
68    fn from(value: T) -> Self {
69        LogIter {
70            inner: value,
71            _p: PhantomData,
72        }
73    }
74}
75
76impl<T, R> Iterator for LogIter<T, R>
77where
78    T: Read,
79    R: DeserializeOwned,
80{
81    type Item = Result<R, LogIOError>;
82
83    fn next(&mut self) -> Option<Self::Item> {
84        let mut line = Vec::with_capacity(64); // Line are mostly at least 64 bytes
85
86        let mut buf = [0u8; 1];
87        loop {
88            let size = match self.inner.read(&mut buf) {
89                Ok(size) => size,
90                Err(e) => return Some(Err(e.into())),
91            };
92
93            if size == 0 {
94                break;
95            }
96
97            let byte = buf[0];
98
99            if byte == b'\n' && !line.is_empty() {
100                break;
101            }
102
103            if byte == 0x00 || (line.is_empty() && byte == b' ') {
104                continue;
105            }
106
107            line.push(byte);
108        }
109
110        if line.is_empty() {
111            return None;
112        }
113
114        Some(Ok(match serde_json::from_slice(&line) {
115            Ok(event) => event,
116            Err(e) => {
117                #[cfg(test)]
118                dbg!(String::from_utf8_lossy(&line).to_string());
119
120                return Some(Err(e.into()));
121            }
122        }))
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use crate::logs::LogEventContentKind;
129    use crate::modules::io::models::log_iter::LogIter;
130    use std::fs;
131    use std::fs::File;
132    use std::io::{BufReader, Cursor};
133
134    #[test]
135    fn log_reader_reads_completed_file_correctly() {
136        let data = r#"{ "timestamp":"2020-09-21T19:04:44Z", "event":"Repair", "Item":"Paint", "Cost":1 }
137{ "timestamp":"2020-09-21T19:04:51Z", "event":"Repair", "Item":"Wear", "Cost":10 }"#;
138
139        let cursor = Cursor::new(data);
140        let mut reader = LogIter::new(cursor);
141
142        assert!(reader.next().is_some());
143        assert!(reader.next().is_some());
144
145        assert!(dbg!(reader.next()).is_none());
146    }
147
148    #[test]
149    fn log_reader_handles_trailing_newlines_correctly() {
150        let data = r#"{ "timestamp":"2020-09-21T19:04:44Z", "event":"Repair", "Item":"Paint", "Cost":1 }
151{ "timestamp":"2020-09-21T19:04:51Z", "event":"Repair", "Item":"Wear", "Cost":10 }
152"#;
153
154        let cursor = Cursor::new(data);
155        let mut reader = LogIter::new(cursor);
156
157        assert!(reader.next().is_some());
158        assert!(reader.next().is_some());
159
160        assert!(dbg!(reader.next()).is_none());
161    }
162
163    #[test]
164    fn last_lines_are_read_correctly() {
165        fs::write("c.tmp", "").unwrap();
166        let file = File::open("c.tmp").unwrap();
167        let buf_reader = BufReader::new(file);
168
169        let mut reader = LogIter::new(buf_reader);
170
171        assert!(reader.next().is_none());
172
173        fs::write(
174            "c.tmp",
175            r#"{"timestamp":"2022-10-22T15:10:41Z","event":"Fileheader","part":1,"language":"English/UK","Odyssey":true,"gameversion":"4.0.0.1450","build":"r286858/r0 "}"#,
176        )
177            .unwrap();
178
179        assert_eq!(
180            reader.next().unwrap().unwrap().content.kind(),
181            LogEventContentKind::FileHeader
182        );
183
184        fs::write("c.tmp", r#"{"timestamp":"2022-10-22T15:10:41Z","event":"Fileheader","part":1,"language":"English/UK","Odyssey":true,"gameversion":"4.0.0.1450","build":"r286858/r0 "}
185{"timestamp":"2022-10-22T15:12:05Z","event":"Commander","FID":"F123456789","Name":"TEST"}"#)
186            .unwrap();
187
188        assert_eq!(
189            reader.next().unwrap().unwrap().content.kind(),
190            LogEventContentKind::Commander
191        );
192
193        fs::remove_file("c.tmp").unwrap();
194    }
195}