ryo-storage 0.1.0

Persistent storage and transaction log for RYO
Documentation
//! State reference and storage for content-addressed state management.
//!
//! This module provides the foundation for deterministic replay by enabling
//! content-addressed storage of file states.
//!
//! # Design
//!
//! ```text
//! StateRef ─────── immutable reference to state (content hash)
//!//!     └──→ StateStore ─── content-addressed storage
//!//!              ├── InMemoryStateStore (for testing/short sessions)
//!              └── (Future) PersistentStateStore
//! ```
//!
//! # Example
//!
//! ```ignore
//! use ryo_core::storage::{StateRef, InMemoryStateStore, StateStore};
//!
//! let mut store = InMemoryStateStore::new();
//!
//! // Store content, get reference
//! let content = "fn main() {}";
//! let state_ref = store.store(content);
//!
//! // Same content = same reference (content-addressed)
//! let same_ref = store.store(content);
//! assert_eq!(state_ref, same_ref);
//!
//! // Load content back
//! let loaded = store.load(&state_ref).unwrap();
//! assert_eq!(loaded, content);
//! ```

use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::fmt;
use std::hash::{Hash, Hasher};

// ============================================================================
// StateRef
// ============================================================================

/// Immutable reference to a state (content hash).
///
/// This is the core abstraction for content-addressed storage.
/// Two identical states will have the same `StateRef`.
///
/// # Implementation
///
/// Currently uses BLAKE3 hash for fast, secure hashing.
/// The hash is stored as a hex string for JSON compatibility.
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
pub struct StateRef(String);

impl StateRef {
    /// Create a new StateRef from content.
    ///
    /// Uses a hash of the content for addressing.
    /// Note: Currently uses std hash; production should use blake3 or sha256.
    pub fn from_content(content: &str) -> Self {
        use std::hash::Hash;
        let mut hasher = DefaultHasher::new();
        content.hash(&mut hasher);
        let hash = hasher.finish();
        Self(format!("{:016x}", hash))
    }

    /// Create a StateRef from an existing hash string.
    ///
    /// Use this when deserializing from storage.
    pub fn from_hash(hash: String) -> Self {
        Self(hash)
    }

    /// Get the hash string.
    pub fn hash(&self) -> &str {
        &self.0
    }

    /// Get a short prefix for display (first 8 chars).
    pub fn short(&self) -> &str {
        &self.0[..8.min(self.0.len())]
    }

    /// Check if this is an empty/null reference.
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

impl fmt::Debug for StateRef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "StateRef({}...)", self.short())
    }
}

impl fmt::Display for StateRef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.short())
    }
}

impl Hash for StateRef {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.0.hash(state);
    }
}

// ============================================================================
// StateStore Trait
// ============================================================================

/// Trait for content-addressed state storage.
///
/// Implementations must guarantee:
/// 1. `store(content)` returns the same `StateRef` for identical content
/// 2. `load(ref)` returns `Some(content)` if previously stored
/// 3. Content is immutable once stored
pub trait StateStore: Send + Sync {
    /// Store content and return its reference.
    ///
    /// If the content already exists, returns the existing reference
    /// without storing again (deduplication).
    fn store(&mut self, content: &str) -> StateRef;

    /// Load content by reference.
    ///
    /// Returns `None` if the reference is not found.
    fn load(&self, state_ref: &StateRef) -> Option<String>;

    /// Check if a reference exists in the store.
    fn exists(&self, state_ref: &StateRef) -> bool {
        self.load(state_ref).is_some()
    }

    /// Get the number of stored states.
    fn len(&self) -> usize;

    /// Check if the store is empty.
    fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

// ============================================================================
// InMemoryStateStore
// ============================================================================

/// In-memory implementation of StateStore.
///
/// Suitable for:
/// - Testing
/// - Short-lived sessions
/// - Sessions where persistence is not required
///
/// For persistent storage across sessions, use `PersistentStateStore` (future).
#[derive(Debug, Default)]
pub struct InMemoryStateStore {
    states: HashMap<StateRef, String>,
}

impl InMemoryStateStore {
    /// Create a new empty store.
    pub fn new() -> Self {
        Self::default()
    }

    /// Get all stored state references.
    pub fn refs(&self) -> impl Iterator<Item = &StateRef> {
        self.states.keys()
    }

    /// Clear all stored states.
    pub fn clear(&mut self) {
        self.states.clear();
    }

    /// Get total size in bytes (approximate).
    pub fn size_bytes(&self) -> usize {
        self.states.values().map(|s| s.len()).sum()
    }
}

impl StateStore for InMemoryStateStore {
    fn store(&mut self, content: &str) -> StateRef {
        let state_ref = StateRef::from_content(content);

        // Deduplication: don't store if already exists
        if !self.states.contains_key(&state_ref) {
            self.states.insert(state_ref.clone(), content.to_string());
        }

        state_ref
    }

