use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[allow(clippy::struct_excessive_bools)] pub struct Config {
#[serde(default)]
pub rules: HashMap<String, RuleConfig>,
#[serde(default = "default_default_enabled")]
pub default_enabled: bool,
#[serde(default)]
pub custom_rules: Vec<String>,
#[serde(default = "default_gitignore")]
pub gitignore: bool,
#[serde(default)]
pub front_matter: Option<String>,
#[serde(default)]
pub no_inline_config: bool,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default = "default_fix")]
pub fix: bool,
}
fn default_default_enabled() -> bool {
true
}
fn default_gitignore() -> bool {
true
}
fn default_fix() -> bool {
true
}
impl Default for Config {
fn default() -> Self {
Self {
rules: HashMap::new(),
default_enabled: true,
custom_rules: Vec::new(),
gitignore: default_gitignore(),
front_matter: None,
no_inline_config: false,
exclude: Vec::new(),
fix: true,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RuleConfig {
Enabled(bool),
Config(HashMap<String, toml::Value>),
}
impl Config {
#[must_use]
pub fn config(&self) -> &HashMap<String, RuleConfig> {
&self.rules
}
#[must_use]
pub fn apply_rule_filters(mut self, select: &[String], ignore: &[String]) -> Self {
let select_all = select.iter().any(|code| code.eq_ignore_ascii_case("all"));
if !select.is_empty() && !select_all {
self.default_enabled = false;
for code in select {
self.rules
.entry(code.to_uppercase())
.or_insert(RuleConfig::Enabled(true));
}
}
for code in ignore {
self.rules
.insert(code.to_uppercase(), RuleConfig::Enabled(false));
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn select_empty_is_noop() {
let config = Config::default().apply_rule_filters(&[], &[]);
assert!(config.default_enabled);
assert!(config.rules.is_empty());
}
#[test]
fn select_restricts_to_listed_rules() {
let config = Config::default().apply_rule_filters(&["md001".to_owned()], &[]);
assert!(!config.default_enabled);
assert!(matches!(
config.rules.get("MD001"),
Some(RuleConfig::Enabled(true))
));
assert_eq!(config.rules.len(), 1);
}
#[test]
fn select_all_is_noop() {
let config = Config::default().apply_rule_filters(&["ALL".to_owned()], &[]);
assert!(config.default_enabled);
assert!(config.rules.is_empty());
}
#[test]
fn select_preserves_existing_rule_config() {
let mut base = Config::default();
let mut params = HashMap::new();
params.insert("line_length".to_owned(), toml::Value::Integer(100));
base.rules
.insert("MD013".to_owned(), RuleConfig::Config(params));
let config = base.apply_rule_filters(&["MD013".to_owned()], &[]);
match config.rules.get("MD013") {
Some(RuleConfig::Config(params)) => {
assert_eq!(params.get("line_length"), Some(&toml::Value::Integer(100)));
}
other => panic!("expected preserved MD013 config, got {other:?}"),
}
}
#[test]
fn ignore_force_disables_rule() {
let mut base = Config::default();
base.rules
.insert("MD013".to_owned(), RuleConfig::Enabled(true));
let config = base.apply_rule_filters(&[], &["md013".to_owned()]);
assert!(matches!(
config.rules.get("MD013"),
Some(RuleConfig::Enabled(false))
));
}
#[test]
fn ignore_wins_over_select() {
let config = Config::default().apply_rule_filters(
&["MD001".to_owned(), "MD013".to_owned()],
&["MD013".to_owned()],
);
assert!(matches!(
config.rules.get("MD001"),
Some(RuleConfig::Enabled(true))
));
assert!(matches!(
config.rules.get("MD013"),
Some(RuleConfig::Enabled(false))
));
}
}