use crate::error::{AuditError, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditConfig {
pub workspace_path: PathBuf,
pub docs_path: PathBuf,
pub excluded_files: Vec<String>,
pub excluded_crates: Vec<String>,
pub severity_threshold: IssueSeverity,
pub fail_on_critical: bool,
pub example_timeout: Duration,
pub output_format: OutputFormat,
pub database_path: Option<PathBuf>,
pub verbose: bool,
pub quiet: bool,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
pub enum IssueSeverity {
Info,
#[default]
Warning,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum OutputFormat {
#[default]
Console,
Json,
Markdown,
}
#[derive(Debug, Clone, Default)]
pub struct AuditConfigBuilder {
config: AuditConfig,
}
impl AuditConfigBuilder {
pub fn new() -> Self {
Self {
config: AuditConfig {
workspace_path: PathBuf::from("."),
docs_path: PathBuf::from("docs"),
excluded_files: vec![
"*.tmp".to_string(),
"*.bak".to_string(),
".git/**".to_string(),
"target/**".to_string(),
],
excluded_crates: vec![],
severity_threshold: IssueSeverity::default(),
fail_on_critical: true,
example_timeout: Duration::from_secs(30),
output_format: OutputFormat::default(),
database_path: None,
verbose: false,
quiet: false,
},
}
}
pub fn workspace_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.config.workspace_path = path.into();
self
}
pub fn docs_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.config.docs_path = path.into();
self
}
pub fn exclude_files<I, S>(mut self, patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.config.excluded_files.extend(patterns.into_iter().map(Into::into));
self
}
pub fn exclude_crates<I, S>(mut self, crates: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.config.excluded_crates.extend(crates.into_iter().map(Into::into));
self
}
pub fn severity_threshold(mut self, threshold: IssueSeverity) -> Self {
self.config.severity_threshold = threshold;
self
}
pub fn fail_on_critical(mut self, fail: bool) -> Self {
self.config.fail_on_critical = fail;
self
}
pub fn example_timeout(mut self, timeout: Duration) -> Self {
self.config.example_timeout = timeout;
self
}
pub fn output_format(mut self, format: OutputFormat) -> Self {
self.config.output_format = format;
self
}
pub fn database_path<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
self.config.database_path = path.map(Into::into);
self
}
pub fn verbose(mut self, verbose: bool) -> Self {
self.config.verbose = verbose;
self
}
pub fn quiet(mut self, quiet: bool) -> Self {
self.config.quiet = quiet;
self
}
pub fn build(self) -> Result<AuditConfig> {
let config = self.config;
if !config.workspace_path.exists() {
return Err(AuditError::WorkspaceNotFound { path: config.workspace_path });
}
if !config.docs_path.exists() {
return Err(AuditError::ConfigurationError {
message: format!(
"Documentation path does not exist: {}",
config.docs_path.display()
),
});
}
if config.verbose && config.quiet {
return Err(AuditError::ConfigurationError {
message: "Cannot enable both verbose and quiet modes".to_string(),
});
}
if config.example_timeout.as_secs() == 0 {
return Err(AuditError::ConfigurationError {
message: "Example timeout must be greater than 0".to_string(),
});
}
Ok(config)
}
}
impl AuditConfig {
pub fn builder() -> AuditConfigBuilder {
AuditConfigBuilder::new()
}
pub fn from_file<P: Into<PathBuf>>(path: P) -> Result<Self> {
let path = path.into();
let content = std::fs::read_to_string(&path)
.map_err(|e| AuditError::IoError { path: path.clone(), details: e.to_string() })?;
let config: AuditConfig = toml::from_str(&content)
.map_err(|e| AuditError::TomlError { file_path: path, details: e.to_string() })?;
Ok(config)
}
pub fn save_to_file<P: Into<PathBuf>>(&self, path: P) -> Result<()> {
let path = path.into();
let content = toml::to_string_pretty(self).map_err(|e| AuditError::TomlError {
file_path: path.clone(),
details: e.to_string(),
})?;
std::fs::write(&path, content)
.map_err(|e| AuditError::IoError { path, details: e.to_string() })?;
Ok(())
}
pub fn get_database_path(&self) -> PathBuf {
self.database_path.clone().unwrap_or_else(|| self.workspace_path.join(".adk-doc-audit.db"))
}
}
impl Default for AuditConfig {
fn default() -> Self {
AuditConfig {
workspace_path: PathBuf::from("."),
docs_path: PathBuf::from("docs"),
excluded_files: vec![
"*.tmp".to_string(),
"*.bak".to_string(),
".git/**".to_string(),
"target/**".to_string(),
],
excluded_crates: vec![],
severity_threshold: IssueSeverity::default(),
fail_on_critical: true,
example_timeout: Duration::from_secs(30),
output_format: OutputFormat::default(),
database_path: None,
verbose: false,
quiet: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_config_builder_default() {
let temp_dir = std::env::temp_dir();
let workspace_path = temp_dir.join("test_workspace");
let docs_path = temp_dir.join("test_docs");
std::fs::create_dir_all(&workspace_path).unwrap();
std::fs::create_dir_all(&docs_path).unwrap();
let config =
AuditConfig::builder().workspace_path(&workspace_path).docs_path(&docs_path).build();
assert!(config.is_ok());
let config = config.unwrap();
assert_eq!(config.workspace_path, workspace_path);
assert_eq!(config.docs_path, docs_path);
assert_eq!(config.severity_threshold, IssueSeverity::Warning);
assert!(config.fail_on_critical);
assert_eq!(config.example_timeout, Duration::from_secs(30));
std::fs::remove_dir_all(&workspace_path).ok();
std::fs::remove_dir_all(&docs_path).ok();
}
#[test]
fn test_config_builder_customization() {
let config = AuditConfig::builder()
.workspace_path("/tmp/workspace")
.docs_path("/tmp/docs")
.severity_threshold(IssueSeverity::Critical)
.fail_on_critical(false)
.example_timeout(Duration::from_secs(60))
.verbose(true)
.build();
assert!(config.is_err());
}
#[test]
fn test_config_validation_errors() {
let result = AuditConfig::builder().verbose(true).quiet(true).build();
assert!(result.is_err());
let result = AuditConfig::builder().example_timeout(Duration::from_secs(0)).build();
assert!(result.is_err());
}
#[test]
fn test_severity_ordering() {
assert!(IssueSeverity::Info < IssueSeverity::Warning);
assert!(IssueSeverity::Warning < IssueSeverity::Critical);
}
}