radicle-feed 0.1.1

A Radicle feed library for implementing Radicle COB feeds
Documentation
use std::fmt;

use radicle::cob::{ObjectId, TypeName};
use radicle::git::Oid;
use radicle::prelude::RepoId;

use crate::entry::ActionEntry;

/// Trait for abstracting storage operations used by the rad-feed library.
/// Consumers can implement this trait to provide their own storage backend.
pub trait FeedStorage {
    type Error: std::error::Error + Send + Sync + 'static;

    /// Initialize the storage (e.g., create tables, setup schema, etc.)
    fn initialize(&mut self) -> Result<(), Self::Error>;

    /// Get the last processed operation ID for a specific repository and COB (Collaborative Object)
    fn get_last_processed_operation(
        &self,
        rid: &RepoId,
        cob_id: &ObjectId,
        cob_type: &TypeName,
    ) -> Result<Option<Oid>, Self::Error>;

    /// Update the last processed operation ID for a specific repository and COB
    fn update_last_processed_operation(
        &mut self,
        rid: &RepoId,
        cob_id: &ObjectId,
        cob_type: &TypeName,
        last_action_id: &Oid,
    ) -> Result<(), Self::Error>;

    /// Check if an operation already exists in storage (for duplicate prevention)
    fn operation_exists(&self, operation_id: &Oid) -> Result<bool, Self::Error>;

    /// Insert a batch of operation actions into storage
    fn insert_batch(&mut self, entries: &[ActionEntry]) -> Result<(), Self::Error>;

    /// Get statistics about stored data (optional, used for debugging/monitoring)
    #[allow(dead_code)]
    fn get_stats(&self) -> Result<StorageStats, Self::Error>;

    /// Load all actions sorted by timestamp (optional, used for querying)
    fn load_all_sorted(&self) -> Result<Vec<ActionEntry>, Self::Error> {
        Ok(Vec::new())
    }
}

/// Statistics about the storage backend
#[derive(Debug, Default)]
pub struct StorageStats {
    pub total_actions: u64,
    pub actions_by_type: std::collections::HashMap<String, u64>,
    pub tracked_objects: u64,
}

impl fmt::Display for StorageStats {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "=== Storage Statistics ===")?;
        writeln!(f, "Total actions: {}", self.total_actions)?;

        for (kind, count) in &self.actions_by_type {
            writeln!(f, "{}: {}", kind, count)?;
        }

        writeln!(f, "Tracked objects: {}", self.tracked_objects)?;
        Ok(())
    }
}

pub mod error {
    use snafu::Snafu;

    /// Error type for storage operations
    #[derive(Debug, Snafu)]
    #[snafu(visibility(pub))]
    pub enum StorageError {
        /// Generic storage error with message
        #[snafu(display("Storage error: {message}"))]
        Generic { message: String },
        /// Error occurred during initialization
        #[snafu(display("Storage initialization error: {message}"))]
        Initialization { message: String },
        /// Error occurred during data retrieval
        #[snafu(display("Storage retrieval error: {message}"))]
        Retrieval { message: String },
        /// Error occurred during data insertion
        #[snafu(display("Storage insertion error: {message}"))]
        Insertion { message: String },
        /// Error occurred during data update
        #[snafu(display("Storage update error: {message}"))]
        Update { message: String },
    }
}

/// In-memory storage implementation for testing or simple use cases
pub struct MemoryStorage {
    actions: std::collections::HashMap<String, ActionEntry>,
    last_processed: std::collections::HashMap<String, Oid>,
}

impl MemoryStorage {
    pub fn new() -> Self {
        Self {
            actions: std::collections::HashMap::new(),
            last_processed: std::collections::HashMap::new(),
        }
    }

    fn tracking_key(rid: &RepoId, cob_id: &ObjectId, cob_type: &TypeName) -> String {
        format!("{}:{}:{}", rid, cob_id, cob_type)
    }
}

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

impl FeedStorage for MemoryStorage {
    type Error = error::StorageError;

    fn initialize(&mut self) -> Result<(), Self::Error> {
        // Nothing to do for in-memory storage
        Ok(())
    }

    fn get_last_processed_operation(
        &self,
        rid: &RepoId,
        cob_id: &ObjectId,
        cob_type: &TypeName,
    ) -> Result<Option<Oid>, Self::Error> {
        let key = Self::tracking_key(rid, cob_id, cob_type);
        Ok(self.last_processed.get(&key).copied())
    }

    fn update_last_processed_operation(
        &mut self,
        rid: &RepoId,
        cob_id: &ObjectId,
        cob_type: &TypeName,
        last_operation_id: &Oid,
    ) -> Result<(), Self::Error> {
        let key = Self::tracking_key(rid, cob_id, cob_type);
        self.last_processed.insert(key, *last_operation_id);
        Ok(())
    }

    fn operation_exists(&self, operation_id: &Oid) -> Result<bool, Self::Error> {
        Ok(self.actions.contains_key(&operation_id.to_string()))
    }

    fn insert_batch(&mut self, entries: &[ActionEntry]) -> Result<(), Self::Error> {
        for entry in entries {
            self.actions
                .insert(entry.operation_id.to_string(), entry.clone());
        }
        Ok(())
    }

    fn get_stats(&self) -> Result<StorageStats, Self::Error> {
        let mut stats = StorageStats {
            total_actions: self.actions.len() as u64,
            tracked_objects: self.last_processed.len() as u64,
            ..Default::default()
        };

        for entry in self.actions.values() {
            *stats
                .actions_by_type
                .entry(entry.typename.to_string().clone())
                .or_insert(0) += 1;
        }

        Ok(stats)
    }

    fn load_all_sorted(&self) -> Result<Vec<ActionEntry>, Self::Error> {
        let mut entries: Vec<_> = self.actions.values().cloned().collect();
        entries.sort_by_key(|entry| entry.timestamp);
        Ok(entries)
    }
}