123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- pub mod contlines;
- use contlines::ContinuationLines;
- struct ParsingContext {
- current_record_type: Option<String>,
- }
- #[derive(Eq, PartialEq, Debug)]
- pub struct Record {
- pub rec_type: Option<String>,
- pub fields: Vec<(String, String)>,
- }
- impl Record {
- pub fn write<W>(&self, w: &mut W) -> std::io::Result<()>
- where W: std::io::Write
- {
- for &(ref name, ref value) in self.fields.iter() {
- write!(w, "{}: {}\n", name, value)?;
- }
- write!(w, "\n")
- }
- pub fn size(&self) -> usize {
- self.fields.len()
- }
- }
- #[derive(Eq, PartialEq, Debug)]
- pub struct Recfile {
- pub records: Vec<Record>,
- }
- impl Recfile {
- pub fn write<W>(&self, w: &mut W) -> std::io::Result<()>
- where W: std::io::Write
- {
- for r in self.records.iter() {
- r.write(w)?;
- }
- Ok(())
- }
- pub fn filter_by_type(&mut self, type_name: &str) {
- self.records.retain(|r| match r.rec_type {
- Some(ref t) => t == type_name,
- None => false,
- });
- }
- }
- impl Recfile {
- pub fn parse<I>(i: I) -> Result<Recfile, String>
- where I: std::io::BufRead
- {
- let mut iter = ContinuationLines::new(i.lines());
- let mut current = Record {
- fields: vec![],
- rec_type: None,
- };
- let mut buf = vec![];
- let mut ctx = ParsingContext {
- current_record_type: None,
- };
- while let Some(Ok(ln)) = iter.next() {
- let ln = ln.trim_left_matches(' ');
- if ln.starts_with('#') {
- // skip comment lines
- } else if ln.is_empty() {
- if !current.fields.is_empty() {
- buf.push(current);
- current = Record {
- rec_type: ctx.current_record_type.clone(),
- fields: vec![],
- };
- }
- } else if ln.starts_with('+') {
- if let Some(val) = current.fields.last_mut() {
- val.1.push_str("\n");
- val.1.push_str(
- if ln[1..].starts_with(' ') {
- &ln[2..]
- } else {
- &ln[1..]
- });
- } else {
- return Err(format!(
- "Found continuation line in nonsensical place: {}",
- ln));
- }
- } else if let Some(pos) = ln.find(':') {
- let (key, val) = ln.split_at(pos);
- current.fields.push((
- key.to_owned(),
- val[1..].trim_left().to_owned()));
- if key == "%rec" {
- ctx.current_record_type = Some(val[1..].trim_left().to_owned());
- }
- } else {
- return Err(format!("Invalid line: {:?}", ln));
- }
- }
- if !current.fields.is_empty() {
- buf.push(current);
- }
- Ok(Recfile { records: buf })
- }
- }
- #[cfg(test)]
- mod tests {
- use ::{Recfile,Record};
- fn test_parse(input: &[u8], expected: Vec<Vec<(&str, &str)>>) {
- let file = Recfile {
- records: expected.iter().map( |v| {
- Record {
- rec_type: None,
- fields: v.iter().map( |&(k, v)| {
- (k.to_owned(), v.to_owned())
- }).collect(),
- }
- }).collect(),
- };
- assert_eq!(Recfile::parse(input), Ok(file));
- }
- #[test]
- fn empty_file() {
- test_parse(b"\n", vec![]);
- }
- #[test]
- fn only_comments() {
- test_parse(b"# an empty file\n", vec![]);
- }
- #[test]
- fn one_section() {
- test_parse(b"hello: yes\n", vec![ vec![ ("hello", "yes") ] ]);
- }
- #[test]
- fn two_sections() {
- test_parse(
- b"hello: yes\n\ngoodbye: no\n",
- vec![
- vec![ ("hello", "yes") ],
- vec![ ("goodbye", "no") ],
- ],
- );
- }
- #[test]
- fn continuation_with_space() {
- test_parse(
- b"hello: yes\n+ but also no\n",
- vec![
- vec![ ("hello", "yes\nbut also no") ],
- ],
- );
- }
- #[test]
- fn continuation_without_space() {
- test_parse(
- b"hello: yes\n+but also no\n",
- vec![
- vec![ ("hello", "yes\nbut also no") ],
- ],
- );
- }
- #[test]
- fn continuation_with_two_spaces() {
- test_parse(
- b"hello: yes\n+ but also no\n",
- vec![
- vec![ ("hello", "yes\n but also no") ],
- ],
- );
- }
- }
|