ryo-suggest 0.1.0

[experimental] Pattern-based suggestion engine for RYO
Documentation
//! SuggestId - Unique identifier for suggestions with lifecycle support
//!
//! Uses (index, generation) design for stale detection:
//! - Stable while AnalysisContext is unchanged
//! - Invalidated (generation mismatch) when underlying symbol changes
//! - Allows detection of stale references

use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// Unique identifier for a suggestion coupled to AnalysisContext lifecycle
///
/// Uses (index, generation) design for stale detection.
///
/// Format: "S001g0", "S042g3", etc.
///
/// # Example
///
/// ```
/// use ryo_suggest::SuggestId;
///
/// let id = SuggestId::new(1, 0);
/// assert_eq!(id.to_string(), "S001g0");
/// assert_eq!(id.index(), 1);
/// assert_eq!(id.generation(), 0);
///
/// let parsed: SuggestId = "S042g3".parse().unwrap();
/// assert_eq!(parsed.index(), 42);
/// assert_eq!(parsed.generation(), 3);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SuggestId {
    index: u32,
    generation: u32,
}

impl SuggestId {
    /// Create a new SuggestId with index and generation
    pub fn new(index: u32, generation: u32) -> Self {
        Self { index, generation }
    }

    /// Create a SuggestId from raw values (for deserialization)
    #[allow(dead_code)]
    pub(crate) fn from_raw(index: u32, generation: u32) -> Self {
        Self { index, generation }
    }

    /// Get the index component
    pub fn index(&self) -> u32 {
        self.index
    }

    /// Get the generation component
    pub fn generation(&self) -> u32 {
        self.generation
    }

    /// Check if this ID matches a given generation
    pub fn is_generation(&self, gen: u32) -> bool {
        self.generation == gen
    }

    /// Create a new ID with bumped generation
    pub fn with_generation(&self, generation: u32) -> Self {
        Self {
            index: self.index,
            generation,
        }
    }
}

impl fmt::Display for SuggestId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "S{:03}g{}", self.index, self.generation)
    }
}

/// Error when parsing a SuggestId from string
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseSuggestIdError {
    /// Invalid format (missing prefix or separator)
    InvalidFormat,
    /// Invalid index number
    InvalidIndex,
    /// Invalid generation number
    InvalidGeneration,
}

impl fmt::Display for ParseSuggestIdError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidFormat => write!(f, "Invalid SuggestId format (expected S###g#)"),
            Self::InvalidIndex => write!(f, "Invalid index in SuggestId"),
            Self::InvalidGeneration => write!(f, "Invalid generation in SuggestId"),
        }
    }
}

impl std::error::Error for ParseSuggestIdError {}

impl FromStr for SuggestId {
    type Err = ParseSuggestIdError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.trim();

        // Must start with 'S' or 's'
        let rest = s
            .strip_prefix('S')
            .or_else(|| s.strip_prefix('s'))
            .ok_or(ParseSuggestIdError::InvalidFormat)?;

        // Find 'g' separator
        let (index_str, gen_str) = rest
            .split_once('g')
            .or_else(|| rest.split_once('G'))
            .ok_or(ParseSuggestIdError::InvalidFormat)?;

        let index: u32 = index_str
            .parse()
            .map_err(|_| ParseSuggestIdError::InvalidIndex)?;
        let generation: u32 = gen_str
            .parse()
            .map_err(|_| ParseSuggestIdError::InvalidGeneration)?;

        Ok(SuggestId { index, generation })
    }
}

// Serialize as "S001g0" string format
impl Serialize for SuggestId {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for SuggestId {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        s.parse().map_err(serde::de::Error::custom)
    }
}

/// Generator for sequential SuggestIds
#[derive(Debug, Clone)]
pub struct SuggestIdGenerator {
    next_index: u32,
}

impl Default for SuggestIdGenerator {
    fn default() -> Self {
        Self::new()
    }
}

impl SuggestIdGenerator {
    /// Create a new generator starting at index 1
    pub fn new() -> Self {
        Self { next_index: 1 }
    }

    /// Create a generator starting at a specific index
    pub fn starting_at(index: u32) -> Self {
        Self {
            next_index: index.max(1),
        }
    }

    /// Generate the next ID with generation 0
    pub fn next_id(&mut self) -> SuggestId {
        let id = SuggestId::new(self.next_index, 0);
        self.next_index += 1;
        id
    }

    /// Generate the next ID with a specific generation
    pub fn next_id_with_generation(&mut self, generation: u32) -> SuggestId {
        let id = SuggestId::new(self.next_index, generation);
        self.next_index += 1;
        id
    }

