1use serde_yaml_ng::Value;
2use crate::common::pool::{DynamicPool, PathMap, ChildrenMap, KeyList, YamlValueList};
3use crate::common::bit;
4
5pub struct ParsedManifest {
8 pub file_key_idx: u16,
9}
10
11pub 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 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 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 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
91fn 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, ¤t, 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, ¤t, 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 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
157fn 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
206fn 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
272fn 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
300fn 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
384struct Token {
386 text: String,
387 is_path: bool,
388}
389
390fn 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
424fn 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
436fn 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 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 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 assert_ne!(pm_session.file_key_idx, pm_cache.file_key_idx);
655
656 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}