commitlint_rs/rule/
scope.rs

1use crate::{message::Message, result::Violation, rule::Rule};
2use serde::{Deserialize, Serialize};
3
4use super::Level;
5
6/// Scope represents the subject-empty rule.
7#[derive(Clone, Debug, Deserialize, Serialize)]
8#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9pub struct Scope {
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    /// Options represents the options of the rule.
17    /// If the option is empty, it means that no scope is allowed.
18    options: Vec<String>,
19
20    /// Optional scope.
21    /// If true, even if the scope is not present, it is allowed.
22    optional: bool,
23}
24
25/// Scope represents the scope rule.
26impl Rule for Scope {
27    const NAME: &'static str = "scope";
28    const LEVEL: Level = Level::Error;
29
30    fn message(&self, message: &Message) -> String {
31        if self.options.is_empty() {
32            return "scopes are not allowed".to_string();
33        }
34
35        format!(
36            "scope {} is not allowed. Only {:?} are allowed",
37            message.scope.as_ref().unwrap_or(&"".to_string()),
38            self.options
39        )
40    }
41
42    fn validate(&self, message: &Message) -> Option<Violation> {
43        match &message.scope {
44            None => {
45                if self.options.is_empty() || self.optional {
46                    return None;
47                }
48            }
49            Some(scope) if scope.is_empty() => {
50                if self.options.is_empty() {
51                    return None;
52                }
53            }
54            Some(scope) if self.options.contains(scope) => {
55                return None;
56            }
57            _ => {}
58        }
59
60        Some(Violation {
61            level: self.level.unwrap_or(Self::LEVEL),
62            message: self.message(message),
63        })
64    }
65}
66
67/// Default implementation of Scope.
68impl Default for Scope {
69    fn default() -> Self {
70        Self {
71            level: Some(Self::LEVEL),
72            optional: false,
73            options: vec![],
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    mod empty_options {
83        use super::*;
84
85        #[test]
86        fn test_empty_scope() {
87            let rule = Scope::default();
88
89            let message = Message {
90                body: None,
91                description: None,
92                footers: None,
93                r#type: None,
94                raw: "".to_string(),
95                scope: Some("".to_string()),
96                subject: None,
97            };
98
99            let violation = rule.validate(&message);
100            assert!(violation.is_none());
101        }
102
103        #[test]
104        fn test_none_scope() {
105            let rule = Scope::default();
106
107            let message = Message {
108                body: None,
109                description: None,
110                footers: None,
111                r#type: None,
112                raw: "".to_string(),
113                scope: None,
114                subject: None,
115            };
116
117            let violation = rule.validate(&message);
118            assert!(violation.is_none());
119        }
120
121        #[test]
122        fn test_scope() {
123            let rule = Scope::default();
124
125            let message = Message {
126                body: None,
127                description: None,
128                footers: None,
129                r#type: Some("feat".to_string()),
130                raw: "feat(web): broadcast $destroy event on scope destruction".to_string(),
131                scope: Some("web".to_string()),
132                subject: None,
133            };
134
135            let violation = rule.validate(&message);
136            assert!(violation.is_some());
137            assert_eq!(violation.clone().unwrap().level, Level::Error);
138            assert_eq!(
139                violation.unwrap().message,
140                "scopes are not allowed".to_string()
141            );
142        }
143    }
144
145    mod scopes {
146        use super::*;
147        #[test]
148        fn test_empty_scope() {
149            let rule = Scope {
150                options: vec!["api".to_string(), "web".to_string()],
151                ..Default::default()
152            };
153
154            let message = Message {
155                body: None,
156                description: None,
157                footers: None,
158                r#type: None,
159                raw: "".to_string(),
160                scope: Some("".to_string()),
161                subject: None,
162            };
163
164            let violation = rule.validate(&message);
165            assert!(violation.is_some());
166            assert_eq!(violation.clone().unwrap().level, Level::Error);
167            assert_eq!(
168                violation.unwrap().message,
169                "scope  is not allowed. Only [\"api\", \"web\"] are allowed"
170            );
171        }
172
173        #[test]
174        fn test_none_scope() {
175            let rule = Scope {
176                options: vec!["api".to_string(), "web".to_string()],
177                ..Default::default()
178            };
179
180            let message = Message {
181                body: None,
182                description: None,
183                footers: None,
184                r#type: None,
185                raw: "".to_string(),
186                scope: None,
187                subject: None,
188            };
189
190            let violation = rule.validate(&message);
191            assert!(violation.is_some());
192            assert_eq!(violation.clone().unwrap().level, Level::Error);
193            assert_eq!(
194                violation.unwrap().message,
195                "scope  is not allowed. Only [\"api\", \"web\"] are allowed".to_string()
196            );
197        }
198
199        #[test]
200        fn test_valid_scope() {
201            let rule = Scope {
202                options: vec!["api".to_string(), "web".to_string()],
203                ..Default::default()
204            };
205
206            let message = Message {
207                body: None,
208                description: None,
209                footers: None,
210                r#type: Some("feat".to_string()),
211                raw: "feat(web): broadcast $destroy event on scope destruction".to_string(),
212                scope: Some("web".to_string()),
213                subject: None,
214            };
215
216            assert!(rule.validate(&message).is_none());
217        }
218
219        #[test]
220        fn test_invalid_scope() {
221            let rule = Scope {
222                options: vec!["api".to_string(), "web".to_string()],
223                ..Default::default()
224            };
225
226            let message = Message {
227                body: None,
228                description: None,
229                footers: None,
230                r#type: Some("feat".to_string()),
231                raw: "feat(invalid): broadcast $destroy event on scope destruction".to_string(),
232                scope: Some("invalid".to_string()),
233                subject: None,
234            };
235
236            let violation = rule.validate(&message);
237            assert!(violation.is_some());
238            assert_eq!(violation.clone().unwrap().level, Level::Error);
239            assert_eq!(
240                violation.unwrap().message,
241                "scope invalid is not allowed. Only [\"api\", \"web\"] are allowed".to_string()
242            );
243        }
244
245        #[test]
246        fn test_optional_scope_with_non_empty_scope() {
247            let rule = Scope {
248                options: vec!["api".to_string(), "web".to_string()],
249                optional: true,
250                ..Default::default()
251            };
252
253            let message = Message {
254                body: None,
255                description: None,
256                footers: None,
257                r#type: Some("feat".to_string()),
258                raw: "feat(invalid): broadcast $destroy event on scope destruction".to_string(),
259                scope: Some("invalid".to_string()),
260                subject: None,
261            };
262
263            let violation = rule.validate(&message);
264            assert!(violation.is_some());
265            assert_eq!(violation.clone().unwrap().level, Level::Error);
266            assert_eq!(
267                violation.unwrap().message,
268                "scope invalid is not allowed. Only [\"api\", \"web\"] are allowed".to_string()
269            );
270        }
271
272        #[test]
273        fn test_optional_scope_with_empty_scope() {
274            let rule = Scope {
275                options: vec!["api".to_string(), "web".to_string()],
276                optional: true,
277                ..Default::default()
278            };
279
280            let message = Message {
281                body: None,
282                description: None,
283                footers: None,
284                r#type: Some("feat".to_string()),
285                raw: "feat: broadcast $destroy event on scope destruction".to_string(),
286                scope: None,
287                subject: None,
288            };
289
290            let violation = rule.validate(&message);
291            assert!(violation.is_none());
292        }
293    }
294}