1use 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 Scope {
10 level: Option<Level>,
15
16 options: Vec<String>,
19
20 optional: bool,
23}
24
25impl 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
67impl 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}