ryo-analysis 0.1.0

Code graph and discovery engine for the RYO project
Documentation
//! FileRegistry - Central file management registry.
//!
//! # Single Point of Access
//!
//! All file operations must go through FileRegistry:
//! - Get FileId: `register()` or `lookup()`
//! - Get metadata: `path()`, `crate_name()`, etc.
//! - FileId is the only way to reference files internally

use super::FileId;
use ryo_symbol::WorkspaceFilePath;
use serde::Serialize;
use slotmap::SlotMap;
use std::collections::HashMap;

/// Invalid FileId error.
///
/// Indicates access to a non-existent or removed file.
#[derive(Debug, Clone, thiserror::Error)]
#[error("invalid file id: {0:?}")]
pub struct InvalidFileId(pub FileId);

/// Central file management registry.
///
/// # Responsibilities
/// - Bidirectional conversion: WorkspaceFilePath ↔ FileId
/// - File lifecycle management
///
/// # Thread Safety
/// - Read operations are thread-safe
/// - Write operations require exclusive control
#[derive(Clone, Serialize)]
pub struct FileRegistry {
    /// FileId → WorkspaceFilePath (reverse lookup)
    id_to_path: SlotMap<FileId, WorkspaceFilePath>,

    /// WorkspaceFilePath → FileId (forward lookup)
    path_to_id: HashMap<WorkspaceFilePath, FileId>,
}

impl FileRegistry {
    /// Create a new registry.
    pub fn new() -> Self {
        Self {
            id_to_path: SlotMap::with_key(),
            path_to_id: HashMap::new(),
        }
    }

    /// Create a registry with pre-allocated capacity.
    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            id_to_path: SlotMap::with_capacity_and_key(capacity),
            path_to_id: HashMap::with_capacity(capacity),
        }
    }

    // === Registration ===

    /// Register a file (returns existing ID if already registered).
    ///
    /// # Returns
    /// - `FileId`: Registration success (new or existing)
    pub fn register(&mut self, path: WorkspaceFilePath) -> FileId {
        // Check if already registered
        if let Some(&existing_id) = self.path_to_id.get(&path) {
            return existing_id;
        }

        // Register new file
        let id = self.id_to_path.insert(path.clone());
        self.path_to_id.insert(path, id);

        id
    }

    /// Remove a file from the registry.
    ///
    /// # Returns
    /// - `Some(WorkspaceFilePath)`: The removed file's path
    /// - `None`: File not found
    pub fn remove(&mut self, id: FileId) -> Option<WorkspaceFilePath> {
        let path = self.id_to_path.remove(id)?;
        self.path_to_id.remove(&path);
        Some(path)
    }

    // === Lookup ===

    /// WorkspaceFilePath → FileId (O(1) hash lookup).
    #[inline]
    pub fn lookup(&self, path: &WorkspaceFilePath) -> Option<FileId> {
        self.path_to_id.get(path).copied()
    }

    /// FileId → WorkspaceFilePath (O(1) array access).
    #[inline]
    pub fn path(&self, id: FileId) -> Option<&WorkspaceFilePath> {
        self.id_to_path.get(id)
    }

    /// FileId → crate name (convenience method).
    #[inline]
    pub fn crate_name(&self, id: FileId) -> Option<&str> {
        self.id_to_path.get(id).map(|p| p.crate_name().as_str())
    }

    /// Check if FileId is valid (including generation check).
    #[inline]
    pub fn contains(&self, id: FileId) -> bool {
        self.id_to_path.contains_key(id)
    }

    // === Metadata Mutation ===

    /// Update file path (e.g., after rename/move).
    ///
    /// This updates the path while preserving the FileId.
    pub fn update_path(
        &mut self,
        id: FileId,
        new_path: WorkspaceFilePath,
    ) -> Result<WorkspaceFilePath, InvalidFileId> {
        let old_path = self.id_to_path.get(id).ok_or(InvalidFileId(id))?.clone();

        // Update path mappings
        self.path_to_id.remove(&old_path);
        self.path_to_id.insert(new_path.clone(), id);
        self.id_to_path[id] = new_path;

        Ok(old_path)
    }

    // === Iteration ===

    /// Iterate over all files.
    pub fn iter(&self) -> impl Iterator<Item = (FileId, &WorkspaceFilePath)> {
        self.id_to_path.iter()
    }

    /// Iterate over files in a specific crate.
    pub fn iter_in_crate<'a>(&'a self, crate_name: &'a str) -> impl Iterator<Item = FileId> + 'a {
        self.id_to_path
            .iter()
            .filter(move |(_, path)| path.crate_name().as_str() == crate_name)
            .map(|(id, _)| id)
    }

    // === Statistics ===

    /// Get the number of registered files.
    #[inline]
    pub fn len(&self) -> usize {
        self.id_to_path.len()
    }

    /// Check if the registry is empty.
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.id_to_path.is_empty()
    }
}

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

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

    fn test_path(relative: &str, crate_name: &str) -> WorkspaceFilePath {
        WorkspaceFilePath::new_for_test(relative, "/project", crate_name)
    }

    #[test]
    fn test_register_and_lookup() {
        let mut registry = FileRegistry::new();
        let path = test_path("src/lib.rs", "my-crate");

        let id = registry.register(path.clone());

        assert!(registry.contains(id));
        assert_eq!(registry.lookup(&path), Some(id));
        assert_eq!(registry.path(id), Some(&path));
        assert_eq!(registry.crate_name(id), Some("my-crate"));
    }

    #[test]
    fn test_register_duplicate() {
        let mut registry = FileRegistry::new();
        let path = test_path("src/lib.rs", "my-crate");

        let id1 = registry.register(path.clone());
        let id2 = registry.register(path);

        assert_eq!(id1, id2);
    }

    #[test]
    fn test_remove() {
        let mut registry = FileRegistry::new();
        let path = test_path("src/lib.rs", "my-crate");

        let id = registry.register(path.clone());
        assert!(registry.contains(id));

        let removed = registry.remove(id);
        assert!(removed.is_some());
        assert!(!registry.contains(id));
        assert!(registry.lookup(&path).is_none());
    }

    #[test]
    fn test_update_path() {
        let mut registry = FileRegistry::new();
        let old_path = test_path("src/old.rs", "my-crate");
        let new_path = test_path("src/new.rs", "my-crate");

        let id = registry.register(old_path.clone());

        // Update path
        let returned_old = registry.update_path(id, new_path.clone()).unwrap();
        assert_eq!(returned_old, old_path);

        // Verify state
        assert!(registry.lookup(&old_path).is_none());
        assert_eq!(registry.lookup(&new_path), Some(id));
        assert_eq!(registry.path(id), Some(&new_path));
    }

    #[test]
    fn test_iter_in_crate() {
        let mut registry = FileRegistry::new();

        registry.register(test_path("src/lib.rs", "crate-a"));
        registry.register(test_path("src/foo.rs", "crate-a"));
        registry.register(test_path("src/lib.rs", "crate-b"));

        let crate_a_files: Vec<_> = registry.iter_in_crate("crate-a").collect();
        assert_eq!(crate_a_files.len(), 2);

        let crate_b_files: Vec<_> = registry.iter_in_crate("crate-b").collect();
        assert_eq!(crate_b_files.len(), 1);
    }
}