1use itertools::Itertools;
2use mun_paths::RelativePathBuf;
3
4const DEFAULT_FILE_NAME: &str = "mod.mun";
5const META_LINE: &str = "//-";
6
7#[derive(Debug, Eq, PartialEq)]
11pub struct Fixture {
12 pub relative_path: RelativePathBuf,
14
15 pub text: String,
17}
18
19impl Fixture {
20 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 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 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
95pub 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}