commitlint_rs/rule/
scope_format.rs

1use crate::{message::Message, result::Violation, rule::Rule};
2use serde::{Deserialize, Serialize};
3
4use super::Level;
5
6/// ScopeFormat represents the scope-format rule.
7#[derive(Clone, Debug, Deserialize, Serialize)]
8#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9pub struct ScopeFormat {
10    /// Level represents the level of the rule.
11    ///
12    // Note that currently the default literal is not supported.
13    // See: https://github.com/serde-rs/serde/issues/368
14    level: Option<Level>,
15
16    /// Format represents the format of the scope.
17    format: Option<String>,
18}
19
20/// ScopeFormat represents the scope-format rule.
21impl Rule for ScopeFormat {
22    const NAME: &'static str = "scope-format";
23    const LEVEL: Level = Level::Error;
24
25    fn message(&self, _message: &Message) -> String {
26        format!(
27            "scope format does not match format: {}",
28            self.format.as_ref().unwrap()
29        )
30    }
31
32    fn validate(&self, message: &Message) -> Option<Violation> {
33        if let Some(format) = &self.format {
34            let regex = match regex::Regex::new(format) {
35                Ok(regex) => regex,
36                Err(err) => {
37                    return Some(Violation {
38                        level: self.level.unwrap_or(Self::LEVEL),
39                        message: err.to_string(),
40                    });
41                }
42            };
43
44            match &message.scope {
45                None => {
46                    return Some(Violation {
47                        level: self.level.unwrap_or(Self::LEVEL),
48                        message: "found no scope".to_string(),
49                    });
50                }
51                Some(description) => {
52                    if !regex.is_match(description) {
53                        return Some(Violation {
54                            level: self.level.unwrap_or(Self::LEVEL),
55                            message: self.message(message),
56                        });
57                    }
58                }
59            }
60        }
61
62        None
63    }
64}
65
66/// Default implementation of ScopeFormat.
67impl Default for ScopeFormat {
68    fn default() -> Self {
69        Self {
70            level: Some(Self::LEVEL),
71            format: None,
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_invalid_description_format() {
82        let rule = ScopeFormat {
83            format: Some(r"^[a-z].*".to_string()),
84            ..Default::default()
85        };
86
87        let message = Message {
88            body: None,
89            description: Some("Add new flag".to_string()),
90            footers: None,
91            r#type: Some("feat".to_string()),
92            raw: "feat(scope): Add new flag".to_string(),
93            scope: Some("scope".to_string()),
94            subject: None,
95        };
96
97        assert!(rule.validate(&message).is_none());
98    }
99
100    #[test]
101    fn test_valid_description_format() {
102        let rule = ScopeFormat {
103            format: Some(r"^[a-z].*".to_string()),
104            ..Default::default()
105        };
106
107        let message = Message {
108            body: None,
109            description: Some("Add new flag".to_string()),
110            footers: None,
111            r#type: Some("feat".to_string()),
112            raw: "feat(Scope): Add new flag".to_string(),
113            scope: Some("Scope".to_string()),
114            subject: None,
115        };
116
117        let violation = rule.validate(&message);
118        assert!(violation.is_some());
119        assert_eq!(violation.clone().unwrap().level, Level::Error);
120        assert_eq!(
121            violation.unwrap().message,
122            "scope format does not match format: ^[a-z].*".to_string()
123        );
124    }
125
126    #[test]
127    fn test_invalid_regex() {
128        let rule = ScopeFormat {
129            format: Some(r"(".to_string()),
130            ..Default::default()
131        };
132
133        let message = Message {
134            body: None,
135            description: Some("Add regex".to_string()),
136            footers: None,
137            r#type: Some("feat".to_string()),
138            raw: "feat(scope): Add regex".to_string(),
139            scope: Some("scope".to_string()),
140            subject: None,
141        };
142
143        let violation = rule.validate(&message);
144        assert!(violation.is_some());
145        assert_eq!(violation.clone().unwrap().level, Level::Error);
146        assert!(violation.unwrap().message.contains("regex parse error"));
147    }
148}