Skip to main content

feagi_structures/genomic/brain_regions/
region_id.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5RegionID - UUID-based unique identifier for brain regions.
6
7Provides type safety and global uniqueness for brain region identifiers.
8*/
9
10use crate::FeagiDataError;
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12use std::fmt::{Display, Formatter};
13use std::str::FromStr;
14use uuid::Uuid;
15
16/// Unique identifier for a brain region, based on UUID v7.
17///
18/// This struct provides type safety and ensures global uniqueness for brain region IDs.
19/// UUID v7 is time-ordered, which provides better database indexing and sortability.
20/// It handles serialization to and deserialization from string representations of UUIDs.
21///
22/// # Examples
23///
24/// ```
25/// use feagi_structures::genomic::brain_regions::RegionID;
26///
27/// // Generate a new time-ordered RegionID
28/// let region_id = RegionID::new();
29///
30/// // Convert to string for storage/display
31/// let id_string = region_id.to_string();
32///
33/// // Parse from string
34/// let parsed_id = RegionID::from_string(&id_string).unwrap();
35/// assert_eq!(region_id, parsed_id);
36/// ```
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub struct RegionID {
39    uuid: Uuid,
40}
41
42impl RegionID {
43    /// Generates a new, time-ordered RegionID (UUID v7).
44    ///
45    /// UUID v7 uses a timestamp-based approach, providing natural sorting
46    /// and better database performance compared to random UUIDs.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use feagi_structures::genomic::brain_regions::RegionID;
52    ///
53    /// let region_id = RegionID::new();
54    /// assert_ne!(region_id.to_string(), "");
55    /// ```
56    pub fn new() -> Self {
57        Self {
58            uuid: Uuid::now_v7(),
59        }
60    }
61
62    /// Creates a RegionID from a UUID.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use feagi_structures::genomic::brain_regions::RegionID;
68    /// use uuid::Uuid;
69    ///
70    /// let uuid = Uuid::now_v7();
71    /// let region_id = RegionID::from_uuid(uuid);
72    /// assert_eq!(region_id.as_uuid(), uuid);
73    /// ```
74    pub fn from_uuid(uuid: Uuid) -> Self {
75        Self { uuid }
76    }
77
78    /// Tries to create a RegionID from a string.
79    ///
80    /// Returns an error if the string is not a valid UUID.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use feagi_structures::genomic::brain_regions::RegionID;
86    ///
87    /// let region_id = RegionID::from_string("550e8400-e29b-41d4-a716-446655440000").unwrap();
88    /// assert_eq!(region_id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
89    /// ```
90    pub fn from_string(s: &str) -> Result<Self, FeagiDataError> {
91        Uuid::parse_str(s)
92            .map(RegionID::from_uuid)
93            .map_err(|e| FeagiDataError::BadParameters(format!("Invalid RegionID string: {}", e)))
94    }
95
96    /// Returns the underlying UUID.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use feagi_structures::genomic::brain_regions::RegionID;
102    ///
103    /// let region_id = RegionID::new();
104    /// let uuid = region_id.as_uuid();
105    /// ```
106    pub fn as_uuid(&self) -> Uuid {
107        self.uuid
108    }
109
110    /// Returns the byte representation of the UUID.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use feagi_structures::genomic::brain_regions::RegionID;
116    ///
117    /// let region_id = RegionID::new();
118    /// let bytes = region_id.as_bytes();
119    /// assert_eq!(bytes.len(), 16);
120    /// ```
121    pub fn as_bytes(&self) -> &[u8; 16] {
122        self.uuid.as_bytes()
123    }
124}
125
126impl Default for RegionID {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132impl Display for RegionID {
133    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
134        write!(f, "{}", self.uuid)
135    }
136}
137
138impl FromStr for RegionID {
139    type Err = FeagiDataError;
140
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        Uuid::parse_str(s)
143            .map(RegionID::from_uuid)
144            .map_err(|e| FeagiDataError::BadParameters(format!("Invalid RegionID string: {}", e)))
145    }
146}
147
148// Implement From<Uuid> for RegionID
149impl From<Uuid> for RegionID {
150    fn from(uuid: Uuid) -> Self {
151        RegionID::from_uuid(uuid)
152    }
153}
154
155// Implement From<RegionID> for Uuid
156impl From<RegionID> for Uuid {
157    fn from(region_id: RegionID) -> Self {
158        region_id.uuid
159    }
160}
161
162// Implement Serialize for RegionID
163impl Serialize for RegionID {
164    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
165    where
166        S: Serializer,
167    {
168        serializer.serialize_str(&self.uuid.to_string())
169    }
170}
171
172// Implement Deserialize for RegionID
173impl<'de> Deserialize<'de> for RegionID {
174    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175    where
176        D: Deserializer<'de>,
177    {
178        let s = String::deserialize(deserializer)?;
179        RegionID::from_string(&s).map_err(serde::de::Error::custom)
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_region_id_new() {
189        let id1 = RegionID::new();
190        let id2 = RegionID::new();
191
192        // Each new ID should be unique
193        assert_ne!(id1, id2);
194    }
195
196    #[test]
197    fn test_region_id_from_uuid() {
198        let uuid = Uuid::now_v7();
199        let region_id = RegionID::from_uuid(uuid);
200
201        assert_eq!(region_id.as_uuid(), uuid);
202    }
203
204    #[test]
205    fn test_region_id_from_string() {
206        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
207        let region_id = RegionID::from_string(uuid_str).unwrap();
208
209        assert_eq!(region_id.to_string(), uuid_str);
210    }
211
212    #[test]
213    fn test_region_id_from_string_invalid() {
214        let result = RegionID::from_string("not-a-uuid");
215        assert!(result.is_err());
216    }
217
218    #[test]
219    fn test_region_id_display() {
220        let region_id = RegionID::new();
221        let display_str = region_id.to_string();
222
223        // Should be a valid UUID string (36 characters with dashes)
224        assert_eq!(display_str.len(), 36);
225        assert!(display_str.contains('-'));
226    }
227
228    #[test]
229    fn test_region_id_from_str() {
230        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
231        let region_id: RegionID = uuid_str.parse().unwrap();
232
233        assert_eq!(region_id.to_string(), uuid_str);
234    }
235
236    #[test]
237    fn test_region_id_serialization() {
238        let region_id = RegionID::new();
239
240        // Serialize to JSON
241        let json = serde_json::to_string(&region_id).unwrap();
242
243        // Should be a quoted UUID string
244        assert!(json.starts_with('"'));
245        assert!(json.ends_with('"'));
246        assert_eq!(json.len(), 38); // 36 + 2 quotes
247    }
248
249    #[test]
250    fn test_region_id_deserialization() {
251        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
252        let json = format!("\"{}\"", uuid_str);
253
254        let region_id: RegionID = serde_json::from_str(&json).unwrap();
255
256        assert_eq!(region_id.to_string(), uuid_str);
257    }
258
259    #[test]
260    fn test_region_id_roundtrip() {
261        let original = RegionID::new();
262
263        // Serialize
264        let json = serde_json::to_string(&original).unwrap();
265
266        // Deserialize
267        let deserialized: RegionID = serde_json::from_str(&json).unwrap();
268
269        assert_eq!(original, deserialized);
270    }
271
272    #[test]
273    fn test_region_id_as_bytes() {
274        let region_id = RegionID::new();
275        let bytes = region_id.as_bytes();
276
277        assert_eq!(bytes.len(), 16);
278    }
279
280    #[test]
281    fn test_region_id_default() {
282        let id1 = RegionID::default();
283        let id2 = RegionID::default();
284
285        // Each default should generate a new unique ID
286        assert_ne!(id1, id2);
287    }
288
289    #[test]
290    fn test_region_id_equality() {
291        let uuid = Uuid::now_v7();
292        let id1 = RegionID::from_uuid(uuid);
293        let id2 = RegionID::from_uuid(uuid);
294
295        assert_eq!(id1, id2);
296    }
297
298    #[test]
299    fn test_region_id_hash() {
300        use std::collections::HashSet;
301
302        let id1 = RegionID::new();
303        let id2 = RegionID::new();
304
305        let mut set = HashSet::new();
306        set.insert(id1);
307        set.insert(id2);
308
309        assert_eq!(set.len(), 2);
310        assert!(set.contains(&id1));
311        assert!(set.contains(&id2));
312    }
313}