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