lib.rs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. struct ParsingContext {
  2. continuation_line: bool,
  3. current_record_type: Option<String>,
  4. }
  5. #[derive(Eq, PartialEq, Debug)]
  6. pub struct Record {
  7. pub rec_type: Option<String>,
  8. pub fields: Vec<(String, String)>,
  9. }
  10. #[derive(Eq, PartialEq, Debug)]
  11. pub struct Recfile {
  12. pub records: Vec<Record>,
  13. }
  14. impl Recfile {
  15. pub fn parse<I>(i: I) -> Result<Recfile, String>
  16. where I: std::io::BufRead
  17. {
  18. let mut iter = i.lines();
  19. let mut current = Record {
  20. fields: vec![],
  21. rec_type: None,
  22. };
  23. let mut buf = vec![];
  24. let mut ctx = ParsingContext {
  25. continuation_line: false,
  26. current_record_type: None,
  27. };
  28. while let Some(Ok(ln)) = iter.next() {
  29. let ln = ln.trim_left_matches(' ');
  30. if ln.starts_with('#') {
  31. // skip comment lines
  32. } else if ln.is_empty() {
  33. if !current.fields.is_empty() {
  34. buf.push(current);
  35. current = Record {
  36. rec_type: ctx.current_record_type.clone(),
  37. fields: vec![],
  38. };
  39. }
  40. } else if ln.starts_with('+') {
  41. if let Some(val) = current.fields.last_mut() {
  42. val.1.push_str("\n");
  43. val.1.push_str(
  44. if ln[1..].starts_with(' ') {
  45. &ln[2..]
  46. } else {
  47. &ln[1..]
  48. });
  49. } else {
  50. return Err(format!(
  51. "Found continuation line in nonsensical place: {}",
  52. ln));
  53. }
  54. } else if let Some(pos) = ln.find(':') {
  55. let (key, val) = ln.split_at(pos);
  56. current.fields.push((
  57. key.to_owned(),
  58. val[1..].trim_left().to_owned()));
  59. if key == "%rec" {
  60. ctx.current_record_type = Some(val[1..].trim_left().to_owned());
  61. }
  62. } else {
  63. return Err(format!("Invalid line: {:?}", ln));
  64. }
  65. }
  66. if !current.fields.is_empty() {
  67. buf.push(current);
  68. }
  69. Ok(Recfile { records: buf })
  70. }
  71. }
  72. #[cfg(test)]
  73. mod tests {
  74. use ::{Recfile,Record};
  75. fn test_parse(input: &[u8], expected: Vec<Vec<(&str, &str)>>) {
  76. let file = Recfile {
  77. records: expected.iter().map( |v| {
  78. Record {
  79. fields: v.iter().map( |&(k, v)| {
  80. (k.to_owned(), v.to_owned())
  81. }).collect(),
  82. }
  83. }).collect(),
  84. };
  85. assert_eq!(Recfile::parse(input), Ok(file));
  86. }
  87. #[test]
  88. fn empty_file() {
  89. test_parse(b"\n", vec![]);
  90. }
  91. #[test]
  92. fn only_comments() {
  93. test_parse(b"# an empty file\n", vec![]);
  94. }
  95. #[test]
  96. fn one_section() {
  97. test_parse(b"hello: yes\n", vec![ vec![ ("hello", "yes") ] ]);
  98. }
  99. #[test]
  100. fn two_sections() {
  101. test_parse(
  102. b"hello: yes\n\ngoodbye: no\n",
  103. vec![
  104. vec![ ("hello", "yes") ],
  105. vec![ ("goodbye", "no") ],
  106. ],
  107. );
  108. }
  109. #[test]
  110. fn continuation_with_space() {
  111. test_parse(
  112. b"hello: yes\n+ but also no\n",
  113. vec![
  114. vec![ ("hello", "yes\nbut also no") ],
  115. ],
  116. );
  117. }
  118. #[test]
  119. fn continuation_without_space() {
  120. test_parse(
  121. b"hello: yes\n+but also no\n",
  122. vec![
  123. vec![ ("hello", "yes\nbut also no") ],
  124. ],
  125. );
  126. }
  127. #[test]
  128. fn continuation_with_two_spaces() {
  129. test_parse(
  130. b"hello: yes\n+ but also no\n",
  131. vec![
  132. vec![ ("hello", "yes\n but also no") ],
  133. ],
  134. );
  135. }
  136. }