1use serde::{Deserialize, Serialize};
19
20use super::RuleKind;
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct LoadedRule {
29 pub identifier: String,
31
32 pub license_expression: String,
35
36 pub text: String,
38
39 pub rule_kind: RuleKind,
41
42 pub is_false_positive: bool,
44
45 pub is_required_phrase: bool,
47
48 #[serde(default)]
49 pub skip_for_required_phrase_generation: bool,
50
51 pub relevance: Option<u8>,
54
55 pub minimum_coverage: Option<u8>,
57
58 pub has_stored_minimum_coverage: bool,
60
61 pub is_continuous: bool,
63
64 pub referenced_filenames: Option<Vec<String>>,
66
67 pub ignorable_urls: Option<Vec<String>>,
69
70 pub ignorable_emails: Option<Vec<String>>,
72
73 pub ignorable_copyrights: Option<Vec<String>>,
75
76 pub ignorable_holders: Option<Vec<String>>,
78
79 pub ignorable_authors: Option<Vec<String>>,
81
82 pub language: Option<String>,
84
85 pub notes: Option<String>,
87
88 pub is_deprecated: bool,
90
91 #[serde(default)]
92 pub replaced_by: Vec<String>,
93}
94
95impl LoadedRule {
97 pub fn derive_identifier(filename: &str) -> String {
101 filename.to_string()
102 }
103
104 pub fn derive_rule_kind(
108 is_license_text: bool,
109 is_license_notice: bool,
110 is_license_reference: bool,
111 is_license_tag: bool,
112 is_license_intro: bool,
113 is_license_clue: bool,
114 ) -> Result<RuleKind, RuleKindError> {
115 RuleKind::from_rule_flags(
116 is_license_text,
117 is_license_notice,
118 is_license_reference,
119 is_license_tag,
120 is_license_intro,
121 is_license_clue,
122 )
123 .map_err(|_| RuleKindError::MultipleFlagsSet)
124 }
125
126 pub fn normalize_license_expression(
132 expression: Option<&str>,
133 is_false_positive: bool,
134 ) -> Result<String, LicenseExpressionError> {
135 match expression {
136 Some(expr) if !expr.trim().is_empty() => {
137 Ok(normalize_trivial_outer_parens(expr.trim()))
138 }
139 Some(_) => {
140 if is_false_positive {
141 Ok("unknown".to_string())
142 } else {
143 Err(LicenseExpressionError::EmptyExpression)
144 }
145 }
146 None => {
147 if is_false_positive {
148 Ok("unknown".to_string())
149 } else {
150 Err(LicenseExpressionError::MissingExpression)
151 }
152 }
153 }
154 }
155
156 pub fn normalize_optional_string(s: Option<&str>) -> Option<String> {
160 s.map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
161 }
162
163 pub fn normalize_optional_list(list: Option<&[String]>) -> Option<Vec<String>> {
167 list.map(|l| {
168 l.iter()
169 .map(|s| s.trim().to_string())
170 .filter(|s| !s.is_empty())
171 .collect::<Vec<_>>()
172 })
173 .filter(|l: &Vec<String>| !l.is_empty())
174 }
175
176 pub fn validate_rule_kind_flags(
181 rule_kind: RuleKind,
182 is_false_positive: bool,
183 ) -> Result<(), RuleKindError> {
184 if is_false_positive && rule_kind != RuleKind::None {
185 return Err(RuleKindError::FalsePositiveWithFlags);
186 }
187 if !is_false_positive && rule_kind == RuleKind::None {
188 return Err(RuleKindError::NoFlagsSet);
189 }
190 Ok(())
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
196pub enum RuleKindError {
197 #[error("rule has multiple is_license_* flags set")]
198 MultipleFlagsSet,
199 #[error("non-false-positive rule has no is_license_* flags set")]
200 NoFlagsSet,
201 #[error("false-positive rule cannot have is_license_* flags set")]
202 FalsePositiveWithFlags,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
207pub enum LicenseExpressionError {
208 #[error("license_expression is required for non-false-positive rules")]
209 MissingExpression,
210 #[error("license_expression cannot be empty for non-false-positive rules")]
211 EmptyExpression,
212}
213
214fn has_trivial_outer_parens(s: &str) -> bool {
219 let trimmed = s.trim();
220 if !trimmed.starts_with('(') || !trimmed.ends_with(')') {
221 return false;
222 }
223 let mut depth = 0;
224 let chars: Vec<char> = trimmed.chars().collect();
225 for (i, c) in chars.iter().enumerate() {
226 if *c == '(' {
227 depth += 1;
228 } else if *c == ')' {
229 depth -= 1;
230 if depth == 0 && i < chars.len() - 1 {
231 return false;
232 }
233 }
234 }
235 depth == 0
236}
237
238fn normalize_trivial_outer_parens(expr: &str) -> String {
242 let trimmed = expr.trim();
243 if has_trivial_outer_parens(trimmed) {
244 let inner = &trimmed[1..trimmed.len() - 1];
245 normalize_trivial_outer_parens(inner)
246 } else {
247 trimmed.to_string()
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_derive_identifier() {
257 assert_eq!(LoadedRule::derive_identifier("mit.LICENSE"), "mit.LICENSE");
258 assert_eq!(
259 LoadedRule::derive_identifier("gpl-2.0_12.RULE"),
260 "gpl-2.0_12.RULE"
261 );
262 }
263
264 #[test]
265 fn test_derive_rule_kind_single_flag() {
266 assert_eq!(
267 LoadedRule::derive_rule_kind(true, false, false, false, false, false),
268 Ok(RuleKind::Text)
269 );
270 assert_eq!(
271 LoadedRule::derive_rule_kind(false, true, false, false, false, false),
272 Ok(RuleKind::Notice)
273 );
274 assert_eq!(
275 LoadedRule::derive_rule_kind(false, false, true, false, false, false),
276 Ok(RuleKind::Reference)
277 );
278 assert_eq!(
279 LoadedRule::derive_rule_kind(false, false, false, true, false, false),
280 Ok(RuleKind::Tag)
281 );
282 assert_eq!(
283 LoadedRule::derive_rule_kind(false, false, false, false, true, false),
284 Ok(RuleKind::Intro)
285 );
286 assert_eq!(
287 LoadedRule::derive_rule_kind(false, false, false, false, false, true),
288 Ok(RuleKind::Clue)
289 );
290 }
291
292 #[test]
293 fn test_derive_rule_kind_none() {
294 assert_eq!(
295 LoadedRule::derive_rule_kind(false, false, false, false, false, false),
296 Ok(RuleKind::None)
297 );
298 }
299
300 #[test]
301 fn test_derive_rule_kind_multiple_flags() {
302 assert_eq!(
303 LoadedRule::derive_rule_kind(true, true, false, false, false, false),
304 Err(RuleKindError::MultipleFlagsSet)
305 );
306 }
307
308 #[test]
309 fn test_normalize_license_expression_with_value() {
310 assert_eq!(
311 LoadedRule::normalize_license_expression(Some("mit"), false),
312 Ok("mit".to_string())
313 );
314 }
315
316 #[test]
317 fn test_normalize_license_expression_false_positive_fallback() {
318 assert_eq!(
319 LoadedRule::normalize_license_expression(None, true),
320 Ok("unknown".to_string())
321 );
322 assert_eq!(
323 LoadedRule::normalize_license_expression(Some(""), true),
324 Ok("unknown".to_string())
325 );
326 assert_eq!(
327 LoadedRule::normalize_license_expression(Some(" "), true),
328 Ok("unknown".to_string())
329 );
330 }
331
332 #[test]
333 fn test_normalize_license_expression_missing_error() {
334 assert_eq!(
335 LoadedRule::normalize_license_expression(None, false),
336 Err(LicenseExpressionError::MissingExpression)
337 );
338 }
339
340 #[test]
341 fn test_normalize_license_expression_empty_error() {
342 assert_eq!(
343 LoadedRule::normalize_license_expression(Some(""), false),
344 Err(LicenseExpressionError::EmptyExpression)
345 );
346 }
347
348 #[test]
349 fn test_normalize_trivial_outer_parens() {
350 assert_eq!(normalize_trivial_outer_parens("mit"), "mit");
351 assert_eq!(normalize_trivial_outer_parens("(mit)"), "mit");
352 assert_eq!(normalize_trivial_outer_parens("((mit))"), "mit");
353 assert_eq!(
354 normalize_trivial_outer_parens("(mit OR apache-2.0)"),
355 "mit OR apache-2.0"
356 );
357 assert_eq!(
358 normalize_trivial_outer_parens("(mit) OR (apache-2.0)"),
359 "(mit) OR (apache-2.0)"
360 );
361 }
362
363 #[test]
364 fn test_normalize_optional_string() {
365 assert_eq!(LoadedRule::normalize_optional_string(None), None);
366 assert_eq!(LoadedRule::normalize_optional_string(Some("")), None);
367 assert_eq!(LoadedRule::normalize_optional_string(Some(" ")), None);
368 assert_eq!(
369 LoadedRule::normalize_optional_string(Some("hello")),
370 Some("hello".to_string())
371 );
372 assert_eq!(
373 LoadedRule::normalize_optional_string(Some(" hello ")),
374 Some("hello".to_string())
375 );
376 }
377
378 #[test]
379 fn test_normalize_optional_list() {
380 assert_eq!(LoadedRule::normalize_optional_list(None), None);
381 assert_eq!(LoadedRule::normalize_optional_list(Some(&[])), None);
382 assert_eq!(
383 LoadedRule::normalize_optional_list(Some(&["a".to_string(), "b".to_string()])),
384 Some(vec!["a".to_string(), "b".to_string()])
385 );
386 assert_eq!(
387 LoadedRule::normalize_optional_list(Some(&[" a ".to_string(), " b ".to_string()])),
388 Some(vec!["a".to_string(), "b".to_string()])
389 );
390 assert_eq!(
391 LoadedRule::normalize_optional_list(Some(&["".to_string(), " ".to_string()])),
392 None
393 );
394 }
395
396 #[test]
397 fn test_validate_rule_kind_flags() {
398 assert!(LoadedRule::validate_rule_kind_flags(RuleKind::Text, false).is_ok());
399 assert_eq!(
400 LoadedRule::validate_rule_kind_flags(RuleKind::None, false),
401 Err(RuleKindError::NoFlagsSet)
402 );
403 assert!(LoadedRule::validate_rule_kind_flags(RuleKind::None, true).is_ok());
404 assert_eq!(
405 LoadedRule::validate_rule_kind_flags(RuleKind::Text, true),
406 Err(RuleKindError::FalsePositiveWithFlags)
407 );
408 }
409
410 #[test]
411 fn test_serde_roundtrip() {
412 let rule = LoadedRule {
413 identifier: "mit.LICENSE".to_string(),
414 license_expression: "mit".to_string(),
415 text: "MIT License".to_string(),
416 rule_kind: RuleKind::Text,
417 is_false_positive: false,
418 is_required_phrase: false,
419 skip_for_required_phrase_generation: false,
420 relevance: Some(100),
421 minimum_coverage: Some(90),
422 has_stored_minimum_coverage: true,
423 is_continuous: false,
424 referenced_filenames: Some(vec!["MIT.txt".to_string()]),
425 ignorable_urls: None,
426 ignorable_emails: None,
427 ignorable_copyrights: None,
428 ignorable_holders: None,
429 ignorable_authors: None,
430 language: None,
431 notes: Some("Test note".to_string()),
432 is_deprecated: false,
433 replaced_by: vec![],
434 };
435
436 let json = serde_json::to_string(&rule).unwrap();
437 let deserialized: LoadedRule = serde_json::from_str(&json).unwrap();
438 assert_eq!(rule, deserialized);
439 }
440}