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