archaven 1.0.0

A small Rust dependency rule checker for modular architectures.
Documentation
use archaven::{Access, Archaven, ArchavenError, DependencyGraph, Rule, RuleSet, Violations};
use std::fs;

#[test]
fn check_scans_rust_files_and_returns_printable_violations() {
    let temp = tempfile::tempdir().unwrap();
    let src = temp.path().join("src");

    fs::create_dir_all(src.join("app/sales/orders/domain")).unwrap();
    fs::create_dir_all(src.join("app/sales/orders/infrastructure/adapter")).unwrap();

    fs::write(
        src.join("app/sales/orders/domain/order.rs"),
        r"
            use crate::app::billing::invoices::application::command::issue_invoice::IssueInvoice;

            pub fn place_order(command: IssueInvoice) {
                let _ = command;
            }
        ",
    )
    .unwrap();

    fs::write(
        src.join("app/sales/orders/infrastructure/adapter/billing_client.rs"),
        r"
            pub fn call_billing() {
                crate::app::billing::invoices::application::query::invoice_status::get_status();
            }
        ",
    )
    .unwrap();

    let violations = Archaven::new()
        .rule(
            Rule::between("app::*")
                .named("bounded contexts")
                .deny_all()
                .allow(
                    Access::from("*::infrastructure::adapter::**")
                        .to_any([
                            "*::application::command::**",
                            "*::application::query::**",
                        ])
                        .because("bounded contexts may communicate only through adapters calling command/query APIs"),
                ),
        )
        .check(&src)
        .unwrap();

    assert_eq!(violations.len(), 1);

    let formatted = violations.to_string();
    assert!(formatted.contains("bounded contexts"));
    assert!(formatted.contains("src/app/sales/orders/domain/order.rs"));
    assert!(formatted.contains("adapters calling command/query APIs"));
}

#[test]
fn check_can_ignore_module_composition_roots_by_file_glob() {
    let temp = tempfile::tempdir().unwrap();
    let src = temp.path().join("src");

    fs::create_dir_all(src.join("app/sales/orders/application/command")).unwrap();
    fs::create_dir_all(src.join("app/sales/orders/infrastructure/repository")).unwrap();

    fs::write(
        src.join("app/sales/orders/mod.rs"),
        r"
            mod application;
            mod infrastructure;

            pub fn wire_module() {
                crate::app::sales::orders::infrastructure::repository::sql_order_repository::new();
            }
        ",
    )
    .unwrap();

    fs::write(
        src.join("app/sales/orders/application/command/create_order.rs"),
        r"
            pub fn create_order() {
                crate::app::sales::orders::infrastructure::repository::sql_order_repository::new();
            }
        ",
    )
    .unwrap();

    fs::write(
        src.join("app/sales/orders/infrastructure/repository/sql_order_repository.rs"),
        r"
            pub fn new() {}
        ",
    )
    .unwrap();

    let violations = Archaven::new()
        .rule(
            Rule::within("app::*::*")
                .named("module internals")
                .deny_all()
                .ignore_files(["**/mod.rs"])
                .allow(Access::from("application::**").to("domain::**")),
        )
        .check(&src)
        .unwrap();

    assert_eq!(violations.len(), 1);

    let formatted = violations.to_string();
    assert!(formatted.contains("application/command/create_order.rs"));
    assert!(!formatted.contains("app/sales/orders/mod.rs"));
}

#[test]
fn check_can_allow_only_module_roots_in_matching_directories() {
    let temp = tempfile::tempdir().unwrap();
    let src = temp.path().join("src");

    fs::create_dir_all(src.join("app/sales/orders/application")).unwrap();
    fs::create_dir_all(src.join("app/sales/orders/domain")).unwrap();
    fs::create_dir_all(src.join("app/billing/invoices")).unwrap();

    fs::write(src.join("app/sales/orders/mod.rs"), "").unwrap();
    fs::write(src.join("app/sales/orders/application.rs"), "").unwrap();
    fs::write(src.join("app/sales/orders/domain.rs"), "").unwrap();
    fs::write(src.join("app/sales/orders/helper.rs"), "").unwrap();
    fs::write(src.join("app/billing/invoices.rs"), "").unwrap();

    let violations = Archaven::new()
        .rule(
            Rule::directories("app::*::*")
                .named("module root files")
                .allow_only_module_roots(),
        )
        .check(&src)
        .unwrap();

    assert_eq!(violations.len(), 1);

    let formatted = violations.to_string();
    assert!(formatted.contains("module root files"));
    assert!(formatted.contains("app/sales/orders/helper.rs"));
    assert!(formatted.contains("only module root files are allowed"));
}

