tree-type 0.4.5

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

use assert2::assert;
use assert2::check;
use assert2::let_assert;
use tempfile::TempDir;

use tree_type::tree_type;

tree_type! {
    ProjectRoot {
        src/,
        target/,
        config/ {
            local/,
            prod/
        }
    }
}

#[test]
fn test_setup_creates_all_directories() {
    let_assert!(Ok(tempdir) = TempDir::new());
    let project = ProjectRoot::new(tempdir.path()).unwrap();

    let_assert!(Ok(_errors) = project.setup());

    check!(project.exists());
    check!(project.src().exists());
    check!(project.target().exists());
    check!(project.config().exists());
    check!(project.config().local().exists());
    check!(project.config().prod().exists());
}

tree_type! {
    IssuesDir {
        [id: String]/ as IssueDir {
            attachments/,
            comments/,
        }
    }
}

// TODO: setup() doesn't recurse into existing dynamic ID instances
// #[test]
// fn test_setup_with_dynamic_ids() {
//     let_assert!(Ok(tempdir) = TempDir::new());
//     let issues = IssuesDir::new(tempdir.path()).unwrap();
//
//     let issue1 = issues.id("1".to_string());
//     let issue2 = issues.id("2".to_string());
//     let_assert!(Ok(()) = issue1.create());
//     let_assert!(Ok(()) = issue2.create());
//
//     check!(!issue1.attachments().exists());
//     check!(!issue1.comments().exists());
//     check!(!issue2.attachments().exists());
//     check!(!issue2.comments().exists());
//
//     let_assert!(Ok(_errors) = issues.setup());
//
//     check!(issue1.attachments().exists());
//     check!(issue1.comments().exists());
//     check!(issue2.attachments().exists());
//     check!(issue2.comments().exists());
// }

#[test]
fn test_setup_idempotent() {
    let_assert!(Ok(tempdir) = TempDir::new());
    let project = ProjectRoot::new(tempdir.path()).unwrap();

    let_assert!(Ok(_errors) = project.setup());
    check!(project.exists());

    let_assert!(Ok(_errors) = project.setup());
    check!(project.exists());
}

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

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

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

#[test]
fn test_setup_creates_files_with_defaults() {
    let_assert!(Ok(tempdir) = TempDir::new());
    let project = ProjectWithDefaults::new(tempdir.path()).unwrap();

    let_assert!(Ok(_errors) = project.setup());

    check!(project.src().exists());
    check!(project.readme().exists());
    check!(project.config().exists());

    let_assert!(Ok(readme_content) = std::fs::read_to_string(project.readme().as_path()));
    assert!(readme_content.starts_with("# Project at"));

    let_assert!(Ok(config_content) = std::fs::read_to_string(project.config().as_path()));
    assert!(config_content == "# Config\ndefault = true\n");
}

#[test]
fn test_setup_skips_existing_files() {
    let_assert!(Ok(tempdir) = TempDir::new());
    let project = ProjectWithDefaults::new(tempdir.path()).unwrap();

    let_assert!(Ok(()) = std::fs::create_dir_all(tempdir.path()));
    let_assert!(Ok(()) = std::fs::write(project.readme().as_path(), "# Custom\n"));

    let_assert!(Ok(_errors) = project.setup());

    let_assert!(Ok(readme_content) = std::fs::read_to_string(project.readme().as_path()));
    assert!(readme_content == "# Custom\n");

    check!(project.config().exists());
}

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

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

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

#[test]
fn test_setup_collects_errors() {
    let_assert!(Ok(tempdir) = TempDir::new());
    let project = ProjectWithErrors::new(tempdir.path()).unwrap();

    let_assert!(Err(errors) = project.setup());

    assert!(errors.len() == 1);

    let_assert!(tree_type::BuildError::File(path, _) = &errors[0]);
    assert!(path == &project.failing_file().as_path());

    check!(project.src().exists());
    check!(project.working_file().exists());
    check!(!project.failing_file().exists());
}