gruel_air/sema/
module_path.rs1use std::path::Path;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ModulePath {
26 Std,
30
31 ExplicitRue { path: String },
35
36 Simple { path: String },
44}
45
46impl ModulePath {
47 pub fn parse(import_path: &str) -> Self {
60 if import_path == "std" {
62 return ModulePath::Std;
63 }
64
65 if import_path.ends_with(".gruel") {
67 return ModulePath::ExplicitRue {
68 path: import_path.to_string(),
69 };
70 }
71
72 ModulePath::Simple {
74 path: import_path.to_string(),
75 }
76 }
77
78 pub fn resolve<'a, I>(&self, loaded_paths: I) -> Option<String>
89 where
90 I: Iterator<Item = &'a String>,
91 {
92 match self {
93 ModulePath::Std => {
94 None
96 }
97 ModulePath::ExplicitRue { path } => self.resolve_explicit(path, loaded_paths),
98 ModulePath::Simple { path } => self.resolve_simple(path, loaded_paths),
99 }
100 }
101
102 fn resolve_explicit<'a, I>(&self, import_path: &str, loaded_paths: I) -> Option<String>
104 where
105 I: Iterator<Item = &'a String>,
106 {
107 let collected: Vec<_> = loaded_paths.collect();
108
109 for file_path in &collected {
111 if *file_path == import_path {
112 return Some((*file_path).clone());
113 }
114 }
115
116 for file_path in &collected {
119 if file_path.ends_with(import_path) {
120 let prefix_len = file_path.len() - import_path.len();
122 if prefix_len == 0 || file_path.as_bytes()[prefix_len - 1] == b'/' {
123 return Some((*file_path).clone());
124 }
125 }
126 }
127
128 None
129 }
130
131 fn resolve_simple<'a, I>(&self, import_path: &str, loaded_paths: I) -> Option<String>
133 where
134 I: Iterator<Item = &'a String>,
135 {
136 let import_with_gruel = format!("{}.gruel", import_path);
137 let collected: Vec<_> = loaded_paths.collect();
138
139 let basename = Path::new(import_path)
141 .file_name()
142 .and_then(|s| s.to_str())
143 .unwrap_or(import_path);
144 let facade_name = format!("_{}.gruel", basename);
145
146 for file_path in &collected {
148 if *file_path == &import_with_gruel {
149 return Some((*file_path).clone());
150 }
151 }
152
153 for file_path in &collected {
156 if file_path.ends_with(&import_with_gruel) {
157 let prefix_len = file_path.len() - import_with_gruel.len();
159 if prefix_len == 0 || file_path.as_bytes()[prefix_len - 1] == b'/' {
160 return Some((*file_path).clone());
161 }
162 }
163 }
164
165 for file_path in &collected {
167 if let Some(file_name) = Path::new(file_path.as_str())
168 .file_stem()
169 .and_then(|s| s.to_str())
170 && file_name == basename
171 {
172 return Some((*file_path).clone());
173 }
174 }
175
176 for file_path in &collected {
178 if let Some(file_name) = Path::new(file_path.as_str())
179 .file_name()
180 .and_then(|s| s.to_str())
181 && file_name == facade_name
182 {
183 return Some((*file_path).clone());
184 }
185 }
186
187 None
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
200 fn test_parse_std() {
201 assert_eq!(ModulePath::parse("std"), ModulePath::Std);
202 }
203
204 #[test]
205 fn test_parse_explicit_gruel() {
206 assert_eq!(
207 ModulePath::parse("foo.gruel"),
208 ModulePath::ExplicitRue {
209 path: "foo.gruel".to_string()
210 }
211 );
212 assert_eq!(
213 ModulePath::parse("utils/strings.gruel"),
214 ModulePath::ExplicitRue {
215 path: "utils/strings.gruel".to_string()
216 }
217 );
218 }
219
220 #[test]
221 fn test_parse_simple() {
222 assert_eq!(
223 ModulePath::parse("foo"),
224 ModulePath::Simple {
225 path: "foo".to_string()
226 }
227 );
228 assert_eq!(
229 ModulePath::parse("utils/strings"),
230 ModulePath::Simple {
231 path: "utils/strings".to_string()
232 }
233 );
234 }
235
236 #[test]
241 fn test_resolve_std_not_supported() {
242 let paths = ["main.gruel".to_string()];
243 let module = ModulePath::Std;
244 assert_eq!(module.resolve(paths.iter()), None);
245 }
246
247 #[test]
252 fn test_resolve_explicit_exact_match() {
253 let paths = ["foo.gruel".to_string(), "bar.gruel".to_string()];
254 let module = ModulePath::ExplicitRue {
255 path: "foo.gruel".to_string(),
256 };
257 assert_eq!(module.resolve(paths.iter()), Some("foo.gruel".to_string()));
258 }
259
260 #[test]
261 fn test_resolve_explicit_suffix_match() {
262 let paths = ["/project/src/foo.gruel".to_string()];
263 let module = ModulePath::ExplicitRue {
264 path: "foo.gruel".to_string(),
265 };
266 assert_eq!(
267 module.resolve(paths.iter()),
268 Some("/project/src/foo.gruel".to_string())
269 );
270 }
271
272 #[test]
273 fn test_resolve_explicit_no_false_substring_match() {
274 let paths = ["xfoo.gruel".to_string()];
276 let module = ModulePath::ExplicitRue {
277 path: "foo.gruel".to_string(),
278 };
279 assert_eq!(module.resolve(paths.iter()), None);
280 }
281
282 #[test]
283 fn test_resolve_explicit_nested_path() {
284 let paths = ["/project/utils/strings.gruel".to_string()];
285 let module = ModulePath::ExplicitRue {
286 path: "utils/strings.gruel".to_string(),
287 };
288 assert_eq!(
289 module.resolve(paths.iter()),
290 Some("/project/utils/strings.gruel".to_string())
291 );
292 }
293
294 #[test]
299 fn test_resolve_simple_exact_match() {
300 let paths = ["foo.gruel".to_string()];
301 let module = ModulePath::Simple {
302 path: "foo".to_string(),
303 };
304 assert_eq!(module.resolve(paths.iter()), Some("foo.gruel".to_string()));
305 }
306
307 #[test]
308 fn test_resolve_simple_suffix_match() {
309 let paths = ["/project/src/foo.gruel".to_string()];
310 let module = ModulePath::Simple {
311 path: "foo".to_string(),
312 };
313 assert_eq!(
314 module.resolve(paths.iter()),
315 Some("/project/src/foo.gruel".to_string())
316 );
317 }
318
319 #[test]
320 fn test_resolve_simple_nested_path() {
321 let paths = ["/project/utils/strings.gruel".to_string()];
322 let module = ModulePath::Simple {
323 path: "utils/strings".to_string(),
324 };
325 assert_eq!(
326 module.resolve(paths.iter()),
327 Some("/project/utils/strings.gruel".to_string())
328 );
329 }
330
331 #[test]
332 fn test_resolve_simple_basename_match() {
333 let paths = ["src/math.gruel".to_string()];
335 let module = ModulePath::Simple {
336 path: "math".to_string(),
337 };
338 assert_eq!(
339 module.resolve(paths.iter()),
340 Some("src/math.gruel".to_string())
341 );
342 }
343
344 #[test]
345 fn test_resolve_simple_facade_file() {
346 let paths = ["_utils.gruel".to_string()];
347 let module = ModulePath::Simple {
348 path: "utils".to_string(),
349 };
350 assert_eq!(
351 module.resolve(paths.iter()),
352 Some("_utils.gruel".to_string())
353 );
354 }
355
356 #[test]
357 fn test_resolve_simple_prefers_regular_over_facade() {
358 let paths = ["_foo.gruel".to_string(), "foo.gruel".to_string()];
360 let module = ModulePath::Simple {
361 path: "foo".to_string(),
362 };
363 assert_eq!(module.resolve(paths.iter()), Some("foo.gruel".to_string()));
364 }
365
366 #[test]
367 fn test_resolve_simple_no_false_substring_match() {
368 let paths = ["mathematics.gruel".to_string()];
370 let module = ModulePath::Simple {
371 path: "math".to_string(),
372 };
373 assert_eq!(module.resolve(paths.iter()), None);
375 }
376
377 #[test]
382 fn test_resolve_not_found() {
383 let paths = ["other.gruel".to_string()];
384 let module = ModulePath::Simple {
385 path: "foo".to_string(),
386 };
387 assert_eq!(module.resolve(paths.iter()), None);
388 }
389
390 #[test]
391 fn test_resolve_empty_paths() {
392 let paths: Vec<String> = vec![];
393 let module = ModulePath::Simple {
394 path: "foo".to_string(),
395 };
396 assert_eq!(module.resolve(paths.iter()), None);
397 }
398}