Skip to main content

hfx_core/
id.rs

1//! Catchment and snap target identity types.
2
3/// Errors from constructing identity types.
4#[derive(Debug, thiserror::Error)]
5pub enum IdError {
6    /// Returned when an ID is zero. ID 0 is reserved as the terminal sink sentinel.
7    #[error("id must be non-zero (0 is reserved as the terminal sink sentinel)")]
8    ZeroId,
9
10    /// Returned when an ID is negative.
11    #[error("id must be positive, got {value}")]
12    NegativeId {
13        /// The invalid raw value.
14        value: i64,
15    },
16}
17
18/// A unique identifier for a catchment atom.
19///
20/// Invariant: the wrapped value is always strictly positive. Zero is reserved as
21/// the terminal sink sentinel; negatives are invalid.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub struct AtomId(i64);
24
25impl AtomId {
26    /// Construct an [`AtomId`] from a raw `i64`.
27    ///
28    /// # Errors
29    ///
30    /// | Condition | Error variant |
31    /// |-----------|---------------|
32    /// | `raw == 0` | [`IdError::ZeroId`] |
33    /// | `raw < 0` | [`IdError::NegativeId`] |
34    pub fn new(raw: i64) -> Result<Self, IdError> {
35        match raw {
36            0 => Err(IdError::ZeroId),
37            v if v < 0 => Err(IdError::NegativeId { value: raw }),
38            _ => Ok(Self(raw)),
39        }
40    }
41
42    /// Return the underlying `i64` value.
43    pub fn get(self) -> i64 {
44        self.0
45    }
46}
47
48/// A unique identifier for a snap target.
49///
50/// Kept as a distinct type from [`AtomId`] so that the two cannot be
51/// accidentally mixed at call sites. Invariant: the wrapped value is always
52/// strictly positive.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
54pub struct SnapId(i64);
55
56impl SnapId {
57    /// Construct a [`SnapId`] from a raw `i64`.
58    ///
59    /// # Errors
60    ///
61    /// | Condition | Error variant |
62    /// |-----------|---------------|
63    /// | `raw == 0` | [`IdError::ZeroId`] |
64    /// | `raw < 0` | [`IdError::NegativeId`] |
65    pub fn new(raw: i64) -> Result<Self, IdError> {
66        match raw {
67            0 => Err(IdError::ZeroId),
68            v if v < 0 => Err(IdError::NegativeId { value: raw }),
69            _ => Ok(Self(raw)),
70        }
71    }
72
73    /// Return the underlying `i64` value.
74    pub fn get(self) -> i64 {
75        self.0
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn atom_id_accepts_positive() {
85        let id = AtomId::new(1).unwrap();
86        assert_eq!(id.get(), 1);
87    }
88
89    #[test]
90    fn atom_id_rejects_zero() {
91        assert!(matches!(AtomId::new(0), Err(IdError::ZeroId)));
92    }
93
94    #[test]
95    fn atom_id_rejects_negative() {
96        assert!(matches!(
97            AtomId::new(-5),
98            Err(IdError::NegativeId { value: -5 })
99        ));
100    }
101
102    #[test]
103    fn snap_id_accepts_positive() {
104        let id = SnapId::new(42).unwrap();
105        assert_eq!(id.get(), 42);
106    }
107
108    #[test]
109    fn snap_id_rejects_zero() {
110        assert!(matches!(SnapId::new(0), Err(IdError::ZeroId)));
111    }
112
113    #[test]
114    fn snap_id_rejects_negative() {
115        assert!(matches!(
116            SnapId::new(-1),
117            Err(IdError::NegativeId { value: -1 })
118        ));
119    }
120
121    #[test]
122    fn atom_and_snap_are_distinct_types() {
123        // Compile-time check: AtomId and SnapId are not interchangeable.
124        // This test simply exercises both constructors to confirm they exist
125        // as separate types.
126        let _a: AtomId = AtomId::new(10).unwrap();
127        let _s: SnapId = SnapId::new(10).unwrap();
128    }
129
130    #[test]
131    fn atom_id_max_value_succeeds() {
132        let id = AtomId::new(i64::MAX).unwrap();
133        assert_eq!(id.get(), i64::MAX);
134    }
135
136    #[test]
137    fn atom_id_min_value_fails_with_negative_id() {
138        assert!(matches!(
139            AtomId::new(i64::MIN),
140            Err(IdError::NegativeId { value: i64::MIN })
141        ));
142    }
143
144    #[test]
145    fn atom_id_equality() {
146        let a = AtomId::new(7).unwrap();
147        let b = AtomId::new(7).unwrap();
148        assert_eq!(a, b);
149    }
150
151    #[test]
152    fn atom_id_ordering() {
153        let a = AtomId::new(1).unwrap();
154        let b = AtomId::new(2).unwrap();
155        assert!(a < b);
156    }
157
158    #[test]
159    fn atom_id_usable_in_hash_set() {
160        use std::collections::HashSet;
161        let mut set = HashSet::new();
162        set.insert(AtomId::new(1).unwrap());
163        set.insert(AtomId::new(2).unwrap());
164        set.insert(AtomId::new(1).unwrap());
165        assert_eq!(set.len(), 2);
166        assert!(set.contains(&AtomId::new(1).unwrap()));
167    }
168}