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