Skip to main content

haystack_core/kinds/
ref_.rs

1// Haystack Ref — an entity reference.
2
3use std::fmt;
4use std::hash::{Hash, Hasher};
5
6/// Haystack Ref — an entity reference.
7///
8/// `val` is the identifier (alphanumeric, `_`, `:`, `-`, `.`, `~`).
9/// `dis` is an optional display name (cosmetic — ignored in equality/hash).
10///
11/// Zinc: `@abc-123` or `@abc-123 "Display Name"`.
12#[derive(Debug, Clone)]
13pub struct HRef {
14    pub val: String,
15    pub dis: Option<String>,
16}
17
18impl HRef {
19    pub fn new(val: impl Into<String>, dis: Option<String>) -> Self {
20        Self {
21            val: val.into(),
22            dis,
23        }
24    }
25
26    pub fn from_val(val: impl Into<String>) -> Self {
27        Self {
28            val: val.into(),
29            dis: None,
30        }
31    }
32}
33
34impl HRef {
35    /// Check if this ref's identifier is a valid Haystack ref.
36    /// Valid refs are non-empty and contain only: ASCII letters, digits, `_`, `:`, `-`, `.`, `~`.
37    pub fn is_valid(&self) -> bool {
38        !self.val.is_empty()
39            && self
40                .val
41                .chars()
42                .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | ':' | '-' | '.' | '~'))
43    }
44}
45
46impl PartialEq for HRef {
47    fn eq(&self, other: &Self) -> bool {
48        self.val == other.val
49    }
50}
51
52impl Eq for HRef {}
53
54impl Hash for HRef {
55    fn hash<H: Hasher>(&self, state: &mut H) {
56        self.val.hash(state);
57    }
58}
59
60impl fmt::Display for HRef {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "@{}", self.val)?;
63        if let Some(ref dis) = self.dis {
64            write!(f, " '{dis}'")?;
65        }
66        Ok(())
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use std::collections::HashSet;
74
75    #[test]
76    fn ref_display_without_dis() {
77        let r = HRef::from_val("site-1");
78        assert_eq!(r.to_string(), "@site-1");
79    }
80
81    #[test]
82    fn ref_display_with_dis() {
83        let r = HRef::new("site-1", Some("Main Site".into()));
84        assert_eq!(r.to_string(), "@site-1 'Main Site'");
85    }
86
87    #[test]
88    fn ref_equality_ignores_dis() {
89        let a = HRef::new("site-1", Some("Building A".into()));
90        let b = HRef::new("site-1", Some("Different Name".into()));
91        let c = HRef::from_val("site-1");
92        assert_eq!(a, b);
93        assert_eq!(a, c);
94    }
95
96    #[test]
97    fn ref_hash_ignores_dis() {
98        let a = HRef::new("site-1", Some("A".into()));
99        let b = HRef::new("site-1", Some("B".into()));
100        let mut set = HashSet::new();
101        set.insert(a);
102        assert!(set.contains(&b));
103    }
104
105    #[test]
106    fn ref_inequality() {
107        let a = HRef::from_val("site-1");
108        let b = HRef::from_val("site-2");
109        assert_ne!(a, b);
110    }
111
112    #[test]
113    fn ref_from_val_convenience() {
114        let r = HRef::from_val("abc");
115        assert_eq!(r.val, "abc");
116        assert_eq!(r.dis, None);
117    }
118}