tree-type 0.4.5

Rust macros for creating type-safe filesystem tree structures
Documentation
#![allow(deprecated)]
#![allow(dead_code)]

use std::fs;
use tempfile::TempDir;
use tree_type::tree_type;

tree_type! {
    TestRoot {
        #[required]
        #[default(default_config)]
        config("config.toml"),
        #[required]
        data/ {
            #[optional]
            logs/
        }
    }
}

fn default_config(_: &TestRootConfig) -> Result<String, std::io::Error> {
    Ok("# Default configuration\nversion = 1".to_string())
}

tree_type! {
    ProjectWithDefaults {
        src/,
        #[default(default_readme)]
        readme("README.md"),
        #[default(default_config_file)]
        config("config.toml"),
    }
}

fn default_readme(file: &ProjectWithDefaultsReadme) -> Result<String, std::io::Error> {
    Ok(format!(
        "# Project at {}\n\nDefault readme content\n",
        file.as_path().display()
    ))
}

fn default_config_file(_file: &ProjectWithDefaultsConfig) -> Result<String, std::io::Error> {
    Ok("# Config\ndefault = true\n".to_string())
}

tree_type! {
    ProjectWithErrors {
        src/,
        #[default(failing_default)]
        failing_file("fail.txt"),
        #[default(working_default)]
        working_file("work.txt"),
    }
}

fn failing_default(_file: &ProjectWithErrorsFailingFile) -> Result<String, std::io::Error> {
    Err(std::io::Error::other("intentional error"))
}

fn working_default(_file: &ProjectWithErrorsWorkingFile) -> Result<String, std::io::Error> {
    Ok("working content\n".to_string())
}

tree_type! {
    DynamicProject {
        [id: String]/ as ProjectDir {
            #[required]
            src/,
            #[default(project_config)]
            config("config.toml")
        }
    }
}

fn project_config(file: &ProjectDirConfig) -> Result<String, std::io::Error> {
    Ok(format!(
        "# Config for project\npath = \"{}\"\n",
        file.as_path().display()
    ))
}

tree_type! {
    ComplexAttributes {
        #[required]
        required_dir/,
        #[optional]
        optional_dir/,
        #[required]
        #[default(complex_default)]
        required_with_default("file.txt"),
        #[optional]
        #[default(optional_default)]
        optional_with_default("optional.txt")
    }
}

fn complex_default(_: &ComplexAttributesRequiredWithDefault) -> Result<String, std::io::Error> {
    Ok("required default content".to_string())
}

fn optional_default(_: &ComplexAttributesOptionalWithDefault) -> Result<String, std::io::Error> {
    Ok("optional default content".to_string())
}

#[test]
fn test_sync_method_exists_and_works() {
    let temp_dir = TempDir::new().unwrap();
    let root = TestRoot::new(temp_dir.path()).unwrap();

    // Test that sync method exists and can be called
    let result = root.sync();
    assert!(result.is_ok(), "sync() should succeed: {:?}", result);

    // Verify structure was created
    assert!(root.exists(), "Root directory should exist");
    assert!(root.config().exists(), "Config file should be created");
    assert!(root.data().exists(), "Data directory should be created");

    // Verify config has default content
    let config_content = std::fs::read_to_string(root.config().as_path()).unwrap();
    assert!(config_content.contains("Default configuration"));
}

#[test]
fn test_deprecated_methods_still_work() {
    let temp_dir = TempDir::new().unwrap();
    let root = TestRoot::new(temp_dir.path()).unwrap();

    // Test that deprecated methods still work
    let sync_result_1 = root.sync();
    assert!(sync_result_1.is_ok(), "sync() should work");

    let sync_result = root.sync();
    assert!(sync_result.is_ok(), "sync() should work");
}

#[test]
fn test_sync_creates_files_with_defaults() {
    let temp_dir = TempDir::new().unwrap();
    let project = ProjectWithDefaults::new(temp_dir.path()).unwrap();

    let result = project.sync();
    assert!(result.is_ok(), "sync() should succeed: {:?}", result);

    assert!(project.src().exists(), "src/ should be created");
    assert!(project.readme().exists(), "README.md should be created");
    assert!(project.config().exists(), "config.toml should be created");

    let readme_content = fs::read_to_string(project.readme().as_path()).unwrap();
    assert!(readme_content.starts_with("# Project at"));

    let config_content = fs::read_to_string(project.config().as_path()).unwrap();
    assert_eq!(config_content, "# Config\ndefault = true\n");
}

#[test]
fn test_sync_skips_existing_files() {
    let temp_dir = TempDir::new().unwrap();
    let project = ProjectWithDefaults::new(temp_dir.path()).unwrap();

    // Create directory and pre-existing file
    fs::create_dir_all(temp_dir.path()).unwrap();
    fs::write(project.readme().as_path(), "# Custom\n").unwrap();

    let result = project.sync();
    assert!(result.is_ok(), "sync() should succeed: {:?}", result);

    // Verify existing file wasn't overwritten
    let readme_content = fs::read_to_string(project.readme().as_path()).unwrap();
    assert_eq!(readme_content, "# Custom\n");

    // Verify other files were created
    assert!(project.config().exists(), "config.toml should be created");
}

