Skip to main content

state_engine/
state.rs

1use serde_json::Value;
2use std::collections::{HashMap, HashSet};
3use std::path::PathBuf;
4use crate::core::fixed_bits;
5use crate::core::manifest::{Manifest, ConfigValue};
6use crate::core::parser::{Value as ParseValue, parse};
7use crate::ports::provided::{ManifestError, StateError};
8use crate::ports::required::FileClient;
9use crate::store::Store;
10use crate::load::Load;
11
12use std::sync::Arc;
13
14pub struct State {
15    manifest_dir: PathBuf,
16    manifest_file: Box<dyn FileClient>,
17    manifest: Manifest,
18    state_keys: Vec<u16>,
19    state_vals: Vec<Value>,
20    store: Store,
21    load: Load,
22    max_recursion: usize,
23    called_keys: HashSet<String>,
24}
25
26impl State {
27    /// Creates a new State with the given manifest directory.
28    ///
29    /// # Examples
30    ///
31    /// ```
32    /// use state_engine::State;
33    ///
34    /// let state = State::new("./examples/manifest");
35    /// ```
36    pub fn new(manifest_dir: &str) -> Self {
37        Self {
38            manifest_dir: PathBuf::from(manifest_dir),
39            manifest_file: Box::new(crate::ports::default::DefaultFileClient),
40            manifest: Manifest::new(),
41            state_keys: vec![0],
42            state_vals: vec![Value::Null],
43            store: Store::new(),
44            load: Load::new(),
45            max_recursion: 20,
46            called_keys: HashSet::new(),
47        }
48    }
49
50    pub fn with_in_memory(mut self, client: Arc<dyn crate::ports::required::InMemoryClient>) -> Self {
51        self.store = self.store.with_in_memory(Arc::clone(&client));
52        self.load = self.load.with_in_memory(client);
53        self
54    }
55
56    pub fn with_kvs(mut self, client: Arc<dyn crate::ports::required::KVSClient>) -> Self {
57        self.store = self.store.with_kvs(Arc::clone(&client));
58        self.load = self.load.with_kvs(client);
59        self
60    }
61
62    pub fn with_db(mut self, client: Arc<dyn crate::ports::required::DbClient>) -> Self {
63        self.load = self.load.with_db(client);
64        self
65    }
66
67    pub fn with_env(mut self, client: Arc<dyn crate::ports::required::EnvClient>) -> Self {
68        self.load = self.load.with_env(client);
69        self
70    }
71
72    pub fn with_http(mut self, client: Arc<dyn crate::ports::required::HttpClient>) -> Self {
73        self.store = self.store.with_http(Arc::clone(&client));
74        self.load = self.load.with_http(client);
75        self
76    }
77
78    pub fn with_file(mut self, client: Arc<dyn crate::ports::required::FileClient>) -> Self {
79        self.store = self.store.with_file(Arc::clone(&client));
80        self.load = self.load.with_file(client);
81        self
82    }
83
84    pub fn with_manifest_file(mut self, client: impl FileClient + 'static) -> Self {
85        self.manifest_file = Box::new(client);
86        self
87    }
88
89    fn load_manifest(&mut self, file: &str) -> Result<(), ManifestError> {
90        crate::fn_log!("State", "load_manifest", file);
91        if self.manifest.is_loaded(file) {
92            return Ok(());
93        }
94
95        let yml_path  = self.manifest_dir.join(format!("{}.yml",  file));
96        let yaml_path = self.manifest_dir.join(format!("{}.yaml", file));
97        let yml_key   = yml_path.to_string_lossy();
98        let yaml_key  = yaml_path.to_string_lossy();
99        let yml_content  = self.manifest_file.get(&yml_key);
100        let yaml_content = self.manifest_file.get(&yaml_key);
101
102        let content = match (yml_content, yaml_content) {
103            (Some(_), Some(_)) => return Err(ManifestError::AmbiguousFile(
104                format!("both '{}.yml' and '{}.yaml' exist.", file, file)
105            )),
106            (Some(c), None) => c,
107            (None, Some(c)) => c,
108            (None, None) => return Err(ManifestError::FileNotFound(
109                format!("'{}.yml' or '{}.yaml'", file, file)
110            )),
111        };
112
113        let yaml_root: serde_yaml_ng::Value = serde_yaml_ng::from_str(&content)
114            .map_err(|e| ManifestError::ParseError(format!("YAML parse error: {}", e)))?;
115
116        let pm = parse(
117            file,
118            yaml_to_parse_value(yaml_root),
119            &mut self.manifest.dynamic,
120            &mut self.manifest.keys,
121            &mut self.manifest.values,
122            &mut self.manifest.path_map,
123            &mut self.manifest.children_map,
124        ).map_err(|e| ManifestError::ParseError(e))?;
125
126        self.manifest.insert(file.to_string(), pm);
127        Ok(())
128    }
129
130    fn split_key<'k>(key: &'k str) -> (&'k str, &'k str) {
131        match key.find('.') {
132            Some(pos) => (&key[..pos], &key[pos + 1..]),
133            None => (key, ""),
134        }
135    }
136
137    fn find_state_value(&self, key_idx: u16) -> Option<usize> {
138        self.state_keys.iter().skip(1).position(|&k| k == key_idx).map(|p| p + 1)
139    }
140
141    /// Resolves a `${...}`-containing template string by calling `State::get()` for each placeholder.
142    fn resolve_template(&mut self, template: &str) -> Result<Option<String>, StateError> {
143        let mut result = String::new();
144        let mut remaining = template;
145        while let Some(start) = remaining.find("${") {
146            result.push_str(&remaining[..start]);
147            remaining = &remaining[start + 2..];
148            let end = match remaining.find('}') {
149                Some(e) => e,
150                None => return Ok(None),
151            };
152            let path = &remaining[..end];
153            remaining = &remaining[end + 1..];
154            let resolved = match self.get(path)? {
155                Some(Value::String(s)) => s,
156                Some(Value::Number(n)) => n.to_string(),
157                Some(Value::Bool(b))   => b.to_string(),
158                _ => return Ok(None),
159            };
160            result.push_str(&resolved);
161        }
162        result.push_str(remaining);
163        Ok(Some(result))
164    }
165
166    /// Resolves a `ConfigValue` to a `serde_json::Value`.
167    /// `Placeholder` → `State::get()` (returns Value as-is for connection objects).
168    /// `Str` with `${...}` → template resolution → String.
169    /// `Str` static → String.
170    fn resolve_config_value(&mut self, cv: ConfigValue) -> Result<Option<Value>, StateError> {
171        match cv {
172            ConfigValue::Client(c) => Ok(Some(Value::Number(c.into()))),
173            ConfigValue::Placeholder(path) => self.get(&path),
174            ConfigValue::Str(s) if s.contains("${") => {
175                Ok(self.resolve_template(&s)?.map(Value::String))
176            }
177            ConfigValue::Str(s) => Ok(Some(Value::String(s))),
178            ConfigValue::Map(pairs) => {
179                let mut map = serde_json::Map::new();
180                for (k, v) in pairs {
181                    map.insert(k, Value::String(v));
182                }
183                Ok(Some(Value::Object(map)))
184            }
185        }
186    }
187
188    /// Resolves ManifestStore::build_config output into a HashMap for Store/Load.
189    fn resolve_config(&mut self, meta_idx: u16) -> Result<Option<HashMap<String, Value>>, StateError> {
190        let entries = match self.manifest.build_config(meta_idx) {
191            Some(e) => e,
192            None => return Ok(None),
193        };
194
195        let mut config = HashMap::new();
196        for (key, cv) in entries {
197            if let Some(v) = self.resolve_config_value(cv)? {
198                config.insert(key, v);
199            }
200        }
201        Ok(Some(config))
202    }
203
204    /// Returns the value for `key`, checking state cache → _store → _load in order.
205    ///
206    /// # Examples
207    ///
208    /// ```
209    /// use state_engine::State;
210    /// use state_engine::InMemoryClient;
211    /// use serde_json::{json, Value};
212    ///
213    /// struct MockInMemory { data: std::sync::Mutex<std::collections::HashMap<String, Value>> }
214    /// impl MockInMemory { fn new() -> Self { Self { data: Default::default() } } }
215    /// impl InMemoryClient for MockInMemory {
216    ///     fn get(&self, key: &str) -> Option<Value> { self.data.lock().unwrap().get(key).cloned() }
217    ///     fn set(&self, key: &str, value: Value) -> bool { self.data.lock().unwrap().insert(key.to_string(), value); true }
218    ///     fn delete(&self, key: &str) -> bool { self.data.lock().unwrap().remove(key).is_some() }
219    /// }
220    ///
221    /// let client = MockInMemory::new();
222    /// let mut state = State::new("./examples/manifest")
223    ///     .with_in_memory(std::sync::Arc::new(client));
224    ///
225    /// // set then get
226    /// state.set("connection.common", json!({"host": "localhost"}), None).unwrap();
227    /// assert!(state.get("connection.common").unwrap().is_some());
228    /// ```
229    pub fn get(&mut self, key: &str) -> Result<Option<Value>, StateError> {
230        crate::fn_log!("State", "get", key);
231        if self.called_keys.len() >= self.max_recursion {
232            return Err(StateError::RecursionLimitExceeded);
233        }
234        if self.called_keys.contains(&key.to_string()) {
235            return Err(StateError::RecursionLimitExceeded);
236        }
237
238        self.called_keys.insert(key.to_string());
239
240        let (file, path) = Self::split_key(key);
241        let file = file.to_string();
242        let path = path.to_string();
243
244        if let Err(e) = self.load_manifest(&file) {
245            self.called_keys.remove(key);
246            return Err(StateError::ManifestLoadFailed(e.to_string()));
247        }
248
249        let key_idx = match self.manifest.find(&file, &path) {
250            Some(idx) => idx,
251            None => {
252                self.called_keys.remove(key);
253                return Err(StateError::KeyNotFound(key.to_string()));
254            }
255        };
256
257        if let Some(sv_idx) = self.find_state_value(key_idx) {
258            let val = self.state_vals.get(sv_idx).cloned();
259            self.called_keys.remove(key);
260            return Ok(val);
261        }
262
263        let meta = self.manifest.get_meta(&file, &path);
264
265        let has_state_client = meta.load
266            .map(|load_idx| self.manifest.get_client(load_idx) == fixed_bits::CLIENT_STATE)
267            .unwrap_or(false);
268
269        if !has_state_client {
270            if let Some(store_idx) = meta.store {
271                match self.resolve_config(store_idx) {
272                    Ok(Some(config)) => {
273                        if let Some(value) = self.store.get(&config) {
274                            self.state_keys.push(key_idx);
275                            self.state_vals.push(value.clone());
276                            self.called_keys.remove(key);
277                            return Ok(Some(value));
278                        }
279                    }
280                    Ok(None) => {}
281                    Err(e) => {
282                        self.called_keys.remove(key);
283                        return Err(e);
284                    }
285                }
286            }
287        }
288
289        // CLIENT_STATE: extract key path directly from build_config without resolving
290        if has_state_client {
291            if let Some(load_idx) = meta.load {
292                let state_key = self.manifest.build_config(load_idx)
293                    .and_then(|entries| entries.into_iter().find(|(k, _)| k == "key"))
294                    .and_then(|(_, cv)| match cv {
295                        ConfigValue::Placeholder(p) => Some(p),
296                        ConfigValue::Str(s) => Some(s),
297                        _ => None,
298                    });
299                let result = match state_key {
300                    Some(k) => self.get(&k),
301                    None => Ok(None),
302                };
303                self.called_keys.remove(key);
304                return result;
305            }
306        }
307
308        let result = if let Some(load_idx) = meta.load {
309            match self.resolve_config(load_idx) {
310                Ok(Some(mut config)) => {
311                    if !config.contains_key("client") {
312                        self.called_keys.remove(key);
313                        return Ok(None);
314                    }
315
316                    // unqualify map keys for Load
317                    if let Some(Value::Object(map_obj)) = config.get("map").cloned() {
318                        let mut unqualified = serde_json::Map::new();
319                        for (qk, v) in map_obj {
320                            let field = qk.rfind('.').map_or(qk.as_str(), |p| &qk[p+1..]);
321                            unqualified.insert(field.to_string(), v);
322                        }
323                        config.insert("map".to_string(), Value::Object(unqualified));
324                    }
325
326                    match self.load.handle(&config) {
327                        Ok(loaded) => {
328                            if let Some(store_idx) = meta.store {
329                                match self.resolve_config(store_idx) {
330                                    Ok(Some(store_config)) => {
331                                        if self.store.set(&store_config, loaded.clone(), None).unwrap_or(false) {
332                                            self.state_keys.push(key_idx);
333                                            self.state_vals.push(loaded.clone());
334                                        }
335                                    }
336                                    Ok(None) => {
337                                        self.state_keys.push(key_idx);
338                                        self.state_vals.push(loaded.clone());
339                                    }
340                                    Err(_) => {} // write-through cache failure is non-fatal; loaded value is still returned
341                                }
342                            } else {
343                                self.state_keys.push(key_idx);
344                                self.state_vals.push(loaded.clone());
345                            }
346                            Ok(Some(loaded))
347                        }
348                        Err(e) => Err(StateError::LoadFailed(e)),
349                    }
350                }
351                Ok(None) => Ok(None),
352                Err(e) => Err(e),
353            }
354        } else { Ok(None) };
355
356        self.called_keys.remove(key);
357        result
358    }
359
360    /// Writes `value` to the _store backend for `key`.
361    ///
362    /// # Examples
363    ///
364    /// ```
365    /// # use state_engine::State;
366    /// # use state_engine::InMemoryClient;
367    /// # use serde_json::{json, Value};
368    /// # struct MockInMemory { data: std::sync::Mutex<std::collections::HashMap<String, Value>> }
369    /// # impl MockInMemory { fn new() -> Self { Self { data: Default::default() } } }
370    /// # impl InMemoryClient for MockInMemory {
371    /// #     fn get(&self, key: &str) -> Option<Value> { self.data.lock().unwrap().get(key).cloned() }
372    /// #     fn set(&self, key: &str, value: Value) -> bool { self.data.lock().unwrap().insert(key.to_string(), value); true }
373    /// #     fn delete(&self, key: &str) -> bool { self.data.lock().unwrap().remove(key).is_some() }
374    /// # }
375    /// let client = MockInMemory::new();
376    /// let mut state = State::new("./examples/manifest")
377    ///     .with_in_memory(std::sync::Arc::new(client));
378    ///
379    /// assert!(state.set("connection.common", json!({"host": "localhost"}), None).unwrap());
380    /// ```
381    pub fn set(&mut self, key: &str, value: Value, ttl: Option<u64>) -> Result<bool, StateError> {
382        crate::fn_log!("State", "set", key);
383        let (file, path) = Self::split_key(key);
384        let file = file.to_string();
385        let path = path.to_string();
386
387        if let Err(e) = self.load_manifest(&file) {
388            return Err(StateError::ManifestLoadFailed(e.to_string()));
389        }
390
391        let key_idx = match self.manifest.find(&file, &path) {
392            Some(idx) => idx,
393            None => return Err(StateError::KeyNotFound(key.to_string())),
394        };
395
396        let meta = self.manifest.get_meta(&file, &path);
397
398        if let Some(store_idx) = meta.store {
399            match self.resolve_config(store_idx)? {
400                Some(config) => {
401                    return match self.store.set(&config, value.clone(), ttl) {
402                        Ok(ok) => {
403                            if ok {
404                                if let Some(sv_idx) = self.find_state_value(key_idx) {
405                                    self.state_vals[sv_idx] = value;
406                                } else {
407                                    self.state_keys.push(key_idx);
408                                    self.state_vals.push(value);
409                                }
410                            }
411                            Ok(ok)
412                        }
413                        Err(e) => Err(StateError::StoreFailed(e)),
414                    };
415                }
416                None => {}
417            }
418        }
419        Ok(false)
420    }
421
422    /// Removes the value for `key` from the _store backend.
423    ///
424    /// # Examples
425    ///
426    /// ```
427    /// # use state_engine::State;
428    /// # use state_engine::InMemoryClient;
429    /// # use serde_json::{json, Value};
430    /// # struct MockInMemory { data: std::sync::Mutex<std::collections::HashMap<String, Value>> }
431    /// # impl MockInMemory { fn new() -> Self { Self { data: Default::default() } } }
432    /// # impl InMemoryClient for MockInMemory {
433    /// #     fn get(&self, key: &str) -> Option<Value> { self.data.lock().unwrap().get(key).cloned() }
434    /// #     fn set(&self, key: &str, value: Value) -> bool { self.data.lock().unwrap().insert(key.to_string(), value); true }
435    /// #     fn delete(&self, key: &str) -> bool { self.data.lock().unwrap().remove(key).is_some() }
436    /// # }
437    /// let client = MockInMemory::new();
438    /// let mut state = State::new("./examples/manifest")
439    ///     .with_in_memory(std::sync::Arc::new(client));
440    ///
441    /// state.set("connection.common", json!({"host": "localhost"}), None).unwrap();
442    /// assert!(state.delete("connection.common").unwrap());
443    /// // after delete, store has no data; _load is attempted but EnvClient is not configured here
444    /// assert!(state.get("connection.common").is_err() || state.get("connection.common").unwrap().is_none());
445    /// ```
446    pub fn delete(&mut self, key: &str) -> Result<bool, StateError> {
447        crate::fn_log!("State", "delete", key);
448        let (file, path) = Self::split_key(key);
449        let file = file.to_string();
450        let path = path.to_string();
451
452        if let Err(e) = self.load_manifest(&file) {
453            return Err(StateError::ManifestLoadFailed(e.to_string()));
454        }
455
456        let key_idx = match self.manifest.find(&file, &path) {
457            Some(idx) => idx,
458            None => return Err(StateError::KeyNotFound(key.to_string())),
459        };
460
461        let meta = self.manifest.get_meta(&file, &path);
462
463        if let Some(store_idx) = meta.store {
464            match self.resolve_config(store_idx)? {
465                Some(config) => {
466                    return match self.store.delete(&config) {
467                        Ok(ok) => {
468                            if ok {
469                                if let Some(sv_idx) = self.find_state_value(key_idx) {
470                                    self.state_keys[sv_idx] = 0;
471                                    self.state_vals[sv_idx] = Value::Null;
472                                }
473                            }
474                            Ok(ok)
475                        }
476                        Err(e) => Err(StateError::StoreFailed(e)),
477                    };
478                }
479                None => {}
480            }
481        }
482        Ok(false)
483    }
484
485    /// Returns `true` if a value exists for `key` in state cache or _store.
486    /// Does not trigger _load.
487    ///
488    /// # Examples
489    ///
490    /// ```
491    /// # use state_engine::State;
492    /// # use state_engine::InMemoryClient;
493    /// # use serde_json::{json, Value};
494    /// # struct MockInMemory { data: std::sync::Mutex<std::collections::HashMap<String, Value>> }
495    /// # impl MockInMemory { fn new() -> Self { Self { data: Default::default() } } }
496    /// # impl InMemoryClient for MockInMemory {
497    /// #     fn get(&self, key: &str) -> Option<Value> { self.data.lock().unwrap().get(key).cloned() }
498    /// #     fn set(&self, key: &str, value: Value) -> bool { self.data.lock().unwrap().insert(key.to_string(), value); true }
499    /// #     fn delete(&self, key: &str) -> bool { self.data.lock().unwrap().remove(key).is_some() }
500    /// # }
501    /// let client = MockInMemory::new();
502    /// let mut state = State::new("./examples/manifest")
503    ///     .with_in_memory(std::sync::Arc::new(client));
504    ///
505    /// assert!(!state.exists("connection.common").unwrap());
506    /// state.set("connection.common", json!({"host": "localhost"}), None).unwrap();
507    /// assert!(state.exists("connection.common").unwrap());
508    /// ```
509    pub fn exists(&mut self, key: &str) -> Result<bool, StateError> {
510        crate::fn_log!("State", "exists", key);
511        let (file, path) = Self::split_key(key);
512        let file = file.to_string();
513        let path = path.to_string();
514
515        if let Err(e) = self.load_manifest(&file) {
516            return Err(StateError::ManifestLoadFailed(e.to_string()));
517        }
518
519        let key_idx = match self.manifest.find(&file, &path) {
520            Some(idx) => idx,
521            None => return Err(StateError::KeyNotFound(key.to_string())),
522        };
523
524        if let Some(sv_idx) = self.find_state_value(key_idx) {
525            return Ok(!self.state_vals.get(sv_idx).map_or(true, |v| v.is_null()));
526        }
527
528        let meta = self.manifest.get_meta(&file, &path);
529        if let Some(store_idx) = meta.store {
530            if let Some(config) = self.resolve_config(store_idx)? {
531                return Ok(self.store.get(&config).is_some());
532            }
533        }
534        Ok(false)
535    }
536}
537
538fn yaml_to_parse_value(v: serde_yaml_ng::Value) -> ParseValue {
539    match v {
540        serde_yaml_ng::Value::Mapping(m) => ParseValue::Mapping(
541            m.into_iter()
542                .filter_map(|(k, v)| {
543                    let key = match k {
544                        serde_yaml_ng::Value::String(s) => s,
545                        _ => return None,
546                    };
547                    Some((key, yaml_to_parse_value(v)))
548                })
549                .collect(),
550        ),
551        serde_yaml_ng::Value::String(s) => ParseValue::Scalar(s),
552        serde_yaml_ng::Value::Number(n) => ParseValue::Scalar(n.to_string()),
553        serde_yaml_ng::Value::Bool(b)   => ParseValue::Scalar(b.to_string()),
554        serde_yaml_ng::Value::Null      => ParseValue::Null,
555        _                               => ParseValue::Null,
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562    use crate::ports::required::{KVSClient, DbClient, EnvClient, FileClient};
563    use serde_json::Value;
564    use std::collections::HashMap;
565    use std::sync::Arc;
566
567    struct StubKVS;
568    impl KVSClient for StubKVS {
569        fn get(&self, _: &str) -> Option<String> { None }
570        fn set(&self, _: &str, _: String, _: Option<u64>) -> bool { false }
571        fn delete(&self, _: &str) -> bool { false }
572    }
573
574    struct StubDb;
575    impl DbClient for StubDb {
576        fn get(&self, _: &Value, _: &str, _: &[&str], _: Option<&str>) -> Option<Vec<HashMap<String, Value>>> { None }
577        fn set(&self, _: &Value, _: &str, _: &HashMap<String, Value>, _: Option<&str>) -> bool { false }
578        fn delete(&self, _: &Value, _: &str, _: Option<&str>) -> bool { false }
579    }
580
581    struct StubEnv;
582    impl EnvClient for StubEnv {
583        fn get(&self, _: &str) -> Option<String> { None }
584        fn set(&self, _: &str, _: String) -> bool { false }
585        fn delete(&self, _: &str) -> bool { false }
586    }
587
588    struct StubFile;
589    impl FileClient for StubFile {
590        fn get(&self, _: &str) -> Option<String> { None }
591        fn set(&self, _: &str, _: String) -> bool { false }
592        fn delete(&self, _: &str) -> bool { false }
593    }
594
595    struct StubHttp;
596    impl crate::ports::required::HttpClient for StubHttp {
597        fn get(&self, _: &str, _: Option<&HashMap<String, String>>) -> Option<Value> { None }
598        fn set(&self, _: &str, _: Value, _: Option<&HashMap<String, String>>) -> bool { false }
599        fn delete(&self, _: &str, _: Option<&HashMap<String, String>>) -> bool { false }
600    }
601
602    #[test]
603    fn test_with_clients_build() {
604        let _ = State::new("./examples/manifest").with_kvs(Arc::new(StubKVS));
605        let _ = State::new("./examples/manifest").with_db(Arc::new(StubDb));
606        let _ = State::new("./examples/manifest").with_env(Arc::new(StubEnv));
607        let _ = State::new("./examples/manifest").with_http(Arc::new(StubHttp));
608        let _ = State::new("./examples/manifest").with_file(Arc::new(StubFile));
609    }
610}