use anyhow::{bail, Result};
use toml_edit::{DocumentMut, Item};
const DEFAULT_RULE_COMPILATION_ENABLED: bool = false;
const DEFAULT_RULE_COMPILE_MIN_REINFORCEMENT: i64 = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RuleCompilationConfig {
pub enabled: bool,
pub min_reinforcement: i64,
}
pub fn rule_compilation_config() -> Result<RuleCompilationConfig> {
let mut doc = super::read_config_doc_or_default()?;
ensure_defaults(&mut doc)?;
rule_compilation_config_from_doc(&doc)
}
pub(super) fn ensure_defaults(doc: &mut DocumentMut) -> Result<()> {
let rules = super::top_table_mut(doc, "rule_compilation")?;
super::set_bool_if_missing(rules, "enabled", DEFAULT_RULE_COMPILATION_ENABLED);
super::set_i64_if_missing(
rules,
"rule_compile_min_reinforcement",
DEFAULT_RULE_COMPILE_MIN_REINFORCEMENT,
);
Ok(())
}
fn rule_compilation_config_from_doc(doc: &DocumentMut) -> Result<RuleCompilationConfig> {
let Some(table) = doc.get("rule_compilation").and_then(Item::as_table) else {
return Ok(RuleCompilationConfig {
enabled: DEFAULT_RULE_COMPILATION_ENABLED,
min_reinforcement: DEFAULT_RULE_COMPILE_MIN_REINFORCEMENT,
});
};
let enabled = match table.get("enabled") {
Some(item) => item
.as_bool()
.ok_or_else(|| anyhow::anyhow!("rule_compilation.enabled must be a boolean"))?,
None => DEFAULT_RULE_COMPILATION_ENABLED,
};
let min_reinforcement = match table.get("rule_compile_min_reinforcement") {
Some(item) => item.as_integer().ok_or_else(|| {
anyhow::anyhow!("rule_compilation.rule_compile_min_reinforcement must be an integer")
})?,
None => DEFAULT_RULE_COMPILE_MIN_REINFORCEMENT,
};
if min_reinforcement < 1 {
bail!(
"rule_compilation.rule_compile_min_reinforcement must be >= 1, got {min_reinforcement}"
);
}
Ok(RuleCompilationConfig {
enabled,
min_reinforcement,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn with_rules_config_path<T>(path: &std::path::Path, f: impl FnOnce() -> T) -> T {
let _guard = super::super::TEST_ENV_LOCK
.lock()
.expect("env lock should acquire");
let old = std::env::var("REMEM_CONFIG").ok();
unsafe { std::env::set_var("REMEM_CONFIG", path) };
let result = f();
match old {
Some(value) => unsafe { std::env::set_var("REMEM_CONFIG", value) },
None => unsafe { std::env::remove_var("REMEM_CONFIG") },
}
result
}
fn rules_config_path(label: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"remem-{label}-{}-{}.toml",
std::process::id(),
chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
))
}
#[test]
fn default_config_disables_rule_compilation() {
let text = super::super::default_config_text();
assert!(text.contains("[rule_compilation]"), "{text}");
assert!(text.contains("enabled = false"), "{text}");
assert!(
text.contains("rule_compile_min_reinforcement = 3"),
"{text}"
);
}
#[test]
fn rule_compilation_config_reads_enabled_and_threshold() -> Result<()> {
let path = rules_config_path("rule-compilation-config");
with_rules_config_path(&path, || -> Result<()> {
super::super::init_config()?;
super::super::set_config_value("rule_compilation.enabled", "true")?;
super::super::set_config_value("rule_compilation.rule_compile_min_reinforcement", "5")?;
let config = rule_compilation_config()?;
assert!(config.enabled);
assert_eq!(config.min_reinforcement, 5);
Ok(())
})?;
std::fs::remove_file(path)?;
Ok(())
}
#[test]
fn rule_compilation_config_rejects_zero_threshold() -> Result<()> {
let path = rules_config_path("rule-compilation-zero");
with_rules_config_path(&path, || -> Result<()> {
std::fs::write(
&path,
"[rule_compilation]\nrule_compile_min_reinforcement = 0\n",
)?;
let err = rule_compilation_config().expect_err("zero threshold must fail closed");
assert!(
err.to_string()
.contains("rule_compile_min_reinforcement must be >= 1"),
"{err}"
);
Ok(())
})?;
std::fs::remove_file(path)?;
Ok(())
}
#[test]
fn rule_compilation_config_rejects_malformed_threshold() -> Result<()> {
for (label, value) in [("string", "\"5\""), ("float", "5.0")] {
let path = rules_config_path(&format!("rule-compilation-{label}"));
with_rules_config_path(&path, || -> Result<()> {
std::fs::write(
&path,
format!("[rule_compilation]\nrule_compile_min_reinforcement = {value}\n"),
)?;
let err = rule_compilation_config()
.expect_err("present malformed threshold must fail closed");
assert!(
err.to_string()
.contains("rule_compile_min_reinforcement must be an integer"),
"{err}"
);
Ok(())
})?;
std::fs::remove_file(path)?;
}
Ok(())
}
#[test]
fn rule_compilation_config_rejects_malformed_enabled() -> Result<()> {
for (label, value) in [("string", "\"true\""), ("integer", "1")] {
let path = rules_config_path(&format!("rule-compilation-enabled-{label}"));
with_rules_config_path(&path, || -> Result<()> {
std::fs::write(&path, format!("[rule_compilation]\nenabled = {value}\n"))?;
let err =
rule_compilation_config().expect_err("present malformed enabled must fail");
assert!(
err.to_string()
.contains("rule_compilation.enabled must be a boolean"),
"{err}"
);
Ok(())
})?;
std::fs::remove_file(path)?;
}
Ok(())
}
}