use std::collections::BTreeMap;
use serde::Deserialize;
use crate::config;
const PRESET_FILES: &[(&str, &str)] = &[
(
"conventional_commits",
include_str!("presets_data/conventional_commits.toml"),
),
(
"title_body_separator",
include_str!("presets_data/title_body_separator.toml"),
),
("forbid_wip", include_str!("presets_data/forbid_wip.toml")),
(
"security_related_edits_mention",
include_str!("presets_data/security_related_edits_mention.toml"),
),
];
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PresetConfig {
#[serde(default)]
assertions: Vec<config::Assertion>,
}
fn load_presets() -> Result<BTreeMap<String, Vec<config::Assertion>>, config::ConfigError> {
let mut presets = BTreeMap::new();
for (name, raw_content) in PRESET_FILES {
let parsed: PresetConfig =
toml::from_str(raw_content).map_err(config::ConfigError::Toml)?;
config::validate_assertions(&parsed.assertions)?;
if presets
.insert((*name).to_owned(), parsed.assertions)
.is_some()
{
return Err(config::ConfigError::Semantic(format!(
"duplicate embedded preset name: '{name}'"
)));
}
}
Ok(presets)
}
fn normalize_cli_preset_name(name: &str) -> String {
name.trim().to_ascii_lowercase().replace('-', "_")
}
pub fn validate_cli_preset_names(names: &[String]) -> Result<(), config::ConfigError> {
for name in names {
let trimmed = name.trim();
if trimmed.is_empty() {
return Err(config::ConfigError::Semantic(
"preset name cannot be empty".to_owned(),
));
}
if !trimmed.chars().all(|character| {
character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-'
}) {
return Err(config::ConfigError::Semantic(format!(
"invalid preset name '{trimmed}': use lowercase letters, digits, and dashes"
)));
}
}
Ok(())
}
pub fn select_assertions_from_presets(
selected_names: &[String],
) -> Result<Vec<config::Assertion>, config::ConfigError> {
let registry = load_presets()?;
let mut merged = Vec::new();
for name in selected_names {
let normalized = normalize_cli_preset_name(name);
let Some(assertions) = registry.get(&normalized) else {
return Err(config::ConfigError::Semantic(format!(
"unknown preset: '{name}'"
)));
};
for assertion in assertions {
merged.push(config::Assertion {
alias: assertion.alias.clone(),
skip: assertion.skip,
description: assertion.description.clone(),
banner: assertion.banner.clone(),
hint: assertion.hint.clone(),
severity: assertion.severity,
must_satisfy: config::ConditionContainer {
condition: clone_condition(&assertion.must_satisfy.condition),
},
skip_if: assertion
.skip_if
.as_ref()
.map(|skip_if| config::ConditionContainer {
condition: clone_condition(&skip_if.condition),
}),
custom_meta: assertion.custom_meta.clone(),
});
}
}
Ok(merged)
}
fn clone_condition(condition: &config::Condition) -> config::Condition {
match condition {
config::Condition::MsgMatchAny(value) => {
config::Condition::MsgMatchAny(config::MsgMatchCondition {
name: value.name.clone(),
mode: match value.mode {
config::MsgMode::Raw => config::MsgMode::Raw,
config::MsgMode::Title => config::MsgMode::Title,
config::MsgMode::Body => config::MsgMode::Body,
},
patterns: value.patterns.clone(),
})
}
config::Condition::MsgMatchNone(value) => {
config::Condition::MsgMatchNone(config::MsgMatchCondition {
name: value.name.clone(),
mode: match value.mode {
config::MsgMode::Raw => config::MsgMode::Raw,
config::MsgMode::Title => config::MsgMode::Title,
config::MsgMode::Body => config::MsgMode::Body,
},
patterns: value.patterns.clone(),
})
}
config::Condition::DiffMatchAny(value) => {
config::Condition::DiffMatchAny(config::DiffMatchCondition {
name: value.name.clone(),
mode: match value.mode {
config::DiffMode::Raw => config::DiffMode::Raw,
config::DiffMode::File => config::DiffMode::File,
config::DiffMode::Line => config::DiffMode::Line,
},
patterns: value.patterns.clone(),
})
}
config::Condition::DiffMatchNone(value) => {
config::Condition::DiffMatchNone(config::DiffMatchCondition {
name: value.name.clone(),
mode: match value.mode {
config::DiffMode::Raw => config::DiffMode::Raw,
config::DiffMode::File => config::DiffMode::File,
config::DiffMode::Line => config::DiffMode::Line,
},
patterns: value.patterns.clone(),
})
}
config::Condition::BranchMatch(value) => {
config::Condition::BranchMatch(config::BranchMatchCondition {
name: value.name.clone(),
patterns: value.patterns.clone(),
})
}
config::Condition::ThresholdCompare(value) => {
config::Condition::ThresholdCompare(config::ThresholdCondition {
name: value.name.clone(),
metric: match value.metric {
config::ThresholdMetric::LineCount => config::ThresholdMetric::LineCount,
config::ThresholdMetric::FileCount => config::ThresholdMetric::FileCount,
},
operator: match value.operator {
config::ThresholdOperator::Lte => config::ThresholdOperator::Lte,
config::ThresholdOperator::Gte => config::ThresholdOperator::Gte,
},
value: value.value,
})
}
}
}
#[cfg(test)]
mod tests;