ryo-suggest 0.1.0

[experimental] Pattern-based suggestion engine for RYO
Documentation
//! SuggestRegistry - Registration and lookup for Suggest implementations
//!
//! Manages a collection of Suggest trait implementations for pattern detection.

use std::collections::HashMap;

use crate::pattern::{PatternBasedSuggest, RuleStore};
use crate::store::SuggestIndex;
use crate::suggest::{Suggest, SuggestBox, SuggestCategory};

/// Registry of all Suggest implementations
pub struct SuggestRegistry {
    /// Registered suggest implementations
    suggests: Vec<SuggestBox>,

    /// Name to index mapping for O(1) lookup
    name_to_index: HashMap<&'static str, SuggestIndex>,

    /// Category to indices mapping for filtering
    category_to_indices: HashMap<SuggestCategory, Vec<SuggestIndex>>,
}

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

impl SuggestRegistry {
    /// Create a new empty registry
    pub fn new() -> Self {
        Self {
            suggests: Vec::new(),
            name_to_index: HashMap::new(),
            category_to_indices: HashMap::new(),
        }
    }

    /// Register a Suggest implementation
    ///
    /// # Panics
    ///
    /// Panics if a suggest with the same name is already registered.
    pub fn register<S: Suggest + 'static>(&mut self, suggest: S) {
        let name = suggest.name();
        let category = suggest.category();

        if self.name_to_index.contains_key(name) {
            panic!("Suggest '{}' is already registered", name);
        }

        let index = SuggestIndex(self.suggests.len());

