1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
17#[serde(rename_all = "lowercase")]
18pub enum Severity {
19 Error,
21 Warning,
23 Info,
25 Hint,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Diagnostic {
44 pub severity: Severity,
46 pub message: String,
48 #[serde(default)]
50 pub path: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ValidationResult {
59 pub valid: bool,
61 pub errors: Vec<String>,
63 pub warnings: Vec<String>,
65 #[serde(default)]
67 pub hints: Vec<String>,
68 #[serde(default)]
70 pub diagnostics: Vec<Diagnostic>,
71 #[serde(default)]
73 pub yaml_type: Option<YamlType>,
74}
75
76impl ValidationResult {
77 pub fn from_diagnostics(yaml_type: YamlType, diags: Vec<Diagnostic>) -> Self {
79 let errors: Vec<String> = diags
80 .iter()
81 .filter(|d| d.severity == Severity::Error)
82 .map(|d| d.message.clone())
83 .collect();
84 let warnings: Vec<String> = diags
85 .iter()
86 .filter(|d| d.severity == Severity::Warning)
87 .map(|d| d.message.clone())
88 .collect();
89 let hints: Vec<String> = diags
90 .iter()
91 .filter(|d| matches!(d.severity, Severity::Info | Severity::Hint))
92 .map(|d| d.message.clone())
93 .collect();
94 let valid = errors.is_empty();
95 Self {
96 valid,
97 errors,
98 warnings,
99 hints,
100 diagnostics: diags,
101 yaml_type: Some(yaml_type),
102 }
103 }
104}
105
106#[allow(missing_docs)]
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
113#[serde(rename_all = "lowercase")]
114pub enum YamlType {
115 K8sDeployment,
117 K8sService,
118 K8sConfigMap,
119 K8sSecret,
120 K8sIngress,
121 K8sHPA,
122 K8sCronJob,
123 K8sJob,
124 K8sPVC,
125 K8sNetworkPolicy,
126 K8sStatefulSet,
127 K8sDaemonSet,
128 K8sRole,
129 K8sClusterRole,
130 K8sRoleBinding,
131 K8sClusterRoleBinding,
132 K8sServiceAccount,
133 K8sGeneric,
135 GitLabCI,
137 GitHubActions,
138 DockerCompose,
140 Prometheus,
142 Alertmanager,
143 HelmValues,
145 Ansible,
146 OpenAPI,
148 Generic,
150}
151
152impl std::fmt::Display for YamlType {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 match self {
155 Self::K8sDeployment => write!(f, "K8s Deployment"),
156 Self::K8sService => write!(f, "K8s Service"),
157 Self::K8sConfigMap => write!(f, "K8s ConfigMap"),
158 Self::K8sSecret => write!(f, "K8s Secret"),
159 Self::K8sIngress => write!(f, "K8s Ingress"),
160 Self::K8sHPA => write!(f, "K8s HPA"),
161 Self::K8sCronJob => write!(f, "K8s CronJob"),
162 Self::K8sJob => write!(f, "K8s Job"),
163 Self::K8sPVC => write!(f, "K8s PVC"),
164 Self::K8sNetworkPolicy => write!(f, "K8s NetworkPolicy"),
165 Self::K8sStatefulSet => write!(f, "K8s StatefulSet"),
166 Self::K8sDaemonSet => write!(f, "K8s DaemonSet"),
167 Self::K8sRole => write!(f, "K8s Role"),
168 Self::K8sClusterRole => write!(f, "K8s ClusterRole"),
169 Self::K8sRoleBinding => write!(f, "K8s RoleBinding"),
170 Self::K8sClusterRoleBinding => write!(f, "K8s ClusterRoleBinding"),
171 Self::K8sServiceAccount => write!(f, "K8s ServiceAccount"),
172 Self::K8sGeneric => write!(f, "K8s (generic)"),
173 Self::GitLabCI => write!(f, "GitLab CI"),
174 Self::GitHubActions => write!(f, "GitHub Actions"),
175 Self::DockerCompose => write!(f, "Docker Compose"),
176 Self::Prometheus => write!(f, "Prometheus"),
177 Self::Alertmanager => write!(f, "Alertmanager"),
178 Self::HelmValues => write!(f, "Helm Values"),
179 Self::Ansible => write!(f, "Ansible Playbook"),
180 Self::OpenAPI => write!(f, "OpenAPI"),
181 Self::Generic => write!(f, "Generic YAML"),
182 }
183 }
184}
185
186pub trait ConfigValidator {
225 fn yaml_type(&self) -> YamlType;
227
228 fn validate_structure(&self) -> Vec<Diagnostic>;
234
235 fn validate_semantics(&self) -> Vec<Diagnostic>;
241
242 fn validate(&self) -> ValidationResult {
244 let mut diags = self.validate_structure();
245 diags.extend(self.validate_semantics());
246 ValidationResult::from_diagnostics(self.yaml_type(), diags)
247 }
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct RepairResult {
255 pub valid: bool,
257 pub repaired_yaml: String,
259 pub errors: Vec<String>,
261 pub warnings: Vec<String>,
263 pub llm_fields: Vec<String>,
265 pub summary: String,
267}
268
269#[allow(missing_docs)]
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct FixRecord {
273 pub field_path: String,
274 pub old_value: serde_json::Value,
275 pub new_value: serde_json::Value,
276 pub reason: String,
277}
278
279#[allow(missing_docs)]
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct AmbiguityQuestion {
283 pub field_path: String,
284 pub question: String,
285 pub options: Vec<String>,
286 #[serde(default)]
287 pub default: Option<String>,
288}
289
290#[allow(missing_docs)]
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct ValidationSession {
294 pub session_id: String,
295 pub yaml_original: String,
296 pub yaml_current: String,
297 pub fixes_applied: Vec<FixRecord>,
298 pub pending_ambiguities: Vec<AmbiguityQuestion>,
299 pub status: SessionStatus,
300 pub created_at: String,
301}
302
303#[allow(missing_docs)]
305#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
306#[serde(rename_all = "lowercase")]
307pub enum SessionStatus {
308 InProgress,
309 WaitingUser,
310 Completed,
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_severity_ordering() {
319 assert!(Severity::Error < Severity::Warning);
323 assert!(Severity::Warning < Severity::Info);
324 assert!(Severity::Info < Severity::Hint);
325 }
326
327 #[test]
328 fn test_severity_serde_roundtrip() {
329 let sev = Severity::Warning;
330 let json = serde_json::to_string(&sev).unwrap();
331 assert_eq!(json, r#""warning""#);
332 let parsed: Severity = serde_json::from_str(&json).unwrap();
333 assert_eq!(parsed, Severity::Warning);
334 }
335
336 #[test]
337 fn test_yaml_type_display() {
338 assert_eq!(format!("{}", YamlType::K8sDeployment), "K8s Deployment");
339 assert_eq!(format!("{}", YamlType::DockerCompose), "Docker Compose");
340 assert_eq!(format!("{}", YamlType::GitLabCI), "GitLab CI");
341 assert_eq!(format!("{}", YamlType::GitHubActions), "GitHub Actions");
342 assert_eq!(format!("{}", YamlType::Prometheus), "Prometheus");
343 assert_eq!(format!("{}", YamlType::Generic), "Generic YAML");
344 }
345
346 #[test]
347 fn test_yaml_type_serde_roundtrip() {
348 let yt = YamlType::K8sService;
349 let json = serde_json::to_string(&yt).unwrap();
350 let parsed: YamlType = serde_json::from_str(&json).unwrap();
351 assert_eq!(parsed, YamlType::K8sService);
352 }
353
354 #[test]
355 fn test_validation_result_from_diagnostics() {
356 let diags = vec![
357 Diagnostic {
358 severity: Severity::Error,
359 message: "Test error".to_string(),
360 path: Some("spec".to_string()),
361 },
362 Diagnostic {
363 severity: Severity::Warning,
364 message: "Test warning".to_string(),
365 path: None,
366 },
367 Diagnostic {
368 severity: Severity::Info,
369 message: "Info message".to_string(),
370 path: None,
371 },
372 ];
373
374 let result = ValidationResult::from_diagnostics(YamlType::K8sDeployment, diags);
375
376 assert!(!result.valid);
377 assert_eq!(result.errors.len(), 1);
378 assert_eq!(result.warnings.len(), 1);
379 assert_eq!(result.hints.len(), 1);
380 assert_eq!(result.diagnostics.len(), 3);
381 assert_eq!(result.yaml_type, Some(YamlType::K8sDeployment));
382 }
383
384 #[test]
385 fn test_validation_result_valid_when_no_errors() {
386 let diags = vec![
387 Diagnostic {
388 severity: Severity::Warning,
389 message: "Warning only".to_string(),
390 path: None,
391 },
392 ];
393
394 let result = ValidationResult::from_diagnostics(YamlType::DockerCompose, diags);
395
396 assert!(result.valid);
397 assert!(result.errors.is_empty());
398 assert_eq!(result.warnings.len(), 1);
399 }
400
401 #[test]
402 fn test_diagnostic_path_default() {
403 let diag = Diagnostic {
404 severity: Severity::Error,
405 message: "Error".to_string(),
406 path: None,
407 };
408 assert!(diag.path.is_none());
409
410 let diag_with_path = Diagnostic {
411 severity: Severity::Error,
412 message: "Error".to_string(),
413 path: Some("spec > containers > 0".to_string()),
414 };
415 assert_eq!(diag_with_path.path, Some("spec > containers > 0".to_string()));
416 }
417
418 #[test]
419 fn test_session_status_serde() {
420 let status = SessionStatus::InProgress;
421 let json = serde_json::to_string(&status).unwrap();
422 assert_eq!(json, r#""inprogress""#);
424 let parsed: SessionStatus = serde_json::from_str(&json).unwrap();
425 assert_eq!(parsed, SessionStatus::InProgress);
426 }
427}