Skip to main content

haystack_core/data/
grid.rs

1// Haystack Grid — a two-dimensional tagged data structure.
2
3use super::dict::HDict;
4use std::fmt;
5use std::sync::Arc;
6
7/// A single column in a Haystack Grid.
8///
9/// Each column has a name and optional metadata dict.
10#[derive(Debug, Clone, PartialEq)]
11pub struct HCol {
12    pub name: String,
13    pub meta: HDict,
14}
15
16impl HCol {
17    /// Create a column with just a name and empty metadata.
18    pub fn new(name: impl Into<String>) -> Self {
19        Self {
20            name: name.into(),
21            meta: HDict::new(),
22        }
23    }
24
25    /// Create a column with a name and metadata dict.
26    pub fn with_meta(name: impl Into<String>, meta: HDict) -> Self {
27        Self {
28            name: name.into(),
29            meta,
30        }
31    }
32}
33
34/// Haystack Grid — the fundamental tabular data structure.
35///
36/// A grid has:
37/// - `meta`: grid-level metadata (an `HDict`)
38/// - `cols`: ordered list of columns (`HCol`)
39/// - `rows`: ordered list of row dicts (`HDict`)
40#[derive(Debug, Clone, Default, PartialEq)]
41pub struct HGrid {
42    pub meta: HDict,
43    pub cols: Vec<HCol>,
44    pub rows: Vec<HDict>,
45}
46
47impl HGrid {
48    /// Create an empty grid.
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Create a grid from its constituent parts.
54    ///
55    /// Note: No validation is performed on column/row consistency for performance.
56    /// Callers are responsible for ensuring rows contain only tags matching column names.
57    pub fn from_parts(meta: HDict, cols: Vec<HCol>, rows: Vec<HDict>) -> Self {
58        Self { meta, cols, rows }
59    }
60
61    /// Build a grid from Arc-wrapped rows, avoiding clones when possible.
62    ///
63    /// Uses `Arc::try_unwrap()` to move the inner HDict when the reference count
64    /// is 1 (which is the common case in request pipelines). Falls back to clone
65    /// only for shared references.
66    pub fn from_parts_arc(meta: HDict, cols: Vec<HCol>, rows: Vec<Arc<HDict>>) -> Self {
67        let owned_rows: Vec<HDict> = rows
68            .into_iter()
69            .map(|arc| Arc::try_unwrap(arc).unwrap_or_else(|a| (*a).clone()))
70            .collect();
71        Self {
72            meta,
73            cols,
74            rows: owned_rows,
75        }
76    }
77
78    /// Look up a column by name. Returns `None` if not found.
79    pub fn col(&self, name: &str) -> Option<&HCol> {
80        self.cols.iter().find(|c| c.name == name)
81    }
82
83    /// Returns `true` if the grid has no rows.
84    pub fn is_empty(&self) -> bool {
85        self.rows.is_empty()
86    }
87
88    /// Returns `true` if this grid represents an error response.
89    ///
90    /// An error grid has an `err` marker tag in its metadata.
91    pub fn is_err(&self) -> bool {
92        self.meta.has("err")
93    }
94
95    /// Returns the number of rows.
96    pub fn len(&self) -> usize {
97        self.rows.len()
98    }
99
100    /// Returns a reference to the row at the given index.
101    pub fn row(&self, index: usize) -> Option<&HDict> {
102        self.rows.get(index)
103    }
104
105    /// Iterate over rows.
106    pub fn iter(&self) -> impl Iterator<Item = &HDict> {
107        self.rows.iter()
108    }
109
110    /// Returns the number of columns.
111    pub fn num_cols(&self) -> usize {
112        self.cols.len()
113    }
114
115    /// Returns an iterator over column names.
116    pub fn col_names(&self) -> impl Iterator<Item = &str> {
117        self.cols.iter().map(|c| c.name.as_str())
118    }
119}
120
121impl fmt::Display for HGrid {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "HGrid(cols: [")?;
124        for (i, col) in self.cols.iter().enumerate() {
125            if i > 0 {
126                write!(f, ", ")?;
127            }
128            write!(f, "{}", col.name)?;
129        }
130        write!(f, "], rows: {})", self.rows.len())
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::kinds::{Kind, Number};
138
139    fn sample_grid() -> HGrid {
140        let cols = vec![HCol::new("id"), HCol::new("dis"), HCol::new("area")];
141
142        let mut row1 = HDict::new();
143        row1.set("id", Kind::Ref(crate::kinds::HRef::from_val("site-1")));
144        row1.set("dis", Kind::Str("Site One".into()));
145        row1.set(
146            "area",
147            Kind::Number(Number::new(4500.0, Some("ft\u{00B2}".into()))),
148        );
149
150        let mut row2 = HDict::new();
151        row2.set("id", Kind::Ref(crate::kinds::HRef::from_val("site-2")));
152        row2.set("dis", Kind::Str("Site Two".into()));
153        row2.set(
154            "area",
155            Kind::Number(Number::new(3200.0, Some("ft\u{00B2}".into()))),
156        );
157
158        HGrid::from_parts(HDict::new(), cols, vec![row1, row2])
159    }
160
161    #[test]
162    fn empty_grid() {
163        let g = HGrid::new();
164        assert!(g.is_empty());
165        assert_eq!(g.len(), 0);
166        assert_eq!(g.num_cols(), 0);
167        assert!(!g.is_err());
168        assert_eq!(g.row(0), None);
169    }
170
171    #[test]
172    fn grid_with_data() {
173        let g = sample_grid();
174        assert!(!g.is_empty());
175        assert_eq!(g.len(), 2);
176        assert_eq!(g.num_cols(), 3);
177    }
178
179    #[test]
180    fn col_lookup() {
181        let g = sample_grid();
182
183        let id_col = g.col("id").unwrap();
184        assert_eq!(id_col.name, "id");
185
186        let dis_col = g.col("dis").unwrap();
187        assert_eq!(dis_col.name, "dis");
188
189        assert!(g.col("nonexistent").is_none());
190    }
191
192    #[test]
193    fn col_names() {
194        let g = sample_grid();
195        let names: Vec<&str> = g.col_names().collect();
196        assert_eq!(names, vec!["id", "dis", "area"]);
197    }
198
199    #[test]
200    fn row_access() {
201        let g = sample_grid();
202
203        let r0 = g.row(0).unwrap();
204        assert_eq!(r0.get("dis"), Some(&Kind::Str("Site One".into())));
205
206        let r1 = g.row(1).unwrap();
207        assert_eq!(r1.get("dis"), Some(&Kind::Str("Site Two".into())));
208
209        assert!(g.row(2).is_none());
210    }
211
212    #[test]
213    fn iteration() {
214        let g = sample_grid();
215        let rows: Vec<&HDict> = g.iter().collect();
216        assert_eq!(rows.len(), 2);
217    }
218
219    #[test]
220    fn is_err_false_for_normal_grid() {
221        let g = sample_grid();
222        assert!(!g.is_err());
223    }
224
225    #[test]
226    fn is_err_true_with_err_marker() {
227        let mut meta = HDict::new();
228        meta.set("err", Kind::Marker);
229        meta.set("dis", Kind::Str("some error message".into()));
230
231        let g = HGrid::from_parts(meta, vec![], vec![]);
232        assert!(g.is_err());
233        assert!(g.is_empty());
234    }
235
236    #[test]
237    fn col_with_meta() {
238        let mut meta = HDict::new();
239        meta.set("unit", Kind::Str("kW".into()));
240
241        let col = HCol::with_meta("power", meta);
242        assert_eq!(col.name, "power");
243        assert!(col.meta.has("unit"));
244    }
245
246    #[test]
247    fn display() {
248        let g = sample_grid();
249        let s = g.to_string();
250        assert!(s.contains("id"));
251        assert!(s.contains("dis"));
252        assert!(s.contains("area"));
253        assert!(s.contains("rows: 2"));
254    }
255
256    #[test]
257    fn equality() {
258        let a = sample_grid();
259        let b = sample_grid();
260        assert_eq!(a, b);
261    }
262
263    #[test]
264    fn default_is_empty() {
265        let g = HGrid::default();
266        assert!(g.is_empty());
267        assert_eq!(g.num_cols(), 0);
268    }
269
270    #[test]
271    fn from_parts() {
272        let cols = vec![HCol::new("name")];
273        let mut row = HDict::new();
274        row.set("name", Kind::Str("test".into()));
275
276        let g = HGrid::from_parts(HDict::new(), cols, vec![row]);
277        assert_eq!(g.len(), 1);
278        assert_eq!(g.num_cols(), 1);
279    }
280}