    /// Peek at the next ID without consuming
    pub fn peek(&self) -> SuggestId {
        SuggestId::new(self.next_index, 0)
    }

    /// Reset the generator to start at 1
    pub fn reset(&mut self) {
        self.next_index = 1;
    }

    /// Get the current index value
    pub fn current_index(&self) -> u32 {
        self.next_index
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_suggest_id_new() {
        let id = SuggestId::new(1, 0);
        assert_eq!(id.index(), 1);
        assert_eq!(id.generation(), 0);
    }

    #[test]
    fn test_suggest_id_display() {
        assert_eq!(SuggestId::new(1, 0).to_string(), "S001g0");
        assert_eq!(SuggestId::new(42, 3).to_string(), "S042g3");
        assert_eq!(SuggestId::new(999, 10).to_string(), "S999g10");
        assert_eq!(SuggestId::new(1000, 0).to_string(), "S1000g0");
    }

    #[test]
    fn test_suggest_id_parse() {
        let id: SuggestId = "S001g0".parse().unwrap();
        assert_eq!(id.index(), 1);
        assert_eq!(id.generation(), 0);

        let id2: SuggestId = "S042g3".parse().unwrap();
        assert_eq!(id2.index(), 42);
        assert_eq!(id2.generation(), 3);

        let id3: SuggestId = "s123G5".parse().unwrap();
        assert_eq!(id3.index(), 123);
        assert_eq!(id3.generation(), 5);
    }

    #[test]
    fn test_suggest_id_parse_errors() {
        assert_eq!(
            "001g0".parse::<SuggestId>().unwrap_err(),
            ParseSuggestIdError::InvalidFormat
        );
        assert_eq!(
            "S001".parse::<SuggestId>().unwrap_err(),
            ParseSuggestIdError::InvalidFormat
        );
        assert_eq!(
            "Sabcg0".parse::<SuggestId>().unwrap_err(),
            ParseSuggestIdError::InvalidIndex
        );
        assert_eq!(
            "S001gabc".parse::<SuggestId>().unwrap_err(),
            ParseSuggestIdError::InvalidGeneration
        );
    }

    #[test]
    fn test_suggest_id_serde() {
        let id = SuggestId::new(42, 3);
        let json = serde_json::to_string(&id).unwrap();
        assert_eq!(json, "\"S042g3\"");

        let parsed: SuggestId = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed, id);
    }

    #[test]
    fn test_suggest_id_ordering() {
        let id1 = SuggestId::new(1, 0);
        let id2 = SuggestId::new(2, 0);
        let id3 = SuggestId::new(1, 1);

        assert!(id1 < id2);
        assert!(id1 < id3); // Same index, but generation is second in ordering

        let mut ids = vec![id2, id3, id1];
        ids.sort();
        assert_eq!(ids, vec![id1, id3, id2]);
    }

    #[test]
    fn test_suggest_id_is_generation() {
        let id = SuggestId::new(1, 3);
        assert!(id.is_generation(3));
        assert!(!id.is_generation(0));
        assert!(!id.is_generation(4));
    }

    #[test]
    fn test_suggest_id_with_generation() {
        let id = SuggestId::new(42, 0);
        let bumped = id.with_generation(5);
        assert_eq!(bumped.index(), 42);
        assert_eq!(bumped.generation(), 5);
    }

    #[test]
    fn test_suggest_id_generator() {
        let mut gen = SuggestIdGenerator::new();
        assert_eq!(gen.next_id(), SuggestId::new(1, 0));
        assert_eq!(gen.next_id(), SuggestId::new(2, 0));
        assert_eq!(gen.next_id(), SuggestId::new(3, 0));
        assert_eq!(gen.peek(), SuggestId::new(4, 0));
        assert_eq!(gen.next_id(), SuggestId::new(4, 0));
    }

    #[test]
    fn test_suggest_id_generator_with_generation() {
        let mut gen = SuggestIdGenerator::new();
        let id = gen.next_id_with_generation(5);
        assert_eq!(id.index(), 1);
        assert_eq!(id.generation(), 5);
    }

    #[test]
    fn test_suggest_id_generator_starting_at() {
        let mut gen = SuggestIdGenerator::starting_at(10);
        assert_eq!(gen.next_id(), SuggestId::new(10, 0));
        assert_eq!(gen.next_id(), SuggestId::new(11, 0));
    }

    #[test]
    fn test_suggest_id_generator_reset() {
        let mut gen = SuggestIdGenerator::new();
        gen.next_id();
        gen.next_id();
        gen.reset();
        assert_eq!(gen.next_id(), SuggestId::new(1, 0));
    }
}