Skip to main content

nifty_cli/
project.rs

1//! What a command operates on: a project folder with a `main.nf`, or a single `.nf` file.
2
3use std::path::{Path, PathBuf};
4
5use nifty_syntax::project::{self, ENTRY_FILE, SOURCE_EXTENSION};
6
7use crate::error::CliError;
8
9/// One source file of a command target.
10#[derive(Debug, PartialEq, Eq)]
11pub(crate) struct ProjectFile {
12    /// Where the file is, for reading it and naming it in messages.
13    pub path: PathBuf,
14    /// Where the file sits in its project. A single file is a project of one, so it is the
15    /// project's entry file.
16    pub relative: PathBuf,
17}
18
19/// The source files that a command target refers to: every `.nf` file of a project
20/// folder, `main.nf` first, or a single `.nf` file on its own.
21pub(crate) fn source_files(target: &Path) -> Result<Vec<ProjectFile>, CliError> {
22    if target.is_dir() {
23        if !target.join(ENTRY_FILE).is_file() {
24            return Err(CliError::new(format!(
25                "{} has no {ENTRY_FILE}; create one or run `nf create <name>`",
26                target.display()
27            )));
28        }
29        let files = project::source_files(target)
30            .map_err(|error| CliError::new(format!("can't list {}: {error}", target.display())))?;
31        return Ok(files
32            .into_iter()
33            .map(|relative| ProjectFile {
34                path: target.join(&relative),
35                relative,
36            })
37            .collect());
38    }
39    if target.is_file() && project::is_source(target) {
40        return Ok(vec![ProjectFile {
41            path: target.to_path_buf(),
42            relative: PathBuf::from(ENTRY_FILE),
43        }]);
44    }
45    Err(CliError::new(format!(
46        "{} is not a Nifty project folder or .{SOURCE_EXTENSION} file",
47        target.display()
48    )))
49}
50
51#[cfg(test)]
52mod tests {
53    use std::fs;
54
55    use super::*;
56    use crate::test_support::scratch_dir;
57
58    #[test]
59    fn a_folder_resolves_to_all_its_source_files_main_first() {
60        let dir = scratch_dir("folder");
61        fs::create_dir(dir.join("ui")).expect("create ui");
62        for file in [ENTRY_FILE, "ui/header.nf", "app.nf"] {
63            fs::write(dir.join(file), "").expect("write a source file");
64        }
65        let files = source_files(&dir).expect("folder has main.nf");
66        assert_eq!(files[0].path, dir.join(ENTRY_FILE));
67        let relative: Vec<&Path> = files.iter().map(|file| file.relative.as_path()).collect();
68        assert_eq!(
69            relative,
70            [ENTRY_FILE, "app.nf", "ui/header.nf"].map(Path::new)
71        );
72    }
73
74    #[test]
75    fn a_folder_without_main_is_an_error() {
76        let dir = scratch_dir("empty");
77        fs::write(dir.join("app.nf"), "").expect("write app.nf");
78        let error = source_files(&dir).expect_err("no main.nf");
79        assert!(error.to_string().contains("has no main.nf"));
80    }
81
82    #[test]
83    fn a_single_file_stays_on_its_own() {
84        let dir = scratch_dir("files");
85        let source = dir.join("app.nf");
86        let other = dir.join("notes.txt");
87        fs::write(dir.join(ENTRY_FILE), "").expect("write main.nf");
88        fs::write(&source, "").expect("write app.nf");
89        fs::write(&other, "").expect("write notes.txt");
90
91        let files = source_files(&source).expect("an .nf file");
92        assert_eq!(
93            files,
94            [ProjectFile {
95                path: source,
96                relative: PathBuf::from(ENTRY_FILE)
97            }]
98        );
99        assert!(source_files(&other).is_err());
100        assert!(source_files(&dir.join("missing.nf")).is_err());
101    }
102}