Skip to main content

state_engine/common/
parser.rs

1use serde_yaml_ng::Value;
2use crate::common::pool::{DynamicPool, PathMap, ChildrenMap, KeyList, YamlValueList};
3use crate::common::bit;
4
5/// Thin record for a single loaded manifest file.
6/// Stores only the key_idx of the file root record in the shared KeyList.
7pub struct ParsedManifest {
8    pub file_key_idx: u16,
9}
10
11/// Parses a YAML manifest string, appending into shared pool structures.
12/// Returns a `ParsedManifest` referencing the file root record's index.
13///
14/// # Examples
15///
16/// ```
17/// use state_engine::common::parser::parse;
18/// use state_engine::common::pool::{DynamicPool, PathMap, ChildrenMap, KeyList, YamlValueList};
19/// use state_engine::common::bit;
20///
21/// let yaml = "
22/// user:
23///   _store:
24///     client: KVS
25///     key: 'user:${session.sso_user_id}'
26///     ttl: 14400
27///   id:
28///     _state:
29///       type: integer
30/// ";
31///
32/// let mut dynamic = DynamicPool::new();
33/// let mut path_map = PathMap::new();
34/// let mut children_map = ChildrenMap::new();
35/// let mut keys = KeyList::new();
36/// let mut values = YamlValueList::new();
37///
38/// let pm = parse("cache", yaml, &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
39///
40/// // file root record is at pm.file_key_idx
41/// let root = keys.get(pm.file_key_idx).unwrap();
42/// let dyn_idx = bit::get(root, bit::OFFSET_DYNAMIC, bit::MASK_DYNAMIC) as u16;
43/// assert_eq!(dynamic.get(dyn_idx), Some("cache"));
44/// ```
45pub fn parse(
46    filename: &str,
47    yaml: &str,
48    dynamic: &mut DynamicPool,
49    path_map: &mut PathMap,
50    children_map: &mut ChildrenMap,
51    keys: &mut KeyList,
52    values: &mut YamlValueList,
53) -> Result<ParsedManifest, String> {
54    let root: Value = serde_yaml_ng::from_str(yaml)
55        .map_err(|e| format!("YAML parse error: {}", e))?;
56
57    let Value::Mapping(mapping) = root else {
58        return Err("YAML root must be a mapping".to_string());
59    };
60
61    // filename root record (placeholder, child index filled below)
62    let dyn_idx = dynamic.intern(filename);
63    let mut file_record = bit::new();
64    file_record = bit::set(file_record, bit::OFFSET_DYNAMIC, bit::MASK_DYNAMIC, dyn_idx as u64);
65    let file_idx = keys.push(file_record);
66
67    // traverse top-level keys
68    let mut child_indices: Vec<u16> = Vec::new();
69    for (key, value) in &mapping {
70        let key_str = yaml_str(key)?;
71        let child_idx = traverse_field_key(key_str, value, filename, &[], dynamic, path_map, children_map, keys, values)?;
72        child_indices.push(child_idx);
73    }
74
75    // update file record with children
76    let file_record = keys.get(file_idx).unwrap();
77    let file_record = match child_indices.len() {
78        0 => file_record,
79        1 => bit::set(file_record, bit::OFFSET_CHILD, bit::MASK_CHILD, child_indices[0] as u64),
80        _ => {
81            let children_idx = children_map.push(child_indices);
82            let r = bit::set(file_record, bit::OFFSET_HAS_CHILDREN, bit::MASK_HAS_CHILDREN, 1);
83            bit::set(r, bit::OFFSET_CHILD, bit::MASK_CHILD, children_idx as u64)
84        }
85    };
86    keys.set(file_idx, file_record);
87
88    Ok(ParsedManifest { file_key_idx: file_idx })
89}
90
91/// Traverses a field key node (non-meta key).
92/// `ancestors` excludes filename — only field key path segments (for qualify).
93fn traverse_field_key(
94    key_str: &str,
95    value: &Value,
96    filename: &str,
97    ancestors: &[&str],
98    dynamic: &mut DynamicPool,
99    path_map: &mut PathMap,
100    children_map: &mut ChildrenMap,
101    keys: &mut KeyList,
102    values: &mut YamlValueList,
103) -> Result<u16, String> {
104    let dyn_idx = dynamic.intern(key_str);
105    let mut record = bit::new();
106    record = bit::set(record, bit::OFFSET_ROOT, bit::MASK_ROOT, bit::ROOT_NULL);
107    record = bit::set(record, bit::OFFSET_DYNAMIC, bit::MASK_DYNAMIC, dyn_idx as u64);
108
109    let key_idx = keys.push(record);
110
111    let mut current: Vec<&str> = ancestors.to_vec();
112    current.push(key_str);
113
114    if let Value::Mapping(mapping) = value {
115        let mut child_indices: Vec<u16> = Vec::new();
116        let mut meta_indices: Vec<u16> = Vec::new();
117
118        for (k, v) in mapping {
119            let k_str = yaml_str(k)?;
120            if k_str.starts_with('_') {
121                let meta_idx = traverse_meta_key(k_str, v, filename, &current, dynamic, path_map, children_map, keys, values)?;
122                meta_indices.push(meta_idx);
123            } else {
124                let child_idx = traverse_field_key(k_str, v, filename, &current, dynamic, path_map, children_map, keys, values)?;
125                child_indices.push(child_idx);
126            }
127        }
128
129        let all_children: Vec<u16> = child_indices.iter()
130            .chain(meta_indices.iter())
131            .copied()
132            .collect();
133
134        let record = keys.get(key_idx).unwrap();
135        let record = match all_children.len() {
136            0 => record,
137            1 => bit::set(record, bit::OFFSET_CHILD, bit::MASK_CHILD, all_children[0] as u64),
138            _ => {
139                let children_idx = children_map.push(all_children);
140                let r = bit::set(record, bit::OFFSET_HAS_CHILDREN, bit::MASK_HAS_CHILDREN, 1);
141                bit::set(r, bit::OFFSET_CHILD, bit::MASK_CHILD, children_idx as u64)
142            }
143        };
144        keys.set(key_idx, record);
145    } else {
146        // scalar value → is_leaf
147        let val_idx = build_yaml_value(value, filename, ancestors, dynamic, path_map, values)?;
148        let record = keys.get(key_idx).unwrap();
149        let record = bit::set(record, bit::OFFSET_IS_LEAF, bit::MASK_IS_LEAF, 1);
150        let record = bit::set(record, bit::OFFSET_CHILD, bit::MASK_CHILD, val_idx as u64);
151        keys.set(key_idx, record);
152    }
153
154    Ok(key_idx)
155}
156
157/// Traverses a meta key node (_load, _store, _state).
158fn traverse_meta_key(
159    key_str: &str,
160    value: &Value,
161    filename: &str,
162    ancestors: &[&str],
163    dynamic: &mut DynamicPool,
164    path_map: &mut PathMap,
165    children_map: &mut ChildrenMap,
166    keys: &mut KeyList,
167    values: &mut YamlValueList,
168) -> Result<u16, String> {
169    let root_val = match key_str {
170        "_load"  => bit::ROOT_LOAD,
171        "_store" => bit::ROOT_STORE,
172        "_state" => bit::ROOT_STATE,
173        _ => bit::ROOT_NULL,
174    };
175
176    let mut record = bit::new();
177    record = bit::set(record, bit::OFFSET_ROOT, bit::MASK_ROOT, root_val);
178
179    let key_idx = keys.push(record);
180
181    if let Value::Mapping(mapping) = value {
182        let mut child_indices: Vec<u16> = Vec::new();
183
184        for (k, v) in mapping {
185            let k_str = yaml_str(k)?;
186            let child_idx = traverse_prop_key(k_str, v, filename, ancestors, dynamic, path_map, children_map, keys, values)?;
187            child_indices.push(child_idx);
188        }
189
190        let record = keys.get(key_idx).unwrap();
191        let record = match child_indices.len() {
192            0 => record,
193            1 => bit::set(record, bit::OFFSET_CHILD, bit::MASK_CHILD, child_indices[0] as u64),
194            _ => {
195                let children_idx = children_map.push(child_indices);
196                let r = bit::set(record, bit::OFFSET_HAS_CHILDREN, bit::MASK_HAS_CHILDREN, 1);
197                bit::set(r, bit::OFFSET_CHILD, bit::MASK_CHILD, children_idx as u64)
198            }
199        };
200        keys.set(key_idx, record);
201    }
202
203    Ok(key_idx)
204}
205
206/// Traverses a prop key node (client, key, ttl, table, connection, where, map, type).
207fn traverse_prop_key(
208    key_str: &str,
209    value: &Value,
210    filename: &str,
211    ancestors: &[&str],
212    dynamic: &mut DynamicPool,
213    path_map: &mut PathMap,
214    children_map: &mut ChildrenMap,
215    keys: &mut KeyList,
216    values: &mut YamlValueList,
217) -> Result<u16, String> {
218    let (prop_val, client_val) = match key_str {
219        "client"     => (bit::PROP_NULL, parse_client(value)),
220        "type"       => (bit::PROP_TYPE, bit::CLIENT_NULL),
221        "key"        => (bit::PROP_KEY, bit::CLIENT_NULL),
222        "connection" => (bit::PROP_CONNECTION, bit::CLIENT_NULL),
223        "map"        => (bit::PROP_MAP, bit::CLIENT_NULL),
224        "ttl"        => (bit::PROP_TTL, bit::CLIENT_NULL),
225        "table"      => (bit::PROP_TABLE, bit::CLIENT_NULL),
226        "where"      => (bit::PROP_WHERE, bit::CLIENT_NULL),
227        _            => (bit::PROP_NULL, bit::CLIENT_NULL),
228    };
229
230    let mut record = bit::new();
231    record = bit::set(record, bit::OFFSET_PROP, bit::MASK_PROP, prop_val);
232    record = bit::set(record, bit::OFFSET_CLIENT, bit::MASK_CLIENT, client_val);
233
234    if key_str == "type" {
235        let type_val = parse_type(value);
236        record = bit::set(record, bit::OFFSET_TYPE, bit::MASK_TYPE, type_val);
237    }
238
239    let key_idx = keys.push(record);
240
241    if key_str == "map" {
242        if let Value::Mapping(mapping) = value {
243            let mut child_indices: Vec<u16> = Vec::new();
244            for (k, v) in mapping {
245                let k_str = yaml_str(k)?;
246                let child_idx = traverse_map_key(k_str, v, filename, ancestors, dynamic, path_map, keys, values)?;
247                child_indices.push(child_idx);
248            }
249            let record = keys.get(key_idx).unwrap();
250            let record = match child_indices.len() {
251                0 => record,
252                1 => bit::set(record, bit::OFFSET_CHILD, bit::MASK_CHILD, child_indices[0] as u64),
253                _ => {
254                    let children_idx = children_map.push(child_indices);
255                    let r = bit::set(record, bit::OFFSET_HAS_CHILDREN, bit::MASK_HAS_CHILDREN, 1);
256                    bit::set(r, bit::OFFSET_CHILD, bit::MASK_CHILD, children_idx as u64)
257                }
258            };
259            keys.set(key_idx, record);
260        }
261    } else if key_str != "client" {
262        let val_idx = build_yaml_value(value, filename, ancestors, dynamic, path_map, values)?;
263        let record = keys.get(key_idx).unwrap();
264        let record = bit::set(record, bit::OFFSET_IS_LEAF, bit::MASK_IS_LEAF, 1);
265        let record = bit::set(record, bit::OFFSET_CHILD, bit::MASK_CHILD, val_idx as u64);
266        keys.set(key_idx, record);
267    }
268
269    Ok(key_idx)
270}
271
272/// Traverses a map child key (is_path=true).
273fn traverse_map_key(
274    key_str: &str,
275    value: &Value,
276    filename: &str,
277    ancestors: &[&str],
278    dynamic: &mut DynamicPool,
279    path_map: &mut PathMap,
280    keys: &mut KeyList,
281    values: &mut YamlValueList,
282) -> Result<u16, String> {
283    let qualified = build_qualified_path(filename, ancestors, key_str);
284    let seg_indices: Vec<u16> = qualified.split('.')
285        .map(|seg| dynamic.intern(seg))
286        .collect();
287    let path_idx = path_map.push(seg_indices);
288
289    let mut record = bit::new();
290    record = bit::set(record, bit::OFFSET_IS_PATH, bit::MASK_IS_PATH, 1);
291    record = bit::set(record, bit::OFFSET_DYNAMIC, bit::MASK_DYNAMIC, path_idx as u64);
292
293    let val_idx = build_yaml_value(value, filename, ancestors, dynamic, path_map, values)?;
294    record = bit::set(record, bit::OFFSET_IS_LEAF, bit::MASK_IS_LEAF, 1);
295    record = bit::set(record, bit::OFFSET_CHILD, bit::MASK_CHILD, val_idx as u64);
296
297    Ok(keys.push(record))
298}
299
300/// Builds a YAML value record ([u64; 2]) from a scalar or template string.
301fn build_yaml_value(
302    value: &Value,
303    filename: &str,
304    ancestors: &[&str],
305    dynamic: &mut DynamicPool,
306    path_map: &mut PathMap,
307    values: &mut YamlValueList,
308) -> Result<u16, String> {
309    let s = match value {
310        Value::String(s) => s.clone(),
311        Value::Number(n) => n.to_string(),
312        Value::Bool(b)   => b.to_string(),
313        Value::Null      => return Ok(0),
314        _ => return Err(format!("unexpected value type: {:?}", value)),
315    };
316
317    let tokens = split_template(&s);
318    if tokens.len() > 6 {
319        return Err(format!("value '{}' has {} tokens, max 6", s, tokens.len()));
320    }
321    let is_template = tokens.len() > 1;
322
323    let mut vo = [0u64; 2];
324
325    if is_template {
326        vo[0] = bit::set(vo[0], bit::VO_OFFSET_IS_TEMPLATE, bit::VO_MASK_IS_TEMPLATE, 1);
327    }
328
329    const TOKEN_OFFSETS: [(u32, u32); 6] = [
330        (bit::VO_OFFSET_T0_IS_PATH, bit::VO_OFFSET_T0_DYNAMIC),
331        (bit::VO_OFFSET_T1_IS_PATH, bit::VO_OFFSET_T1_DYNAMIC),
332        (bit::VO_OFFSET_T2_IS_PATH, bit::VO_OFFSET_T2_DYNAMIC),
333        (bit::VO_OFFSET_T3_IS_PATH, bit::VO_OFFSET_T3_DYNAMIC),
334        (bit::VO_OFFSET_T4_IS_PATH, bit::VO_OFFSET_T4_DYNAMIC),
335        (bit::VO_OFFSET_T5_IS_PATH, bit::VO_OFFSET_T5_DYNAMIC),
336    ];
337
338    for (i, token) in tokens.iter().enumerate().take(6) {
339        let dyn_idx = if token.is_path {
340            let qualified = qualify_path(&token.text, filename, ancestors);
341            let seg_indices: Vec<u16> = qualified.split('.')
342                .map(|seg| dynamic.intern(seg))
343                .collect();
344            path_map.push(seg_indices)
345        } else {
346            dynamic.intern(&token.text)
347        };
348
349        let word = if i < 3 { 0 } else { 1 };
350        let (off_is_path, off_dynamic) = TOKEN_OFFSETS[i];
351        vo[word] = bit::set(vo[word], off_is_path, bit::VO_MASK_IS_PATH, token.is_path as u64);
352        vo[word] = bit::set(vo[word], off_dynamic, bit::VO_MASK_DYNAMIC, dyn_idx as u64);
353    }
354
355    Ok(values.push(vo))
356}
357
358fn parse_client(value: &Value) -> u64 {
359    let s = match value { Value::String(s) => s.as_str(), _ => "" };
360    match s {
361        "State"    => bit::CLIENT_STATE,
362        "InMemory" => bit::CLIENT_IN_MEMORY,
363        "Env"      => bit::CLIENT_ENV,
364        "KVS"      => bit::CLIENT_KVS,
365        "Db"       => bit::CLIENT_DB,
366        "API"      => bit::CLIENT_API,
367        "File"     => bit::CLIENT_FILE,
368        _          => bit::CLIENT_NULL,
369    }
370}
371
372fn parse_type(value: &Value) -> u64 {
373    let s = match value { Value::String(s) => s.as_str(), _ => "" };
374    match s {
375        "integer"  => bit::TYPE_I64,
376        "string"   => bit::TYPE_UTF8,
377        "float"    => bit::TYPE_F64,
378        "boolean"  => bit::TYPE_BOOLEAN,
379        "datetime" => bit::TYPE_DATETIME,
380        _          => bit::TYPE_NULL,
381    }
382}
383
384/// A single template token: either a literal string or a path placeholder.
385struct Token {
386    text: String,
387    is_path: bool,
388}
389
390/// Splits a string by `${}` placeholders into tokens.
391/// `"user:${session.id}"` → [Token("user:", false), Token("session.id", true)]
392fn split_template(s: &str) -> Vec<Token> {
393    let mut tokens = Vec::new();
394    let mut rest = s;
395
396    loop {
397        if let Some(start) = rest.find("${") {
398            if start > 0 {
399                tokens.push(Token { text: rest[..start].to_string(), is_path: false });
400            }
401            rest = &rest[start + 2..];
402            if let Some(end) = rest.find('}') {
403                tokens.push(Token { text: rest[..end].to_string(), is_path: true });
404                rest = &rest[end + 1..];
405            } else {
406                tokens.push(Token { text: rest.to_string(), is_path: false });
407                break;
408            }
409        } else {
410            if !rest.is_empty() {
411                tokens.push(Token { text: rest.to_string(), is_path: false });
412            }
413            break;
414        }
415    }
416
417    if tokens.is_empty() {
418        tokens.push(Token { text: s.to_string(), is_path: false });
419    }
420
421    tokens
422}
423
424/// Qualifies a placeholder path to an absolute path.
425fn qualify_path(path: &str, filename: &str, ancestors: &[&str]) -> String {
426    if path.contains('.') {
427        return path.to_string();
428    }
429    if ancestors.is_empty() {
430        format!("{}.{}", filename, path)
431    } else {
432        format!("{}.{}.{}", filename, ancestors.join("."), path)
433    }
434}
435
436/// Builds a qualified path string for map keys: `filename.ancestors.key_str`
437fn build_qualified_path(filename: &str, ancestors: &[&str], key_str: &str) -> String {
438    if ancestors.is_empty() {
439        format!("{}.{}", filename, key_str)
440    } else {
441        format!("{}.{}.{}", filename, ancestors.join("."), key_str)
442    }
443}
444
445fn yaml_str(value: &Value) -> Result<&str, String> {
446    match value {
447        Value::String(s) => Ok(s.as_str()),
448        _ => Err(format!("expected string key, got {:?}", value)),
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455    use crate::common::bit;
456
457    fn make_pools() -> (DynamicPool, PathMap, ChildrenMap, KeyList, YamlValueList) {
458        (DynamicPool::new(), PathMap::new(), ChildrenMap::new(), KeyList::new(), YamlValueList::new())
459    }
460
461    const YAML_SESSION: &str = "
462sso_user_id:
463  _state:
464    type: integer
465  _store:
466    client: InMemory
467    key: 'request-attributes-user-key'
468  _load:
469    client: InMemory
470    key: 'request-header-user-key'
471";
472
473    const YAML_CACHE: &str = "
474user:
475  _store:
476    client: KVS
477    key: 'user:${session.sso_user_id}'
478    ttl: 14400
479  _load:
480    client: Db
481    connection: ${connection.tenant}
482    table: 'users'
483    where: 'sso_user_id=${session.sso_user_id}'
484    map:
485      id: 'id'
486      org_id: 'sso_org_id'
487  id:
488    _state:
489      type: integer
490  org_id:
491    _state:
492      type: integer
493";
494
495    #[test]
496    fn test_parse_session_yaml() {
497        let (mut dynamic, mut path_map, mut children_map, mut keys, mut values) = make_pools();
498        let pm = parse("session", YAML_SESSION, &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
499
500        let idx = dynamic.intern("sso_user_id");
501        assert_ne!(idx, 0);
502        assert!(keys.get(pm.file_key_idx).is_some());
503    }
504
505    #[test]
506    fn test_field_key_record_root_is_null() {
507        let (mut dynamic, mut path_map, mut children_map, mut keys, mut values) = make_pools();
508        let pm = parse("session", YAML_SESSION, &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
509
510        // first child of file record should be a field key (ROOT_NULL)
511        let file_record = keys.get(pm.file_key_idx).unwrap();
512        let child_idx = bit::get(file_record, bit::OFFSET_CHILD, bit::MASK_CHILD) as u16;
513        let record = keys.get(child_idx).unwrap();
514        assert_eq!(bit::get(record, bit::OFFSET_ROOT, bit::MASK_ROOT), bit::ROOT_NULL);
515    }
516
517    #[test]
518    fn test_meta_key_record_root_index() {
519        let (mut dynamic, mut path_map, mut children_map, mut keys, mut values) = make_pools();
520        let pm = parse("session", YAML_SESSION, &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
521
522        let mut found = false;
523        let start = pm.file_key_idx;
524        for i in start..start + 20 {
525            if let Some(r) = keys.get(i) {
526                if bit::get(r, bit::OFFSET_ROOT, bit::MASK_ROOT) == bit::ROOT_STATE {
527                    found = true;
528                    break;
529                }
530            }
531        }
532        assert!(found, "_state record with ROOT_STATE not found");
533    }
534
535    #[test]
536    fn test_type_index_integer() {
537        let (mut dynamic, mut path_map, mut children_map, mut keys, mut values) = make_pools();
538        let pm = parse("session", YAML_SESSION, &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
539
540        let mut found = false;
541        let start = pm.file_key_idx;
542        for i in start..start + 20 {
543            if let Some(r) = keys.get(i) {
544                if bit::get(r, bit::OFFSET_TYPE, bit::MASK_TYPE) == bit::TYPE_I64 {
545                    found = true;
546                    break;
547                }
548            }
549        }
550        assert!(found, "type=integer record not found");
551    }
552
553    #[test]
554    fn test_static_value_interned() {
555        let (mut dynamic, mut path_map, mut children_map, mut keys, mut values) = make_pools();
556        parse("session", YAML_SESSION, &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
557
558        let idx = dynamic.intern("request-attributes-user-key");
559        assert_ne!(idx, 0);
560    }
561
562    #[test]
563    fn test_template_value_is_template_flag() {
564        let (mut dynamic, mut path_map, mut children_map, mut keys, mut values) = make_pools();
565        parse("cache", YAML_CACHE, &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
566
567        let mut found = false;
568        for i in 1..=30 {
569            if let Some(vo) = values.get(i) {
570                if bit::get(vo[0], bit::VO_OFFSET_IS_TEMPLATE, bit::VO_MASK_IS_TEMPLATE) == 1 {
571                    found = true;
572                    break;
573                }
574            }
575        }
576        assert!(found, "no is_template=1 value record found");
577    }
578
579    #[test]
580    fn test_path_token_stored_in_path_map() {
581        let (mut dynamic, mut path_map, mut children_map, mut keys, mut values) = make_pools();
582        parse("cache", YAML_CACHE, &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
583
584        assert!(path_map.get(1).is_some(), "path map is empty");
585    }
586
587    #[test]
588    fn test_split_template_static() {
589        let tokens = split_template("request-attributes-user-key");
590        assert_eq!(tokens.len(), 1);
591        assert!(!tokens[0].is_path);
592        assert_eq!(tokens[0].text, "request-attributes-user-key");
593    }
594
595    #[test]
596    fn test_split_template_path_only() {
597        let tokens = split_template("${connection.tenant}");
598        assert_eq!(tokens.len(), 1);
599        assert!(tokens[0].is_path);
600        assert_eq!(tokens[0].text, "connection.tenant");
601    }
602
603    #[test]
604    fn test_split_template_mixed() {
605        let tokens = split_template("user:${session.sso_user_id}");
606        assert_eq!(tokens.len(), 2);
607        assert!(!tokens[0].is_path);
608        assert_eq!(tokens[0].text, "user:");
609        assert!(tokens[1].is_path);
610        assert_eq!(tokens[1].text, "session.sso_user_id");
611    }
612
613    #[test]
614    fn test_qualify_path_absolute() {
615        assert_eq!(qualify_path("connection.common", "cache", &["user"]), "connection.common");
616    }
617
618    #[test]
619    fn test_qualify_path_relative() {
620        assert_eq!(qualify_path("org_id", "cache", &["user"]), "cache.user.org_id");
621    }
622
623    #[test]
624    fn test_qualify_path_relative_no_ancestors() {
625        assert_eq!(qualify_path("org_id", "cache", &[]), "cache.org_id");
626    }
627
628    #[test]
629    fn test_client_kvs_record() {
630        let (mut dynamic, mut path_map, mut children_map, mut keys, mut values) = make_pools();
631        let pm = parse("cache", YAML_CACHE, &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
632
633        let mut found = false;
634        let start = pm.file_key_idx;
635        for i in start..start + 30 {
636            if let Some(r) = keys.get(i) {
637                if bit::get(r, bit::OFFSET_CLIENT, bit::MASK_CLIENT) == bit::CLIENT_KVS {
638                    found = true;
639                    break;
640                }
641            }
642        }
643        assert!(found, "CLIENT_KVS record not found");
644    }
645
646    #[test]
647    fn test_two_files_globally_unique_key_idx() {
648        // Both session and cache parsed into the same pools — key_idx must be globally unique
649        let (mut dynamic, mut path_map, mut children_map, mut keys, mut values) = make_pools();
650        let pm_session = parse("session", YAML_SESSION, &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
651        let pm_cache   = parse("cache",   YAML_CACHE,   &mut dynamic, &mut path_map, &mut children_map, &mut keys, &mut values).unwrap();
652
653        // file root indices must differ
654        assert_ne!(pm_session.file_key_idx, pm_cache.file_key_idx);
655
656        // each file root record holds correct dynamic string
657        let sess_rec = keys.get(pm_session.file_key_idx).unwrap();
658        let sess_dyn = bit::get(sess_rec, bit::OFFSET_DYNAMIC, bit::MASK_DYNAMIC) as u16;
659        assert_eq!(dynamic.get(sess_dyn), Some("session"));
660
661        let cache_rec = keys.get(pm_cache.file_key_idx).unwrap();
662        let cache_dyn = bit::get(cache_rec, bit::OFFSET_DYNAMIC, bit::MASK_DYNAMIC) as u16;
663        assert_eq!(dynamic.get(cache_dyn), Some("cache"));
664    }
665}