1use crate::decode_anomaly::DecodeAnomaly;
2use crate::decode_output::DecodeOutput;
3use crate::decode_result::DecodeResult;
4use crate::incoming_log_level::IncomingLogLevel;
5use crate::log_level::LogLevel;
6use crate::log_line::LogLine;
7
8pub struct LogDecoder {
9 buffered: Option<(LogLevel, String)>,
10 previous_level: LogLevel,
11}
12
13impl LogDecoder {
14 #[must_use]
15 pub const fn new() -> Self {
16 Self {
17 buffered: None,
18 previous_level: LogLevel::None,
19 }
20 }
21
22 pub fn feed(&mut self, level: IncomingLogLevel, text: &str) -> DecodeResult {
23 match level {
24 IncomingLogLevel::Cont => self.feed_cont(text),
25 IncomingLogLevel::Debug => self.feed_non_cont(LogLevel::Debug, text),
26 IncomingLogLevel::Error => self.feed_non_cont(LogLevel::Error, text),
27 IncomingLogLevel::Info => self.feed_non_cont(LogLevel::Info, text),
28 IncomingLogLevel::None => self.feed_non_cont(LogLevel::None, text),
29 IncomingLogLevel::Unknown(raw) => self.feed_non_cont(LogLevel::Unknown(raw), text),
30 IncomingLogLevel::Warn => self.feed_non_cont(LogLevel::Warn, text),
31 }
32 }
33
34 fn feed_cont(&mut self, text: &str) -> DecodeResult {
35 if let Some((level, mut buffer)) = self.buffered.take() {
36 buffer.push_str(text);
37 if let Some(without_newline) = buffer.strip_suffix('\n') {
38 DecodeResult {
39 output: DecodeOutput::Line(LogLine {
40 level,
41 text: without_newline.to_owned(),
42 }),
43 anomaly: None,
44 }
45 } else {
46 self.buffered = Some((level, buffer));
47 DecodeResult {
48 output: DecodeOutput::None,
49 anomaly: None,
50 }
51 }
52 } else {
53 self.feed_orphan_cont(text)
54 }
55 }
56
57 fn feed_orphan_cont(&mut self, text: &str) -> DecodeResult {
58 let level = self.previous_level;
59 if let Some(without_newline) = text.strip_suffix('\n') {
60 DecodeResult {
61 output: DecodeOutput::Line(LogLine {
62 level,
63 text: without_newline.to_owned(),
64 }),
65 anomaly: Some(DecodeAnomaly::OrphanCont),
66 }
67 } else {
68 self.buffered = Some((level, text.to_owned()));
69 DecodeResult {
70 output: DecodeOutput::None,
71 anomaly: Some(DecodeAnomaly::OrphanCont),
72 }
73 }
74 }
75
76 fn feed_non_cont(&mut self, level: LogLevel, text: &str) -> DecodeResult {
77 self.previous_level = level;
78 let stale = self.buffered.take();
79 match (text.strip_suffix('\n'), stale) {
80 (Some(without_newline), Some((stale_level, stale_text))) => DecodeResult {
81 output: DecodeOutput::TwoLines {
82 earlier: LogLine {
83 level: stale_level,
84 text: stale_text,
85 },
86 current: LogLine {
87 level,
88 text: without_newline.to_owned(),
89 },
90 },
91 anomaly: Some(DecodeAnomaly::StaleBufferAbandoned),
92 },
93 (Some(without_newline), None) => DecodeResult {
94 output: DecodeOutput::Line(LogLine {
95 level,
96 text: without_newline.to_owned(),
97 }),
98 anomaly: None,
99 },
100 (None, Some((stale_level, stale_text))) => {
101 self.buffered = Some((level, text.to_owned()));
102 DecodeResult {
103 output: DecodeOutput::Line(LogLine {
104 level: stale_level,
105 text: stale_text,
106 }),
107 anomaly: Some(DecodeAnomaly::StaleBufferAbandoned),
108 }
109 }
110 (None, None) => {
111 self.buffered = Some((level, text.to_owned()));
112 DecodeResult {
113 output: DecodeOutput::None,
114 anomaly: None,
115 }
116 }
117 }
118 }
119}
120
121impl Default for LogDecoder {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::LogDecoder;
130 use crate::decode_anomaly::DecodeAnomaly;
131 use crate::decode_output::DecodeOutput;
132 use crate::decode_result::DecodeResult;
133 use crate::incoming_log_level::IncomingLogLevel;
134 use crate::log_level::LogLevel;
135 use crate::log_line::LogLine;
136
137 #[test]
138 fn feed_complete_info_line() {
139 let mut decoder = LogDecoder::new();
140 let result = decoder.feed(IncomingLogLevel::Info, "hello\n");
141
142 assert_eq!(
143 result,
144 DecodeResult {
145 output: DecodeOutput::Line(LogLine {
146 level: LogLevel::Info,
147 text: "hello".to_owned(),
148 }),
149 anomaly: None,
150 }
151 );
152 }
153
154 #[test]
155 fn feed_partial_without_newline() {
156 let mut decoder = LogDecoder::new();
157 let result = decoder.feed(IncomingLogLevel::Info, "hello");
158
159 assert_eq!(
160 result,
161 DecodeResult {
162 output: DecodeOutput::None,
163 anomaly: None,
164 }
165 );
166 }
167
168 #[test]
169 fn feed_cont_completion() {
170 let mut decoder = LogDecoder::new();
171 decoder.feed(IncomingLogLevel::Info, "hello ");
172 let result = decoder.feed(IncomingLogLevel::Cont, "world\n");
173
174 assert_eq!(
175 result,
176 DecodeResult {
177 output: DecodeOutput::Line(LogLine {
178 level: LogLevel::Info,
179 text: "hello world".to_owned(),
180 }),
181 anomaly: None,
182 }
183 );
184 }
185
186 #[test]
187 fn feed_multi_part_cont() {
188 let mut decoder = LogDecoder::new();
189 decoder.feed(IncomingLogLevel::Info, "part1 ");
190 decoder.feed(IncomingLogLevel::Cont, "part2 ");
191 let result = decoder.feed(IncomingLogLevel::Cont, "part3\n");
192
193 assert_eq!(
194 result,
195 DecodeResult {
196 output: DecodeOutput::Line(LogLine {
197 level: LogLevel::Info,
198 text: "part1 part2 part3".to_owned(),
199 }),
200 anomaly: None,
201 }
202 );
203 }
204
205 #[test]
206 fn feed_non_cont_while_buffering() {
207 let mut decoder = LogDecoder::new();
208 decoder.feed(IncomingLogLevel::Info, "stale");
209 let result = decoder.feed(IncomingLogLevel::Warn, "fresh\n");
210
211 assert_eq!(
212 result,
213 DecodeResult {
214 output: DecodeOutput::TwoLines {
215 earlier: LogLine {
216 level: LogLevel::Info,
217 text: "stale".to_owned(),
218 },
219 current: LogLine {
220 level: LogLevel::Warn,
221 text: "fresh".to_owned(),
222 },
223 },
224 anomaly: Some(DecodeAnomaly::StaleBufferAbandoned),
225 }
226 );
227 }
228
229 #[test]
230 fn feed_buffer_replacement() {
231 let mut decoder = LogDecoder::new();
232 decoder.feed(IncomingLogLevel::Info, "first");
233 let result = decoder.feed(IncomingLogLevel::Warn, "second");
234
235 assert_eq!(
236 result,
237 DecodeResult {
238 output: DecodeOutput::Line(LogLine {
239 level: LogLevel::Info,
240 text: "first".to_owned(),
241 }),
242 anomaly: Some(DecodeAnomaly::StaleBufferAbandoned),
243 }
244 );
245
246 let follow_up = decoder.feed(IncomingLogLevel::Cont, "more\n");
247 assert_eq!(
248 follow_up,
249 DecodeResult {
250 output: DecodeOutput::Line(LogLine {
251 level: LogLevel::Warn,
252 text: "secondmore".to_owned(),
253 }),
254 anomaly: None,
255 }
256 );
257 }
258
259 #[test]
260 fn feed_orphan_cont() {
261 let mut decoder = LogDecoder::new();
262 let result = decoder.feed(IncomingLogLevel::Cont, "ghost\n");
263
264 assert_eq!(
265 result,
266 DecodeResult {
267 output: DecodeOutput::Line(LogLine {
268 level: LogLevel::None,
269 text: "ghost".to_owned(),
270 }),
271 anomaly: Some(DecodeAnomaly::OrphanCont),
272 }
273 );
274 }
275
276 #[test]
277 fn feed_orphan_cont_previous_level() {
278 let mut decoder = LogDecoder::new();
279 decoder.feed(IncomingLogLevel::Warn, "complete\n");
280 let result = decoder.feed(IncomingLogLevel::Cont, "ghost\n");
281
282 assert_eq!(
283 result,
284 DecodeResult {
285 output: DecodeOutput::Line(LogLine {
286 level: LogLevel::Warn,
287 text: "ghost".to_owned(),
288 }),
289 anomaly: Some(DecodeAnomaly::OrphanCont),
290 }
291 );
292 }
293
294 #[test]
295 fn feed_none_level() {
296 let mut decoder = LogDecoder::new();
297 let result = decoder.feed(IncomingLogLevel::None, "no-level\n");
298
299 assert_eq!(
300 result,
301 DecodeResult {
302 output: DecodeOutput::Line(LogLine {
303 level: LogLevel::None,
304 text: "no-level".to_owned(),
305 }),
306 anomaly: None,
307 }
308 );
309 }
310
311 #[test]
312 fn feed_unknown_level() {
313 let mut decoder = LogDecoder::new();
314 let result = decoder.feed(IncomingLogLevel::Unknown(9999), "weird\n");
315
316 assert_eq!(
317 result,
318 DecodeResult {
319 output: DecodeOutput::Line(LogLine {
320 level: LogLevel::Unknown(9999),
321 text: "weird".to_owned(),
322 }),
323 anomaly: None,
324 }
325 );
326 }
327
328 #[test]
329 fn feed_debug_level() {
330 let mut decoder = LogDecoder::new();
331 let result = decoder.feed(IncomingLogLevel::Debug, "trace\n");
332
333 assert_eq!(
334 result,
335 DecodeResult {
336 output: DecodeOutput::Line(LogLine {
337 level: LogLevel::Debug,
338 text: "trace".to_owned(),
339 }),
340 anomaly: None,
341 }
342 );
343 }
344
345 #[test]
346 fn feed_error_level() {
347 let mut decoder = LogDecoder::new();
348 let result = decoder.feed(IncomingLogLevel::Error, "boom\n");
349
350 assert_eq!(
351 result,
352 DecodeResult {
353 output: DecodeOutput::Line(LogLine {
354 level: LogLevel::Error,
355 text: "boom".to_owned(),
356 }),
357 anomaly: None,
358 }
359 );
360 }
361
362 #[test]
363 fn feed_orphan_cont_without_newline_buffers_text() {
364 let mut decoder = LogDecoder::new();
365 let result = decoder.feed(IncomingLogLevel::Cont, "fragment");
366
367 assert_eq!(
368 result,
369 DecodeResult {
370 output: DecodeOutput::None,
371 anomaly: Some(DecodeAnomaly::OrphanCont),
372 }
373 );
374
375 let follow_up = decoder.feed(IncomingLogLevel::Cont, " rest\n");
376 assert_eq!(
377 follow_up,
378 DecodeResult {
379 output: DecodeOutput::Line(LogLine {
380 level: LogLevel::None,
381 text: "fragment rest".to_owned(),
382 }),
383 anomaly: None,
384 }
385 );
386 }
387
388 #[test]
389 fn default_construction() {
390 let mut default_decoder = LogDecoder::default();
391 let new_decoder_result = LogDecoder::new().feed(IncomingLogLevel::Info, "compare\n");
392 let default_result = default_decoder.feed(IncomingLogLevel::Info, "compare\n");
393
394 assert_eq!(default_result, new_decoder_result);
395 }
396}