knave-expand 0.14.21

The reading, scoping and expansion stages of knave
Documentation
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Clone, Debug, Default)]
pub enum SearchScope {
    #[default]
    Program,
    InProject {
        tasks: Option<PathBuf>,
    },
    Tests {
        tasks: Option<PathBuf>,
    },
    Tasks {
        tasks: PathBuf,
        tests: Option<PathBuf>,
    },
}

impl SearchScope {
    pub const TASKS_DIRECTORY: &'static str = "tasks";
    pub const TESTS_DIRECTORY: &'static str = "tests";
    const TEST_TREE: &'static str = "test";

    pub fn in_project(tasks: &Path) -> Self {
        Self::InProject {
            tasks: Self::canonical(tasks),
        }
    }

    pub fn tests(tasks: &Path) -> Self {
        Self::Tests {
            tasks: Self::canonical(tasks),
        }
    }

    pub fn tasks(tasks: &Path, tests: &Path) -> Self {
        Self::Tasks {
            tasks: Self::canonical(tasks).unwrap_or_else(|| tasks.to_path_buf()),
            tests: Self::canonical(tests),
        }
    }

    pub fn primary_reaches(&self, path: &Path) -> bool {
        match self {
            Self::Tasks { tests, .. } => !Self::lies_under(path, tests.as_deref()),
            _ => true,
        }
    }

    pub fn fallback_reaches(&self, path: &Path) -> bool {
        match self {
            Self::Tasks { tasks, .. } => {
                self.primary_reaches(path) && !Self::lies_under(path, Some(tasks))
            }
            _ => true,
        }
    }

    pub fn holds_task(&self, file: &Path) -> bool {
        Self::lies_under(file, self.tasks_directory())
    }

    pub fn directory_name(&self) -> &'static str {
        match self {
            Self::Tasks { .. } => Self::TASKS_DIRECTORY,
            _ => Self::TEST_TREE,
        }
    }

    fn tasks_directory(&self) -> Option<&Path> {
        match self {
            Self::Program => None,
            Self::InProject { tasks } | Self::Tests { tasks } => tasks.as_deref(),
            Self::Tasks { tasks, .. } => Some(tasks),
        }
    }

    fn lies_under(path: &Path, directory: Option<&Path>) -> bool {
        let Some(directory) = directory else {
            return false;
        };
        fs::canonicalize(path).is_ok_and(|path| path.starts_with(directory))
    }

    fn canonical(directory: &Path) -> Option<PathBuf> {
        fs::canonicalize(directory).ok()
    }
}