Skip to main content

state_engine/
manifest.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4use crate::common::parser::{ParsedManifest, parse};
5use crate::common::pool::{DynamicPool, PathMap, ChildrenMap, KeyList, YamlValueList};
6use crate::common::bit;
7use crate::ports::provided::ManifestError;
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 pools shared globally across files.
18/// Each file is parsed on first access and appended to the shared pools.
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    files: HashMap<String, ParsedManifest>,
32    // shared pools across all loaded files
33    pub dynamic: DynamicPool,
34    pub path_map: PathMap,
35    pub children_map: ChildrenMap,
36    pub keys: KeyList,
37    pub values: YamlValueList,
38}
39
40impl Manifest {
41    pub fn new(manifest_dir: &str) -> Self {
42        Self {
43            manifest_dir: PathBuf::from(manifest_dir),
44            files: HashMap::new(),
45            dynamic: DynamicPool::new(),
46            path_map: PathMap::new(),
47            children_map: ChildrenMap::new(),
48            keys: KeyList::new(),
49            values: YamlValueList::new(),
50        }
51    }
52
53    /// Loads and parses a manifest file by name (without extension) if not cached.
54    /// Second call for the same file is a no-op (cached).
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use state_engine::Manifest;
60    ///
61    /// let mut store = Manifest::new("./examples/manifest");
62    ///
63    /// // first load parses and caches
64    /// assert!(store.load("cache").is_ok());
65    ///
66    /// // second load is a no-op
67    /// assert!(store.load("cache").is_ok());
68    ///
69    /// // nonexistent file returns Err
70    /// assert!(store.load("nonexistent").is_err());
71    ///
72    /// // after load, keys are globally unique across files
73    /// assert!(store.load("session").is_ok());
74    /// let cache_idx  = store.find("cache",   "user").unwrap();
75    /// let session_idx = store.find("session", "sso_user_id").unwrap();
76    /// assert_ne!(cache_idx, session_idx);
77    /// ```
78    pub fn load(&mut self, file: &str) -> Result<(), ManifestError> {
79        crate::fn_log!("Manifest", "load", file);
80        if self.files.contains_key(file) {
81            return Ok(());
82        }
83
84        let yml_path  = self.manifest_dir.join(format!("{}.yml", file));
85        let yaml_path = self.manifest_dir.join(format!("{}.yaml", file));
86        let yml_exists  = yml_path.exists();
87        let yaml_exists = yaml_path.exists();
88
89        if yml_exists && yaml_exists {
90            return Err(ManifestError::AmbiguousFile(format!(
91                "both '{}.yml' and '{}.yaml' exist.",
92                file, file
93            )));
94        }
95
96        let path = if yml_exists {
97            yml_path
98        } else if yaml_exists {
99            yaml_path
100        } else {
101            return Err(ManifestError::FileNotFound(format!(
102                "'{}.yml' or '{}.yaml'", file, file
103            )));
104        };
105
106        let content = fs::read_to_string(&path)
107            .map_err(|e| ManifestError::ReadError(e.to_string()))?;
108
109        let pm = parse(
110            file,
111            &content,
112            &mut self.dynamic,
113            &mut self.path_map,
114            &mut self.children_map,
115            &mut self.keys,
116            &mut self.values,
117        ).map_err(|e| ManifestError::ParseError(e))?;
118
119        self.files.insert(file.to_string(), pm);
120        Ok(())
121    }
122
123    /// Returns the file_key_idx for a loaded file.
124    pub fn file_key_idx(&self, file: &str) -> Option<u16> {
125        self.files.get(file).map(|pm| pm.file_key_idx)
126    }
127
128    /// Looks up a key record index by dot-separated path within a file.
129    /// Returns `None` if file is not loaded or path not found.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use state_engine::Manifest;
135    ///
136    /// let mut store = Manifest::new("./examples/manifest");
137    /// store.load("cache").unwrap();
138    ///
139    /// // "user" exists
140    /// assert!(store.find("cache", "user").is_some());
141    ///
142    /// // "user.id" exists
143    /// assert!(store.find("cache", "user.id").is_some());
144    ///
145    /// // unknown path returns None
146    /// assert!(store.find("cache", "never").is_none());
147    /// ```
148    pub fn find(&self, file: &str, path: &str) -> Option<u16> {
149        let file_idx = self.files.get(file)?.file_key_idx;
150        let file_record = self.keys.get(file_idx)?;
151
152        if path.is_empty() {
153            return Some(file_idx);
154        }
155
156        let segments: Vec<&str> = path.split('.').collect();
157        let top_level = self.children_of(file_record);
158        self.find_in(&segments, &top_level)
159    }
160
161    /// Recursively walks the Trie to find the record matching `segments`.
162    fn find_in(&self, segments: &[&str], candidates: &[u16]) -> Option<u16> {
163        let target = segments[0];
164        let rest   = &segments[1..];
165
166        for &idx in candidates {
167            let record = self.keys.get(idx)?;
168
169            // skip meta keys
170            if bit::get(record, bit::OFFSET_ROOT, bit::MASK_ROOT) != bit::ROOT_NULL {
171                continue;
172            }
173
174            let dyn_idx = bit::get(record, bit::OFFSET_DYNAMIC, bit::MASK_DYNAMIC) as u16;
175            if self.dynamic.get(dyn_idx)? != target {
176                continue;
177            }
178
179            if rest.is_empty() {
180                return Some(idx);
181            }
182
183            let next = self.children_of(record);
184            if next.is_empty() {
185                return None;
186            }
187            return self.find_in(rest, &next);
188        }
189
190        None
191    }
192
193    /// Returns the direct field-key children indices of a record.
194    fn children_of(&self, record: u64) -> Vec<u16> {
195        let child_idx = bit::get(record, bit::OFFSET_CHILD, bit::MASK_CHILD) as u16;
196        if child_idx == 0 {
197            return vec![];
198        }
199        let has_children = bit::get(record, bit::OFFSET_HAS_CHILDREN, bit::MASK_HAS_CHILDREN);
200        if has_children == 1 {
201            self.children_map.get(child_idx)
202                .map(|s| s.to_vec())
203                .unwrap_or_default()
204        } else {
205            vec![child_idx]
206        }
207    }
208
209    /// Returns meta record indices (_load/_store/_state) for a dot-path node.
210    /// Collects from root to node; child overrides parent.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use state_engine::Manifest;
216    ///
217    /// let mut store = Manifest::new("./examples/manifest");
218    /// store.load("cache").unwrap();
219    ///
220    /// let meta = store.get_meta("cache", "user");
221    /// assert!(meta.load.is_some());
222    /// assert!(meta.store.is_some());
223    ///
224    /// // leaf node has _state
225    /// let meta = store.get_meta("cache", "user.id");
226    /// assert!(meta.state.is_some());
227    /// ```
228    pub fn get_meta(&self, file: &str, path: &str) -> MetaIndices {
229        crate::fn_log!("Manifest", "get_meta", &format!("{}.{}", file, path));
230        let file_idx = match self.files.get(file) {
231            Some(pm) => pm.file_key_idx,
232            None => return MetaIndices::default(),
233        };
234
235        let file_record = match self.keys.get(file_idx) {
236            Some(r) => r,
237            None => return MetaIndices::default(),
238        };
239
240        let segments: Vec<&str> = if path.is_empty() {
241            vec![]
242        } else {
243            path.split('.').collect()
244        };
245
246        let mut meta = MetaIndices::default();
247
248        // collect meta from file root level
249        self.collect_meta(file_record, &mut meta);
250
251        // walk path segments, collecting meta at each level
252        let mut candidates = self.children_of(file_record);
253        for segment in &segments {
254            let mut found_idx = None;
255            for &idx in &candidates {
256                let record = match self.keys.get(idx) {
257                    Some(r) => r,
258                    None => continue,
259                };
260                if bit::get(record, bit::OFFSET_ROOT, bit::MASK_ROOT) != bit::ROOT_NULL {
261                    continue;
262                }
263                let dyn_idx = bit::get(record, bit::OFFSET_DYNAMIC, bit::MASK_DYNAMIC) as u16;
264                if self.dynamic.get(dyn_idx) == Some(segment) {
265                    self.collect_meta(record, &mut meta);
266                    found_idx = Some(idx);
267                    break;
268                }
269            }
270            match found_idx {
271                Some(idx) => {
272                    let record = self.keys.get(idx).unwrap();
273                    candidates = self.children_of(record);
274                }
275                None => return MetaIndices::default(),
276            }
277        }
278
279        meta
280    }
281
282    /// Scans children of `record` for meta records and updates `meta`.
283    fn collect_meta(&self, record: u64, meta: &mut MetaIndices) {
284        let children = self.children_of(record);
285        for &idx in &children {
286            let child = match self.keys.get(idx) {
287                Some(r) => r,
288                None => continue,
289            };
290            let root = bit::get(child, bit::OFFSET_ROOT, bit::MASK_ROOT);
291            match root {
292                bit::ROOT_LOAD  => meta.load  = Some(idx),
293                bit::ROOT_STORE => meta.store = Some(idx),
294                bit::ROOT_STATE => meta.state = Some(idx),
295                _ => {}
296            }
297        }
298    }
299
300    /// Returns indices of field-key leaf values for a node (meta keys and nulls excluded).
301    /// Returns a vec of (dynamic_index, yaml_value_index) for each leaf child.
302    ///
303    /// # Examples
304    ///
305    /// ```
306    /// use state_engine::Manifest;
307    ///
308    /// let mut store = Manifest::new("./examples/manifest");
309    /// store.load("connection").unwrap();
310    ///
311    /// // "tag", "driver", "charset" are static leaf values
312    /// let values = store.get_value("connection", "common");
313    /// assert!(!values.is_empty());
314    /// ```
315    pub fn get_value(&self, file: &str, path: &str) -> Vec<(u16, u16)> {
316        let node_idx = match self.find(file, path) {
317            Some(idx) => idx,
318            None => return vec![],
319        };
320
321        let record = match self.keys.get(node_idx) {
322            Some(r) => r,
323            None => return vec![],
324        };
325
326        let mut result = Vec::new();
327        let children = self.children_of(record);
328
329        for &idx in &children {
330            let child = match self.keys.get(idx) {
331                Some(r) => r,
332                None => continue,
333            };
334            // skip meta keys
335            if bit::get(child, bit::OFFSET_ROOT, bit::MASK_ROOT) != bit::ROOT_NULL {
336                continue;
337            }
338            // only leaf nodes with a value
339            if bit::get(child, bit::OFFSET_IS_LEAF, bit::MASK_IS_LEAF) == 0 {
340                continue;
341            }
342            let dyn_idx   = bit::get(child, bit::OFFSET_DYNAMIC, bit::MASK_DYNAMIC) as u16;
343            let value_idx = bit::get(child, bit::OFFSET_CHILD,   bit::MASK_CHILD)   as u16;
344            result.push((dyn_idx, value_idx));
345        }
346
347        result
348    }
349}