use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Info,
Hint,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Diagnostic {
pub severity: Severity,
pub message: String,
#[serde(default)]
pub path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
#[serde(default)]
pub hints: Vec<String>,
#[serde(default)]
pub diagnostics: Vec<Diagnostic>,
#[serde(default)]
pub yaml_type: Option<YamlType>,
}
impl ValidationResult {
pub fn from_diagnostics(yaml_type: YamlType, diags: Vec<Diagnostic>) -> Self {
let errors: Vec<String> = diags
.iter()
.filter(|d| d.severity == Severity::Error)
.map(|d| d.message.clone())
.collect();
let warnings: Vec<String> = diags
.iter()
.filter(|d| d.severity == Severity::Warning)
.map(|d| d.message.clone())
.collect();
let hints: Vec<String> = diags
.iter()
.filter(|d| matches!(d.severity, Severity::Info | Severity::Hint))
.map(|d| d.message.clone())
.collect();
let valid = errors.is_empty();
Self {
valid,
errors,
warnings,
hints,
diagnostics: diags,
yaml_type: Some(yaml_type),
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum YamlType {
K8sDeployment,
K8sService,
K8sConfigMap,
K8sSecret,
K8sIngress,
K8sHPA,
K8sCronJob,
K8sJob,
K8sPVC,
K8sNetworkPolicy,
K8sStatefulSet,
K8sDaemonSet,
K8sRole,
K8sClusterRole,
K8sRoleBinding,
K8sClusterRoleBinding,
K8sServiceAccount,
K8sGeneric,
GitLabCI,
GitHubActions,
DockerCompose,
Prometheus,
Alertmanager,
HelmValues,
Ansible,
OpenAPI,
Generic,
}
impl std::fmt::Display for YamlType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::K8sDeployment => write!(f, "K8s Deployment"),
Self::K8sService => write!(f, "K8s Service"),
Self::K8sConfigMap => write!(f, "K8s ConfigMap"),
Self::K8sSecret => write!(f, "K8s Secret"),
Self::K8sIngress => write!(f, "K8s Ingress"),
Self::K8sHPA => write!(f, "K8s HPA"),
Self::K8sCronJob => write!(f, "K8s CronJob"),
Self::K8sJob => write!(f, "K8s Job"),
Self::K8sPVC => write!(f, "K8s PVC"),
Self::K8sNetworkPolicy => write!(f, "K8s NetworkPolicy"),
Self::K8sStatefulSet => write!(f, "K8s StatefulSet"),
Self::K8sDaemonSet => write!(f, "K8s DaemonSet"),
Self::K8sRole => write!(f, "K8s Role"),
Self::K8sClusterRole => write!(f, "K8s ClusterRole"),
Self::K8sRoleBinding => write!(f, "K8s RoleBinding"),
Self::K8sClusterRoleBinding => write!(f, "K8s ClusterRoleBinding"),
Self::K8sServiceAccount => write!(f, "K8s ServiceAccount"),
Self::K8sGeneric => write!(f, "K8s (generic)"),
Self::GitLabCI => write!(f, "GitLab CI"),
Self::GitHubActions => write!(f, "GitHub Actions"),
Self::DockerCompose => write!(f, "Docker Compose"),
Self::Prometheus => write!(f, "Prometheus"),
Self::Alertmanager => write!(f, "Alertmanager"),
Self::HelmValues => write!(f, "Helm Values"),
Self::Ansible => write!(f, "Ansible Playbook"),
Self::OpenAPI => write!(f, "OpenAPI"),
Self::Generic => write!(f, "Generic YAML"),
}
}
}
pub trait ConfigValidator {
fn yaml_type(&self) -> YamlType;
fn validate_structure(&self) -> Vec<Diagnostic>;
fn validate_semantics(&self) -> Vec<Diagnostic>;
fn validate(&self) -> ValidationResult {
let mut diags = self.validate_structure();
diags.extend(self.validate_semantics());
ValidationResult::from_diagnostics(self.yaml_type(), diags)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepairResult {
pub valid: bool,
pub repaired_yaml: String,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub llm_fields: Vec<String>,
pub summary: String,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixRecord {
pub field_path: String,
pub old_value: serde_json::Value,
pub new_value: serde_json::Value,
pub reason: String,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmbiguityQuestion {
pub field_path: String,
pub question: String,
pub options: Vec<String>,
#[serde(default)]
pub default: Option<String>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationSession {
pub session_id: String,
pub yaml_original: String,
pub yaml_current: String,
pub fixes_applied: Vec<FixRecord>,
pub pending_ambiguities: Vec<AmbiguityQuestion>,
pub status: SessionStatus,
pub created_at: String,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SessionStatus {
InProgress,
WaitingUser,
Completed,
}
#[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::Hint);
}
#[test]
fn test_severity_serde_roundtrip() {
let sev = Severity::Warning;
let json = serde_json::to_string(&sev).unwrap();
assert_eq!(json, r#""warning""#);
let parsed: Severity = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, Severity::Warning);
}
#[test]
fn test_yaml_type_display() {
assert_eq!(format!("{}", YamlType::K8sDeployment), "K8s Deployment");
assert_eq!(format!("{}", YamlType::DockerCompose), "Docker Compose");
assert_eq!(format!("{}", YamlType::GitLabCI), "GitLab CI");
assert_eq!(format!("{}", YamlType::GitHubActions), "GitHub Actions");
assert_eq!(format!("{}", YamlType::Prometheus), "Prometheus");
assert_eq!(format!("{}", YamlType::Generic), "Generic YAML");
}
#[test]
fn test_yaml_type_serde_roundtrip() {
let yt = YamlType::K8sService;
let json = serde_json::to_string(&yt).unwrap();
let parsed: YamlType = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, YamlType::K8sService);
}
#[test]
fn test_validation_result_from_diagnostics() {
let diags = vec![
Diagnostic {
severity: Severity::Error,
message: "Test error".to_string(),
path: Some("spec".to_string()),
},
Diagnostic {
severity: Severity::Warning,
message: "Test warning".to_string(),
path: None,
},
Diagnostic {
severity: Severity::Info,
message: "Info message".to_string(),
path: None,
},
];
let result = ValidationResult::from_diagnostics(YamlType::K8sDeployment, diags);
assert!(!result.valid);
assert_eq!(result.errors.len(), 1);
assert_eq!(result.warnings.len(), 1);
assert_eq!(result.hints.len(), 1);
assert_eq!(result.diagnostics.len(), 3);
assert_eq!(result.yaml_type, Some(YamlType::K8sDeployment));
}
#[test]
fn test_validation_result_valid_when_no_errors() {
let diags = vec![
Diagnostic {
severity: Severity::Warning,
message: "Warning only".to_string(),
path: None,
},
];
let result = ValidationResult::from_diagnostics(YamlType::DockerCompose, diags);
assert!(result.valid);
assert!(result.errors.is_empty());
assert_eq!(result.warnings.len(), 1);
}
#[test]
fn test_diagnostic_path_default() {
let diag = Diagnostic {
severity: Severity::Error,
message: "Error".to_string(),
path: None,
};
assert!(diag.path.is_none());
let diag_with_path = Diagnostic {
severity: Severity::Error,
message: "Error".to_string(),
path: Some("spec > containers > 0".to_string()),
};
assert_eq!(diag_with_path.path, Some("spec > containers > 0".to_string()));
}
#[test]
fn test_session_status_serde() {
let status = SessionStatus::InProgress;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, r#""inprogress""#);
let parsed: SessionStatus = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, SessionStatus::InProgress);
}
}