konveyor-core 0.0.4

Shared types, gRPC protocol, and provider SDK for the Konveyor migration ecosystem
Documentation
//! Konveyor output format types.
//!
//! Mirrors the analyzer-lsp output YAML/JSON format so that the fix engine
//! and other Konveyor-compatible tooling can consume analysis results.

use crate::incident::Incident;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// A set of rules and their matched violations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleSet {
    /// Ruleset name.
    pub name: String,

    /// Ruleset description.
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub description: String,

    /// Tags generated by matched tagging rules.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,

    /// Map of rule ID -> Violation for matched rules.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub violations: BTreeMap<String, Violation>,

    /// Map of rule ID -> Violation for informational rules.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub insights: BTreeMap<String, Violation>,

    /// Map of rule ID -> error string for failed evaluations.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub errors: BTreeMap<String, String>,

    /// Rule IDs evaluated but not matched.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub unmatched: Vec<String>,

    /// Rule IDs skipped.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub skipped: Vec<String>,
}

/// A violation produced when a rule matches.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Violation {
    /// Description of the violation (from the rule).
    pub description: String,

    /// Severity category.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub category: Option<Category>,

    /// Labels from the rule.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub labels: Vec<String>,

    /// Individual match instances.
    #[serde(default)]
    pub incidents: Vec<Incident>,

    /// Hyperlinks for docs/fixes.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub links: Vec<Link>,

    /// Story points per incident.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub effort: Option<i32>,
}

/// Severity category for a violation.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Category {
    Mandatory,
    Optional,
    Potential,
}

/// A hyperlink.
#[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"));
    }
}