adk_doc_audit/
config.rs

1use crate::error::{AuditError, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use std::time::Duration;
5
6/// Configuration for the documentation audit system.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AuditConfig {
9    /// Path to the workspace root containing Cargo.toml
10    pub workspace_path: PathBuf,
11
12    /// Path to the documentation directory
13    pub docs_path: PathBuf,
14
15    /// Files to exclude from audit (glob patterns)
16    pub excluded_files: Vec<String>,
17
18    /// Crates to exclude from analysis
19    pub excluded_crates: Vec<String>,
20
21    /// Minimum severity level to report
22    pub severity_threshold: IssueSeverity,
23
24    /// Whether to fail CI/CD on critical issues
25    pub fail_on_critical: bool,
26
27    /// Timeout for compiling code examples
28    pub example_timeout: Duration,
29
30    /// Output format for reports
31    pub output_format: OutputFormat,
32
33    /// Path to audit database (for incremental audits)
34    pub database_path: Option<PathBuf>,
35
36    /// Enable verbose logging
37    pub verbose: bool,
38
39    /// Enable quiet mode (minimal output)
40    pub quiet: bool,
41}
42
43/// Severity levels for audit issues.
44#[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/// Output formats for audit reports.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
56pub enum OutputFormat {
57    #[default]
58    Console,
59    Json,
60    Markdown,
61}
62
63/// Builder for creating AuditConfig instances.
64#[derive(Debug, Clone, Default)]
65pub struct AuditConfigBuilder {
66    config: AuditConfig,
67}
68
69impl AuditConfigBuilder {
70    /// Create a new builder with default values.
71    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    /// Set the workspace path.
95    pub fn workspace_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
96        self.config.workspace_path = path.into();
97        self
98    }
99
100    /// Set the documentation path.
101    pub fn docs_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
102        self.config.docs_path = path.into();
103        self
104    }
105
106    /// Add files to exclude from audit.
107    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    /// Add crates to exclude from analysis.
117    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    /// Set the severity threshold.
127    pub fn severity_threshold(mut self, threshold: IssueSeverity) -> Self {
128        self.config.severity_threshold = threshold;
129        self
130    }
131
132    /// Set whether to fail on critical issues.
133    pub fn fail_on_critical(mut self, fail: bool) -> Self {
134        self.config.fail_on_critical = fail;
135        self
136    }
137
138    /// Set the timeout for compiling examples.
139    pub fn example_timeout(mut self, timeout: Duration) -> Self {
140        self.config.example_timeout = timeout;
141        self
142    }
143
144    /// Set the output format.
145    pub fn output_format(mut self, format: OutputFormat) -> Self {
146        self.config.output_format = format;
147        self
148    }
149
150    /// Set the database path for incremental audits.
151    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    /// Enable verbose logging.
157    pub fn verbose(mut self, verbose: bool) -> Self {
158        self.config.verbose = verbose;
159        self
160    }
161
162    /// Enable quiet mode.
163    pub fn quiet(mut self, quiet: bool) -> Self {
164        self.config.quiet = quiet;
165        self
166    }
167
168    /// Build the configuration, validating settings.
169    pub fn build(self) -> Result<AuditConfig> {
170        let config = self.config;
171
172        // Validate configuration
173        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    /// Create a new builder.
204    pub fn builder() -> AuditConfigBuilder {
205        AuditConfigBuilder::new()
206    }
207
208    /// Load configuration from a TOML file.
209    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    /// Save configuration to a TOML file.
221    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    /// Get the default database path if none is configured.
235    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        // Create a basic config without validation for default case
243        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        // Create temporary directories for testing
272        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        // Create the directories
277        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        // Clean up
293        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        // This will fail because paths don't exist, but we can test the builder logic
309        assert!(config.is_err());
310    }
311
312    #[test]
313    fn test_config_validation_errors() {
314        // Test conflicting verbose/quiet
315        let result = AuditConfig::builder().verbose(true).quiet(true).build();
316        assert!(result.is_err());
317
318        // Test zero timeout
319        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}