ryo-app 0.1.0

[preview] Application layer for RYO - Project management, Intent handling, API
Documentation
//! Storage trait for dependency injection.
//!
//! This module defines the `Storage` trait that abstracts persistent storage
//! operations. Implementations are provided in `ryo-storage` crate.
//!
//! # Design
//!
//! The storage trait enables:
//! - **DI**: Different backends (Local, Network, Cloud) can be injected
//! - **Testing**: Mock storage for unit tests
//! - **Future flexibility**: Easy to add new storage backends
//!
//! # Example
//!
//! ```ignore
//! use ryo_app::{Api, Storage};
//! use ryo_storage::RyoStorage;
//!
//! // Production: use real storage
//! let storage = Box::new(RyoStorage::global()?);
//! let api = Api::new(storage);
//!
//! // Testing: use mock storage
//! let mock = Box::new(MockStorage::new());
//! let api = Api::new(mock);
//! ```

use ryo_storage::{StorageResult, TxLog};
use std::path::Path;

/// Abstract storage trait for session persistence.
///
/// This trait is implemented by `ryo-storage` crate and injected into `Api`.
pub trait Storage: Send + Sync {
    /// Initialize storage (create directories, etc.)
    fn init(&mut self) -> StorageResult<()>;

    /// Save a transaction log, returning the session ID.
    fn save(&mut self, log: &TxLog) -> StorageResult<String>;

    /// Load a transaction log by session ID.
    fn load(&self, session_id: &str) -> StorageResult<TxLog>;

    /// List all session IDs.
    fn list_sessions(&self) -> StorageResult<Vec<String>>;

    /// List sessions for a specific project.
    fn sessions_for_project(&self, project_path: &Path) -> StorageResult<Vec<String>>;

    /// Delete a session by ID.
    fn delete(&mut self, session_id: &str) -> StorageResult<()>;
}

/// In-memory storage for testing.
#[derive(Default)]
pub struct InMemoryStorage {
    sessions: std::collections::HashMap<String, TxLog>,
}

impl InMemoryStorage {
    pub fn new() -> Self {
        Self::default()
    }
}

impl Storage for InMemoryStorage {
    fn init(&mut self) -> StorageResult<()> {
        Ok(())
    }

    fn save(&mut self, log: &TxLog) -> StorageResult<String> {
        let id = uuid::Uuid::new_v4().to_string();
        self.sessions.insert(id.clone(), log.clone());
        Ok(id)
    }

    fn load(&self, session_id: &str) -> StorageResult<TxLog> {
        self.sessions
            .get(session_id)
            .cloned()
            .ok_or_else(|| ryo_storage::StorageError::SessionNotFound(session_id.to_string()))
    }

    fn list_sessions(&self) -> StorageResult<Vec<String>> {
        Ok(self.sessions.keys().cloned().collect())
    }

    fn sessions_for_project(&self, _project_path: &Path) -> StorageResult<Vec<String>> {
        // Simple implementation: return all sessions
        self.list_sessions()
    }

    fn delete(&mut self, session_id: &str) -> StorageResult<()> {
        self.sessions
            .remove(session_id)
            .map(|_| ())
            .ok_or_else(|| ryo_storage::StorageError::SessionNotFound(session_id.to_string()))
    }
}

// ============================================================================
// Tests
// ============================================================================

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

    // ------------------------------------------------------------------------
    // InMemoryStorage Tests
    // ------------------------------------------------------------------------

    #[test]
    fn test_new() {
        let storage = InMemoryStorage::new();
        assert!(storage.sessions.is_empty());
    }

    #[test]
    fn test_default() {
        let storage = InMemoryStorage::default();
        assert!(storage.sessions.is_empty());
    }

    #[test]
    fn test_init() {
        let mut storage = InMemoryStorage::new();
        assert!(storage.init().is_ok());
    }

    #[test]
    fn test_save_and_load() {
        let mut storage = InMemoryStorage::new();
        let log = TxLog::new();

        let id = storage.save(&log).unwrap();
        assert!(!id.is_empty());

        let loaded = storage.load(&id).unwrap();
        assert_eq!(loaded.entries().len(), log.entries().len());
    }

    #[test]
    fn test_load_nonexistent() {
        let storage = InMemoryStorage::new();
        let result = storage.load("nonexistent-id");
        assert!(result.is_err());
    }

    #[test]
    fn test_list_sessions_empty() {
        let storage = InMemoryStorage::new();
        let sessions = storage.list_sessions().unwrap();
        assert!(sessions.is_empty());
    }

    #[test]
    fn test_list_sessions_with_data() {
        let mut storage = InMemoryStorage::new();
        let log1 = TxLog::new();
        let log2 = TxLog::new();

        let id1 = storage.save(&log1).unwrap();
        let id2 = storage.save(&log2).unwrap();

        let sessions = storage.list_sessions().unwrap();
        assert_eq!(sessions.len(), 2);
        assert!(sessions.contains(&id1));
        assert!(sessions.contains(&id2));
    }

    #[test]
    fn test_sessions_for_project() {
        let mut storage = InMemoryStorage::new();
        let log = TxLog::new();
        storage.save(&log).unwrap();

        // InMemoryStorage returns all sessions regardless of project
        let sessions = storage
            .sessions_for_project(Path::new("/some/project"))
            .unwrap();
        assert_eq!(sessions.len(), 1);
    }

    #[test]
    fn test_delete() {
        let mut storage = InMemoryStorage::new();
        let log = TxLog::new();
        let id = storage.save(&log).unwrap();

        assert!(storage.load(&id).is_ok());
        assert!(storage.delete(&id).is_ok());
        assert!(storage.load(&id).is_err());
    }

    #[test]
    fn test_delete_nonexistent() {
        let mut storage = InMemoryStorage::new();
        let result = storage.delete("nonexistent-id");
        assert!(result.is_err());
    }

    #[test]
    fn test_multiple_saves() {
        let mut storage = InMemoryStorage::new();

        for i in 0..5 {
            let log = TxLog::with_project(format!("project-{}", i));
            storage.save(&log).unwrap();
        }

        assert_eq!(storage.list_sessions().unwrap().len(), 5);
    }

    // ------------------------------------------------------------------------
    // Storage Trait Object Tests
    // ------------------------------------------------------------------------

    #[test]
    fn test_as_dyn_storage() {
        let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
        let sessions = storage.list_sessions().unwrap();
        assert!(sessions.is_empty());
    }

    #[test]
    fn test_dyn_storage_save_load() {
        let mut storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
        let log = TxLog::new();

        let id = storage.save(&log).unwrap();
        let loaded = storage.load(&id).unwrap();
        assert_eq!(loaded.entries().len(), log.entries().len());
    }
}