mun_hir/
fixture.rs

1use itertools::Itertools;
2use mun_paths::RelativePathBuf;
3
4const DEFAULT_FILE_NAME: &str = "mod.mun";
5const META_LINE: &str = "//-";
6
7/// A `Fixture` describes an single file in a project workspace. `Fixture`s can be parsed from a
8/// single string with the `parse` function. Using that function enables users to conveniently
9/// describe an entire workspace in a single string.
10#[derive(Debug, Eq, PartialEq)]
11pub struct Fixture {
12    /// The relative path of this file
13    pub relative_path: RelativePathBuf,
14
15    /// The text of the file
16    pub text: String,
17}
18
19impl Fixture {
20    /// Parses text which looks like this:
21    ///
22    /// ```not_rust
23    /// //- /foo.mun
24    /// fn hello_world() {
25    /// }
26    ///
27    /// //- /bar.mun
28    /// fn baz() {
29    /// }
30    /// ```
31    ///
32    /// into two separate `Fixture`s one with `relative_path` 'foo.mun' and one with 'bar.mun'.
33    pub fn parse(text: impl AsRef<str>) -> Vec<Fixture> {
34        let text = trim_raw_string_literal(text);
35        let mut result: Vec<Fixture> = Vec::new();
36
37        // If the text does not contain any meta tags, insert a default meta tag at the start.
38        let default_start = if text.contains(META_LINE) {
39            None
40        } else {
41            Some(format!("{} /{}", META_LINE, DEFAULT_FILE_NAME))
42        };
43
44        for (idx, line) in default_start
45            .as_deref()
46            .into_iter()
47            .chain(text.lines())
48            .enumerate()
49        {
50            if line.contains(META_LINE) {
51                assert!(
52                    line.starts_with(META_LINE),
53                    "Metadata line {} has invalid indentation. \
54                     All metadata lines need to have the same indentation \n\
55                     The offending line: {:?}",
56                    idx,
57                    line
58                );
59            }
60
61            if line.starts_with(META_LINE) {
62                let meta = Fixture::parse_meta_line(line);
63                result.push(meta);
64            } else if let Some(entry) = result.last_mut() {
65                entry.text.push_str(line);
66                entry.text.push('\n');
67            }
68        }
69
70        result
71    }
72
73    /// Parses a fixture meta line like:
74    /// ```
75    /// //- /main.mun
76    /// ```
77    fn parse_meta_line(line: impl AsRef<str>) -> Fixture {
78        let line = line.as_ref();
79        assert!(line.starts_with(META_LINE));
80
81        let line = line[META_LINE.len()..].trim();
82        let components = line.split_ascii_whitespace().collect::<Vec<_>>();
83
84        let path = components[0].to_string();
85        assert!(path.starts_with('/'));
86        let relative_path = RelativePathBuf::from(&path[1..]);
87
88        Fixture {
89            relative_path,
90            text: String::new(),
91        }
92    }
93}
94
95/// Turns a string that is likely to come from a raw string literal into something that is
96/// probably intended.
97///
98/// * Strips the first newline if there is one
99/// * Removes any initial indentation
100///
101/// Example usecase:
102///
103/// ```
104/// # fn do_something(s: &str) {}
105/// do_something(r#"
106///      fn func() {
107///         // code
108///      }
109/// "#)
110/// ```
111///
112/// Results in the string (with no leading newline):
113/// ```not_rust
114/// fn func() {
115///     // code
116/// }
117/// ```
118pub fn trim_raw_string_literal(text: impl AsRef<str>) -> String {
119    let mut text = text.as_ref();
120    if text.starts_with('\n') {
121        text = &text[1..];
122    }
123
124    let minimum_indentation = text
125        .lines()
126        .filter(|it| !it.trim().is_empty())
127        .map(|it| it.len() - it.trim_start().len())
128        .min()
129        .unwrap_or(0);
130
131    text.lines()
132        .map(|line| {
133            if line.len() <= minimum_indentation {
134                line.trim_start_matches(' ')
135            } else {
136                &line[minimum_indentation..]
137            }
138        })
139        .join("\n")
140}
141
142#[cfg(test)]
143mod test {
144    use super::*;
145
146    #[test]
147    fn trim_raw_string_literal() {
148        assert_eq!(
149            &super::trim_raw_string_literal(
150                r#"
151            fn hello_world() {
152                // code
153            }
154        "#
155            ),
156            "fn hello_world() {\n    // code\n}\n"
157        );
158    }
159
160    #[test]
161    fn empty_fixture() {
162        assert_eq!(
163            Fixture::parse(""),
164            vec![Fixture {
165                relative_path: RelativePathBuf::from(DEFAULT_FILE_NAME),
166                text: "".to_owned()
167            }]
168        );
169    }
170
171    #[test]
172    fn single_fixture() {
173        assert_eq!(
174            Fixture::parse(format!("{} /foo.mun\nfn hello_world() {{}}", META_LINE)),
175            vec![Fixture {
176                relative_path: RelativePathBuf::from("foo.mun"),
177                text: "fn hello_world() {}\n".to_owned()
178            }]
179        );
180    }
181
182    #[test]
183    fn multiple_fixtures() {
184        assert_eq!(
185            Fixture::parse(
186                r#"
187                //- /foo.mun
188                fn hello_world() {
189                }
190
191                //- /bar.mun
192                fn baz() {
193                }
194            "#
195            ),
196            vec![
197                Fixture {
198                    relative_path: RelativePathBuf::from("foo.mun"),
199                    text: "fn hello_world() {\n}\n\n".to_owned()
200                },
201                Fixture {
202                    relative_path: RelativePathBuf::from("bar.mun"),
203                    text: "fn baz() {\n}\n".to_owned()
204                }
205            ]
206        );
207    }
208
209    #[test]
210    #[should_panic]
211    fn incorrectly_indented_fixture() {
212        Fixture::parse(
213            r"
214        //- /foo.mun
215          fn foo() {}
216          //- /bar.mun
217          pub fn baz() {}
218          ",
219        );
220    }
221}