Skip to main content

state_engine/
manifest.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use crate::core::parser::{ParsedManifest, Value, parse};
4use crate::core::pool::DynamicPool;
5use crate::core::fixed_bits;
6use crate::ports::provided::ManifestError;
7use crate::ports::required::FileClient;
8
9/// Indices of meta records for a given node, collected from root to node (child overrides parent).
10#[derive(Debug, Default)]
11pub struct MetaIndices {
12    pub load:  Option<u16>,
13    pub store: Option<u16>,
14    pub state: Option<u16>,
15}
16
17/// Manages parsed manifest files with all vecs shared globally across files.
18/// Each file is parsed on first access and appended to the shared vecs.
19///
20/// # Examples
21///
22/// ```
23/// use state_engine::Manifest;
24///
25/// let mut store = Manifest::new("./examples/manifest");
26/// assert!(store.load("cache").is_ok());
27/// assert!(store.load("nonexistent").is_err());
28/// ```
29pub struct Manifest {
30    manifest_dir: PathBuf,
31    file: Box<dyn FileClient>,
32    files: HashMap<String, ParsedManifest>,
33    pub dynamic: DynamicPool,
34    pub keys: Vec<u64>,
35    pub values: Vec<[u64; 2]>,
36    pub path_map: Vec<Vec<u16>>,
37    pub children_map: Vec<Vec<u16>>,
38}
39
40impl Manifest {
41    pub fn new(manifest_dir: &str) -> Self {
42        Self {
43            manifest_dir: PathBuf::from(manifest_dir),
44            file: Box::new(crate::ports::default::DefaultFileClient),
45            files: HashMap::new(),
46            dynamic: DynamicPool::new(),
47            keys: vec![0],
48            values: vec![[0, 0]],
49            path_map: vec![vec![]],
50            children_map: vec![vec![]],
51        }
52    }
53
54    /// Replaces the default FileClient. Useful for WASI/JS environments without std::fs.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use state_engine::{Manifest, FileClient};
60    ///
61    /// struct MockFile;
62    /// impl FileClient for MockFile {
63    ///     fn get(&self, path: &str) -> Option<String> {
64    ///         if path.ends_with("cache.yml") { Some("user:\n  id:\n    _state:\n      type: string\n".into()) }
65    ///         else { None }
66    ///     }
67    ///     fn set(&self, _: &str, _: String) -> bool { false }
68    ///     fn delete(&self, _: &str) -> bool { false }
69    /// }
70    ///
71    /// let mut m = Manifest::new("/any/path").with_file(MockFile);
72    /// assert!(m.load("cache").is_ok());
73    /// assert!(m.file_key_idx("cache").is_some());
74    /// assert!(m.file_key_idx("missing").is_none());
75    /// ```
76    pub fn with_file(mut self, client: impl FileClient + 'static) -> Self {
77        self.file = Box::new(client);
78        self
79    }
80
81    /// Loads and parses a manifest file by name (without extension) if not cached.
82    /// Second call for the same file is a no-op (cached).
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use state_engine::Manifest;
88    ///
89    /// let mut store = Manifest::new("./examples/manifest");
90    ///
91    /// // first load parses and caches
92    /// assert!(store.load("cache").is_ok());
93    ///
94    /// // second load is a no-op
95    /// assert!(store.load("cache").is_ok());
96    ///
97    /// // nonexistent file returns Err
98    /// assert!(store.load("nonexistent").is_err());
99    ///
100    /// // after load, keys are globally unique across files
101    /// assert!(store.load("session").is_ok());
102    /// let cache_idx  = store.find("cache",   "user").unwrap();
103    /// let session_idx = store.find("session", "sso_user_id").unwrap();
104    /// assert_ne!(cache_idx, session_idx);
105    /// ```
106    pub fn load(&mut self, file: &str) -> Result<(), ManifestError> {
107        crate::fn_log!("Manifest", "load", file);
108        if self.files.contains_key(file) {
109            return Ok(());
110        }
111
112        let yml_path  = self.manifest_dir.join(format!("{}.yml", file));
113        let yaml_path = self.manifest_dir.join(format!("{}.yaml", file));
114
115        let yml_key  = yml_path.to_string_lossy();
116        let yaml_key = yaml_path.to_string_lossy();
117        let yml_content  = self.file.get(&yml_key);
118        let yaml_content = self.file.get(&yaml_key);
119
120        let content = match (yml_content, yaml_content) {
121            (Some(_), Some(_)) => return Err(ManifestError::AmbiguousFile(format!(
122                "both '{}.yml' and '{}.yaml' exist.", file, file
123            ))),
124            (Some(c), None) => c,
125            (None, Some(c)) => c,
126            (None, None) => return Err(ManifestError::FileNotFound(format!(
127                "'{}.yml' or '{}.yaml'", file, file
128            ))),
129        };
130
131        let yaml_root: serde_yaml_ng::Value = serde_yaml_ng::from_str(&content)
132            .map_err(|e| ManifestError::ParseError(format!("YAML parse error: {}", e)))?;
133
134        let pm = parse(
135            file,
136            yaml_to_value(yaml_root),
137            &mut self.dynamic,
138            &mut self.keys,
139            &mut self.values,
140            &mut self.path_map,
141            &mut self.children_map,
142        ).map_err(|e| ManifestError::ParseError(e))?;
143
144        self.files.insert(file.to_string(), pm);
145        Ok(())
146    }
147
148    /// Returns the file_key_idx for a loaded file.
149    pub fn file_key_idx(&self, file: &str) -> Option<u16> {
150        self.files.get(file).map(|pm| pm.file_key_idx)
151    }
152
153    /// Looks up a key record index by dot-separated path within a file.
154    /// Returns `None` if file is not loaded or path not found.
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use state_engine::Manifest;
160    ///
161    /// let mut store = Manifest::new("./examples/manifest");
162    /// store.load("cache").unwrap();
163    ///
164    /// // "user" exists
165    /// assert!(store.find("cache", "user").is_some());
166    ///
167    /// // "user.id" exists
168    /// assert!(store.find("cache", "user.id").is_some());
169    ///
170    /// // unknown path returns None
171    /// assert!(store.find("cache", "never").is_none());
172    /// ```
173    pub fn find(&self, file: &str, path: &str) -> Option<u16> {
174        let file_idx = self.files.get(file)?.file_key_idx;
175        let file_record = self.keys.get(file_idx as usize).copied()?;
176
177        if path.is_empty() {
178            return Some(file_idx);
179        }
180
181        let segments: Vec<&str> = path.split('.').collect();
182        let top_level = self.children_of(file_record);
183        self.find_in(&segments, &top_level)
184    }
185
186    /// Recursively walks the Trie to find the record matching `segments`.
187    fn find_in(&self, segments: &[&str], candidates: &[u16]) -> Option<u16> {
188        let target = segments[0];
189        let rest   = &segments[1..];
190
191        for &idx in candidates {
192            let record = self.keys.get(idx as usize).copied()?;
193
194            // skip meta keys
195            if fixed_bits::get(record, fixed_bits::K_OFFSET_ROOT, fixed_bits::K_MASK_ROOT) != fixed_bits::ROOT_NULL {
196                continue;
197            }
198
199            let dyn_idx = fixed_bits::get(record, fixed_bits::K_OFFSET_DYNAMIC, fixed_bits::K_MASK_DYNAMIC) as u16;
200            if self.dynamic.get(dyn_idx)? != target {
201                continue;
202            }
203
204            if rest.is_empty() {
205                return Some(idx);
206            }
207
208            let next = self.children_of(record);
209            if next.is_empty() {
210                return None;
211            }
212            return self.find_in(rest, &next);
213        }
214
215        None
216    }
217
218    /// Returns the direct field-key children indices of a record.
219    fn children_of(&self, record: u64) -> Vec<u16> {
220        let child_idx = fixed_bits::get(record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD) as usize;
221        if child_idx == 0 {
222            return vec![];
223        }
224        let has_children = fixed_bits::get(record, fixed_bits::K_OFFSET_HAS_CHILDREN, fixed_bits::K_MASK_HAS_CHILDREN);
225        if has_children == 1 {
226            self.children_map.get(child_idx)
227                .map(|s: &Vec<u16>| s.to_vec())
228                .unwrap_or_default()
229        } else {
230            vec![child_idx as u16]
231        }
232    }
233
234    /// Returns meta record indices (_load/_store/_state) for a dot-path node.
235    /// Collects from root to node; child overrides parent.
236    ///
237    /// # Examples
238    ///
239    /// ```
240    /// use state_engine::Manifest;
241    ///
242    /// let mut store = Manifest::new("./examples/manifest");
243    /// store.load("cache").unwrap();
244    ///
245    /// let meta = store.get_meta("cache", "user");
246    /// assert!(meta.load.is_some());
247    /// assert!(meta.store.is_some());
248    ///
249    /// // leaf node has _state
250    /// let meta = store.get_meta("cache", "user.id");
251    /// assert!(meta.state.is_some());
252    /// ```
253    pub fn get_meta(&self, file: &str, path: &str) -> MetaIndices {
254        crate::fn_log!("Manifest", "get_meta", &format!("{}.{}", file, path));
255        let file_idx = match self.files.get(file) {
256            Some(pm) => pm.file_key_idx,
257            None => return MetaIndices::default(),
258        };
259
260        let file_record = match self.keys.get(file_idx as usize).copied() {
261            Some(r) => r,
262            None => return MetaIndices::default(),
263        };
264
265        let segments: Vec<&str> = if path.is_empty() {
266            vec![]
267        } else {
268            path.split('.').collect()
269        };
270
271        let mut meta = MetaIndices::default();
272
273        // collect meta from file root level
274        self.collect_meta(file_record, &mut meta);
275
276        // walk path segments, collecting meta at each level
277        let mut candidates = self.children_of(file_record);
278        for segment in &segments {
279            let mut found_idx = None;
280            for &idx in &candidates {
281                let record = match self.keys.get(idx as usize).copied() {
282                    Some(r) => r,
283                    None => continue,
284                };
285                if fixed_bits::get(record, fixed_bits::K_OFFSET_ROOT, fixed_bits::K_MASK_ROOT) != fixed_bits::ROOT_NULL {
286                    continue;
287                }
288                let dyn_idx = fixed_bits::get(record, fixed_bits::K_OFFSET_DYNAMIC, fixed_bits::K_MASK_DYNAMIC) as u16;
289                if self.dynamic.get(dyn_idx) == Some(segment) {
290                    self.collect_meta(record, &mut meta);
291                    found_idx = Some(idx);
292                    break;
293                }
294            }
295            match found_idx {
296                Some(idx) => {
297                    let record = self.keys[idx as usize];
298                    candidates = self.children_of(record);
299                }
300                None => return MetaIndices::default(),
301            }
302        }
303
304        meta
305    }
306
307    /// Scans children of `record` for meta records and updates `meta`.
308    fn collect_meta(&self, record: u64, meta: &mut MetaIndices) {
309        let children = self.children_of(record);
310        for &idx in &children {
311            let child = match self.keys.get(idx as usize).copied() {
312                Some(r) => r,
313                None => continue,
314            };
315            let root = fixed_bits::get(child, fixed_bits::K_OFFSET_ROOT, fixed_bits::K_MASK_ROOT);
316            match root {
317                fixed_bits::ROOT_LOAD  => meta.load  = Some(idx),
318                fixed_bits::ROOT_STORE => meta.store = Some(idx),
319                fixed_bits::ROOT_STATE => meta.state = Some(idx),
320                _ => {}
321            }
322        }
323    }
324
325    /// Returns indices of field-key leaf values for a node (meta keys and nulls excluded).
326    /// Returns a vec of (dynamic_index, yaml_value_index) for each leaf child.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use state_engine::Manifest;
332    ///
333    /// let mut store = Manifest::new("./examples/manifest");
334    /// store.load("connection").unwrap();
335    ///
336    /// // "tag", "driver", "charset" are static leaf values
337    /// let values = store.get_value("connection", "common");
338    /// assert!(!values.is_empty());
339    /// ```
340    pub fn get_value(&self, file: &str, path: &str) -> Vec<(u16, u16)> {
341        let node_idx = match self.find(file, path) {
342            Some(idx) => idx,
343            None => return vec![],
344        };
345
346        let record = match self.keys.get(node_idx as usize).copied() {
347            Some(r) => r,
348            None => return vec![],
349        };
350
351        let mut result = Vec::new();
352        let children = self.children_of(record);
353
354        for &idx in &children {
355            let child = match self.keys.get(idx as usize).copied() {
356                Some(r) => r,
357                None => continue,
358            };
359            // skip meta keys
360            if fixed_bits::get(child, fixed_bits::K_OFFSET_ROOT, fixed_bits::K_MASK_ROOT) != fixed_bits::ROOT_NULL {
361                continue;
362            }
363            // only leaf nodes with a value
364            if fixed_bits::get(child, fixed_bits::K_OFFSET_IS_LEAF, fixed_bits::K_MASK_IS_LEAF) == 0 {
365                continue;
366            }
367            let dyn_idx   = fixed_bits::get(child, fixed_bits::K_OFFSET_DYNAMIC, fixed_bits::K_MASK_DYNAMIC) as u16;
368            let value_idx = fixed_bits::get(child, fixed_bits::K_OFFSET_CHILD,   fixed_bits::K_MASK_CHILD)   as u16;
369            result.push((dyn_idx, value_idx));
370        }
371
372        result
373    }
374}
375
376fn yaml_to_value(v: serde_yaml_ng::Value) -> Value {
377    match v {
378        serde_yaml_ng::Value::Mapping(m) => Value::Mapping(
379            m.into_iter()
380                .filter_map(|(k, v)| {
381                    let key = match k {
382                        serde_yaml_ng::Value::String(s) => s,
383                        _ => return None,
384                    };
385                    Some((key, yaml_to_value(v)))
386                })
387                .collect(),
388        ),
389        serde_yaml_ng::Value::String(s) => Value::Scalar(s),
390        serde_yaml_ng::Value::Number(n) => Value::Scalar(n.to_string()),
391        serde_yaml_ng::Value::Bool(b)   => Value::Scalar(b.to_string()),
392        serde_yaml_ng::Value::Null      => Value::Null,
393        _                               => Value::Null,
394    }
395}