#[test]
fn test_sync_idempotent() {
    let temp_dir = TempDir::new().unwrap();
    let project = ProjectWithDefaults::new(temp_dir.path()).unwrap();

    // First sync
    let result1 = project.sync();
    assert!(result1.is_ok(), "First sync() should succeed");
    assert!(project.exists(), "Project should exist after first sync");

    // Second sync should also succeed
    let result2 = project.sync();
    assert!(result2.is_ok(), "Second sync() should succeed");
    assert!(
        project.exists(),
        "Project should still exist after second sync"
    );
}

#[test]
fn test_sync_collects_errors() {
    let temp_dir = TempDir::new().unwrap();
    let project = ProjectWithErrors::new(temp_dir.path()).unwrap();

    let result = project.sync();
    assert!(
        result.is_err(),
        "sync() should fail due to error in default function"
    );

    let errors = result.unwrap_err();
    assert_eq!(errors.len(), 1, "Should have exactly one error");

    // Verify partial success - working file should be created
    assert!(
        project.src().exists(),
        "src/ should be created despite error"
    );
    assert!(
        project.working_file().exists(),
        "working_file should be created"
    );
    assert!(
        !project.failing_file().exists(),
        "failing_file should not be created"
    );
}

#[test]
fn test_sync_with_dynamic_ids() {
    let temp_dir = TempDir::new().unwrap();
    let projects = DynamicProject::new(temp_dir.path()).unwrap();

    // Create some dynamic ID instances
    let project1 = projects.id("project1".to_string());
    let project2 = projects.id("project2".to_string());

    project1.create().unwrap();
    project2.create().unwrap();

    // Sync should work on dynamic instances
    let result1 = project1.sync();
    assert!(result1.is_ok(), "sync() should work on dynamic ID instance");

    assert!(project1.src().exists(), "project1 src/ should be created");
    assert!(
        project1.config().exists(),
        "project1 config should be created"
    );

    let result2 = project2.sync();
    assert!(
        result2.is_ok(),
        "sync() should work on second dynamic ID instance"
    );

    assert!(project2.src().exists(), "project2 src/ should be created");
    assert!(
        project2.config().exists(),
        "project2 config should be created"
    );
}

#[test]
fn test_sync_complex_attribute_combinations() {
    let temp_dir = TempDir::new().unwrap();
    let complex = ComplexAttributes::new(temp_dir.path()).unwrap();

    let result = complex.sync();
    assert!(
        result.is_ok(),
        "sync() should handle complex attributes: {:?}",
        result
    );

    // Required items should be created
    assert!(
        complex.required_dir().exists(),
        "required_dir/ should be created"
    );
    assert!(
        complex.required_with_default().exists(),
        "required_with_default should be created"
    );

    // Optional items with defaults should be created
    assert!(
        complex.optional_with_default().exists(),
        "optional_with_default should be created"
    );

    // Verify default content
    let required_content = fs::read_to_string(complex.required_with_default().as_path()).unwrap();
    assert_eq!(required_content, "required default content");

    let optional_content = fs::read_to_string(complex.optional_with_default().as_path()).unwrap();
    assert_eq!(optional_content, "optional default content");
}

#[test]
fn test_sync_validation_integration() {
    let temp_dir = TempDir::new().unwrap();
    let root = TestRoot::new(temp_dir.path()).unwrap();

    // Sync should create structure
    let result = root.sync();
    assert!(result.is_ok(), "sync() should succeed");
}

#[test]
fn test_sync_recreates_missing_required_items() {
    let temp_dir = TempDir::new().unwrap();
    let root = TestRoot::new(temp_dir.path()).unwrap();

    // Initial sync
    root.sync().unwrap();
    assert!(root.data().exists(), "data/ should exist after sync");

    // Remove required directory
    fs::remove_dir_all(root.data().as_path()).unwrap();
    assert!(!root.data().exists(), "data/ should be removed");

    // Sync should recreate it
    let result = root.sync();
    assert!(
        result.is_ok(),
        "sync() should recreate missing required items"
    );
    assert!(root.data().exists(), "data/ should be recreated by sync");
}

#[test]
fn test_sync_nested_structures() {
    let temp_dir = TempDir::new().unwrap();
    let root = TestRoot::new(temp_dir.path()).unwrap();

    let result = root.sync();
    assert!(result.is_ok(), "sync() should handle nested structures");

    // Verify nested structure creation
    assert!(root.exists(), "Root should exist");
    assert!(root.data().exists(), "data/ should exist");

    // Optional nested items should not be created by default
    // (This tests the delegation to existing setup/ensure behavior)
    let _logs_exists = root.data().logs().exists();
    // Note: This depends on current setup() behavior - may create or not create optional items
}