Skip to main content

nifty_syntax/
project.rs

1//! Which files make up a project: the folder holding `main.nf` and every `.nf` file below it.
2
3use std::cmp::Reverse;
4use std::fs;
5use std::io;
6use std::path::{Path, PathBuf};
7
8/// Marks a folder as a project, and is the first of its files.
9pub 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/// The project `file` belongs to: the nearest folder above it that holds a `main.nf`.
18#[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
25/// The source files of the project in `root`, relative to it: `main.nf` first, then every
26/// other `.nf` file in it and its subfolders, sorted. A subfolder with its own `main.nf` is
27/// a separate project, such as an app `nf build` made, so its files are left out.
28///
29/// # Errors
30///
31/// Fails when a folder in the project can't be read.
32pub 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}