Skip to main content

ryo_suggest/
id.rs

1//! SuggestId - Unique identifier for suggestions with lifecycle support
2//!
3//! Uses (index, generation) design for stale detection:
4//! - Stable while AnalysisContext is unchanged
5//! - Invalidated (generation mismatch) when underlying symbol changes
6//! - Allows detection of stale references
7
8use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13/// Unique identifier for a suggestion coupled to AnalysisContext lifecycle
14///
15/// Uses (index, generation) design for stale detection.
16///
17/// Format: "S001g0", "S042g3", etc.
18///
19/// # Example
20///
21/// ```
22/// use ryo_suggest::SuggestId;
23///
24/// let id = SuggestId::new(1, 0);
25/// assert_eq!(id.to_string(), "S001g0");
26/// assert_eq!(id.index(), 1);
27/// assert_eq!(id.generation(), 0);
28///
29/// let parsed: SuggestId = "S042g3".parse().unwrap();
30/// assert_eq!(parsed.index(), 42);
31/// assert_eq!(parsed.generation(), 3);
32/// ```
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
34pub struct SuggestId {
35    index: u32,
36    generation: u32,
37}
38
39impl SuggestId {
40    /// Create a new SuggestId with index and generation
41    pub fn new(index: u32, generation: u32) -> Self {
42        Self { index, generation }
43    }
44
45    /// Create a SuggestId from raw values (for deserialization)
46    #[allow(dead_code)]
47    pub(crate) fn from_raw(index: u32, generation: u32) -> Self {
48        Self { index, generation }
49    }
50
51    /// Get the index component
52    pub fn index(&self) -> u32 {
53        self.index
54    }
55
56    /// Get the generation component
57    pub fn generation(&self) -> u32 {
58        self.generation
59    }
60
61    /// Check if this ID matches a given generation
62    pub fn is_generation(&self, gen: u32) -> bool {
63        self.generation == gen
64    }
65
66    /// Create a new ID with bumped generation
67    pub fn with_generation(&self, generation: u32) -> Self {
68        Self {
69            index: self.index,
70            generation,
71        }
72    }
73}
74
75impl fmt::Display for SuggestId {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        write!(f, "S{:03}g{}", self.index, self.generation)
78    }
79}
80
81/// Error when parsing a SuggestId from string
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum ParseSuggestIdError {
84    /// Invalid format (missing prefix or separator)
85    InvalidFormat,
86    /// Invalid index number
87    InvalidIndex,
88    /// Invalid generation number
89    InvalidGeneration,
90}
91
92impl fmt::Display for ParseSuggestIdError {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            Self::InvalidFormat => write!(f, "Invalid SuggestId format (expected S###g#)"),
96            Self::InvalidIndex => write!(f, "Invalid index in SuggestId"),
97            Self::InvalidGeneration => write!(f, "Invalid generation in SuggestId"),
98        }
99    }
100}
101
102impl std::error::Error for ParseSuggestIdError {}
103
104impl FromStr for SuggestId {
105    type Err = ParseSuggestIdError;
106
107    fn from_str(s: &str) -> Result<Self, Self::Err> {
108        let s = s.trim();
109
110        // Must start with 'S' or 's'
111        let rest = s
112            .strip_prefix('S')
113            .or_else(|| s.strip_prefix('s'))
114            .ok_or(ParseSuggestIdError::InvalidFormat)?;
115
116        // Find 'g' separator
117        let (index_str, gen_str) = rest
118            .split_once('g')
119            .or_else(|| rest.split_once('G'))
120            .ok_or(ParseSuggestIdError::InvalidFormat)?;
121
122        let index: u32 = index_str
123            .parse()
124            .map_err(|_| ParseSuggestIdError::InvalidIndex)?;
125        let generation: u32 = gen_str
126            .parse()
127            .map_err(|_| ParseSuggestIdError::InvalidGeneration)?;
128
129        Ok(SuggestId { index, generation })
130    }
131}
132
133// Serialize as "S001g0" string format
134impl Serialize for SuggestId {
135    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
136    where
137        S: Serializer,
138    {
139        serializer.serialize_str(&self.to_string())
140    }
141}
142
143impl<'de> Deserialize<'de> for SuggestId {
144    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
145    where
146        D: Deserializer<'de>,
147    {
148        let s = String::deserialize(deserializer)?;
149        s.parse().map_err(serde::de::Error::custom)
150    }
151}
152
153/// Generator for sequential SuggestIds
154#[derive(Debug, Clone)]
155pub struct SuggestIdGenerator {
156    next_index: u32,
157}
158
159impl Default for SuggestIdGenerator {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165impl SuggestIdGenerator {
166    /// Create a new generator starting at index 1
167    pub fn new() -> Self {
168        Self { next_index: 1 }
169    }
170
171    /// Create a generator starting at a specific index
172    pub fn starting_at(index: u32) -> Self {
173        Self {
174            next_index: index.max(1),
175        }
176    }
177
178    /// Generate the next ID with generation 0
179    pub fn next_id(&mut self) -> SuggestId {
180        let id = SuggestId::new(self.next_index, 0);
181        self.next_index += 1;
182        id
183    }
184
185    /// Generate the next ID with a specific generation
186    pub fn next_id_with_generation(&mut self, generation: u32) -> SuggestId {
187        let id = SuggestId::new(self.next_index, generation);
188        self.next_index += 1;
189        id
190    }
191
192    /// Peek at the next ID without consuming
193    pub fn peek(&self) -> SuggestId {
194        SuggestId::new(self.next_index, 0)
195    }
196
197    /// Reset the generator to start at 1
198    pub fn reset(&mut self) {
199        self.next_index = 1;
200    }
201
202    /// Get the current index value
203    pub fn current_index(&self) -> u32 {
204        self.next_index
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_suggest_id_new() {
214        let id = SuggestId::new(1, 0);
215        assert_eq!(id.index(), 1);
216        assert_eq!(id.generation(), 0);
217    }
218
219    #[test]
220    fn test_suggest_id_display() {
221        assert_eq!(SuggestId::new(1, 0).to_string(), "S001g0");
222        assert_eq!(SuggestId::new(42, 3).to_string(), "S042g3");
223        assert_eq!(SuggestId::new(999, 10).to_string(), "S999g10");
224        assert_eq!(SuggestId::new(1000, 0).to_string(), "S1000g0");
225    }
226
227    #[test]
228    fn test_suggest_id_parse() {
229        let id: SuggestId = "S001g0".parse().unwrap();
230        assert_eq!(id.index(), 1);
231        assert_eq!(id.generation(), 0);
232
233        let id2: SuggestId = "S042g3".parse().unwrap();
234        assert_eq!(id2.index(), 42);
235        assert_eq!(id2.generation(), 3);
236
237        let id3: SuggestId = "s123G5".parse().unwrap();
238        assert_eq!(id3.index(), 123);
239        assert_eq!(id3.generation(), 5);
240    }
241
242    #[test]
243    fn test_suggest_id_parse_errors() {
244        assert_eq!(
245            "001g0".parse::<SuggestId>().unwrap_err(),
246            ParseSuggestIdError::InvalidFormat
247        );
248        assert_eq!(
249            "S001".parse::<SuggestId>().unwrap_err(),
250            ParseSuggestIdError::InvalidFormat
251        );
252        assert_eq!(
253            "Sabcg0".parse::<SuggestId>().unwrap_err(),
254            ParseSuggestIdError::InvalidIndex
255        );
256        assert_eq!(
257            "S001gabc".parse::<SuggestId>().unwrap_err(),
258            ParseSuggestIdError::InvalidGeneration
259        );
260    }
261
262    #[test]
263    fn test_suggest_id_serde() {
264        let id = SuggestId::new(42, 3);
265        let json = serde_json::to_string(&id).unwrap();
266        assert_eq!(json, "\"S042g3\"");
267
268        let parsed: SuggestId = serde_json::from_str(&json).unwrap();
269        assert_eq!(parsed, id);
270    }
271
272    #[test]
273    fn test_suggest_id_ordering() {
274        let id1 = SuggestId::new(1, 0);
275        let id2 = SuggestId::new(2, 0);
276        let id3 = SuggestId::new(1, 1);
277
278        assert!(id1 < id2);
279        assert!(id1 < id3); // Same index, but generation is second in ordering
280
281        let mut ids = vec![id2, id3, id1];
282        ids.sort();
283        assert_eq!(ids, vec![id1, id3, id2]);
284    }
285
286    #[test]
287    fn test_suggest_id_is_generation() {
288        let id = SuggestId::new(1, 3);
289        assert!(id.is_generation(3));
290        assert!(!id.is_generation(0));
291        assert!(!id.is_generation(4));
292    }
293
294    #[test]
295    fn test_suggest_id_with_generation() {
296        let id = SuggestId::new(42, 0);
297        let bumped = id.with_generation(5);
298        assert_eq!(bumped.index(), 42);
299        assert_eq!(bumped.generation(), 5);
300    }
301
302    #[test]
303    fn test_suggest_id_generator() {
304        let mut gen = SuggestIdGenerator::new();
305        assert_eq!(gen.next_id(), SuggestId::new(1, 0));
306        assert_eq!(gen.next_id(), SuggestId::new(2, 0));
307        assert_eq!(gen.next_id(), SuggestId::new(3, 0));
308        assert_eq!(gen.peek(), SuggestId::new(4, 0));
309        assert_eq!(gen.next_id(), SuggestId::new(4, 0));
310    }
311
312    #[test]
313    fn test_suggest_id_generator_with_generation() {
314        let mut gen = SuggestIdGenerator::new();
315        let id = gen.next_id_with_generation(5);
316        assert_eq!(id.index(), 1);
317        assert_eq!(id.generation(), 5);
318    }
319
320    #[test]
321    fn test_suggest_id_generator_starting_at() {
322        let mut gen = SuggestIdGenerator::starting_at(10);
323        assert_eq!(gen.next_id(), SuggestId::new(10, 0));
324        assert_eq!(gen.next_id(), SuggestId::new(11, 0));
325    }
326
327    #[test]
328    fn test_suggest_id_generator_reset() {
329        let mut gen = SuggestIdGenerator::new();
330        gen.next_id();
331        gen.next_id();
332        gen.reset();
333        assert_eq!(gen.next_id(), SuggestId::new(1, 0));
334    }
335}