1use crate::error::{AuditError, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use std::time::Duration;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AuditConfig {
9 pub workspace_path: PathBuf,
11
12 pub docs_path: PathBuf,
14
15 pub excluded_files: Vec<String>,
17
18 pub excluded_crates: Vec<String>,
20
21 pub severity_threshold: IssueSeverity,
23
24 pub fail_on_critical: bool,
26
27 pub example_timeout: Duration,
29
30 pub output_format: OutputFormat,
32
33 pub database_path: Option<PathBuf>,
35
36 pub verbose: bool,
38
39 pub quiet: bool,
41}
42
43#[derive(
45 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
46)]
47pub enum IssueSeverity {
48 Info,
49 #[default]
50 Warning,
51 Critical,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
56pub enum OutputFormat {
57 #[default]
58 Console,
59 Json,
60 Markdown,
61}
62
63#[derive(Debug, Clone, Default)]
65pub struct AuditConfigBuilder {
66 config: AuditConfig,
67}
68
69impl AuditConfigBuilder {
70 pub fn new() -> Self {
72 Self {
73 config: AuditConfig {
74 workspace_path: PathBuf::from("."),
75 docs_path: PathBuf::from("docs"),
76 excluded_files: vec![
77 "*.tmp".to_string(),
78 "*.bak".to_string(),
79 ".git/**".to_string(),
80 "target/**".to_string(),
81 ],
82 excluded_crates: vec![],
83 severity_threshold: IssueSeverity::default(),
84 fail_on_critical: true,
85 example_timeout: Duration::from_secs(30),
86 output_format: OutputFormat::default(),
87 database_path: None,
88 verbose: false,
89 quiet: false,
90 },
91 }
92 }
93
94 pub fn workspace_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
96 self.config.workspace_path = path.into();
97 self
98 }
99
100 pub fn docs_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
102 self.config.docs_path = path.into();
103 self
104 }
105
106 pub fn exclude_files<I, S>(mut self, patterns: I) -> Self
108 where
109 I: IntoIterator<Item = S>,
110 S: Into<String>,
111 {
112 self.config.excluded_files.extend(patterns.into_iter().map(Into::into));
113 self
114 }
115
116 pub fn exclude_crates<I, S>(mut self, crates: I) -> Self
118 where
119 I: IntoIterator<Item = S>,
120 S: Into<String>,
121 {
122 self.config.excluded_crates.extend(crates.into_iter().map(Into::into));
123 self
124 }
125
126 pub fn severity_threshold(mut self, threshold: IssueSeverity) -> Self {
128 self.config.severity_threshold = threshold;
129 self
130 }
131
132 pub fn fail_on_critical(mut self, fail: bool) -> Self {
134 self.config.fail_on_critical = fail;
135 self
136 }
137
138 pub fn example_timeout(mut self, timeout: Duration) -> Self {
140 self.config.example_timeout = timeout;
141 self
142 }
143
144 pub fn output_format(mut self, format: OutputFormat) -> Self {
146 self.config.output_format = format;
147 self
148 }
149
150 pub fn database_path<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
152 self.config.database_path = path.map(Into::into);
153 self
154 }
155
156 pub fn verbose(mut self, verbose: bool) -> Self {
158 self.config.verbose = verbose;
159 self
160 }
161
162 pub fn quiet(mut self, quiet: bool) -> Self {
164 self.config.quiet = quiet;
165 self
166 }
167
168 pub fn build(self) -> Result<AuditConfig> {
170 let config = self.config;
171
172 if !config.workspace_path.exists() {
174 return Err(AuditError::WorkspaceNotFound { path: config.workspace_path });
175 }
176
177 if !config.docs_path.exists() {
178 return Err(AuditError::ConfigurationError {
179 message: format!(
180 "Documentation path does not exist: {}",
181 config.docs_path.display()
182 ),
183 });
184 }
185
186 if config.verbose && config.quiet {
187 return Err(AuditError::ConfigurationError {
188 message: "Cannot enable both verbose and quiet modes".to_string(),
189 });
190 }
191
192 if config.example_timeout.as_secs() == 0 {
193 return Err(AuditError::ConfigurationError {
194 message: "Example timeout must be greater than 0".to_string(),
195 });
196 }
197
198 Ok(config)
199 }
200}
201
202impl AuditConfig {
203 pub fn builder() -> AuditConfigBuilder {
205 AuditConfigBuilder::new()
206 }
207
208 pub fn from_file<P: Into<PathBuf>>(path: P) -> Result<Self> {
210 let path = path.into();
211 let content = std::fs::read_to_string(&path)
212 .map_err(|e| AuditError::IoError { path: path.clone(), details: e.to_string() })?;
213
214 let config: AuditConfig = toml::from_str(&content)
215 .map_err(|e| AuditError::TomlError { file_path: path, details: e.to_string() })?;
216
217 Ok(config)
218 }
219
220 pub fn save_to_file<P: Into<PathBuf>>(&self, path: P) -> Result<()> {
222 let path = path.into();
223 let content = toml::to_string_pretty(self).map_err(|e| AuditError::TomlError {
224 file_path: path.clone(),
225 details: e.to_string(),
226 })?;
227
228 std::fs::write(&path, content)
229 .map_err(|e| AuditError::IoError { path, details: e.to_string() })?;
230
231 Ok(())
232 }
233
234 pub fn get_database_path(&self) -> PathBuf {
236 self.database_path.clone().unwrap_or_else(|| self.workspace_path.join(".adk-doc-audit.db"))
237 }
238}
239
240impl Default for AuditConfig {
241 fn default() -> Self {
242 AuditConfig {
244 workspace_path: PathBuf::from("."),
245 docs_path: PathBuf::from("docs"),
246 excluded_files: vec![
247 "*.tmp".to_string(),
248 "*.bak".to_string(),
249 ".git/**".to_string(),
250 "target/**".to_string(),
251 ],
252 excluded_crates: vec![],
253 severity_threshold: IssueSeverity::default(),
254 fail_on_critical: true,
255 example_timeout: Duration::from_secs(30),
256 output_format: OutputFormat::default(),
257 database_path: None,
258 verbose: false,
259 quiet: false,
260 }
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use std::time::Duration;
268
269 #[test]
270 fn test_config_builder_default() {
271 let temp_dir = std::env::temp_dir();
273 let workspace_path = temp_dir.join("test_workspace");
274 let docs_path = temp_dir.join("test_docs");
275
276 std::fs::create_dir_all(&workspace_path).unwrap();
278 std::fs::create_dir_all(&docs_path).unwrap();
279
280 let config =
281 AuditConfig::builder().workspace_path(&workspace_path).docs_path(&docs_path).build();
282
283 assert!(config.is_ok());
284
285 let config = config.unwrap();
286 assert_eq!(config.workspace_path, workspace_path);
287 assert_eq!(config.docs_path, docs_path);
288 assert_eq!(config.severity_threshold, IssueSeverity::Warning);
289 assert!(config.fail_on_critical);
290 assert_eq!(config.example_timeout, Duration::from_secs(30));
291
292 std::fs::remove_dir_all(&workspace_path).ok();
294 std::fs::remove_dir_all(&docs_path).ok();
295 }
296
297 #[test]
298 fn test_config_builder_customization() {
299 let config = AuditConfig::builder()
300 .workspace_path("/tmp/workspace")
301 .docs_path("/tmp/docs")
302 .severity_threshold(IssueSeverity::Critical)
303 .fail_on_critical(false)
304 .example_timeout(Duration::from_secs(60))
305 .verbose(true)
306 .build();
307
308 assert!(config.is_err());
310 }
311
312 #[test]
313 fn test_config_validation_errors() {
314 let result = AuditConfig::builder().verbose(true).quiet(true).build();
316 assert!(result.is_err());
317
318 let result = AuditConfig::builder().example_timeout(Duration::from_secs(0)).build();
320 assert!(result.is_err());
321 }
322
323 #[test]
324 fn test_severity_ordering() {
325 assert!(IssueSeverity::Info < IssueSeverity::Warning);
326 assert!(IssueSeverity::Warning < IssueSeverity::Critical);
327 }
328}