fsvalidator 0.3.0

A file structure validator
Documentation
use anyhow::Result;
use fsvalidator::ModelBuilder;

use std::fs;

#[test]
fn test_builder_file_creation() -> Result<()> {
    // Create a test file
    let test_dir = tempfile::tempdir()?;
    let file_path = test_dir.path().join("test_file.txt");
    fs::write(&file_path, "test content")?;

    // Create a file node using builder
    let file_node = ModelBuilder::new_file("test_file.txt")
        .required(true)
        .build();

    // Test basic validation
    assert!(file_node.validate(&file_path).is_ok());

    Ok(())
}

#[test]
fn test_builder_dir_creation() -> Result<()> {
    // Create a test directory
    let test_dir = tempfile::tempdir()?;
    let sub_dir = test_dir.path().join("sub_dir");
    fs::create_dir(&sub_dir)?;

    // Create a dir node using builder
    let dir_node = ModelBuilder::new_dir("sub_dir")
        .required(true)
        .allow_defined_only(false)
        .build();

    // Test validation with direct path to directory
    assert!(dir_node.validate(&sub_dir).is_ok());

    Ok(())
}

#[test]
fn test_builder_dir_with_children() -> Result<()> {
    // Create a test directory structure
    let test_dir = tempfile::tempdir()?;
    let sub_dir = test_dir.path().join("sub_dir");
    fs::create_dir(&sub_dir)?;

    let file_in_sub_dir = sub_dir.join("test_file.txt");
    fs::write(&file_in_sub_dir, "test content")?;

    // Create a nested directory structure using builder
    let dir_node = ModelBuilder::new_dir("sub_dir")
        .required(true)
        .add_file("test_file.txt", true)
        .build();

    // Test validation with direct path to directory
    assert!(dir_node.validate(&sub_dir).is_ok());

    Ok(())
}

#[test]
fn test_builder_complex_structure() -> Result<()> {
    // Create a complex test directory structure
    let test_dir = tempfile::tempdir()?;
    let project_dir = test_dir.path().join("project");
    fs::create_dir(&project_dir)?;
    
    // Create README.md
    fs::write(project_dir.join("README.md"), "# Project")?;
    
    // Create src directory with Rust files
    let src_dir = project_dir.join("src");
    fs::create_dir(&src_dir)?;
    fs::write(src_dir.join("main.rs"), "fn main() {}")?;
    fs::write(src_dir.join("lib.rs"), "pub fn hello() -> &'static str { \"Hello\" }")?;
    
    // Create tests directory with test files
    let tests_dir = project_dir.join("tests");
    fs::create_dir(&tests_dir)?;
    fs::write(tests_dir.join("test_main.rs"), "#[test] fn test() {}")?;

    // Build a complex nested structure with the builder
    let project = ModelBuilder::new_dir("project")
        .required(true)
        .allow_defined_only(true)
        .add_file("README.md", true)
        .add_dir("src", true)
            .add_file_pattern(".*\\.rs", true)
            .up()
        .add_dir("tests", true)
            .add_file_pattern("test_.*\\.rs", true)
            .up()
        .build();

    // Test the complex validation
    let result = project.validate(&project_dir);
    assert!(result.is_ok());

    Ok(())
}

#[test]
fn test_builder_nested_dirs_multiple_levels() -> Result<()> {
    // Create deep nested directory structure
    let test_dir = tempfile::tempdir()?;
    let root_dir = test_dir.path().join("root");
    fs::create_dir(&root_dir)?;
    
    let level1 = root_dir.join("level1");
    fs::create_dir(&level1)?;
    
    let level2 = level1.join("level2");
    fs::create_dir(&level2)?;
    
    let level3 = level2.join("level3");
    fs::create_dir(&level3)?;
    
    fs::write(level3.join("file.txt"), "test content")?;

    // Build a deep nested structure with the builder
    let root = ModelBuilder::new_dir("root")
        .required(true)
        .add_dir("level1", true)
            .add_dir("level2", true)
                .add_dir("level3", true)
                    .add_file("file.txt", true)
                    .up()
                .up()
            .up()
        .build();

    // Test the deep nested validation
    let result = root.validate(&root_dir);
    assert!(result.is_ok());

    Ok(())
}