        self.name_to_index.insert(name, index);
        self.category_to_indices
            .entry(category)
            .or_default()
            .push(index);
        self.suggests.push(Box::new(suggest));
    }

    /// Try to register a Suggest implementation (returns false if already exists)
    pub fn try_register<S: Suggest + 'static>(&mut self, suggest: S) -> bool {
        let name = suggest.name();

        if self.name_to_index.contains_key(name) {
            return false;
        }

        self.register(suggest);
        true
    }

    /// Register all rules from a RuleStore as PatternBasedSuggest
    ///
    /// Returns the number of rules successfully registered.
    /// Rules with duplicate IDs are skipped.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use ryo_suggest::{SuggestRegistry, RuleStore};
    ///
    /// let store = RuleStore::builtin_only()?;
    /// let mut registry = SuggestRegistry::new();
    /// let count = registry.register_from_rule_store(&store);
    /// println!("Registered {} pattern-based rules", count);
    /// ```
    pub fn register_from_rule_store(&mut self, store: &RuleStore) -> usize {
        let mut count = 0;
        for rule in store.all_rules() {
            let suggest = PatternBasedSuggest::new(rule.clone());
            if self.try_register(suggest) {
                count += 1;
            }
        }
        count
    }

    /// Get a Suggest by index
    pub fn get(&self, idx: SuggestIndex) -> Option<&dyn Suggest> {
        self.suggests.get(idx.0).map(|s| s.as_ref())
    }

    /// Get a Suggest by name
    pub fn get_by_name(&self, name: &str) -> Option<(SuggestIndex, &dyn Suggest)> {
        // Case-insensitive lookup
        self.name_to_index
            .iter()
            .find(|(k, _)| k.eq_ignore_ascii_case(name))
            .and_then(|(_, &idx)| self.get(idx).map(|s| (idx, s)))
    }

    /// Get all suggests in a category
    pub fn by_category(&self, category: SuggestCategory) -> Vec<(SuggestIndex, &dyn Suggest)> {
        self.category_to_indices
            .get(&category)
            .map(|indices| {
                indices
                    .iter()
                    .filter_map(|&idx| self.get(idx).map(|s| (idx, s)))
                    .collect()
            })
            .unwrap_or_default()
    }

    /// Iterate over all registered suggests
    pub fn iter(&self) -> impl Iterator<Item = (SuggestIndex, &dyn Suggest)> {
        self.suggests
            .iter()
            .enumerate()
            .map(|(i, s)| (SuggestIndex(i), s.as_ref()))
    }

    /// Get names of all registered suggests
    pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
        self.suggests.iter().map(|s| s.name())
    }

    /// Number of registered suggests
    pub fn len(&self) -> usize {
        self.suggests.len()
    }

    /// Check if registry is empty
    pub fn is_empty(&self) -> bool {
        self.suggests.is_empty()
    }

    /// Check if a suggest with the given name is registered
    pub fn contains(&self, name: &str) -> bool {
        self.name_to_index
            .keys()
            .any(|k| k.eq_ignore_ascii_case(name))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::suggest::{MutationSpec, SafetyLevel, SuggestOpportunity, SuggestResult};
    use ryo_analysis::context::AnalysisContext;
    use ryo_analysis::SymbolId;

    // Test implementation
    struct TestSuggest {
        name: &'static str,
        category: SuggestCategory,
    }

    impl Suggest for TestSuggest {
        fn name(&self) -> &'static str {
            self.name
        }

        fn description(&self) -> &str {
            "Test suggest"
        }

        fn category(&self) -> SuggestCategory {
            self.category
        }

        fn safety_level(&self) -> SafetyLevel {
            SafetyLevel::Auto
        }

        fn detect(&self, _ctx: &AnalysisContext, _symbols: &[SymbolId]) -> Vec<SuggestOpportunity> {
            vec![]
        }

        fn to_mutation_specs(
            &self,
            _ctx: &AnalysisContext,
            _opportunity: &SuggestOpportunity,
        ) -> SuggestResult<Vec<MutationSpec>> {
            Ok(vec![])
        }
    }

    #[test]
    fn test_registry_register_and_get() {
        let mut registry = SuggestRegistry::new();

        registry.register(TestSuggest {
            name: "Test",
            category: SuggestCategory::Derive,
        });

        assert_eq!(registry.len(), 1);
        assert!(registry.contains("Test"));
        assert!(registry.contains("test")); // Case insensitive
    }

    #[test]
    fn test_registry_get_by_name() {
        let mut registry = SuggestRegistry::new();

        registry.register(TestSuggest {
            name: "Builder",
            category: SuggestCategory::Pattern,
        });

        let (idx, suggest) = registry.get_by_name("builder").unwrap();
        assert_eq!(suggest.name(), "Builder");
        assert_eq!(idx.as_usize(), 0);
    }

    #[test]
    fn test_registry_by_category() {
        let mut registry = SuggestRegistry::new();

        registry.register(TestSuggest {
            name: "Default",
            category: SuggestCategory::Derive,
        });
        registry.register(TestSuggest {
            name: "Clone",
            category: SuggestCategory::Derive,
        });
        registry.register(TestSuggest {
            name: "Builder",
            category: SuggestCategory::Pattern,
        });

        let derives = registry.by_category(SuggestCategory::Derive);
        assert_eq!(derives.len(), 2);

        let patterns = registry.by_category(SuggestCategory::Pattern);
        assert_eq!(patterns.len(), 1);

        let performance = registry.by_category(SuggestCategory::Performance);
        assert_eq!(performance.len(), 0);
    }

    #[test]
    #[should_panic(expected = "already registered")]
    fn test_registry_duplicate_panics() {
        let mut registry = SuggestRegistry::new();

        registry.register(TestSuggest {
            name: "Test",
            category: SuggestCategory::Derive,
        });
        registry.register(TestSuggest {
            name: "Test",
            category: SuggestCategory::Derive,
        });
    }

    #[test]
    fn test_registry_try_register() {
        let mut registry = SuggestRegistry::new();

        assert!(registry.try_register(TestSuggest {
            name: "Test",
            category: SuggestCategory::Derive,
        }));

        assert!(!registry.try_register(TestSuggest {
            name: "Test",
            category: SuggestCategory::Derive,
        }));

        assert_eq!(registry.len(), 1);
    }

    #[test]
    fn test_registry_iter() {
        let mut registry = SuggestRegistry::new();

        registry.register(TestSuggest {
            name: "A",
            category: SuggestCategory::Derive,
        });
        registry.register(TestSuggest {
            name: "B",
            category: SuggestCategory::Pattern,
        });
        registry.register(TestSuggest {
            name: "C",
            category: SuggestCategory::Performance,
        });

        let names: Vec<_> = registry.iter().map(|(_, s)| s.name()).collect();
        assert_eq!(names, vec!["A", "B", "C"]);
    }

    #[test]
    fn test_register_from_rule_store() {
        use crate::pattern::RuleStore;

        let store = RuleStore::builtin_only().unwrap();
        let builtin_count = store.len();
        assert!(builtin_count > 0, "Should have builtin rules");

        let mut registry = SuggestRegistry::new();
        let registered = registry.register_from_rule_store(&store);

        assert_eq!(registered, builtin_count);
        assert_eq!(registry.len(), builtin_count);

        // All should be in Lint category
        let lints = registry.by_category(SuggestCategory::Lint);
        assert_eq!(lints.len(), builtin_count);
    }

    #[test]
    fn test_register_from_rule_store_skips_duplicates() {
        use crate::pattern::RuleStore;

        let store = RuleStore::builtin_only().unwrap();

        let mut registry = SuggestRegistry::new();
        let first = registry.register_from_rule_store(&store);
        let second = registry.register_from_rule_store(&store);

        assert!(first > 0);
        assert_eq!(second, 0); // All skipped as duplicates
        assert_eq!(registry.len(), first);
    }
}