zenith_core/data/mod.rs
1//! Runtime data-binding support.
2//!
3//! Provides [`DataContext`] (a flat `BTreeMap<String, String>` of named field
4//! values, plus a `BTreeMap<String, Vec<String>>` of named array columns) and
5//! the [`DataFormat`] / [`format_data_value`] formatter that turns raw field
6//! strings into locale-styled display strings deterministically.
7
8use std::collections::BTreeMap;
9
10pub mod format;
11
12pub use format::{DataFormat, format_data_value};
13
14/// Named data fields and ordered array columns available at scene-compile time.
15///
16/// `fields` holds scalar bindings keyed by dot-separated path (e.g.
17/// `"revenue.total"`). `arrays` holds ordered sequences of raw element strings
18/// keyed by name (e.g. `"sales"` → `["12", "18", "15"]`), populated from JSON
19/// array values and CSV column columns.
20///
21/// Both maps use [`BTreeMap`] for deterministic iteration order on the render
22/// path. No `HashMap`, no randomness, no time.
23#[derive(Debug, Clone, Default)]
24pub struct DataContext {
25 /// Scalar field map. Keyed by dotted path, value is the raw string.
26 pub fields: BTreeMap<String, String>,
27 /// Array column map. Keyed by name, value is the ordered raw element strings.
28 pub arrays: BTreeMap<String, Vec<String>>,
29}
30
31impl DataContext {
32 /// Look up a scalar field value by `path`.
33 ///
34 /// Returns `None` when the path is not present in this context.
35 pub fn get(&self, path: &str) -> Option<&str> {
36 self.fields.get(path).map(String::as_str)
37 }
38
39 /// Look up an ordered array column by `key`.
40 ///
41 /// Returns the element slice in source order, or `None` when the key is
42 /// not present. Used by the data-binding pre-pass to populate
43 /// `ChartSeries.values` from a `data-ref` binding.
44 pub fn get_array(&self, key: &str) -> Option<&[String]> {
45 self.arrays.get(key).map(Vec::as_slice)
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn get_array_returns_slice_for_present_key() {
55 let mut ctx = DataContext::default();
56 ctx.arrays.insert(
57 "sales".to_owned(),
58 vec!["12".to_owned(), "18".to_owned(), "15".to_owned()],
59 );
60 let got: Vec<&str> = ctx
61 .get_array("sales")
62 .unwrap()
63 .iter()
64 .map(String::as_str)
65 .collect();
66 assert_eq!(got, ["12", "18", "15"]);
67 }
68
69 #[test]
70 fn get_array_returns_none_for_missing_key() {
71 let ctx = DataContext::default();
72 assert!(ctx.get_array("missing").is_none());
73 }
74
75 #[test]
76 fn get_scalar_unaffected_by_arrays() {
77 let mut ctx = DataContext::default();
78 ctx.fields.insert("x".to_owned(), "hello".to_owned());
79 ctx.arrays
80 .insert("x".to_owned(), vec!["a".to_owned(), "b".to_owned()]);
81 // Scalar and array can coexist under the same key without interference.
82 assert_eq!(ctx.get("x"), Some("hello"));
83 let got: Vec<&str> = ctx
84 .get_array("x")
85 .unwrap()
86 .iter()
87 .map(String::as_str)
88 .collect();
89 assert_eq!(got, ["a", "b"]);
90 }
91
92 #[test]
93 fn default_has_empty_arrays() {
94 let ctx = DataContext::default();
95 assert!(ctx.arrays.is_empty());
96 }
97}