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") {
128      "!!"
129    } else {
130      "=="
131    }
132  }
133}
134
135#[cfg(test)]
136mod tests {
137  use super::*;
138
139  #[test]
140  fn it_parses() {
141    let specs = parse_specs(
142      vec![
143        "== message 1 ==",
144        "start",
145        "multiple",
146        "",
147        "[expect]",
148        "expected",
149        "multiple",
150        "",
151        "== message 2 (only) (skip) (skip-format-twice) ==",
152        "start2",
153        "",
154        "[expect]",
155        "expected2",
156        "",
157        "== message 3 (trace) ==",
158        "test",
159        "",
160        "[expect]",
161        "test",
162        "",
163      ]
164      .join("\n"),
165      &ParseSpecOptions { default_file_name: "test.ts" },
166    );
167
168    assert_eq!(specs.len(), 3);
169    assert_eq!(
170      specs[0],
171      Spec {
172        file_name: "test.ts".into(),
173        file_text: "start\nmultiple\n".into(),
174        expected_text: "expected\nmultiple\n".into(),
175        message: "message 1".into(),
176        is_only: false,
177        is_trace: false,
178        skip: false,
179        skip_format_twice: false,
180        config: Default::default(),
181      }
182    );
183    assert_eq!(
184      specs[1],
185      Spec {
186        file_name: "test.ts".into(),
187        file_text: "start2\n".into(),
188        expected_text: "expected2\n".into(),
189        message: "message 2 (only) (skip) (skip-format-twice)".into(),
190        is_only: true,
191        is_trace: false,
192        skip: true,
193        skip_format_twice: true,
194        config: Default::default(),
195      }
196    );
197    assert_eq!(
198      specs[2],
199      Spec {
200        file_name: "test.ts".into(),
201        file_text: "test\n".into(),
202        expected_text: "test\n".into(),
203        message: "message 3 (trace)".into(),
204        is_only: true,
205        is_trace: true,
206        skip: false,
207        skip_format_twice: false,
208        config: Default::default(),
209      }
210    );
211  }
212
213  #[test]
214  fn it_parses_with_file_name() {
215    let specs = parse_specs(
216      vec!["-- asdf.ts --", "== message ==", "start", "[expect]", "expected"].join("\n"),
217      &ParseSpecOptions { default_file_name: "test.ts" },
218    );
219
220    assert_eq!(specs.len(), 1);
221    assert_eq!(
222      specs[0],
223      Spec {
224        file_name: "asdf.ts".into(),
225        file_text: "start".into(),
226        expected_text: "expected".into(),
227        message: "message".into(),
228        is_only: false,
229        is_trace: false,
230        skip: false,
231        skip_format_twice: false,
232        config: Default::default(),
233      }
234    );
235  }
236
237  #[test]
238  fn it_parses_with_config() {
239    let specs = parse_specs(
240      vec![
241        "-- asdf.ts --",
242        "~~ test.test: other, lineWidth: 40 ~~",
243        "== message ==",
244        "start",
245        "[expect]",
246        "expected",
247      ]
248      .join("\n"),
249      &ParseSpecOptions { default_file_name: "test.ts" },
250    );
251
252    assert_eq!(specs.len(), 1);
253    assert_eq!(
254      specs[0],
255      Spec {
256        file_name: "asdf.ts".into(),
257        file_text: "start".into(),
258        expected_text: "expected".into(),
259        message: "message".into(),
260        is_only: false,
261        is_trace: false,
262        skip: false,
263        skip_format_twice: false,
264        config: [("test.test".into(), "other".into()), ("lineWidth".into(), 40.into())]
265          .iter()
266          .cloned()
267          .collect(),
268      }
269    );
270  }
271
272  #[test]
273  fn it_parses_markdown() {
274    let specs = parse_specs(
275      vec![
276        "!! message 1 !!",
277        "start",
278        "multiple",
279        "",
280        "[expect]",
281        "expected",
282        "multiple",
283        "",
284        "!! message 2 (only) (skip) (skip-format-twice) !!",
285        "start2",
286        "",
287        "[expect]",
288        "expected2",
289        "",
290      ]
291      .join("\n"),
292      &ParseSpecOptions { default_file_name: "test.md" },
293    );
294
295    assert_eq!(specs.len(), 2);
296    assert_eq!(
297      specs[0],
298      Spec {
299        file_name: "test.md".into(),
300        file_text: "start\nmultiple\n".into(),
301        expected_text: "expected\nmultiple\n".into(),
302        message: "message 1".into(),
303        is_only: false,
304        is_trace: false,
305        skip: false,
306        skip_format_twice: false,
307        config: Default::default(),
308      }
309    );
310    assert_eq!(
311      specs[1],
312      Spec {
313        file_name: "test.md".into(),
314        file_text: "start2\n".into(),
315        expected_text: "expected2\n".into(),
316        message: "message 2 (only) (skip) (skip-format-twice)".into(),
317        is_only: true,
318        is_trace: false,
319        skip: true,
320        skip_format_twice: true,
321        config: Default::default(),
322      }
323    );
324  }
325}