Skip to main content

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}