Skip to main content

remem/runtime_config/
rules.rs

1use 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}