systemprompt_traits/
validation_report.rs

1//! Unified validation report types for startup validation.
2//!
3//! These types provide a consistent structure for reporting validation
4//! errors and warnings across all domains and extensions.
5
6use std::path::PathBuf;
7
8/// A single validation error with actionable information.
9#[derive(Debug, Clone)]
10pub struct ValidationError {
11    /// The field or config key that failed validation
12    pub field: String,
13
14    /// Human-readable error message
15    pub message: String,
16
17    /// Optional path to the file/directory that caused the error
18    pub path: Option<PathBuf>,
19
20    /// Optional suggestion for how to fix the error
21    pub suggestion: Option<String>,
22}
23
24impl ValidationError {
25    #[must_use]
26    pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
27        Self {
28            field: field.into(),
29            message: message.into(),
30            path: None,
31            suggestion: None,
32        }
33    }
34
35    #[must_use]
36    pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
37        self.path = Some(path.into());
38        self
39    }
40
41    #[must_use]
42    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
43        self.suggestion = Some(suggestion.into());
44        self
45    }
46}
47
48impl std::fmt::Display for ValidationError {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(f, "{}\n  {}", self.field, self.message)?;
51        if let Some(ref path) = self.path {
52            write!(f, "\n  Path: {}", path.display())?;
53        }
54        if let Some(ref suggestion) = self.suggestion {
55            write!(f, "\n  To fix: {}", suggestion)?;
56        }
57        Ok(())
58    }
59}
60
61/// A validation warning (non-fatal).
62#[derive(Debug, Clone)]
63pub struct ValidationWarning {
64    /// The field or config key that triggered the warning
65    pub field: String,
66
67    /// Human-readable warning message
68    pub message: String,
69
70    /// Optional suggestion for how to address the warning
71    pub suggestion: Option<String>,
72}
73
74impl ValidationWarning {
75    #[must_use]
76    pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
77        Self {
78            field: field.into(),
79            message: message.into(),
80            suggestion: None,
81        }
82    }
83
84    #[must_use]
85    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
86        self.suggestion = Some(suggestion.into());
87        self
88    }
89}
90
91/// Validation report for a single domain or component.
92#[derive(Debug, Clone, Default)]
93pub struct ValidationReport {
94    /// Domain identifier (e.g., "paths", "web", "content", "mcp")
95    pub domain: String,
96
97    /// List of errors (fatal)
98    pub errors: Vec<ValidationError>,
99
100    /// List of warnings (non-fatal)
101    pub warnings: Vec<ValidationWarning>,
102}
103
104impl ValidationReport {
105    #[must_use]
106    pub fn new(domain: impl Into<String>) -> Self {
107        Self {
108            domain: domain.into(),
109            errors: Vec::new(),
110            warnings: Vec::new(),
111        }
112    }
113
114    pub fn add_error(&mut self, error: ValidationError) {
115        self.errors.push(error);
116    }
117
118    pub fn add_warning(&mut self, warning: ValidationWarning) {
119        self.warnings.push(warning);
120    }
121
122    pub fn has_errors(&self) -> bool {
123        !self.errors.is_empty()
124    }
125
126    pub fn has_warnings(&self) -> bool {
127        !self.warnings.is_empty()
128    }
129
130    pub fn is_clean(&self) -> bool {
131        self.errors.is_empty() && self.warnings.is_empty()
132    }
133
134    /// Merge another report into this one.
135    pub fn merge(&mut self, other: Self) {
136        self.errors.extend(other.errors);
137        self.warnings.extend(other.warnings);
138    }
139}
140
141/// Complete startup validation report.
142#[derive(Debug, Clone, Default)]
143pub struct StartupValidationReport {
144    /// Path to the profile that was validated
145    pub profile_path: Option<PathBuf>,
146
147    /// Reports from each domain
148    pub domains: Vec<ValidationReport>,
149
150    /// Reports from extensions
151    pub extensions: Vec<ValidationReport>,
152}
153
154impl StartupValidationReport {
155    #[must_use]
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    #[must_use]
161    pub fn with_profile_path(mut self, path: impl Into<PathBuf>) -> Self {
162        self.profile_path = Some(path.into());
163        self
164    }
165
166    pub fn add_domain(&mut self, report: ValidationReport) {
167        self.domains.push(report);
168    }
169
170    pub fn add_extension(&mut self, report: ValidationReport) {
171        self.extensions.push(report);
172    }
173
174    pub fn has_errors(&self) -> bool {
175        self.domains.iter().any(ValidationReport::has_errors)
176            || self.extensions.iter().any(ValidationReport::has_errors)
177    }
178
179    pub fn has_warnings(&self) -> bool {
180        self.domains.iter().any(ValidationReport::has_warnings)
181            || self.extensions.iter().any(ValidationReport::has_warnings)
182    }
183
184    pub fn error_count(&self) -> usize {
185        self.domains.iter().map(|r| r.errors.len()).sum::<usize>()
186            + self
187                .extensions
188                .iter()
189                .map(|r| r.errors.len())
190                .sum::<usize>()
191    }
192
193    pub fn warning_count(&self) -> usize {
194        self.domains.iter().map(|r| r.warnings.len()).sum::<usize>()
195            + self
196                .extensions
197                .iter()
198                .map(|r| r.warnings.len())
199                .sum::<usize>()
200    }
201}
202
203impl std::fmt::Display for StartupValidationReport {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        write!(
206            f,
207            "{} error(s), {} warning(s)",
208            self.error_count(),
209            self.warning_count()
210        )
211    }
212}
213
214/// Error type for startup validation failures.
215#[derive(Debug, thiserror::Error)]
216#[error("Startup validation failed with {0}")]
217pub struct StartupValidationError(pub StartupValidationReport);
218
219impl From<StartupValidationReport> for StartupValidationError {
220    fn from(report: StartupValidationReport) -> Self {
221        Self(report)
222    }
223}