lib.rs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. #[macro_use]
  2. extern crate failure;
  3. pub mod contlines;
  4. use contlines::ContinuationLines;
  5. struct ParsingContext {
  6. current_record_type: Option<String>,
  7. }
  8. #[derive(Eq, PartialEq, Debug)]
  9. pub struct Record {
  10. pub rec_type: Option<String>,
  11. pub fields: Vec<(String, String)>,
  12. }
  13. impl Record {
  14. pub fn write<W>(&self, w: &mut W) -> std::io::Result<()>
  15. where
  16. W: std::io::Write,
  17. {
  18. for &(ref name, ref value) in self.fields.iter() {
  19. write!(w, "{}: {}\n", name, value)?;
  20. }
  21. write!(w, "\n")
  22. }
  23. pub fn size(&self) -> usize {
  24. self.fields.len()
  25. }
  26. pub fn get(&self, key: &str) -> Option<&str> {
  27. for (f, v) in self.fields.iter() {
  28. if f == key {
  29. return Some(v);
  30. }
  31. }
  32. None
  33. }
  34. }
  35. #[derive(Eq, PartialEq, Debug)]
  36. pub struct Recfile {
  37. pub records: Vec<Record>,
  38. }
  39. impl Recfile {
  40. pub fn write<W>(&self, w: &mut W) -> std::io::Result<()>
  41. where
  42. W: std::io::Write,
  43. {
  44. for r in self.records.iter() {
  45. r.write(w)?;
  46. }
  47. Ok(())
  48. }
  49. pub fn filter_by_type(&mut self, type_name: &str) {
  50. self.records.retain(|r| match r.rec_type {
  51. Some(ref t) => t == type_name,
  52. None => false,
  53. });
  54. }
  55. pub fn iter(&self) -> impl Iterator<Item=&Record> {
  56. self.records.iter()
  57. }
  58. }
  59. #[derive(Debug, Fail, PartialEq, Eq)]
  60. pub enum RecError {
  61. #[fail(display = "Error parsing records: {}", message)]
  62. GenericError { message: String },
  63. #[fail(display = "Found cont line in nonsensical place: {}", ln)]
  64. BadContLine { ln: String },
  65. #[fail(display = "Invalid line: {}", ln)]
  66. InvalidLine { ln: String },
  67. }
  68. impl Recfile {
  69. pub fn parse<I>(i: I) -> Result<Recfile, RecError>
  70. where
  71. I: std::io::BufRead,
  72. {
  73. let mut iter = ContinuationLines::new(i.lines());
  74. let mut current = Record {
  75. fields: vec![],
  76. rec_type: None,
  77. };
  78. let mut buf = vec![];
  79. let mut ctx = ParsingContext {
  80. current_record_type: None,
  81. };
  82. while let Some(Ok(ln)) = iter.next() {
  83. let ln = ln.trim_start_matches(' ');
  84. if ln.starts_with('#') {
  85. // skip comment lines
  86. } else if ln.is_empty() {
  87. if !current.fields.is_empty() {
  88. buf.push(current);
  89. current = Record {
  90. rec_type: ctx.current_record_type.clone(),
  91. fields: vec![],
  92. };
  93. }
  94. } else if ln.starts_with('+') {
  95. if let Some(val) = current.fields.last_mut() {
  96. val.1.push_str("\n");
  97. val.1.push_str(if ln[1..].starts_with(' ') {
  98. &ln[2..]
  99. } else {
  100. &ln[1..]
  101. });
  102. } else {
  103. return Err(RecError::BadContLine { ln: ln.to_owned() });
  104. }
  105. } else if let Some(pos) = ln.find(':') {
  106. let (key, val) = ln.split_at(pos);
  107. current
  108. .fields
  109. .push((key.to_owned(), val[1..].trim_start().to_owned()));
  110. if key == "%rec" {
  111. ctx.current_record_type = Some(val[1..].trim_start().to_owned());
  112. }
  113. } else {
  114. return Err(RecError::InvalidLine { ln: ln.to_owned() });
  115. }
  116. }
  117. if !current.fields.is_empty() {
  118. buf.push(current);
  119. }
  120. Ok(Recfile { records: buf })
  121. }
  122. }
  123. #[cfg(test)]
  124. mod tests {
  125. use {Recfile, RecError, Record};
  126. fn test_parse(input: &[u8], expected: Vec<Vec<(&str, &str)>>) {
  127. let file = Recfile {
  128. records: expected
  129. .iter()
  130. .map(|v| Record {
  131. rec_type: None,
  132. fields: v
  133. .iter()
  134. .map(|&(k, v)| (k.to_owned(), v.to_owned()))
  135. .collect(),
  136. })
  137. .collect(),
  138. };
  139. assert_eq!(Recfile::parse(input).unwrap(), file);
  140. }
  141. #[test]
  142. fn empty_file() {
  143. test_parse(b"\n", vec![]);
  144. }
  145. #[test]
  146. fn only_comments() {
  147. test_parse(b"# an empty file\n", vec![]);
  148. }
  149. #[test]
  150. fn one_section() {
  151. test_parse(b"hello: yes\n", vec![vec![("hello", "yes")]]);
  152. }
  153. #[test]
  154. fn two_sections() {
  155. test_parse(
  156. b"hello: yes\n\ngoodbye: no\n",
  157. vec![vec![("hello", "yes")], vec![("goodbye", "no")]],
  158. );
  159. }
  160. #[test]
  161. fn continuation_with_space() {
  162. test_parse(
  163. b"hello: yes\n+ but also no\n",
  164. vec![vec![("hello", "yes\nbut also no")]],
  165. );
  166. }
  167. #[test]
  168. fn continuation_without_space() {
  169. test_parse(
  170. b"hello: yes\n+but also no\n",
  171. vec![vec![("hello", "yes\nbut also no")]],
  172. );
  173. }
  174. #[test]
  175. fn continuation_with_two_spaces() {
  176. test_parse(
  177. b"hello: yes\n+ but also no\n",
  178. vec![vec![("hello", "yes\n but also no")]],
  179. );
  180. }
  181. }