commitlint_rs/rule/
type.rs

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