ryo-storage 0.1.0

Persistent storage and transaction log for RYO
Documentation
//! File-based UUID persistence implementation.
//!
//! Implements the `UuidPersistence` trait from `ryo-analysis` using file storage.
//! UUID mappings are stored in `.ryo/uuid-mapping.json` within the workspace root.

use ryo_analysis::{UuidPersistence, UuidPersistenceError};
use std::collections::HashMap;
use std::path::{Path, PathBuf};

/// File-based UUID storage implementation.
///
/// Stores UUID mappings in `.ryo/uuid-mapping.json` relative to the workspace root.
///
/// # Example
///
/// ```ignore
/// use ryo_storage::FileUuidStorage;
/// use ryo_analysis::UuidPersistence;
///
/// let storage = FileUuidStorage::new("/path/to/workspace");
///
/// // Load existing mappings
/// if let Some(mappings) = storage.load() {
///     println!("Loaded {} mappings", mappings.len());
/// }
///
/// // Save new mappings
/// let mut mappings = HashMap::new();
/// mappings.insert("test_crate::Foo".to_string(), "uuid-here".to_string());
/// storage.save(&mappings)?;
/// ```
#[derive(Debug, Clone)]
pub struct FileUuidStorage {
    /// Path to the UUID mapping file.
    file_path: PathBuf,
}

impl FileUuidStorage {
    /// Create a new file-based UUID storage.
    ///
    /// The UUID mapping file will be at `{workspace_root}/.ryo/uuid-mapping.json`.
    pub fn new(workspace_root: impl AsRef<Path>) -> Self {
        let file_path = workspace_root
            .as_ref()
            .join(".ryo")
            .join("uuid-mapping.json");
        Self { file_path }
    }

    /// Get the path to the UUID mapping file.
    pub fn file_path(&self) -> &Path {
        &self.file_path
    }
}

impl UuidPersistence for FileUuidStorage {
    fn load(&self) -> Option<HashMap<String, String>> {
        let content = std::fs::read_to_string(&self.file_path).ok()?;
        serde_json::from_str(&content).ok()
    }

    fn save(&self, mappings: &HashMap<String, String>) -> Result<(), UuidPersistenceError> {
        // Create .ryo directory if it doesn't exist
        if let Some(parent) = self.file_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let content = serde_json::to_string_pretty(mappings)?;
        std::fs::write(&self.file_path, content)?;
        Ok(())
    }
}

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

    #[test]
    fn test_file_uuid_storage_roundtrip() {
        let temp_dir = TempDir::new().unwrap();
        let storage = FileUuidStorage::new(temp_dir.path());

        // Initially empty
        assert!(storage.load().is_none());

        // Save some mappings
        let mut mappings = HashMap::new();
        mappings.insert("test_crate::Foo".to_string(), "uuid-1".to_string());
        mappings.insert("test_crate::Bar".to_string(), "uuid-2".to_string());
        storage.save(&mappings).unwrap();

        // Load and verify
        let loaded = storage.load().unwrap();
        assert_eq!(loaded.len(), 2);
        assert_eq!(loaded.get("test_crate::Foo"), Some(&"uuid-1".to_string()));
        assert_eq!(loaded.get("test_crate::Bar"), Some(&"uuid-2".to_string()));
    }

    #[test]
    fn test_file_uuid_storage_creates_directory() {
        let temp_dir = TempDir::new().unwrap();
        let storage = FileUuidStorage::new(temp_dir.path());

        // .ryo directory doesn't exist yet
        assert!(!temp_dir.path().join(".ryo").exists());

        // Save creates the directory
        let mappings = HashMap::new();
        storage.save(&mappings).unwrap();

        // Now it exists
        assert!(temp_dir.path().join(".ryo").exists());
        assert!(storage.file_path().exists());
    }
}