archunit/files/fluentapi/
match_pattern_file_condition_builder.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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
use crate::{
    common::{Filter, PatternError, ProjectLocator, RegexFactory},
    files::FileInfo,
};

use super::{
    CustomFileCondition, DependOnExternalModuleConditionBuilder, DependOnFileConditionBuilder,
    FileConditionBuilder, MatchPatternFileCondition,
};

/// Shared immutable state for positive and negated file-predicate builders.
///
/// The mood is one boolean consumed later by a shared assertion path. Public positive and negated
/// wrappers make the fluent stages distinct without duplicating rule evaluation.
#[derive(Debug, Clone)]
#[must_use = "a file mood has no effect until a predicate is selected and checked"]
pub struct MatchPatternFileConditionBuilder {
    scope: FileConditionBuilder,
    negated: bool,
}

impl MatchPatternFileConditionBuilder {
    pub(super) const fn new(scope: FileConditionBuilder, negated: bool) -> Self {
        Self { scope, negated }
    }

    /// Returns the selected file scope carried into this mood.
    pub const fn scope(&self) -> &FileConditionBuilder {
        &self.scope
    }

    /// Returns whether the following predicate is negated.
    #[must_use]
    pub const fn is_negated(&self) -> bool {
        self.negated
    }

    /// Returns where Cargo project discovery will begin.
    #[must_use]
    pub const fn project_locator(&self) -> &ProjectLocator {
        self.scope.project_locator()
    }

    /// Returns the scope selectors in chain order.
    #[must_use]
    pub fn filters(&self) -> &[Filter] {
        self.scope.filters()
    }

    /// Returns the first invalid selector retained by the scope.
    #[must_use]
    pub const fn selector_error(&self) -> Option<&PatternError> {
        self.scope.selector_error()
    }

    /// Requires every selected file's final path segment to match `pattern`.
    pub fn have_name(
        self,
        pattern: impl Into<crate::common::PatternSpec>,
    ) -> MatchPatternFileCondition {
        let check_filter = RegexFactory::default().filename_matcher(pattern);
        self.matching(check_filter)
    }

    /// Requires every selected file's containing folder to match `pattern`.
    pub fn be_in_folder(
        self,
        pattern: impl Into<crate::common::PatternSpec>,
    ) -> MatchPatternFileCondition {
        let check_filter = RegexFactory::default().folder_matcher(pattern);
        self.matching(check_filter)
    }

    /// Requires every selected file's complete normalized path to match `pattern`.
    pub fn be_in_path(
        self,
        pattern: impl Into<crate::common::PatternSpec>,
    ) -> MatchPatternFileCondition {
        let check_filter = RegexFactory::default().path_matcher(pattern);
        self.matching(check_filter)
    }

    /// Starts an internal file-dependency rule and enters its object-selector stage.
    pub fn depend_on_files(self) -> DependOnFileConditionBuilder {
        DependOnFileConditionBuilder::new(self)
    }

    /// Starts an external crate-dependency rule and enters its module-selector stage.
    pub fn depend_on_external_modules(self) -> DependOnExternalModuleConditionBuilder {
        DependOnExternalModuleConditionBuilder::new(self)
    }

    /// Judges every selected file with `predicate` and describes the requirement with `message`.
    pub fn adhere_to<F>(self, predicate: F, message: impl Into<String>) -> CustomFileCondition
    where
        F: Fn(&FileInfo) -> bool + Send + Sync + 'static,
    {
        CustomFileCondition::new(self, predicate, message)
    }

    fn matching(self, check_filter: Result<Filter, PatternError>) -> MatchPatternFileCondition {
        MatchPatternFileCondition::new(self, check_filter)
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use crate::files::project_files_in;

    use super::MatchPatternFileConditionBuilder;

    #[test]
    fn carries_one_owned_scope_and_one_mood_flag() {
        let scope = project_files_in("examples/layered")
            .in_folder("src/**")
            .with_name("*.rs");
        let mood = MatchPatternFileConditionBuilder::new(scope, true);

        assert!(mood.is_negated());
        assert_eq!(
            mood.project_locator().path(),
            Some(Path::new("examples/layered"))
        );
        assert_eq!(mood.filters().len(), 2);
        assert!(mood.selector_error().is_none());
    }

    #[test]
    fn preserves_invalid_selector_diagnostics() {
        let scope = project_files_in("examples/layered").in_path("src/[api");
        let mood = MatchPatternFileConditionBuilder::new(scope, false);

        assert!(!mood.is_negated());
        assert_eq!(
            mood.selector_error().map(|error| error.pattern()),
            Some("src/[api")
        );
    }

    #[test]
    fn creates_all_three_predicates_with_the_shared_mood() {
        let scope = project_files_in("examples/layered").in_path("src/**");
        let named =
            MatchPatternFileConditionBuilder::new(scope.clone(), false).have_name("*_service.rs");
        let folder =
            MatchPatternFileConditionBuilder::new(scope.clone(), true).be_in_folder("src/service");
        let path = MatchPatternFileConditionBuilder::new(scope, false).be_in_path("src/**");

        assert!(!named.is_negated());
        assert!(folder.is_negated());
        assert!(!path.is_negated());
        assert_eq!(
            named.check_filter().map(|filter| filter.target()),
            Some(crate::common::PatternTarget::Filename)
        );
        assert_eq!(
            folder.check_filter().map(|filter| filter.target()),
            Some(crate::common::PatternTarget::PathWithoutFilename)
        );
        assert_eq!(
            path.check_filter().map(|filter| filter.target()),
            Some(crate::common::PatternTarget::Path)
        );
    }
}