use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::io;
#[derive(Debug, Deserialize, Default)]
pub struct RuleConfig {
#[serde(flatten)]
pub values: HashMap<String, toml::Value>,
}
#[derive(Debug, Deserialize, Default)]
pub struct Config {
#[serde(default)]
pub global: GlobalConfig,
#[serde(flatten)]
pub rules: HashMap<String, RuleConfig>,
}
#[derive(Debug, Deserialize, Default)]
pub struct GlobalConfig {
#[serde(default)]
pub disable: Vec<String>,
#[serde(default)]
pub enable: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub respect_gitignore: bool,
}
pub fn load_config(config_path: Option<&str>) -> Result<Config, ConfigError> {
if let Some(path) = config_path {
return load_config_from_file(path);
}
for filename in ["rumdl.toml", ".rumdl.toml"] {
if Path::new(filename).exists() {
return load_config_from_file(filename);
}
}
Ok(Config::default())
}
fn load_config_from_file(path: &str) -> Result<Config, ConfigError> {
match fs::read_to_string(path) {
Ok(content) => {
let config: Config = toml::from_str(&content)?;
Ok(config)
}
Err(err) => Err(ConfigError::IoError { source: err, path: path.to_string() }),
}
}
pub fn create_default_config(path: &str) -> Result<(), ConfigError> {
if Path::new(path).exists() {
return Err(ConfigError::FileExists { path: path.to_string() });
}
let default_config = r#"# rumdl configuration file
# Global configuration options
[global]
# List of rules to disable (uncomment and modify as needed)
# disable = ["MD013", "MD033"]
# List of rules to enable exclusively (if provided, only these rules will run)
# enable = ["MD001", "MD003", "MD004"]
# List of file/directory patterns to exclude from linting
exclude = [
# Common directories to exclude
".git",
".github",
"node_modules",
"vendor",
"dist",
"build",
# Specific files or patterns
"CHANGELOG.md",
"LICENSE.md",
]
# Respect .gitignore files when scanning directories
respect_gitignore = true
# Rule-specific configurations (uncomment and modify as needed)
# [MD003]
# style = "atx" # Heading style (atx, atx_closed, setext)
# [MD004]
# style = "asterisk" # Unordered list style (asterisk, plus, dash, consistent)
# [MD007]
# indent = 4 # Unordered list indentation
# [MD013]
# line_length = 100 # Line length
# code_blocks = false # Exclude code blocks from line length check
# tables = false # Exclude tables from line length check
# headings = true # Include headings in line length check
# [MD044]
# names = ["rumdl", "Markdown", "GitHub"] # Proper names that should be capitalized correctly
# code_blocks_excluded = true # Exclude code blocks from proper name check
"#;
match fs::write(path, default_config) {
Ok(_) => Ok(()),
Err(err) => Err(ConfigError::IoError { source: err, path: path.to_string() }),
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Failed to read config file at {path}: {source}")]
IoError {
source: io::Error,
path: String,
},
#[error("Failed to parse TOML: {0}")]
ParseError(#[from] toml::de::Error),
#[error("Configuration file already exists at {path}")]
FileExists {
path: String,
},
}
pub fn get_rule_config_value<T: serde::de::DeserializeOwned>(
config: &Config,
rule_name: &str,
key: &str,
) -> Option<T> {
config
.rules
.get(rule_name)
.and_then(|rule_config| rule_config.values.get(key))
.and_then(|value| T::deserialize(value.clone()).ok())
}