scraps 1.0.1

Scraps is a portable CLI knowledge hub for managing interconnected Markdown documentation with Wiki-link notation.
use crate::cli::config::scrap_config::ScrapConfig;
use crate::constants::CONFIG_FILE_NAME;
use crate::error::ScrapsResult;
use anyhow::anyhow;
use std::env;
use std::path::{Path, PathBuf};

/// Resolves project paths for scraps commands
pub struct PathResolver {
    project_root: PathBuf,
}

impl PathResolver {
    /// Create a new PathResolver
    /// If project_path is None, uses current directory
    pub fn new(project_path: Option<&Path>) -> ScrapsResult<Self> {
        let project_root = match project_path {
            Some(path) => {
                let absolute_path = if path.is_absolute() {
                    path.to_path_buf()
                } else {
                    env::current_dir()?.join(path)
                };

                if !absolute_path.exists() {
                    return Err(anyhow!(
                        "Project directory does not exist: {}",
                        absolute_path.display()
                    ));
                }

                if !absolute_path.is_dir() {
                    return Err(anyhow!(
                        "Path is not a directory: {}",
                        absolute_path.display()
                    ));
                }

                absolute_path
            }
            None => env::current_dir()?,
        };

        Ok(PathResolver { project_root })
    }

    /// Get the scraps directory path.
    ///
    /// In v1 the directory containing `.scraps.toml` IS the wiki root, so this
    /// is simply the project root. The method is retained as the canonical
    /// entry point for callers that read scraps.
    pub fn scraps_dir(&self) -> PathBuf {
        self.project_root.clone()
    }

    /// Get the static directory path
    pub fn static_dir(&self) -> PathBuf {
        self.project_root.join("static")
    }

    /// Get the build output directory path
    pub fn output_dir(&self, config: &ScrapConfig) -> PathBuf {
        match &config.output_dir {
            Some(dir) => self.project_root.join(dir),
            None => self.project_root.join("_site"),
        }
    }

    /// Get the config file path
    pub fn config_path(&self) -> PathBuf {
        self.project_root.join(CONFIG_FILE_NAME)
    }

    /// Get the project root directory
    pub fn project_root(&self) -> &Path {
        &self.project_root
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_fixtures::{simple_temp_dir, SimpleTempDir};
    use rstest::rstest;
    use std::env;

    #[test]
    fn test_new_with_current_directory() {
        let resolver = PathResolver::new(None).unwrap();

        let expected_root = env::current_dir().unwrap();
        assert_eq!(resolver.project_root(), expected_root.as_path());
    }

    #[rstest]
    fn test_new_with_specified_path(#[from(simple_temp_dir)] temp_dir: SimpleTempDir) {
        temp_dir.add_dir("test_project_new");

        let test_project_path = temp_dir.path.join("test_project_new");
        let resolver = PathResolver::new(Some(&test_project_path)).unwrap();
        assert_eq!(
            resolver.project_root().file_name().unwrap(),
            "test_project_new"
        );
    }

    #[test]
    fn test_new_with_nonexistent_path() {
        let nonexistent_path = PathBuf::from("nonexistent_directory");
        let result = PathResolver::new(Some(&nonexistent_path));
        assert!(result.is_err());
    }

    #[rstest]
    fn test_scraps_dir_returns_project_root(#[from(simple_temp_dir)] temp_dir: SimpleTempDir) {
        temp_dir.add_dir("test_project_scraps");

        let test_project_path = temp_dir.path.join("test_project_scraps");
        let resolver = PathResolver::new(Some(&test_project_path)).unwrap();

        let scraps_dir = resolver.scraps_dir();
        assert_eq!(scraps_dir, test_project_path);
    }

    #[rstest]
    fn test_static_dir_path(#[from(simple_temp_dir)] temp_dir: SimpleTempDir) {
        temp_dir.add_dir("test_project_static");

        let test_project_path = temp_dir.path.join("test_project_static");
        let resolver = PathResolver::new(Some(&test_project_path)).unwrap();
        let static_dir = resolver.static_dir();
        assert_eq!(static_dir.file_name().unwrap(), "static");
        assert!(static_dir.starts_with(&test_project_path));
    }

    #[rstest]
    fn test_output_dir_path_default(#[from(simple_temp_dir)] temp_dir: SimpleTempDir) {
        temp_dir.add_dir("test_project_output").add_file(
            "test_project_output/.scraps.toml",
            br#"
[ssg]
title = "Test"
base_url = "http://example.com/"
"#,
        );

        let test_project_path = temp_dir.path.join("test_project_output");
        let resolver = PathResolver::new(Some(&test_project_path)).unwrap();
        let config = ScrapConfig::from_path(Some(&test_project_path)).unwrap();

        let output_dir = resolver.output_dir(&config);
        assert_eq!(output_dir.file_name().unwrap(), "_site");
        assert!(output_dir.starts_with(&test_project_path));
    }

    #[rstest]
    fn test_output_dir_path_custom(#[from(simple_temp_dir)] temp_dir: SimpleTempDir) {
        temp_dir.add_dir("test_project_output_custom").add_file(
            "test_project_output_custom/.scraps.toml",
            br#"
output_dir = "dist"

[ssg]
title = "Test"
base_url = "http://example.com/"
"#,
        );

        let test_project_path = temp_dir.path.join("test_project_output_custom");
        let resolver = PathResolver::new(Some(&test_project_path)).unwrap();
        let config = ScrapConfig::from_path(Some(&test_project_path)).unwrap();

        let output_dir = resolver.output_dir(&config);
        assert_eq!(output_dir.file_name().unwrap(), "dist");
        assert!(output_dir.starts_with(&test_project_path));
    }

    #[rstest]
    fn test_config_path(#[from(simple_temp_dir)] temp_dir: SimpleTempDir) {
        temp_dir.add_dir("test_project_config");

        let test_project_path = temp_dir.path.join("test_project_config");
        let resolver = PathResolver::new(Some(&test_project_path)).unwrap();
        let config_path = resolver.config_path();
        assert_eq!(config_path.file_name().unwrap(), CONFIG_FILE_NAME);
        assert!(config_path.starts_with(&test_project_path));
    }
}