diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-09 18:45:47 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-09 18:46:17 +0800 |
| commit | c6f695bf5e757fdae8115abfcefb547745cd37be (patch) | |
| tree | cb8ebbd1e99b2a46ceab44237e769c83f452927e /mingling_cli/src/proj_mgr/rule_solver.rs | |
| parent | a05379b0dcf8b53fdb644f4dd803c46a1d8325fb (diff) | |
feat(proj_mgr): add toggle mutex validation and program crate name
Support `[[user.toggle-mutex]]` groups in rule.toml to enforce
mutually exclusive toggle options, and derive `program_crate_name`
from `program_name` using snake_case conversion.
Diffstat (limited to 'mingling_cli/src/proj_mgr/rule_solver.rs')
| -rw-r--r-- | mingling_cli/src/proj_mgr/rule_solver.rs | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/mingling_cli/src/proj_mgr/rule_solver.rs b/mingling_cli/src/proj_mgr/rule_solver.rs index 0e00458..7dfb7c7 100644 --- a/mingling_cli/src/proj_mgr/rule_solver.rs +++ b/mingling_cli/src/proj_mgr/rule_solver.rs @@ -44,10 +44,20 @@ pub struct UserRule { pub default: Option<String>, } +/// A `[[user.toggle-mutex]]` group: at most one of `mutex` may be enabled. +#[derive(Debug, Clone)] +pub struct UserMutex { + /// Keys that are mutually exclusive. + pub mutex: Vec<String>, + /// Human-readable reason shown when the constraint is violated. + pub reason: String, +} + /// All rules parsed from `rule.toml`. #[derive(Debug, Default, Clone)] pub struct TemplateRules { pub users: Vec<UserRule>, + pub mutexes: Vec<UserMutex>, pub display: Vec<DisplayRule>, pub hide_files: Vec<HideFileRule>, } @@ -110,6 +120,30 @@ pub fn parse_rules(content: &str) -> Result<TemplateRules, String> { rules.users.push(UserRule { name, default }); } } + + // `[[user.toggle-mutex]]` declares mutually exclusive toggle groups. + if let Some(tables) = user_table + .get("toggle-mutex") + .and_then(|item| item.as_array_of_tables()) + { + for table in tables { + let mutex = table + .get("mutex") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(str::to_owned)) + .collect() + }) + .unwrap_or_default(); + let reason = table + .get("reason") + .and_then(|v| v.as_str()) + .unwrap_or("these options are mutually exclusive") + .to_string(); + rules.mutexes.push(UserMutex { mutex, reason }); + } + } } if let Some(tables) = doc @@ -173,6 +207,31 @@ pub fn resolve_answers( merged } +/// Validate mutually exclusive toggle groups against the resolved answers. +/// +/// Returns an error for the first group where more than one key is enabled. +pub fn validate_mutexes( + answers: &HashMap<String, String>, + rules: &TemplateRules, +) -> Result<(), String> { + for group in &rules.mutexes { + let enabled: Vec<&str> = group + .mutex + .iter() + .filter(|key| is_truthy(key, answers)) + .map(String::as_str) + .collect(); + if enabled.len() > 1 { + return Err(format!( + "{} (enabled: {})", + group.reason, + enabled.join(", ") + )); + } + } + Ok(()) +} + /// Evaluate a boolean rule expression against the checklist answers. /// /// Supported syntax: @@ -395,6 +454,54 @@ name = "completion" } #[test] + fn rules_parse_toggle_mutex() { + let content = r#" +[[user.toggle-mutex]] +mutex = [ "tokio", "async_std", "smol" ] +reason = "You can only select one async runtime" +"#; + let rules = parse_rules(content).unwrap(); + assert_eq!(rules.mutexes.len(), 1); + assert_eq!(rules.mutexes[0].mutex, vec!["tokio", "async_std", "smol"]); + assert_eq!(rules.mutexes[0].reason, "You can only select one async runtime"); + } + + #[test] + fn validate_mutexes_allows_zero_or_one() { + let content = r#" +[[user.toggle-mutex]] +mutex = [ "tokio", "async_std" ] +reason = "one async runtime only" +"#; + let rules = parse_rules(content).unwrap(); + + // None enabled. + assert!(validate_mutexes(&HashMap::new(), &rules).is_ok()); + + // Exactly one enabled. + let mut answers = HashMap::new(); + answers.insert("tokio".into(), "true".into()); + assert!(validate_mutexes(&answers, &rules).is_ok()); + } + + #[test] + fn validate_mutexes_rejects_multiple_enabled() { + let content = r#" +[[user.toggle-mutex]] +mutex = [ "tokio", "async_std" ] +reason = "one async runtime only" +"#; + let rules = parse_rules(content).unwrap(); + + let mut answers = HashMap::new(); + answers.insert("tokio".into(), "true".into()); + answers.insert("async_std".into(), "true".into()); + let err = validate_mutexes(&answers, &rules).unwrap_err(); + assert!(err.contains("one async runtime only"), "unexpected: {err}"); + assert!(err.contains("tokio") && err.contains("async_std")); + } + + #[test] fn toggle_semantics_match_checklist_states() { // rule.toml: `pathf` defaults to true, `tokio` has no default. let content = r#" |