#[test]
fn test_builder_pattern_matching() -> Result<()> {
    // Create a test directory structure with pattern-matching files
    let test_dir = tempfile::tempdir()?;
    let config_dir = test_dir.path().join("config");
    fs::create_dir(&config_dir)?;
    
    fs::write(config_dir.join("settings.json"), "{}")?;
    fs::write(config_dir.join("config.json"), "{}")?;
    fs::write(config_dir.join("user.yaml"), "user: test")?;

    // Build a structure expecting specific patterns
    let config = ModelBuilder::new_dir("config")
        .required(true)
        .add_file_pattern(".*\\.json", true)  // Require at least one JSON file
        .add_file_pattern(".*\\.yaml", false) // YAML files are optional
        .build();

    // Test the pattern matching validation
    let result = config.validate(&config_dir);
    assert!(result.is_ok());

    Ok(())
}

#[test]
fn test_builder_with_excluded_patterns() -> Result<()> {
    // Create a test directory with some files to exclude
    let test_dir = tempfile::tempdir()?;
    let data_dir = test_dir.path().join("data");
    fs::create_dir(&data_dir)?;
    
    fs::write(data_dir.join("data.csv"), "a,b,c")?;
    fs::write(data_dir.join(".hidden"), "hidden file")?;
    fs::write(data_dir.join(".DS_Store"), "mac file")?;

    // Create vectors of patterns to exclude hidden files

    // Build a structure that excludes hidden files
    let data = ModelBuilder::new_dir("data")
        .required(true)
        .allow_defined_only(true)
        .exclude_patterns(vec!["^\\..*", "\\.DS_Store"])
        .add_file_pattern(".*\\.csv", true)
        .build();

    // Test the validation with exclusions
    let result = data.validate(&data_dir);
    assert!(result.is_ok());

    Ok(())
}

#[test]
fn test_builder_validation_failures() -> Result<()> {
    // Create a test directory with missing required file
    let test_dir = tempfile::tempdir()?;
    let docs_dir = test_dir.path().join("docs");
    fs::create_dir(&docs_dir)?;
    
    // Don't create the required README.md

    // Build a structure that requires README.md
    let docs = ModelBuilder::new_dir("docs")
        .required(true)
        .add_file("README.md", true)
        .build();

    // Test the validation - should fail
    let result = docs.validate(&docs_dir);
    assert!(result.is_err());
    
    // Create the missing file and validate again
    fs::write(docs_dir.join("README.md"), "# Docs")?;
    let result = docs.validate(&docs_dir);
    assert!(result.is_ok());

    Ok(())
}

#[test]
fn test_builder_only_defined_children() -> Result<()> {
    // Create a test directory with an unexpected file
    let test_dir = tempfile::tempdir()?;
    let conf_dir = test_dir.path().join("conf");
    fs::create_dir(&conf_dir)?;
    
    fs::write(conf_dir.join("allowed.conf"), "allowed config")?;
    fs::write(conf_dir.join("unexpected.txt"), "unexpected file")?;

    // Build a structure that only allows defined children
    let conf = ModelBuilder::new_dir("conf")
        .required(true)
        .allow_defined_only(true)
        .add_file("allowed.conf", true)
        .build();

    // Test the validation - should fail due to unexpected file
    let result = conf.validate(&conf_dir);
    assert!(result.is_err());
    
    // Remove the unexpected file and validate again
    fs::remove_file(conf_dir.join("unexpected.txt"))?;
    let result = conf.validate(&conf_dir);
    assert!(result.is_ok());

    Ok(())
}