use crate::incident::Incident;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleSet {
pub name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub violations: BTreeMap<String, Violation>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub insights: BTreeMap<String, Violation>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub errors: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unmatched: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skipped: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Violation {
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<Category>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<String>,
#[serde(default)]
pub incidents: Vec<Incident>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub links: Vec<Link>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Category {
Mandatory,
Optional,
Potential,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Link {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_category_serde_roundtrip() {
let cases = vec![
(Category::Mandatory, "\"mandatory\""),
(Category::Optional, "\"optional\""),
(Category::Potential, "\"potential\""),
];
for (cat, expected) in cases {
let json = serde_json::to_string(&cat).unwrap();
assert_eq!(json, expected);
let back: Category = serde_json::from_str(&json).unwrap();
assert_eq!(back, cat);
}
}
#[test]
fn test_ruleset_serde_roundtrip() {
let rs = RuleSet {
name: "test-rules".to_string(),
description: "Test description".to_string(),
tags: vec!["patternfly".to_string()],
violations: BTreeMap::new(),
insights: BTreeMap::new(),
errors: BTreeMap::new(),
unmatched: vec!["rule-1".to_string()],
skipped: Vec::new(),
};
let json = serde_json::to_string(&rs).unwrap();
let back: RuleSet = serde_json::from_str(&json).unwrap();
assert_eq!(back.name, "test-rules");
assert_eq!(back.description, "Test description");
assert_eq!(back.tags, vec!["patternfly"]);
assert_eq!(back.unmatched, vec!["rule-1"]);
}
#[test]
fn test_violation_with_incidents() {
let json = r#"{
"description": "Chip has been renamed to Label",
"category": "mandatory",
"labels": ["change-type=rename"],
"incidents": [
{
"uri": "file:///src/App.tsx",
"message": "Rename Chip to Label",
"lineNumber": 10
}
],
"effort": 1
}"#;
let v: Violation = serde_json::from_str(json).unwrap();
assert_eq!(v.description, "Chip has been renamed to Label");
assert_eq!(v.category, Some(Category::Mandatory));
assert_eq!(v.labels, vec!["change-type=rename"]);
assert_eq!(v.incidents.len(), 1);
assert_eq!(v.incidents[0].file_uri, "file:///src/App.tsx");
assert_eq!(v.incidents[0].line_number, Some(10));
assert_eq!(v.effort, Some(1));
}
#[test]
fn test_ruleset_empty_fields_skipped_in_serialization() {
let rs = RuleSet {
name: "minimal".to_string(),
description: String::new(),
tags: Vec::new(),
violations: BTreeMap::new(),
insights: BTreeMap::new(),
errors: BTreeMap::new(),
unmatched: Vec::new(),
skipped: Vec::new(),
};
let json = serde_json::to_string(&rs).unwrap();
assert!(!json.contains("description"));
assert!(!json.contains("tags"));
assert!(!json.contains("violations"));
assert!(!json.contains("unmatched"));
}
}