dprint_development/
spec_parser.rs

1#[derive(PartialEq, Eq, Debug)]
2pub struct Spec {
3  pub file_name: String,
4  pub message: String,
5  pub file_text: String,
6  pub expected_text: String,
7  pub is_only: bool,
8  pub is_trace: bool,
9  pub skip: bool,
10  pub skip_format_twice: bool,
11  pub config: SpecConfigMap,
12}
13
14pub type SpecConfigMap = serde_json::Map<String, serde_json::Value>;
15
16#[derive(Debug, Clone)]
17pub struct ParseSpecOptions {
18  /// The default file name for a parsed spec.
19  pub default_file_name: &'static str,
20}
21
22pub fn parse_specs(file_text: String, options: &ParseSpecOptions) -> Vec<Spec> {
23  // this function needs a rewrite
24  let file_text = file_text.replace("\r\n", "\n");
25  let (file_path, file_text) = parse_file_path(file_text, options);
26  let (config, file_text) = parse_config(file_text);
27  let lines = file_text.split('\n').collect::<Vec<_>>();
28  let spec_starts = get_spec_starts(&file_path, &lines);
29  let mut specs = Vec::new();
30
31  for i in 0..spec_starts.len() {
32    let start_index = spec_starts[i];
33    let end_index = if spec_starts.len() == i + 1 { lines.len() } else { spec_starts[i + 1] };
34    let message_line = lines[start_index];
35    let spec = parse_single_spec(&file_path, message_line, &lines[(start_index + 1)..end_index], &config);
36
37    specs.push(spec);
38  }
39
40  return specs;
41
42  fn parse_file_path(file_text: String, options: &ParseSpecOptions) -> (String, String) {
43    if !file_text.starts_with("--") {
44      return (options.default_file_name.into(), file_text);
45    }
46    let last_index = file_text.find("--\n").expect("Could not find final --");
47
48    (file_text["--".len()..last_index].trim().into(), file_text[(last_index + "--\n".len())..].into())
49  }
50
51  fn parse_config(file_text: String) -> (SpecConfigMap, String) {
52    if !file_text.starts_with("~~") {
53      return (Default::default(), file_text);
54    }
55    let last_index = file_text.find("~~\n").expect("Could not find final ~~\\n");
56
57    let config_text = file_text["~~".len()..last_index].replace('\n', "");
58    let config_text = config_text.trim();
59    let mut config: SpecConfigMap = Default::default();
60
61    if config_text.starts_with('{') {
62      config = serde_json::from_str(config_text).expect("Error parsing config json.");
63    } else {
64      for item in config_text.split(',') {
65        let first_colon = item.find(':').expect("Could not find colon in config option.");
66        let key = item[0..first_colon].trim();
67        let value = item[first_colon + ":".len()..].trim();
68
69        config.insert(
70          key.into(),
71          match value.parse::<bool>() {
72            Ok(value) => value.into(),
73            Err(_) => match value.parse::<i32>() {
74              Ok(value) => value.into(),
75              Err(_) => value.into(),
76            },
77          },
78        );
79      }
80    }
81
82    (config, file_text[(last_index + "~~\n".len())..].into())
83  }
84
85  fn get_spec_starts(file_name: &str, lines: &[&str]) -> Vec<usize> {
86    let mut result = Vec::new();
87    let message_separator = get_message_separator(file_name);
88
89    if !lines.first().unwrap().starts_with(message_separator) {
90      panic!("All spec files should start with a message. (ex. {0} Message {0})", message_separator);
91    }
92
93    for (i, line) in lines.iter().enumerate() {
94      if line.starts_with(message_separator) {
95        result.push(i);
96      }
97    }
98
99    result
100  }
101
102  fn parse_single_spec(file_name: &str, message_line: &str, lines: &[&str], config: &SpecConfigMap) -> Spec {
103    let file_text = lines.join("\n");
104    let parts = file_text.split("[expect]").collect::<Vec<&str>>();
105    let start_text = parts[0][0..parts[0].len() - "\n".len()].into(); // remove last newline
106    let expected_text = parts[1]["\n".len()..].into(); // remove first newline
107    let lower_case_message_line = message_line.to_ascii_lowercase();
108    let message_separator = get_message_separator(file_name);
109    let is_trace = lower_case_message_line.contains("(trace)");
110
111    Spec {
112      file_name: String::from(file_name),
113      message: message_line[message_separator.len()..message_line.len() - message_separator.len()]
114        .trim()
115        .into(),
116      file_text: start_text,
117      expected_text,
118      is_only: lower_case_message_line.contains("(only)") || is_trace,
119      is_trace,
120      skip: lower_case_message_line.contains("(skip)"),
121      skip_format_twice: lower_case_message_line.contains("(skip-format-twice)"),
122      config: config.clone(),
123    }
124  }
125
126  fn get_message_separator(file_name: &str) -> &'static str {
127    if file_name.ends_with(".md") { "!!" } else { "==" }
128  }
129}
130
131#[cfg(test)]
132mod tests {
133  use super::*;
134
135  #[test]
136  fn it_parses() {
137    let specs = parse_specs(
138      vec![
139        "== message 1 ==",
140        "start",
141        "multiple",
142        "",
143        "[expect]",
144        "expected",
145        "multiple",
146        "",
147        "== message 2 (only) (skip) (skip-format-twice) ==",
148        "start2",
149        "",
150        "[expect]",
151        "expected2",
152        "",
153        "== message 3 (trace) ==",
154        "test",
155        "",
156        "[expect]",
157        "test",
158        "",
159      ]
160      .join("\n"),
161      &ParseSpecOptions { default_file_name: "test.ts" },
162    );
163
164    assert_eq!(specs.len(), 3);
165    assert_eq!(
166      specs[0],
167      Spec {
168        file_name: "test.ts".into(),
169        file_text: "start\nmultiple\n".into(),
170        expected_text: "expected\nmultiple\n".into(),
171        message: "message 1".into(),
172        is_only: false,
173        is_trace: false,
174        skip: false,
175        skip_format_twice: false,
176        config: Default::default(),
177      }
178    );
179    assert_eq!(
180      specs[1],
181      Spec {
182        file_name: "test.ts".into(),
183        file_text: "start2\n".into(),
184        expected_text: "expected2\n".into(),
185        message: "message 2 (only) (skip) (skip-format-twice)".into(),
186        is_only: true,
187        is_trace: false,
188        skip: true,
189        skip_format_twice: true,
190        config: Default::default(),
191      }
192    );
193    assert_eq!(
194      specs[2],
195      Spec {
196        file_name: "test.ts".into(),
197        file_text: "test\n".into(),
198        expected_text: "test\n".into(),
199        message: "message 3 (trace)".into(),
200        is_only: true,
201        is_trace: true,
202        skip: false,
203        skip_format_twice: false,
204        config: Default::default(),
205      }
206    );
207  }
208
209  #[test]
210  fn it_parses_with_file_name() {
211    let specs = parse_specs(
212      vec!["-- asdf.ts --", "== message ==", "start", "[expect]", "expected"].join("\n"),
213      &ParseSpecOptions { default_file_name: "test.ts" },
214    );
215
216    assert_eq!(specs.len(), 1);
217    assert_eq!(
218      specs[0],
219      Spec {
220        file_name: "asdf.ts".into(),
221        file_text: "start".into(),
222        expected_text: "expected".into(),
223        message: "message".into(),
224        is_only: false,
225        is_trace: false,
226        skip: false,
227        skip_format_twice: false,
228        config: Default::default(),
229      }
230    );
231  }
232
233  #[test]
234  fn it_parses_with_config() {
235    let specs = parse_specs(
236      vec![
237        "-- asdf.ts --",
238        "~~ test.test: other, lineWidth: 40 ~~",
239        "== message ==",
240        "start",
241        "[expect]",
242        "expected",
243      ]
244      .join("\n"),
245      &ParseSpecOptions { default_file_name: "test.ts" },
246    );
247
248    assert_eq!(specs.len(), 1);
249    assert_eq!(
250      specs[0],
251      Spec {
252        file_name: "asdf.ts".into(),
253        file_text: "start".into(),
254        expected_text: "expected".into(),
255        message: "message".into(),
256        is_only: false,
257        is_trace: false,
258        skip: false,
259        skip_format_twice: false,
260        config: [("test.test".into(), "other".into()), ("lineWidth".into(), 40.into())]
261          .iter()
262          .cloned()
263          .collect(),
264      }
265    );
266  }
267
268  #[test]
269  fn it_parses_markdown() {
270    let specs = parse_specs(
271      vec![
272        "!! message 1 !!",
273        "start",
274        "multiple",
275        "",
276        "[expect]",
277        "expected",
278        "multiple",
279        "",
280        "!! message 2 (only) (skip) (skip-format-twice) !!",
281        "start2",
282        "",
283        "[expect]",
284        "expected2",
285        "",
286      ]
287      .join("\n"),
288      &ParseSpecOptions { default_file_name: "test.md" },
289    );
290
291    assert_eq!(specs.len(), 2);
292    assert_eq!(
293      specs[0],
294      Spec {
295        file_name: "test.md".into(),
296        file_text: "start\nmultiple\n".into(),
297        expected_text: "expected\nmultiple\n".into(),
298        message: "message 1".into(),
299        is_only: false,
300        is_trace: false,
301        skip: false,
302        skip_format_twice: false,
303        config: Default::default(),
304      }
305    );
306    assert_eq!(
307      specs[1],
308      Spec {
309        file_name: "test.md".into(),
310        file_text: "start2\n".into(),
311        expected_text: "expected2\n".into(),
312        message: "message 2 (only) (skip) (skip-format-twice)".into(),
313        is_only: true,
314        is_trace: false,
315        skip: true,
316        skip_format_twice: true,
317        config: Default::default(),
318      }
319    );
320  }
321}