Skip to main content

state_engine/
state.rs

1use serde_json::Value;
2use std::collections::{HashMap, HashSet};
3use crate::manifest::Manifest;
4use crate::core::fixed_bits;
5use crate::core::codec;
6use crate::store::Store;
7use crate::load::Load;
8use crate::ports::provided::StateError;
9
10// state_values layout: Vec<(key_idx: u16, value: Value)>
11// index 0 is reserved as null slot
12pub struct State<'a> {
13    manifest: Manifest,
14    state_keys: Vec<u16>,
15    state_vals: Vec<Value>,
16    store: Store<'a>,
17    load: Load<'a>,
18    max_recursion: usize,
19    called_keys: HashSet<String>,
20}
21
22impl<'a> State<'a> {
23    /// Creates a new State with the given manifest directory.
24    ///
25    /// # Examples
26    ///
27    /// ```
28    /// use state_engine::State;
29    ///
30    /// let state = State::new("./examples/manifest");
31    /// ```
32    pub fn new(manifest_dir: &str) -> Self {
33        Self {
34            manifest: Manifest::new(manifest_dir),
35            state_keys: vec![0],
36            state_vals: vec![Value::Null],
37            store: Store::new(),
38            load: Load::new(),
39            max_recursion: 20,
40            called_keys: HashSet::new(),
41        }
42    }
43
44    pub fn with_in_memory(mut self, client: &'a dyn crate::ports::required::InMemoryClient) -> Self {
45        self.store = self.store.with_in_memory(client);
46        self.load = self.load.with_in_memory(client);
47        self
48    }
49
50    pub fn with_kvs(mut self, client: &'a dyn crate::ports::required::KVSClient) -> Self {
51        self.store = self.store.with_kvs(client);
52        self.load = self.load.with_kvs(client);
53        self
54    }
55
56    pub fn with_db(mut self, client: &'a dyn crate::ports::required::DbClient) -> Self {
57        self.load = self.load.with_db(client);
58        self
59    }
60
61    pub fn with_env(mut self, client: &'a dyn crate::ports::required::EnvClient) -> Self {
62        self.load = self.load.with_env(client);
63        self
64    }
65
66    pub fn with_http(mut self, client: &'a dyn crate::ports::required::HttpClient) -> Self {
67        self.store = self.store.with_http(client);
68        self.load = self.load.with_http(client);
69        self
70    }
71
72    pub fn with_file(mut self, client: impl crate::ports::required::FileClient + 'static) -> Self {
73        self.manifest = self.manifest.with_file(client);
74        self
75    }
76
77
78    /// Splits "file.path" into ("file", "path").
79    fn split_key<'k>(key: &'k str) -> (&'k str, &'k str) {
80        match key.find('.') {
81            Some(pos) => (&key[..pos], &key[pos + 1..]),
82            None => (key, ""),
83        }
84    }
85
86    /// Resolves a yaml value record to a Value (any type, for connection etc.).
87    /// For non-template single placeholder: returns the resolved Value as-is (including Object).
88    /// For string-compatible values: delegates to resolve_value_to_string.
89    fn resolve_value(&mut self, value_idx: u16) -> Result<Option<Value>, StateError> {
90        crate::fn_log!("State", "resolve_value", &value_idx.to_string());
91        let vo = match self.manifest.values.get(value_idx as usize).copied() {
92            Some(v) => v,
93            None => return Ok(None),
94        };
95        let is_template = fixed_bits::get(vo[0], fixed_bits::V_OFFSET_IS_TEMPLATE, fixed_bits::V_MASK_IS_TEMPLATE) == 1;
96        let is_path = fixed_bits::get(vo[0], fixed_bits::V_OFFSET_T0_IS_PATH, fixed_bits::V_MASK_IS_PATH) == 1;
97        let dyn_idx = fixed_bits::get(vo[0], fixed_bits::V_OFFSET_T0_DYNAMIC, fixed_bits::V_MASK_DYNAMIC) as u16;
98
99        if is_path && dyn_idx != 0 && !is_template {
100            let path_segments = match self.manifest.path_map.get(dyn_idx as usize) {
101                Some(s) => s.to_vec(),
102                None => return Ok(None),
103            };
104            let path_key: String = path_segments.iter()
105                .filter_map(|&seg_idx| self.manifest.dynamic.get(seg_idx).map(|s| s.to_string()))
106                .collect::<Vec<_>>()
107                .join(".");
108            return self.get(&path_key);
109        }
110
111        Ok(self.resolve_value_to_string(value_idx)?.map(Value::String))
112    }
113
114    /// Resolves a yaml value record to a String (for use in store/load config keys).
115    fn resolve_value_to_string(&mut self, value_idx: u16) -> Result<Option<String>, StateError> {
116        crate::fn_log!("State", "resolve_value_to_string", &value_idx.to_string());
117        let vo = match self.manifest.values.get(value_idx as usize).copied() {
118            Some(v) => v,
119            None => return Ok(None),
120        };
121
122        let is_template = fixed_bits::get(vo[0], fixed_bits::V_OFFSET_IS_TEMPLATE, fixed_bits::V_MASK_IS_TEMPLATE) == 1;
123
124        const TOKEN_OFFSETS: [(u32, u32); 6] = [
125            (fixed_bits::V_OFFSET_T0_IS_PATH, fixed_bits::V_OFFSET_T0_DYNAMIC),
126            (fixed_bits::V_OFFSET_T1_IS_PATH, fixed_bits::V_OFFSET_T1_DYNAMIC),
127            (fixed_bits::V_OFFSET_T2_IS_PATH, fixed_bits::V_OFFSET_T2_DYNAMIC),
128            (fixed_bits::V_OFFSET_T3_IS_PATH, fixed_bits::V_OFFSET_T3_DYNAMIC),
129            (fixed_bits::V_OFFSET_T4_IS_PATH, fixed_bits::V_OFFSET_T4_DYNAMIC),
130            (fixed_bits::V_OFFSET_T5_IS_PATH, fixed_bits::V_OFFSET_T5_DYNAMIC),
131        ];
132
133        let mut result = String::new();
134
135        for (i, (off_is_path, off_dynamic)) in TOKEN_OFFSETS.iter().enumerate() {
136            let word = if i < 3 { 0 } else { 1 };
137            let is_path = fixed_bits::get(vo[word], *off_is_path, fixed_bits::V_MASK_IS_PATH) == 1;
138            let dyn_idx = fixed_bits::get(vo[word], *off_dynamic, fixed_bits::V_MASK_DYNAMIC) as u16;
139
140            if dyn_idx == 0 {
141                break;
142            }
143
144            if is_path {
145                let path_segments = match self.manifest.path_map.get(dyn_idx as usize) {
146                    Some(s) => s.to_vec(),
147                    None => return Ok(None),
148                };
149                let path_key: String = path_segments.iter()
150                    .filter_map(|&seg_idx| self.manifest.dynamic.get(seg_idx).map(|s| s.to_string()))
151                    .collect::<Vec<_>>()
152                    .join(".");
153                crate::fn_log!("State", "resolve/get", &path_key);
154                let resolved = self.get(&path_key)?;
155                crate::fn_log!("State", "resolve/got", if resolved.is_some() { "Some" } else { "None" });
156                let resolved = match resolved {
157                    Some(v) => v,
158                    None => return Ok(None),
159                };
160                let s = match &resolved {
161                    Value::String(s) => s.clone(),
162                    Value::Number(n) => n.to_string(),
163                    Value::Bool(b) => b.to_string(),
164                    _ => return Ok(None),
165                };
166                result.push_str(&s);
167            } else {
168                let s = match self.manifest.dynamic.get(dyn_idx) {
169                    Some(s) => s.to_string(),
170                    None => return Ok(None),
171                };
172                result.push_str(&s);
173            }
174
175            if !is_template {
176                break;
177            }
178        }
179
180        Ok(Some(result))
181    }
182
183    /// Builds a store/load config HashMap from a meta record index.
184    fn build_config(&mut self, meta_idx: u16) -> Result<Option<HashMap<String, Value>>, StateError> {
185        crate::fn_log!("State", "build_config", &meta_idx.to_string());
186        let record = match self.manifest.keys.get(meta_idx as usize).copied() {
187            Some(r) => r,
188            None => return Ok(None),
189        };
190        let child_idx = fixed_bits::get(record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD) as usize;
191        if child_idx == 0 { return Ok(None); }
192        let has_children = fixed_bits::get(record, fixed_bits::K_OFFSET_HAS_CHILDREN, fixed_bits::K_MASK_HAS_CHILDREN);
193        let children = if has_children == 1 {
194            match self.manifest.children_map.get(child_idx) {
195                Some(c) => c.to_vec(),
196                None => return Ok(None),
197            }
198        } else {
199            vec![child_idx as u16]
200        };
201
202        let mut config = HashMap::new();
203
204        for &child_idx in &children {
205            let record = match self.manifest.keys.get(child_idx as usize).copied() {
206                Some(r) => r,
207                None => continue,
208            };
209            let prop   = fixed_bits::get(record, fixed_bits::K_OFFSET_PROP,   fixed_bits::K_MASK_PROP)   as u8;
210            let client = fixed_bits::get(record, fixed_bits::K_OFFSET_CLIENT, fixed_bits::K_MASK_CLIENT) as u8;
211            let is_leaf = fixed_bits::get(record, fixed_bits::K_OFFSET_IS_LEAF, fixed_bits::K_MASK_IS_LEAF) == 1;
212            let value_idx = if is_leaf {
213                fixed_bits::get(record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD) as u16
214            } else { 0 };
215
216            if client != 0 {
217                config.insert("client".to_string(), Value::Number(client.into()));
218                continue;
219            }
220
221            let prop_name = match codec::prop_decode(prop as u64) {
222                Some(name) => name,
223                None => continue,
224            };
225
226            if prop_name == "map" {
227                if let Some(map_val) = self.build_map_config(child_idx) {
228                    config.insert("map".to_string(), map_val);
229                }
230            } else if prop_name == "connection" {
231                if value_idx != 0 {
232                    if let Some(v) = self.resolve_value(value_idx)? {
233                        config.insert("connection".to_string(), v);
234                    }
235                }
236            } else if value_idx != 0 {
237                if let Some(s) = self.resolve_value_to_string(value_idx)? {
238                    config.insert(prop_name.to_string(), Value::String(s));
239                }
240            }
241        }
242
243        Ok(Some(config))
244    }
245
246    /// Builds a map config object from a map prop record's children.
247    fn build_map_config(&self, map_idx: u16) -> Option<Value> {
248        let record = self.manifest.keys.get(map_idx as usize).copied()?;
249        let child_idx = fixed_bits::get(record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD) as usize;
250        if child_idx == 0 { return Some(Value::Object(serde_json::Map::new())); }
251
252        let has_children = fixed_bits::get(record, fixed_bits::K_OFFSET_HAS_CHILDREN, fixed_bits::K_MASK_HAS_CHILDREN);
253        let children = if has_children == 1 {
254            self.manifest.children_map.get(child_idx)?.to_vec()
255        } else {
256            vec![child_idx as u16]
257        };
258
259        let mut map = serde_json::Map::new();
260        for &c in &children {
261            let child = self.manifest.keys.get(c as usize).copied()?;
262            let dyn_idx   = fixed_bits::get(child, fixed_bits::K_OFFSET_DYNAMIC, fixed_bits::K_MASK_DYNAMIC) as u16;
263            let value_idx = fixed_bits::get(child, fixed_bits::K_OFFSET_CHILD,   fixed_bits::K_MASK_CHILD)   as usize;
264            let is_path   = fixed_bits::get(child, fixed_bits::K_OFFSET_IS_PATH, fixed_bits::K_MASK_IS_PATH) == 1;
265
266            let key_str = if is_path {
267                let segs = self.manifest.path_map.get(dyn_idx as usize)?;
268                segs.iter()
269                    .filter_map(|&s| self.manifest.dynamic.get(s).map(|x| x.to_string()))
270                    .collect::<Vec<_>>()
271                    .join(".")
272            } else {
273                self.manifest.dynamic.get(dyn_idx)?.to_string()
274            };
275
276            let val_vo = self.manifest.values.get(value_idx).copied()?;
277            let col_dyn = fixed_bits::get(val_vo[0], fixed_bits::V_OFFSET_T0_DYNAMIC, fixed_bits::V_MASK_DYNAMIC) as u16;
278            let col_str = self.manifest.dynamic.get(col_dyn)?.to_string();
279
280            map.insert(key_str, Value::String(col_str));
281        }
282
283        Some(Value::Object(map))
284    }
285
286    /// Finds a state_vals index by key_index (skips null slot at 0).
287    fn find_state_value(&self, key_idx: u16) -> Option<usize> {
288        self.state_keys.iter().skip(1).position(|&k| k == key_idx).map(|p| p + 1)
289    }
290
291    /// Returns the value for `key`, checking state cache → _store → _load in order.
292    ///
293    /// # Examples
294    ///
295    /// ```
296    /// use state_engine::State;
297    /// use state_engine::InMemoryClient;
298    /// use serde_json::{json, Value};
299    ///
300    /// struct MockInMemory { data: std::sync::Mutex<std::collections::HashMap<String, Value>> }
301    /// impl MockInMemory { fn new() -> Self { Self { data: Default::default() } } }
302    /// impl InMemoryClient for MockInMemory {
303    ///     fn get(&self, key: &str) -> Option<Value> { self.data.lock().unwrap().get(key).cloned() }
304    ///     fn set(&self, key: &str, value: Value) -> bool { self.data.lock().unwrap().insert(key.to_string(), value); true }
305    ///     fn delete(&self, key: &str) -> bool { self.data.lock().unwrap().remove(key).is_some() }
306    /// }
307    ///
308    /// let client = MockInMemory::new();
309    /// let mut state = State::new("./examples/manifest")
310    ///     .with_in_memory(&client);
311    ///
312    /// // set then get
313    /// state.set("connection.common", json!({"host": "localhost"}), None).unwrap();
314    /// assert!(state.get("connection.common").unwrap().is_some());
315    /// ```
316    pub fn get(&mut self, key: &str) -> Result<Option<Value>, StateError> {
317        crate::fn_log!("State", "get", key);
318        if self.called_keys.len() >= self.max_recursion {
319            return Err(StateError::RecursionLimitExceeded);
320        }
321        if self.called_keys.contains(&key.to_string()) {
322            return Err(StateError::RecursionLimitExceeded);
323        }
324
325        self.called_keys.insert(key.to_string());
326
327        let (file, path) = Self::split_key(key);
328        let file = file.to_string();
329        let path = path.to_string();
330
331        if let Err(e) = self.manifest.load(&file) {
332            self.called_keys.remove(key);
333            return Err(StateError::ManifestLoadFailed(e.to_string()));
334        }
335
336        let key_idx = match self.manifest.find(&file, &path) {
337            Some(idx) => idx,
338            None => {
339                self.called_keys.remove(key);
340                return Err(StateError::KeyNotFound(key.to_string()));
341            }
342        };
343
344        // check state cache
345        if let Some(sv_idx) = self.find_state_value(key_idx) {
346            let val = self.state_vals.get(sv_idx).cloned();
347            self.called_keys.remove(key);
348            return Ok(val);
349        }
350
351        let meta = self.manifest.get_meta(&file, &path);
352
353        // check if _load client is State (load-only, no store read)
354        let has_state_client = meta.load.and_then(|load_idx| {
355            self.manifest.keys.get(load_idx as usize).copied()
356                .map(|r| fixed_bits::get(r, fixed_bits::K_OFFSET_CLIENT, fixed_bits::K_MASK_CLIENT) == fixed_bits::CLIENT_STATE)
357        }).unwrap_or(false);
358
359        if !has_state_client {
360            if let Some(store_idx) = meta.store {
361                match self.build_config(store_idx) {
362                    Ok(Some(config)) => {
363                        if let Some(value) = self.store.get(&config) {
364                            self.state_keys.push(key_idx);
365                            self.state_vals.push(value.clone());
366                            self.called_keys.remove(key);
367                            return Ok(Some(value));
368                        }
369                    }
370                    Ok(None) => {}
371                    Err(e) => {
372                        self.called_keys.remove(key);
373                        return Err(e);
374                    }
375                }
376            }
377        }
378
379        // try _load
380        let result = if let Some(load_idx) = meta.load {
381            match self.build_config(load_idx) {
382                Ok(Some(mut config)) => {
383                    if !config.contains_key("client") {
384                        self.called_keys.remove(key);
385                        return Ok(None);
386                    }
387
388                    // unqualify map keys for Load
389                    if let Some(Value::Object(map_obj)) = config.get("map").cloned() {
390                        let mut unqualified = serde_json::Map::new();
391                        for (qk, v) in map_obj {
392                            let field = qk.rfind('.').map_or(qk.as_str(), |p| &qk[p+1..]);
393                            unqualified.insert(field.to_string(), v);
394                        }
395                        config.insert("map".to_string(), Value::Object(unqualified));
396                    }
397
398                    match self.load.handle(&config) {
399                        Ok(loaded) => {
400                            if let Some(store_idx) = meta.store {
401                                match self.build_config(store_idx) {
402                                    Ok(Some(store_config)) => {
403                                        if self.store.set(&store_config, loaded.clone(), None).unwrap_or(false) {
404                                            self.state_keys.push(key_idx);
405                                            self.state_vals.push(loaded.clone());
406                                        }
407                                    }
408                                    Ok(None) => {
409                                        self.state_keys.push(key_idx);
410                                        self.state_vals.push(loaded.clone());
411                                    }
412                                    Err(_) => {
413                                        // write-through cache failure is non-fatal
414                                    }
415                                }
416                            } else {
417                                self.state_keys.push(key_idx);
418                                self.state_vals.push(loaded.clone());
419                            }
420                            Ok(Some(loaded))
421                        }
422                        Err(e) => Err(StateError::LoadFailed(e)),
423                    }
424                }
425                Ok(None) => Ok(None),
426                Err(e) => Err(e),
427            }
428        } else { Ok(None) };
429
430        self.called_keys.remove(key);
431        result
432    }
433
434    /// Writes `value` to the _store backend for `key`.
435    ///
436    /// # Examples
437    ///
438    /// ```
439    /// # use state_engine::State;
440    /// # use state_engine::InMemoryClient;
441    /// # use serde_json::{json, Value};
442    /// # struct MockInMemory { data: std::sync::Mutex<std::collections::HashMap<String, Value>> }
443    /// # impl MockInMemory { fn new() -> Self { Self { data: Default::default() } } }
444    /// # impl InMemoryClient for MockInMemory {
445    /// #     fn get(&self, key: &str) -> Option<Value> { self.data.lock().unwrap().get(key).cloned() }
446    /// #     fn set(&self, key: &str, value: Value) -> bool { self.data.lock().unwrap().insert(key.to_string(), value); true }
447    /// #     fn delete(&self, key: &str) -> bool { self.data.lock().unwrap().remove(key).is_some() }
448    /// # }
449    /// let client = MockInMemory::new();
450    /// let mut state = State::new("./examples/manifest")
451    ///     .with_in_memory(&client);
452    ///
453    /// assert!(state.set("connection.common", json!({"host": "localhost"}), None).unwrap());
454    /// ```
455    pub fn set(&mut self, key: &str, value: Value, ttl: Option<u64>) -> Result<bool, StateError> {
456        crate::fn_log!("State", "set", key);
457        let (file, path) = Self::split_key(key);
458        let file = file.to_string();
459        let path = path.to_string();
460
461        if let Err(e) = self.manifest.load(&file) {
462            return Err(StateError::ManifestLoadFailed(e.to_string()));
463        }
464
465        let key_idx = match self.manifest.find(&file, &path) {
466            Some(idx) => idx,
467            None => return Err(StateError::KeyNotFound(key.to_string())),
468        };
469
470        let meta = self.manifest.get_meta(&file, &path);
471
472        if let Some(store_idx) = meta.store {
473            match self.build_config(store_idx)? {
474                Some(config) => {
475                    return match self.store.set(&config, value.clone(), ttl) {
476                        Ok(ok) => {
477                            if ok {
478                                if let Some(sv_idx) = self.find_state_value(key_idx) {
479                                    self.state_vals[sv_idx] = value;
480                                } else {
481                                    self.state_keys.push(key_idx);
482                                    self.state_vals.push(value);
483                                }
484                            }
485                            Ok(ok)
486                        }
487                        Err(e) => Err(StateError::StoreFailed(e)),
488                    };
489                }
490                None => {}
491            }
492        }
493        Ok(false)
494    }
495
496    /// Removes the value for `key` from the _store backend.
497    ///
498    /// # Examples
499    ///
500    /// ```
501    /// # use state_engine::State;
502    /// # use state_engine::InMemoryClient;
503    /// # use serde_json::{json, Value};
504    /// # struct MockInMemory { data: std::sync::Mutex<std::collections::HashMap<String, Value>> }
505    /// # impl MockInMemory { fn new() -> Self { Self { data: Default::default() } } }
506    /// # impl InMemoryClient for MockInMemory {
507    /// #     fn get(&self, key: &str) -> Option<Value> { self.data.lock().unwrap().get(key).cloned() }
508    /// #     fn set(&self, key: &str, value: Value) -> bool { self.data.lock().unwrap().insert(key.to_string(), value); true }
509    /// #     fn delete(&self, key: &str) -> bool { self.data.lock().unwrap().remove(key).is_some() }
510    /// # }
511    /// let client = MockInMemory::new();
512    /// let mut state = State::new("./examples/manifest")
513    ///     .with_in_memory(&client);
514    ///
515    /// state.set("connection.common", json!({"host": "localhost"}), None).unwrap();
516    /// assert!(state.delete("connection.common").unwrap());
517    /// // after delete, store has no data; _load is attempted but EnvClient is not configured here
518    /// assert!(state.get("connection.common").is_err() || state.get("connection.common").unwrap().is_none());
519    /// ```
520    pub fn delete(&mut self, key: &str) -> Result<bool, StateError> {
521        crate::fn_log!("State", "delete", key);
522        let (file, path) = Self::split_key(key);
523        let file = file.to_string();
524        let path = path.to_string();
525
526        if let Err(e) = self.manifest.load(&file) {
527            return Err(StateError::ManifestLoadFailed(e.to_string()));
528        }
529
530        let key_idx = match self.manifest.find(&file, &path) {
531            Some(idx) => idx,
532            None => return Err(StateError::KeyNotFound(key.to_string())),
533        };
534
535        let meta = self.manifest.get_meta(&file, &path);
536
537        if let Some(store_idx) = meta.store {
538            match self.build_config(store_idx)? {
539                Some(config) => {
540                    return match self.store.delete(&config) {
541                        Ok(ok) => {
542                            if ok {
543                                if let Some(sv_idx) = self.find_state_value(key_idx) {
544                                    self.state_keys[sv_idx] = 0;
545                                    self.state_vals[sv_idx] = Value::Null;
546                                }
547                            }
548                            Ok(ok)
549                        }
550                        Err(e) => Err(StateError::StoreFailed(e)),
551                    };
552                }
553                None => {}
554            }
555        }
556        Ok(false)
557    }
558
559    /// Returns `true` if a value exists for `key` in state cache or _store.
560    /// Does not trigger _load.
561    ///
562    /// # Examples
563    ///
564    /// ```
565    /// # use state_engine::State;
566    /// # use state_engine::InMemoryClient;
567    /// # use serde_json::{json, Value};
568    /// # struct MockInMemory { data: std::sync::Mutex<std::collections::HashMap<String, Value>> }
569    /// # impl MockInMemory { fn new() -> Self { Self { data: Default::default() } } }
570    /// # impl InMemoryClient for MockInMemory {
571    /// #     fn get(&self, key: &str) -> Option<Value> { self.data.lock().unwrap().get(key).cloned() }
572    /// #     fn set(&self, key: &str, value: Value) -> bool { self.data.lock().unwrap().insert(key.to_string(), value); true }
573    /// #     fn delete(&self, key: &str) -> bool { self.data.lock().unwrap().remove(key).is_some() }
574    /// # }
575    /// let client = MockInMemory::new();
576    /// let mut state = State::new("./examples/manifest")
577    ///     .with_in_memory(&client);
578    ///
579    /// assert!(!state.exists("connection.common").unwrap());
580    /// state.set("connection.common", json!({"host": "localhost"}), None).unwrap();
581    /// assert!(state.exists("connection.common").unwrap());
582    /// ```
583    pub fn exists(&mut self, key: &str) -> Result<bool, StateError> {
584        crate::fn_log!("State", "exists", key);
585        let (file, path) = Self::split_key(key);
586        let file = file.to_string();
587        let path = path.to_string();
588
589        if let Err(e) = self.manifest.load(&file) {
590            return Err(StateError::ManifestLoadFailed(e.to_string()));
591        }
592
593        let key_idx = match self.manifest.find(&file, &path) {
594            Some(idx) => idx,
595            None => return Err(StateError::KeyNotFound(key.to_string())),
596        };
597
598        if let Some(sv_idx) = self.find_state_value(key_idx) {
599            return Ok(!self.state_vals.get(sv_idx).map_or(true, |v| v.is_null()));
600        }
601
602        let meta = self.manifest.get_meta(&file, &path);
603        if let Some(store_idx) = meta.store {
604            if let Some(config) = self.build_config(store_idx)? {
605                return Ok(self.store.get(&config).is_some());
606            }
607        }
608        Ok(false)
609    }
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615    use crate::ports::required::{KVSClient, DbClient, EnvClient, FileClient};
616    use serde_json::Value;
617    use std::collections::HashMap;
618
619    struct StubKVS;
620    impl KVSClient for StubKVS {
621        fn get(&self, _: &str) -> Option<String> { None }
622        fn set(&self, _: &str, _: String, _: Option<u64>) -> bool { false }
623        fn delete(&self, _: &str) -> bool { false }
624    }
625
626    struct StubDb;
627    impl DbClient for StubDb {
628        fn get(&self, _: &Value, _: &str, _: &[&str], _: Option<&str>) -> Option<Vec<HashMap<String, Value>>> { None }
629        fn set(&self, _: &Value, _: &str, _: &HashMap<String, Value>, _: Option<&str>) -> bool { false }
630        fn delete(&self, _: &Value, _: &str, _: Option<&str>) -> bool { false }
631    }
632
633    struct StubEnv;
634    impl EnvClient for StubEnv {
635        fn get(&self, _: &str) -> Option<String> { None }
636        fn set(&self, _: &str, _: String) -> bool { false }
637        fn delete(&self, _: &str) -> bool { false }
638    }
639
640    struct StubFile;
641    impl FileClient for StubFile {
642        fn get(&self, _: &str) -> Option<String> { None }
643        fn set(&self, _: &str, _: String) -> bool { false }
644        fn delete(&self, _: &str) -> bool { false }
645    }
646
647    struct StubHttp;
648    impl crate::ports::required::HttpClient for StubHttp {
649        fn get(&self, _: &str, _: Option<&HashMap<String, String>>) -> Option<Value> { None }
650        fn set(&self, _: &str, _: Value, _: Option<&HashMap<String, String>>) -> bool { false }
651        fn delete(&self, _: &str, _: Option<&HashMap<String, String>>) -> bool { false }
652    }
653
654    #[test]
655    fn test_with_clients_build() {
656        let kvs  = StubKVS;
657        let db   = StubDb;
658        let env  = StubEnv;
659        let http = StubHttp;
660
661        // each builder returns Self without panic — wiring is correct
662        let _ = State::new("./examples/manifest").with_kvs(&kvs);
663        let _ = State::new("./examples/manifest").with_db(&db);
664        let _ = State::new("./examples/manifest").with_env(&env);
665        let _ = State::new("./examples/manifest").with_http(&http);
666        let _ = State::new("./examples/manifest").with_file(StubFile);
667    }
668}