Skip to main content

tempus_engine/
store.rs

1use std::path::Path;
2
3use crate::metadata::RuleDefinition;
4use crate::RuleEngineError;
5
6/// A named collection of `RuleDefinition` objects loaded from disk.
7///
8/// Rules can be loaded from a JSON array file or a YAML array file.
9/// After loading, individual rules are retrieved by name.
10///
11/// # JSON file format
12/// ```json
13/// [
14///   { "name": "rule-a", "logic": { "...": "..." } },
15///   { "name": "rule-b", "version": "1.0.0", "logic": { "...": "..." } }
16/// ]
17/// ```
18///
19/// # YAML file format
20/// ```yaml
21/// - name: rule-a
22///   logic:
23///     ">": [{"var": "score"}, 700]
24/// ```
25#[derive(Debug, Default)]
26pub struct RuleStore {
27    rules: Vec<RuleDefinition>,
28}
29
30impl RuleStore {
31    /// Create an empty rule store.
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Load rules from a JSON file (array of `RuleDefinition` objects).
37    pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self, RuleEngineError> {
38        let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
39            RuleEngineError::InvalidRule(format!(
40                "cannot read {}: {}",
41                path.as_ref().display(),
42                e
43            ))
44        })?;
45        Self::from_json_str(&content)
46    }
47
48    /// Load rules from a JSON string (array of `RuleDefinition` objects).
49    pub fn from_json_str(json: &str) -> Result<Self, RuleEngineError> {
50        let rules: Vec<RuleDefinition> = serde_json::from_str(json)
51            .map_err(|e| RuleEngineError::InvalidRule(format!("invalid rule JSON: {e}")))?;
52        Ok(Self { rules })
53    }
54
55    /// Load rules from a YAML file (array of `RuleDefinition` objects).
56    ///
57    /// Requires the `yaml` feature to be enabled.
58    #[cfg(feature = "yaml")]
59    pub fn from_yaml_file(path: impl AsRef<Path>) -> Result<Self, RuleEngineError> {
60        let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
61            RuleEngineError::InvalidRule(format!(
62                "cannot read {}: {}",
63                path.as_ref().display(),
64                e
65            ))
66        })?;
67        Self::from_yaml_str(&content)
68    }
69
70    /// Load rules from a YAML string.
71    ///
72    /// Requires the `yaml` feature to be enabled.
73    #[cfg(feature = "yaml")]
74    pub fn from_yaml_str(yaml: &str) -> Result<Self, RuleEngineError> {
75        let value: Value = serde_yaml::from_str(yaml)
76            .map_err(|e| RuleEngineError::InvalidRule(format!("invalid rule YAML: {e}")))?;
77        let json = serde_json::to_string(&value)
78            .map_err(|e| RuleEngineError::InvalidRule(e.to_string()))?;
79        Self::from_json_str(&json)
80    }
81
82    /// Retrieve a rule by name.
83    pub fn get(&self, name: &str) -> Option<&RuleDefinition> {
84        self.rules.iter().find(|r| r.name == name)
85    }
86
87    /// All rules in the store.
88    pub fn all(&self) -> &[RuleDefinition] {
89        &self.rules
90    }
91
92    /// Number of rules in the store.
93    pub fn len(&self) -> usize {
94        self.rules.len()
95    }
96
97    /// Returns `true` if the store contains no rules.
98    pub fn is_empty(&self) -> bool {
99        self.rules.is_empty()
100    }
101
102    /// Add a rule to the store at runtime.
103    pub fn insert(&mut self, rule: RuleDefinition) {
104        if let Some(existing) = self.rules.iter_mut().find(|r| r.name == rule.name) {
105            *existing = rule;
106        } else {
107            self.rules.push(rule);
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use serde_json::json;
116
117    fn sample_json() -> &'static str {
118        r#"[
119            {"name":"rule-a","logic":{">":[{"var":"x"},1]}},
120            {"name":"rule-b","version":"2.0.0","logic":{"==":[{"var":"y"},true]}}
121        ]"#
122    }
123
124    #[test]
125    fn load_from_json_str() {
126        let store = RuleStore::from_json_str(sample_json()).unwrap();
127        assert_eq!(store.len(), 2);
128    }
129
130    #[test]
131    fn get_by_name() {
132        let store = RuleStore::from_json_str(sample_json()).unwrap();
133        let rule = store.get("rule-b").unwrap();
134        assert_eq!(rule.version.as_deref(), Some("2.0.0"));
135    }
136
137    #[test]
138    fn get_missing_returns_none() {
139        let store = RuleStore::from_json_str(sample_json()).unwrap();
140        assert!(store.get("does-not-exist").is_none());
141    }
142
143    #[test]
144    fn insert_adds_and_replaces() {
145        let mut store = RuleStore::new();
146        store.insert(RuleDefinition::new("r", json!({})));
147        assert_eq!(store.len(), 1);
148        store.insert(RuleDefinition::new("r", json!({"==":[1,1]})));
149        assert_eq!(store.len(), 1); // replaced, not duplicated
150    }
151
152    #[test]
153    fn invalid_json_returns_error() {
154        assert!(RuleStore::from_json_str("{bad}").is_err());
155    }
156}