1use std::cmp::Reverse;
4use std::fs;
5use std::io;
6use std::path::{Path, PathBuf};
7
8pub const ENTRY_FILE: &str = "main.nf";
10pub const SOURCE_EXTENSION: &str = "nf";
11
12#[must_use]
13pub fn is_source(path: &Path) -> bool {
14 path.extension().is_some_and(|ext| ext == SOURCE_EXTENSION)
15}
16
17#[must_use]
19pub fn root_of(file: &Path) -> Option<&Path> {
20 file.ancestors()
21 .skip(1)
22 .find(|folder| folder.join(ENTRY_FILE).is_file())
23}
24
25pub fn source_files(root: &Path) -> io::Result<Vec<PathBuf>> {
33 let mut files = Vec::new();
34 collect(root, Path::new(""), &mut files)?;
35 files.sort_by_key(|path| (Reverse(path == Path::new(ENTRY_FILE)), path.clone()));
36 Ok(files)
37}
38
39fn collect(root: &Path, folder: &Path, files: &mut Vec<PathBuf>) -> io::Result<()> {
40 for entry in fs::read_dir(root.join(folder))? {
41 let entry = entry?;
42 let path = folder.join(entry.file_name());
43 if entry.file_type()?.is_dir() {
44 if !root.join(&path).join(ENTRY_FILE).is_file() {
45 collect(root, &path, files)?;
46 }
47 } else if is_source(&path) {
48 files.push(path);
49 }
50 }
51 Ok(())
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 fn write(root: &Path, relative: &str) {
59 let path = root.join(relative);
60 if let Some(folder) = path.parent() {
61 fs::create_dir_all(folder).expect("create folder");
62 }
63 fs::write(path, "").expect("write file");
64 }
65
66 #[test]
67 fn lists_main_first_then_every_other_source_file_sorted() {
68 let root = tempfile::tempdir().expect("scratch folder");
69 for file in [
70 "main.nf",
71 "b.nf",
72 "a.nf",
73 "ui/z.nf",
74 "ui/deep/y.nf",
75 "notes.txt",
76 ] {
77 write(root.path(), file);
78 }
79 let files = source_files(root.path()).expect("the project is readable");
80 let expected = ["main.nf", "a.nf", "b.nf", "ui/deep/y.nf", "ui/z.nf"];
81 assert_eq!(files, expected.map(PathBuf::from));
82 }
83
84 #[test]
85 fn leaves_out_folders_that_are_projects_of_their_own() {
86 let root = tempfile::tempdir().expect("scratch folder");
87 for file in ["main.nf", "build/app.app/main.nf", "build/app.app/ui.nf"] {
88 write(root.path(), file);
89 }
90 let files = source_files(root.path()).expect("the project is readable");
91 assert_eq!(files, [PathBuf::from(ENTRY_FILE)]);
92 }
93
94 #[test]
95 fn a_file_belongs_to_the_nearest_folder_with_a_main_file() {
96 let root = tempfile::tempdir().expect("scratch folder");
97 write(root.path(), "main.nf");
98 write(root.path(), "ui/header.nf");
99 let nested = root.path().join("ui/header.nf");
100 assert_eq!(root_of(&nested), Some(root.path()));
101 assert_eq!(root_of(&root.path().join(ENTRY_FILE)), Some(root.path()));
102 }
103}