123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- #[macro_use]
- extern crate failure;
- 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()
- }
- pub fn get(&self, key: &str) -> Option<&str> {
- for (f, v) in self.fields.iter() {
- if f == key {
- return Some(v);
- }
- }
- None
- }
- }
- #[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,
- });
- }
- pub fn iter(&self) -> impl Iterator<Item=&Record> {
- self.records.iter()
- }
- }
- #[derive(Debug, Fail, PartialEq, Eq)]
- pub enum RecError {
- #[fail(display = "Error parsing records: {}", message)]
- GenericError { message: String },
- #[fail(display = "Found cont line in nonsensical place: {}", ln)]
- BadContLine { ln: String },
- #[fail(display = "Invalid line: {}", ln)]
- InvalidLine { ln: String },
- }
- impl Recfile {
- pub fn parse<I>(i: I) -> Result<Recfile, RecError>
- 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_start_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(RecError::BadContLine { ln: ln.to_owned() });
- }
- } else if let Some(pos) = ln.find(':') {
- let (key, val) = ln.split_at(pos);
- current
- .fields
- .push((key.to_owned(), val[1..].trim_start().to_owned()));
- if key == "%rec" {
- ctx.current_record_type = Some(val[1..].trim_start().to_owned());
- }
- } else {
- return Err(RecError::InvalidLine { ln: ln.to_owned() });
- }
- }
- if !current.fields.is_empty() {
- buf.push(current);
- }
- Ok(Recfile { records: buf })
- }
- }
- #[cfg(test)]
- mod tests {
- use {Recfile, RecError, 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).unwrap(), 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")]],
- );
- }
- }
|