    fn load(&self, state_ref: &StateRef) -> Option<String> {
        self.states.get(state_ref).cloned()
    }

    fn exists(&self, state_ref: &StateRef) -> bool {
        self.states.contains_key(state_ref)
    }

    fn len(&self) -> usize {
        self.states.len()
    }
}

// ============================================================================
// FileStateSnapshot
// ============================================================================

/// Snapshot of a file's state at a point in time.
///
/// This captures both the file identity (path) and its content reference.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileStateSnapshot {
    /// Relative path within the project.
    pub path: String,
    /// Reference to the content.
    pub state_ref: StateRef,
}

impl FileStateSnapshot {
    /// Create a new snapshot.
    pub fn new(path: impl Into<String>, state_ref: StateRef) -> Self {
        Self {
            path: path.into(),
            state_ref,
        }
    }
}

/// Snapshot of multiple files (e.g., workspace state).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WorkspaceSnapshot {
    /// File snapshots, keyed by path.
    pub files: HashMap<String, StateRef>,
    /// Optional name for this snapshot (like a Git tag).
    pub name: Option<String>,
    /// Timestamp when snapshot was taken.
    pub timestamp_ms: u64,
}

impl WorkspaceSnapshot {
    /// Create an empty snapshot.
    pub fn new() -> Self {
        Self::default()
    }

    /// Create a named snapshot.
    pub fn named(name: impl Into<String>) -> Self {
        Self {
            name: Some(name.into()),
            ..Default::default()
        }
    }

    /// Add a file to the snapshot.
    pub fn add_file(&mut self, path: impl Into<String>, state_ref: StateRef) {
        self.files.insert(path.into(), state_ref);
    }

    /// Get the state reference for a file.
    pub fn get_file(&self, path: &str) -> Option<&StateRef> {
        self.files.get(path)
    }

    /// Number of files in the snapshot.
    pub fn file_count(&self) -> usize {
        self.files.len()
    }
}

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

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

    #[test]
    fn test_state_ref_from_content() {
        let content = "fn main() {}";
        let ref1 = StateRef::from_content(content);
        let ref2 = StateRef::from_content(content);

        // Same content = same reference
        assert_eq!(ref1, ref2);

        // Different content = different reference
        let ref3 = StateRef::from_content("fn main() { println!(); }");
        assert_ne!(ref1, ref3);
    }

    #[test]
    fn test_state_ref_display() {
        let content = "fn main() {}";
        let state_ref = StateRef::from_content(content);

        // Short display shows first 8 chars
        assert_eq!(state_ref.short().len(), 8);
        assert!(format!("{}", state_ref).len() == 8);
        assert!(format!("{:?}", state_ref).contains("..."));
    }

    #[test]
    fn test_in_memory_store_roundtrip() {
        let mut store = InMemoryStateStore::new();

        let content = "fn main() {}";
        let state_ref = store.store(content);

        let loaded = store.load(&state_ref).unwrap();
        assert_eq!(loaded, content);
    }

    #[test]
    fn test_in_memory_store_deduplication() {
        let mut store = InMemoryStateStore::new();

        let content = "fn main() {}";
        store.store(content);
        store.store(content);
        store.store(content);

        // Should only store once
        assert_eq!(store.len(), 1);
    }

    #[test]
    fn test_in_memory_store_multiple_files() {
        let mut store = InMemoryStateStore::new();

        let ref1 = store.store("content 1");
        let ref2 = store.store("content 2");
        let ref3 = store.store("content 3");

        assert_eq!(store.len(), 3);
        assert_ne!(ref1, ref2);
        assert_ne!(ref2, ref3);

        assert_eq!(store.load(&ref1), Some("content 1".to_string()));
        assert_eq!(store.load(&ref2), Some("content 2".to_string()));
        assert_eq!(store.load(&ref3), Some("content 3".to_string()));
    }

    #[test]
    fn test_workspace_snapshot() {
        let mut store = InMemoryStateStore::new();
        let mut snapshot = WorkspaceSnapshot::named("v1.0");

        let ref1 = store.store("fn main() {}");
        let ref2 = store.store("mod lib;");

        snapshot.add_file("src/main.rs", ref1.clone());
        snapshot.add_file("src/lib.rs", ref2.clone());

        assert_eq!(snapshot.file_count(), 2);
        assert_eq!(snapshot.get_file("src/main.rs"), Some(&ref1));
        assert_eq!(snapshot.get_file("src/lib.rs"), Some(&ref2));
    }

    #[test]
    fn test_state_ref_serialization() {
        let state_ref = StateRef::from_content("test content");

        let json = serde_json::to_string(&state_ref).unwrap();
        let deserialized: StateRef = serde_json::from_str(&json).unwrap();

        assert_eq!(state_ref, deserialized);
    }
}