Skip to main content

haystack_core/data/
dict.rs

1// Haystack Dict — a mutable tag dictionary backed by HashMap.
2
3use crate::kinds::{HRef, Kind};
4use std::collections::HashMap;
5use std::fmt;
6
7/// Haystack Dict — the fundamental entity/row type in Haystack.
8///
9/// An `HDict` is a mutable dictionary mapping tag names (`String`) to values (`Kind`).
10/// Dicts are used as rows in grids, as entity records, and as metadata containers.
11#[derive(Debug, Clone, Default)]
12pub struct HDict {
13    tags: HashMap<String, Kind>,
14}
15
16impl HDict {
17    /// Create an empty dict.
18    pub fn new() -> Self {
19        Self::default()
20    }
21
22    /// Create a dict from a pre-built HashMap.
23    pub fn from_tags(tags: HashMap<String, Kind>) -> Self {
24        Self { tags }
25    }
26
27    /// Returns `true` if the dict contains a tag with the given name.
28    pub fn has(&self, name: &str) -> bool {
29        self.tags.contains_key(name)
30    }
31
32    /// Returns a reference to the value for the given tag name, if present.
33    pub fn get(&self, name: &str) -> Option<&Kind> {
34        self.tags.get(name)
35    }
36
37    /// Returns `true` if the dict does NOT contain a tag with the given name.
38    pub fn missing(&self, name: &str) -> bool {
39        !self.tags.contains_key(name)
40    }
41
42    /// Returns the `id` tag value if it is a Ref, otherwise `None`.
43    pub fn id(&self) -> Option<&HRef> {
44        match self.tags.get("id") {
45            Some(Kind::Ref(r)) => Some(r),
46            _ => None,
47        }
48    }
49
50    /// Returns the display string for this dict.
51    ///
52    /// Prefers the `dis` tag (if it is a `Str`), then falls back to the
53    /// `id` ref's display name.
54    pub fn dis(&self) -> Option<&str> {
55        if let Some(Kind::Str(s)) = self.tags.get("dis") {
56            return Some(s.as_str());
57        }
58        if let Some(r) = self.id() {
59            return r.dis.as_deref();
60        }
61        None
62    }
63
64    /// Returns `true` if the dict has no tags.
65    pub fn is_empty(&self) -> bool {
66        self.tags.is_empty()
67    }
68
69    /// Returns the number of tags in the dict.
70    pub fn len(&self) -> usize {
71        self.tags.len()
72    }
73
74    /// Set (insert or overwrite) a tag.
75    pub fn set(&mut self, name: impl Into<String>, val: Kind) {
76        self.tags.insert(name.into(), val);
77    }
78
79    /// Remove a tag by name, returning its value if it was present.
80    pub fn remove_tag(&mut self, name: &str) -> Option<Kind> {
81        self.tags.remove(name)
82    }
83
84    /// Merge another dict into this one.
85    ///
86    /// Tags from `other` overwrite tags in `self`. If a tag in `other` is
87    /// `Kind::Remove`, the corresponding tag in `self` is removed instead.
88    pub fn merge(&mut self, other: &HDict) {
89        for (k, v) in &other.tags {
90            match v {
91                Kind::Remove => {
92                    self.tags.remove(k);
93                }
94                _ => {
95                    self.tags.insert(k.clone(), v.clone());
96                }
97            }
98        }
99    }
100
101    /// Returns a reference to the underlying HashMap.
102    pub fn tags(&self) -> &HashMap<String, Kind> {
103        &self.tags
104    }
105
106    /// Iterate over `(name, value)` pairs.
107    pub fn iter(&self) -> impl Iterator<Item = (&str, &Kind)> {
108        self.tags.iter().map(|(k, v)| (k.as_str(), v))
109    }
110
111    /// Iterate over tags sorted by key name.
112    pub fn sorted_iter(&self) -> Vec<(&str, &Kind)> {
113        let mut pairs: Vec<_> = self.tags.iter().map(|(k, v)| (k.as_str(), v)).collect();
114        pairs.sort_unstable_by_key(|(k, _)| *k);
115        pairs
116    }
117
118    /// Iterate over tag names.
119    pub fn tag_names(&self) -> impl Iterator<Item = &str> {
120        self.tags.keys().map(|k| k.as_str())
121    }
122
123    /// Collect all tag names into a HashSet.
124    pub fn tag_name_set(&self) -> std::collections::HashSet<&str> {
125        self.tags.keys().map(|k| k.as_str()).collect()
126    }
127}
128
129impl PartialEq for HDict {
130    fn eq(&self, other: &Self) -> bool {
131        self.tags == other.tags
132    }
133}
134
135impl fmt::Display for HDict {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        write!(f, "HDict({{")?;
138        let mut first = true;
139        // Sort keys for deterministic output
140        let mut keys: Vec<&String> = self.tags.keys().collect();
141        keys.sort();
142        for k in keys {
143            let v = &self.tags[k];
144            if !first {
145                write!(f, ", ")?;
146            }
147            write!(f, "{k}: {v}")?;
148            first = false;
149        }
150        write!(f, "}})")
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::kinds::Number;
158
159    #[test]
160    fn empty_dict() {
161        let d = HDict::new();
162        assert!(d.is_empty());
163        assert_eq!(d.len(), 0);
164        assert!(d.missing("anything"));
165        assert!(!d.has("anything"));
166        assert_eq!(d.get("anything"), None);
167    }
168
169    #[test]
170    fn set_get_has_missing() {
171        let mut d = HDict::new();
172        d.set("site", Kind::Marker);
173        d.set("area", Kind::Number(Number::unitless(4500.0)));
174        d.set("dis", Kind::Str("Main Site".into()));
175
176        assert!(d.has("site"));
177        assert!(!d.missing("site"));
178        assert_eq!(d.get("site"), Some(&Kind::Marker));
179
180        assert!(d.has("area"));
181        assert_eq!(d.get("area"), Some(&Kind::Number(Number::unitless(4500.0))));
182
183        assert!(d.has("dis"));
184        assert_eq!(d.get("dis"), Some(&Kind::Str("Main Site".into())));
185
186        assert!(d.missing("nonexistent"));
187        assert_eq!(d.get("nonexistent"), None);
188
189        assert_eq!(d.len(), 3);
190        assert!(!d.is_empty());
191    }
192
193    #[test]
194    fn id_with_ref() {
195        let mut d = HDict::new();
196        let r = HRef::new("site-1", Some("Main Site".into()));
197        d.set("id", Kind::Ref(r));
198
199        let id = d.id().unwrap();
200        assert_eq!(id.val, "site-1");
201        assert_eq!(id.dis, Some("Main Site".into()));
202    }
203
204    #[test]
205    fn id_with_non_ref() {
206        let mut d = HDict::new();
207        d.set("id", Kind::Str("not-a-ref".into()));
208        assert!(d.id().is_none());
209    }
210
211    #[test]
212    fn id_missing() {
213        let d = HDict::new();
214        assert!(d.id().is_none());
215    }
216
217    #[test]
218    fn dis_from_dis_tag() {
219        let mut d = HDict::new();
220        d.set("dis", Kind::Str("My Building".into()));
221        d.set("id", Kind::Ref(HRef::new("b-1", Some("Ref Dis".into()))));
222
223        // dis tag takes priority over id ref dis
224        assert_eq!(d.dis(), Some("My Building"));
225    }
226
227    #[test]
228    fn dis_from_ref_fallback() {
229        let mut d = HDict::new();
230        d.set(
231            "id",
232            Kind::Ref(HRef::new("b-1", Some("Ref Display".into()))),
233        );
234
235        // No dis tag, falls back to ref dis
236        assert_eq!(d.dis(), Some("Ref Display"));
237    }
238
239    #[test]
240    fn dis_from_ref_without_dis() {
241        let mut d = HDict::new();
242        d.set("id", Kind::Ref(HRef::from_val("b-1")));
243
244        // No dis tag, ref has no dis either
245        assert_eq!(d.dis(), None);
246    }
247
248    #[test]
249    fn dis_missing_entirely() {
250        let d = HDict::new();
251        assert_eq!(d.dis(), None);
252    }
253
254    #[test]
255    fn dis_non_str_dis_tag() {
256        let mut d = HDict::new();
257        // dis tag exists but is not a Str
258        d.set("dis", Kind::Number(Number::unitless(42.0)));
259        assert_eq!(d.dis(), None);
260    }
261
262    #[test]
263    fn merge_updates_and_adds() {
264        let mut base = HDict::new();
265        base.set("site", Kind::Marker);
266        base.set("area", Kind::Number(Number::unitless(1000.0)));
267
268        let mut update = HDict::new();
269        update.set("area", Kind::Number(Number::unitless(2000.0)));
270        update.set("geoCity", Kind::Str("Richmond".into()));
271
272        base.merge(&update);
273
274        assert_eq!(base.get("site"), Some(&Kind::Marker));
275        assert_eq!(
276            base.get("area"),
277            Some(&Kind::Number(Number::unitless(2000.0)))
278        );
279        assert_eq!(base.get("geoCity"), Some(&Kind::Str("Richmond".into())));
280        assert_eq!(base.len(), 3);
281    }
282
283    #[test]
284    fn merge_with_remove() {
285        let mut base = HDict::new();
286        base.set("site", Kind::Marker);
287        base.set("area", Kind::Number(Number::unitless(1000.0)));
288        base.set("dis", Kind::Str("Old Name".into()));
289
290        let mut update = HDict::new();
291        update.set("area", Kind::Remove); // should remove area
292        update.set("dis", Kind::Str("New Name".into())); // should overwrite dis
293
294        base.merge(&update);
295
296        assert!(base.has("site"));
297        assert!(base.missing("area")); // removed
298        assert_eq!(base.get("dis"), Some(&Kind::Str("New Name".into())));
299        assert_eq!(base.len(), 2);
300    }
301
302    #[test]
303    fn remove_tag() {
304        let mut d = HDict::new();
305        d.set("a", Kind::Marker);
306        d.set("b", Kind::Str("hello".into()));
307
308        let removed = d.remove_tag("a");
309        assert_eq!(removed, Some(Kind::Marker));
310        assert!(d.missing("a"));
311        assert_eq!(d.len(), 1);
312
313        let not_found = d.remove_tag("nonexistent");
314        assert_eq!(not_found, None);
315    }
316
317    #[test]
318    fn from_tags() {
319        let mut map = HashMap::new();
320        map.insert("site".to_string(), Kind::Marker);
321        map.insert("dis".to_string(), Kind::Str("Test".into()));
322
323        let d = HDict::from_tags(map);
324        assert_eq!(d.len(), 2);
325        assert!(d.has("site"));
326        assert!(d.has("dis"));
327    }
328
329    #[test]
330    fn tag_iteration() {
331        let mut d = HDict::new();
332        d.set("a", Kind::Marker);
333        d.set("b", Kind::Str("hello".into()));
334        d.set("c", Kind::Number(Number::unitless(3.0)));
335
336        let pairs: Vec<(&str, &Kind)> = d.iter().collect();
337        assert_eq!(pairs.len(), 3);
338
339        // Check all tags are present (order not guaranteed)
340        let names: std::collections::HashSet<&str> = d.tag_names().collect();
341        assert!(names.contains("a"));
342        assert!(names.contains("b"));
343        assert!(names.contains("c"));
344        assert_eq!(names.len(), 3);
345    }
346
347    #[test]
348    fn tag_name_set() {
349        let mut d = HDict::new();
350        d.set("alpha", Kind::Marker);
351        d.set("beta", Kind::Marker);
352
353        let set = d.tag_name_set();
354        assert_eq!(set.len(), 2);
355        assert!(set.contains("alpha"));
356        assert!(set.contains("beta"));
357    }
358
359    #[test]
360    fn equality() {
361        let mut a = HDict::new();
362        a.set("x", Kind::Number(Number::unitless(1.0)));
363        a.set("y", Kind::Str("hi".into()));
364
365        let mut b = HDict::new();
366        b.set("x", Kind::Number(Number::unitless(1.0)));
367        b.set("y", Kind::Str("hi".into()));
368
369        assert_eq!(a, b);
370    }
371
372    #[test]
373    fn inequality_different_values() {
374        let mut a = HDict::new();
375        a.set("x", Kind::Number(Number::unitless(1.0)));
376
377        let mut b = HDict::new();
378        b.set("x", Kind::Number(Number::unitless(2.0)));
379
380        assert_ne!(a, b);
381    }
382
383    #[test]
384    fn inequality_different_keys() {
385        let mut a = HDict::new();
386        a.set("x", Kind::Marker);
387
388        let mut b = HDict::new();
389        b.set("y", Kind::Marker);
390
391        assert_ne!(a, b);
392    }
393
394    #[test]
395    fn display_empty() {
396        let d = HDict::new();
397        assert_eq!(d.to_string(), "HDict({})");
398    }
399
400    #[test]
401    fn display_with_tags() {
402        let mut d = HDict::new();
403        d.set("site", Kind::Marker);
404
405        let s = d.to_string();
406        assert!(s.starts_with("HDict({"));
407        assert!(s.ends_with("})"));
408        assert!(s.contains("site"));
409    }
410
411    #[test]
412    fn overwrite_existing_tag() {
413        let mut d = HDict::new();
414        d.set("val", Kind::Number(Number::unitless(1.0)));
415        d.set("val", Kind::Number(Number::unitless(2.0)));
416
417        assert_eq!(d.len(), 1);
418        assert_eq!(d.get("val"), Some(&Kind::Number(Number::unitless(2.0))));
419    }
420
421    #[test]
422    fn default_is_empty() {
423        let d = HDict::default();
424        assert!(d.is_empty());
425        assert_eq!(d.len(), 0);
426    }
427
428    #[test]
429    fn tags_returns_inner_map() {
430        let mut d = HDict::new();
431        d.set("a", Kind::Marker);
432        let tags = d.tags();
433        assert_eq!(tags.len(), 1);
434        assert_eq!(tags.get("a"), Some(&Kind::Marker));
435    }
436}