remem/runtime_config/
rules.rs1use anyhow::{bail, Result};
2use toml_edit::{DocumentMut, Item};
3
4const DEFAULT_RULE_COMPILATION_ENABLED: bool = false;
5const DEFAULT_RULE_COMPILE_MIN_REINFORCEMENT: i64 = 3;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct RuleCompilationConfig {
9 pub enabled: bool,
10 pub min_reinforcement: i64,
11}
12
13pub fn rule_compilation_config() -> Result<RuleCompilationConfig> {
14 let mut doc = super::read_config_doc_or_default()?;
15 ensure_defaults(&mut doc)?;
16 rule_compilation_config_from_doc(&doc)
17}
18
19pub(super) fn ensure_defaults(doc: &mut DocumentMut) -> Result<()> {
20 let rules = super::top_table_mut(doc, "rule_compilation")?;
21 super::set_bool_if_missing(rules, "enabled", DEFAULT_RULE_COMPILATION_ENABLED);
22 super::set_i64_if_missing(
23 rules,
24 "rule_compile_min_reinforcement",
25 DEFAULT_RULE_COMPILE_MIN_REINFORCEMENT,
26 );
27 Ok(())
28}
29
30fn rule_compilation_config_from_doc(doc: &DocumentMut) -> Result<RuleCompilationConfig> {
31 let Some(table) = doc.get("rule_compilation").and_then(Item::as_table) else {
32 return Ok(RuleCompilationConfig {
33 enabled: DEFAULT_RULE_COMPILATION_ENABLED,
34 min_reinforcement: DEFAULT_RULE_COMPILE_MIN_REINFORCEMENT,
35 });
36 };
37 let enabled = match table.get("enabled") {
38 Some(item) => item
39 .as_bool()
40 .ok_or_else(|| anyhow::anyhow!("rule_compilation.enabled must be a boolean"))?,
41 None => DEFAULT_RULE_COMPILATION_ENABLED,
42 };
43 let min_reinforcement = match table.get("rule_compile_min_reinforcement") {
44 Some(item) => item.as_integer().ok_or_else(|| {
45 anyhow::anyhow!("rule_compilation.rule_compile_min_reinforcement must be an integer")
46 })?,
47 None => DEFAULT_RULE_COMPILE_MIN_REINFORCEMENT,
48 };
49 if min_reinforcement < 1 {
50 bail!(
51 "rule_compilation.rule_compile_min_reinforcement must be >= 1, got {min_reinforcement}"
52 );
53 }
54 Ok(RuleCompilationConfig {
55 enabled,
56 min_reinforcement,
57 })
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 fn with_rules_config_path<T>(path: &std::path::Path, f: impl FnOnce() -> T) -> T {
65 let _guard = super::super::TEST_ENV_LOCK
66 .lock()
67 .expect("env lock should acquire");
68 let old = std::env::var("REMEM_CONFIG").ok();
69 unsafe { std::env::set_var("REMEM_CONFIG", path) };
70 let result = f();
71 match old {
72 Some(value) => unsafe { std::env::set_var("REMEM_CONFIG", value) },
73 None => unsafe { std::env::remove_var("REMEM_CONFIG") },
74 }
75 result
76 }
77
78 fn rules_config_path(label: &str) -> std::path::PathBuf {
79 std::env::temp_dir().join(format!(
80 "remem-{label}-{}-{}.toml",
81 std::process::id(),
82 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
83 ))
84 }
85
86 #[test]
87 fn default_config_disables_rule_compilation() {
88 let text = super::super::default_config_text();
89 assert!(text.contains("[rule_compilation]"), "{text}");
90 assert!(text.contains("enabled = false"), "{text}");
91 assert!(
92 text.contains("rule_compile_min_reinforcement = 3"),
93 "{text}"
94 );
95 }
96
97 #[test]
98 fn rule_compilation_config_reads_enabled_and_threshold() -> Result<()> {
99 let path = rules_config_path("rule-compilation-config");
100 with_rules_config_path(&path, || -> Result<()> {
101 super::super::init_config()?;
102 super::super::set_config_value("rule_compilation.enabled", "true")?;
103 super::super::set_config_value("rule_compilation.rule_compile_min_reinforcement", "5")?;
104
105 let config = rule_compilation_config()?;
106 assert!(config.enabled);
107 assert_eq!(config.min_reinforcement, 5);
108 Ok(())
109 })?;
110 std::fs::remove_file(path)?;
111 Ok(())
112 }
113
114 #[test]
115 fn rule_compilation_config_rejects_zero_threshold() -> Result<()> {
116 let path = rules_config_path("rule-compilation-zero");
117 with_rules_config_path(&path, || -> Result<()> {
118 std::fs::write(
119 &path,
120 "[rule_compilation]\nrule_compile_min_reinforcement = 0\n",
121 )?;
122 let err = rule_compilation_config().expect_err("zero threshold must fail closed");
123 assert!(
124 err.to_string()
125 .contains("rule_compile_min_reinforcement must be >= 1"),
126 "{err}"
127 );
128 Ok(())
129 })?;
130 std::fs::remove_file(path)?;
131 Ok(())
132 }
133
134 #[test]
135 fn rule_compilation_config_rejects_malformed_threshold() -> Result<()> {
136 for (label, value) in [("string", "\"5\""), ("float", "5.0")] {
137 let path = rules_config_path(&format!("rule-compilation-{label}"));
138 with_rules_config_path(&path, || -> Result<()> {
139 std::fs::write(
140 &path,
141 format!("[rule_compilation]\nrule_compile_min_reinforcement = {value}\n"),
142 )?;
143 let err = rule_compilation_config()
144 .expect_err("present malformed threshold must fail closed");
145 assert!(
146 err.to_string()
147 .contains("rule_compile_min_reinforcement must be an integer"),
148 "{err}"
149 );
150 Ok(())
151 })?;
152 std::fs::remove_file(path)?;
153 }
154 Ok(())
155 }
156
157 #[test]
158 fn rule_compilation_config_rejects_malformed_enabled() -> Result<()> {
159 for (label, value) in [("string", "\"true\""), ("integer", "1")] {
160 let path = rules_config_path(&format!("rule-compilation-enabled-{label}"));
161 with_rules_config_path(&path, || -> Result<()> {
162 std::fs::write(&path, format!("[rule_compilation]\nenabled = {value}\n"))?;
163 let err =
164 rule_compilation_config().expect_err("present malformed enabled must fail");
165 assert!(
166 err.to_string()
167 .contains("rule_compilation.enabled must be a boolean"),
168 "{err}"
169 );
170 Ok(())
171 })?;
172 std::fs::remove_file(path)?;
173 }
174 Ok(())
175 }
176}