#[cfg(test)] use std::io::BufRead; #[cfg(test)] use std::iter::Iterator; #[derive(Debug, PartialEq, Eq)] struct RowInput { label: String, entries: Vec, } struct Reader>> { input: Input, row: Option, } impl>> Reader { #[cfg(test)] fn new(input: Input) -> Self { Self { input, row: None } } } impl>> Iterator for Reader { type Item = Result; fn next(&mut self) -> Option { loop { match self.input.next() { None => return Ok(std::mem::take(&mut self.row)).transpose(), Some(Err(e)) => return Some(Err(e)), Some(Ok(line)) if line.is_empty() && self.row.is_some() => { return Ok(std::mem::take(&mut self.row)).transpose() } Some(Ok(line)) if line.is_empty() => {} Some(Ok(line)) if line.starts_with(' ') => match &mut self.row { None => return Some(Err(std::io::Error::other("Entry with no header"))), Some(ref mut row) => row.entries.push(String::from(line.trim())), }, Some(Ok(line)) => { let prev = std::mem::take(&mut self.row); self.row = Some(RowInput { label: line, entries: vec![], }); if prev.is_some() { return Ok(prev).transpose(); } } } } } } #[cfg(test)] fn read_rows(input: impl std::io::Read) -> impl Iterator> { Reader::new(std::io::BufReader::new(input).lines()) } pub fn tablify(_input: &impl std::io::Read) -> String { String::from("Hello, world!") } #[cfg(test)] mod tests { use super::*; #[test] fn test_read_rows() { assert_eq!( read_rows(&b"foo"[..]).flatten().collect::>(), vec![RowInput { label: String::from("foo"), entries: vec![] }] ); assert_eq!( read_rows(&b"bar"[..]).flatten().collect::>(), vec![RowInput { label: String::from("bar"), entries: vec![] }] ); assert_eq!( read_rows(&b"foo\nbar\n"[..]).flatten().collect::>(), vec![ RowInput { label: String::from("foo"), entries: vec![] }, RowInput { label: String::from("bar"), entries: vec![] } ] ); assert_eq!( read_rows(&b"foo\n bar\n"[..]).flatten().collect::>(), vec![RowInput { label: String::from("foo"), entries: vec![String::from("bar")] }] ); assert_eq!( read_rows(&b"foo\n bar\n baz\n"[..]) .flatten() .collect::>(), vec![RowInput { label: String::from("foo"), entries: vec![String::from("bar"), String::from("baz")] }] ); assert_eq!( read_rows(&b"foo\n\nbar\n"[..]) .flatten() .collect::>(), vec![ RowInput { label: String::from("foo"), entries: vec![] }, RowInput { label: String::from("bar"), entries: vec![] } ] ); let bad = read_rows(&b" foo"[..]).next().unwrap(); assert!(bad.is_err()); assert!(format!("{bad:?}").contains("Entry with no header")); let bad2 = read_rows(&b"foo\n\n bar"[..]).nth(1).unwrap(); assert!(bad2.is_err()); assert!(format!("{bad2:?}").contains("Entry with no header")); } }