commitlint_rs/rule/
type.rs1use crate::{message::Message, result::Violation, rule::Rule};
2use serde::{Deserialize, Serialize};
3
4use super::Level;
5
6#[derive(Clone, Debug, Deserialize, Serialize)]
8#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9pub struct Type {
10 level: Option<Level>,
15
16 options: Vec<String>,
19}
20
21impl 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
62impl 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}