archunit/common/matching/
target.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
use std::fmt;

/// The part of an identifier against which a [`crate::Pattern`] is matched.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PatternTarget {
    /// The last path segment, such as `handler.rs`.
    Filename,
    /// The complete normalized path.
    Path,
    /// The path with its final filename removed.
    PathWithoutFilename,
    /// A Rust type name with module or path qualification removed.
    TypeName,
}

impl PatternTarget {
    pub(super) fn extract(self, identifier: &str) -> Option<String> {
        let identifier = normalize_identifier(identifier);
        if identifier.is_empty() {
            return None;
        }

        match self {
            Self::Path => Some(identifier),
            Self::Filename => identifier.rsplit('/').next().map(str::to_owned),
            Self::PathWithoutFilename => {
                let folder = identifier
                    .rsplit_once('/')
                    .map_or(".", |(folder, _)| folder);
                Some(folder.to_owned())
            }
            Self::TypeName => identifier
                .rsplit(['/', ':', '.'])
                .find(|part| !part.is_empty())
                .map(str::to_owned),
        }
    }

    /// Returns the stable diagnostic spelling of this target.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Filename => "filename",
            Self::Path => "path",
            Self::PathWithoutFilename => "path without filename",
            Self::TypeName => "type name",
        }
    }
}

impl fmt::Display for PatternTarget {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

fn normalize_identifier(identifier: &str) -> String {
    let replaced = identifier.trim().replace('\\', "/");
    replaced
        .split('/')
        .filter(|segment| !segment.is_empty())
        .collect::<Vec<_>>()
        .join("/")
}

#[cfg(test)]
mod tests {
    use super::PatternTarget;

    #[test]
    fn extracts_each_target_from_an_identifier() {
        let identifier = r"crates\api\src\handler.rs";

        assert_eq!(
            PatternTarget::Path.extract(identifier).as_deref(),
            Some("crates/api/src/handler.rs")
        );
        assert_eq!(
            PatternTarget::Filename.extract(identifier).as_deref(),
            Some("handler.rs")
        );
        assert_eq!(
            PatternTarget::PathWithoutFilename
                .extract(identifier)
                .as_deref(),
            Some("crates/api/src")
        );
    }

    #[test]
    fn treats_the_project_root_as_a_folder() {
        assert_eq!(
            PatternTarget::PathWithoutFilename
                .extract("lib.rs")
                .as_deref(),
            Some(".")
        );
    }

    #[test]
    fn extracts_unqualified_rust_type_names() {
        for identifier in [
            "crate::api::RequestHandler",
            "crates/api.RequestHandler",
            "RequestHandler",
        ] {
            assert_eq!(
                PatternTarget::TypeName.extract(identifier).as_deref(),
                Some("RequestHandler")
            );
        }
    }

    #[test]
    fn empty_identifiers_have_no_target() {
        for target in [
            PatternTarget::Filename,
            PatternTarget::Path,
            PatternTarget::PathWithoutFilename,
            PatternTarget::TypeName,
        ] {
            assert_eq!(target.extract("  "), None);
        }
    }
}