#[test]
fn directory_rule_rejects_patterns_without_single_wildcards() {
    let graph = archaven::DependencyGraph::new();

    let violations = Rule::directories("app::*::*")
        .allow_only_module_roots()
        .check(&graph)
        .unwrap();

    assert!(violations.is_empty());

    let error = Rule::directories("app::**")
        .allow_only_module_roots()
        .check(&graph)
        .unwrap_err();

    assert!(error
        .to_string()
        .contains("directory rules do not support `**`"));

    let error = Rule::directories("app::sales")
        .allow_only_module_roots()
        .check(&graph)
        .unwrap_err();

    assert!(error
        .to_string()
        .contains("directory rules support at least one `*` segment"));
}

#[test]
fn check_can_ignore_module_roots_in_dependency_rules() {
    let temp = tempfile::tempdir().unwrap();
    let src = temp.path().join("src");

    fs::create_dir_all(src.join("app/sales/orders/application/command")).unwrap();
    fs::create_dir_all(src.join("app/sales/orders/infrastructure/repository")).unwrap();
    fs::create_dir_all(src.join("app/sales/orders/infrastructure")).unwrap();

    fs::write(
        src.join("app/sales/orders/mod.rs"),
        r"
            pub fn wire_mod() {
                crate::app::sales::orders::infrastructure::repository::sql_order_repository::new();
            }
        ",
    )
    .unwrap();

    fs::write(
        src.join("app/sales/orders/infrastructure.rs"),
        r"
            pub fn wire_new_style_root() {
                crate::app::sales::orders::infrastructure::repository::sql_order_repository::new();
            }
        ",
    )
    .unwrap();

    fs::write(
        src.join("app/sales/orders/application/command/create_order.rs"),
        r"
            pub fn create_order() {
                crate::app::sales::orders::infrastructure::repository::sql_order_repository::new();
            }
        ",
    )
    .unwrap();

    fs::write(
        src.join("app/sales/orders/infrastructure/repository/sql_order_repository.rs"),
        r"
            pub fn new() {}
        ",
    )
    .unwrap();

    let violations = Archaven::new()
        .rule(
            Rule::within("app::*::*")
                .named("module internals")
                .deny_all()
                .ignore_module_roots()
                .allow(Access::from("application::**").to("domain::**")),
        )
        .check(&src)
        .unwrap();

    assert_eq!(violations.len(), 1);

    let formatted = violations.to_string();
    assert!(formatted.contains("application/command/create_order.rs"));
    assert!(!formatted.contains("app/sales/orders/mod.rs"));
    assert!(!formatted.contains("app/sales/orders/infrastructure.rs"));
}

#[test]
fn custom_rule_sets_can_read_discovered_directories() {
    struct NoLooseHelperFiles;

    impl RuleSet for NoLooseHelperFiles {
        fn check(&self, graph: &DependencyGraph) -> Result<Violations, ArchavenError> {
            let has_helper = graph.directories().iter().any(|directory| {
                directory.module().to_string() == "app::orders"
                    && directory.files().iter().any(|file| file == "helper.rs")
                    && directory
                        .child_directories()
                        .iter()
                        .any(|directory| directory == "domain")
            });

            assert!(has_helper);
            Ok(Violations::new())
        }
    }

    let temp = tempfile::tempdir().unwrap();
    let src = temp.path().join("src");

    fs::create_dir_all(src.join("app/orders/domain")).unwrap();
    fs::write(src.join("app/orders/helper.rs"), "").unwrap();

    let violations = Archaven::new()
        .rule(NoLooseHelperFiles)
        .check(&src)
        .unwrap();

    assert!(violations.is_empty());
}