use std::cmp::Ordering;
use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Severity {
Error,
#[default]
Warning,
Info,
Style,
Ignore,
}
impl Severity {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"error" => Some(Self::Error),
"warning" => Some(Self::Warning),
"info" => Some(Self::Info),
"style" => Some(Self::Style),
"ignore" | "none" | "off" => Some(Self::Ignore),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Error => "error",
Self::Warning => "warning",
Self::Info => "info",
Self::Style => "style",
Self::Ignore => "ignore",
}
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl Ord for Severity {
fn cmp(&self, other: &Self) -> Ordering {
let self_val = match self {
Self::Error => 0,
Self::Warning => 1,
Self::Info => 2,
Self::Style => 3,
Self::Ignore => 4,
};
let other_val = match other {
Self::Error => 0,
Self::Warning => 1,
Self::Info => 2,
Self::Style => 3,
Self::Ignore => 4,
};
other_val.cmp(&self_val)
}
}
impl PartialOrd for Severity {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RuleCategory {
Structure,
Values,
Template,
Security,
BestPractice,
}
impl RuleCategory {
pub fn prefix(&self) -> &'static str {
match self {
Self::Structure => "HL1",
Self::Values => "HL2",
Self::Template => "HL3",
Self::Security => "HL4",
Self::BestPractice => "HL5",
}
}
pub fn display_name(&self) -> &'static str {
match self {
Self::Structure => "Chart Structure",
Self::Values => "Values Validation",
Self::Template => "Template Syntax",
Self::Security => "Security",
Self::BestPractice => "Best Practice",
}
}
pub fn from_code(code: &str) -> Option<Self> {
if code.starts_with("HL1") {
Some(Self::Structure)
} else if code.starts_with("HL2") {
Some(Self::Values)
} else if code.starts_with("HL3") {
Some(Self::Template)
} else if code.starts_with("HL4") {
Some(Self::Security)
} else if code.starts_with("HL5") {
Some(Self::BestPractice)
} else {
None
}
}
}
impl fmt::Display for RuleCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_name())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RuleCode(pub String);
impl RuleCode {
pub fn new(code: impl Into<String>) -> Self {
Self(code.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn category(&self) -> Option<RuleCategory> {
RuleCategory::from_code(&self.0)
}
pub fn is_structure_rule(&self) -> bool {
self.0.starts_with("HL1")
}
pub fn is_values_rule(&self) -> bool {
self.0.starts_with("HL2")
}
pub fn is_template_rule(&self) -> bool {
self.0.starts_with("HL3")
}
pub fn is_security_rule(&self) -> bool {
self.0.starts_with("HL4")
}
pub fn is_best_practice_rule(&self) -> bool {
self.0.starts_with("HL5")
}
}
impl fmt::Display for RuleCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for RuleCode {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for RuleCode {
fn from(s: String) -> Self {
Self(s)
}
}
#[derive(Debug, Clone)]
pub struct RuleMeta {
pub code: RuleCode,
pub name: &'static str,
pub description: &'static str,
pub severity: Severity,
pub category: RuleCategory,
pub fixable: bool,
}
impl RuleMeta {
pub const fn new(
_code: &'static str,
name: &'static str,
description: &'static str,
severity: Severity,
category: RuleCategory,
fixable: bool,
) -> Self {
Self {
code: RuleCode(String::new()), name,
description,
severity,
category,
fixable,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckFailure {
pub code: RuleCode,
pub severity: Severity,
pub message: String,
pub file: PathBuf,
pub line: u32,
pub column: Option<u32>,
pub fixable: bool,
pub category: RuleCategory,
}
impl CheckFailure {
pub fn new(
code: impl Into<RuleCode>,
severity: Severity,
message: impl Into<String>,
file: impl Into<PathBuf>,
line: u32,
category: RuleCategory,
) -> Self {
Self {
code: code.into(),
severity,
message: message.into(),
file: file.into(),
line,
column: None,
fixable: false,
category,
}
}
pub fn with_column(
code: impl Into<RuleCode>,
severity: Severity,
message: impl Into<String>,
file: impl Into<PathBuf>,
line: u32,
column: u32,
category: RuleCategory,
) -> Self {
Self {
code: code.into(),
severity,
message: message.into(),
file: file.into(),
line,
column: Some(column),
fixable: false,
category,
}
}
pub fn set_fixable(mut self, fixable: bool) -> Self {
self.fixable = fixable;
self
}
}
impl Ord for CheckFailure {
fn cmp(&self, other: &Self) -> Ordering {
match self.file.cmp(&other.file) {
Ordering::Equal => self.line.cmp(&other.line),
other => other,
}
}
}
impl PartialOrd for CheckFailure {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_severity_ordering() {
assert!(Severity::Error > Severity::Warning);
assert!(Severity::Warning > Severity::Info);
assert!(Severity::Info > Severity::Style);
assert!(Severity::Style > Severity::Ignore);
}
#[test]
fn test_severity_from_str() {
assert_eq!(Severity::parse("error"), Some(Severity::Error));
assert_eq!(Severity::parse("WARNING"), Some(Severity::Warning));
assert_eq!(Severity::parse("Info"), Some(Severity::Info));
assert_eq!(Severity::parse("style"), Some(Severity::Style));
assert_eq!(Severity::parse("ignore"), Some(Severity::Ignore));
assert_eq!(Severity::parse("off"), Some(Severity::Ignore));
assert_eq!(Severity::parse("invalid"), None);
}
#[test]
fn test_rule_code_category() {
assert!(RuleCode::new("HL1001").is_structure_rule());
assert!(RuleCode::new("HL2001").is_values_rule());
assert!(RuleCode::new("HL3001").is_template_rule());
assert!(RuleCode::new("HL4001").is_security_rule());
assert!(RuleCode::new("HL5001").is_best_practice_rule());
}
#[test]
fn test_rule_category_from_code() {
assert_eq!(
RuleCategory::from_code("HL1001"),
Some(RuleCategory::Structure)
);
assert_eq!(
RuleCategory::from_code("HL2001"),
Some(RuleCategory::Values)
);
assert_eq!(
RuleCategory::from_code("HL3001"),
Some(RuleCategory::Template)
);
assert_eq!(
RuleCategory::from_code("HL4001"),
Some(RuleCategory::Security)
);
assert_eq!(
RuleCategory::from_code("HL5001"),
Some(RuleCategory::BestPractice)
);
assert_eq!(RuleCategory::from_code("XX1001"), None);
}
#[test]
fn test_check_failure_ordering() {
let f1 = CheckFailure::new(
"HL1001",
Severity::Warning,
"msg1",
"Chart.yaml",
5,
RuleCategory::Structure,
);
let f2 = CheckFailure::new(
"HL1002",
Severity::Info,
"msg2",
"Chart.yaml",
10,
RuleCategory::Structure,
);
let f3 = CheckFailure::new(
"HL1003",
Severity::Error,
"msg3",
"Chart.yaml",
3,
RuleCategory::Structure,
);
let f4 = CheckFailure::new(
"HL3001",
Severity::Error,
"msg4",
"templates/deployment.yaml",
1,
RuleCategory::Template,
);
let mut failures = vec![f1.clone(), f2.clone(), f3.clone(), f4.clone()];
failures.sort();
assert_eq!(failures[0].line, 3);
assert_eq!(failures[1].line, 5);
assert_eq!(failures[2].line, 10);
assert_eq!(
failures[3].file.to_str().unwrap(),
"templates/deployment.yaml"
);
}
}