archunit/
checkable.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//! The execution contract shared by every terminal architecture rule.

use crate::common::CheckLogger;
use crate::common::error::ArchUnitError;
use crate::common::fluentapi::CheckOptions;
use crate::violation::Violation;

/// The complete outcome of running one architecture rule.
///
/// `Ok(Vec::new())` passes, `Ok` with violations is an architecture disagreement, and `Err` means
/// no verdict could be reached.
pub type CheckResult = Result<Vec<Violation>, ArchUnitError>;

pub(crate) fn execute_logged_check<Operation>(
    rule_name: &'static str,
    options: &CheckOptions,
    operation: Operation,
) -> CheckResult
where
    Operation: FnOnce(&CheckLogger<'_>) -> CheckResult,
{
    let logger = CheckLogger::new(options.logging());
    logger.validate()?;
    logger.start_check(rule_name)?;
    let result = operation(&logger);
    match result {
        Ok(violations) => {
            for violation in &violations {
                logger.log_violation(violation.kind().as_str())?;
            }
            logger.end_check(rule_name, violations.len())?;
            Ok(violations)
        }
        Err(error) => {
            let _ = logger.error(format!("{rule_name}: {error}"));
            Err(error)
        }
    }
}

/// A terminal architecture rule that can judge a project.
///
/// Building a fluent rule is lazy; only these methods may touch the filesystem. Test helpers and
/// report consumers depend on this trait rather than on individual rule families.
pub trait Checkable {
    /// Runs the rule with the strict, quiet defaults from [`CheckOptions`].
    fn check(&self) -> CheckResult {
        self.check_with(&CheckOptions::default())
    }

    /// Runs the rule with an explicit immutable options bag.
    fn check_with(&self, options: &CheckOptions) -> CheckResult;
}

impl<T> Checkable for &T
where
    T: Checkable + ?Sized,
{
    fn check_with(&self, options: &CheckOptions) -> CheckResult {
        (**self).check_with(options)
    }
}

#[cfg(test)]
mod tests {
    use super::{CheckResult, Checkable};
    use crate::common::{ArchUnitError, CheckOptions, TechnicalError};

    struct OptionEchoRule;

    impl Checkable for OptionEchoRule {
        fn check_with(&self, options: &CheckOptions) -> CheckResult {
            if options.clears_cache() {
                Err(ArchUnitError::from(TechnicalError::new(
                    "fixture cache clear failed",
                )))
            } else {
                Ok(Vec::new())
            }
        }
    }

    fn run_default(rule: &dyn Checkable) -> CheckResult {
        rule.check()
    }

    #[test]
    fn default_check_uses_defaults_through_an_object_safe_contract() {
        let result = run_default(&OptionEchoRule);

        assert!(matches!(result, Ok(violations) if violations.is_empty()));
    }

    #[test]
    fn explicit_options_reach_the_terminal_by_shared_reference() {
        let options = CheckOptions::new().with_clear_cache(true);
        let result = OptionEchoRule.check_with(&options);

        assert!(matches!(result, Err(ArchUnitError::Technical(_))));
        assert!(options.clears_cache());
    }

    #[test]
    fn borrowed_rules_preserve_the_terminal_contract() {
        fn require_checkable<T: Checkable>(_: T) {}

        require_checkable(&OptionEchoRule);
        assert!(OptionEchoRule.check().is_ok());
    }
}