governor-core 1.3.0

Core domain and application logic for cargo-governor
Documentation
//! Reversible operation trait

use async_trait::async_trait;

use serde::{Deserialize, Serialize};

/// Error type for reversible operations
#[derive(Debug, thiserror::Error)]
pub enum OperationError {
    /// Operation failed
    #[error("Operation failed: {0}")]
    Failed(String),

    /// Rollback failed
    #[error("Rollback failed: {0}")]
    RollbackFailed(String),

    /// IO error
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// Serialization error
    #[error("Serialization error: {0}")]
    SerializationError(String),

    /// State not found for rollback
    #[error("State not found for rollback")]
    StateNotFound,
}

/// Result of an operation execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationResult {
    /// Whether the operation succeeded
    pub success: bool,
    /// Operation name
    pub operation_name: String,
    /// Time taken in milliseconds
    pub duration_ms: u64,
    /// Result message
    pub message: Option<String>,
    /// State for potential rollback
    pub state: Option<serde_json::Value>,
    /// Additional data
    pub data: serde_json::Value,
}

impl OperationResult {
    /// Create a successful result
    #[must_use]
    pub fn success(name: String, duration_ms: u64) -> Self {
        Self {
            success: true,
            operation_name: name,
            duration_ms,
            message: None,
            state: None,
            data: serde_json::Value::Object(serde_json::Map::default()),
        }
    }

    /// Create a failed result
    #[must_use]
    pub fn failed(name: String, message: String) -> Self {
        Self {
            success: false,
            operation_name: name,
            duration_ms: 0,
            message: Some(message),
            state: None,
            data: serde_json::Value::Object(serde_json::Map::default()),
        }
    }

    /// Add state to the result
    #[must_use]
    pub fn with_state(mut self, state: serde_json::Value) -> Self {
        self.state = Some(state);
        self
    }

    /// Add data to the result
    #[must_use]
    pub fn with_data(mut self, data: serde_json::Value) -> Self {
        self.data = data;
        self
    }

    /// Set message
    #[must_use]
    pub fn with_message(mut self, message: String) -> Self {
        self.message = Some(message);
        self
    }
}

/// State for rollback operations
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationState {
    /// Operation name
    pub operation_name: String,
    /// State data (operation-specific)
    pub data: serde_json::Value,
    /// Timestamp when state was captured
    pub captured_at: chrono::DateTime<chrono::Utc>,
}

impl OperationState {
    /// Create a new operation state
    #[must_use]
    pub fn new(operation_name: String, data: serde_json::Value) -> Self {
        Self {
            operation_name,
            data,
            captured_at: chrono::Utc::now(),
        }
    }
}

/// Trait for operations that can be executed and rolled back
#[async_trait]
pub trait ReversibleOperation: Send + Sync {
    /// Get the name of this operation
    fn name(&self) -> &str;

    /// Execute the operation
    async fn execute(&self) -> Result<OperationResult, OperationError>;

    /// Rollback the operation to a previous state
    async fn rollback(&self, state: &OperationState) -> Result<(), OperationError>;

    /// Capture the current state before execution
    async fn capture_state(&self) -> Result<OperationState, OperationError>;

    /// Check if the operation is idempotent (can be safely retried)
    fn is_idempotent(&self) -> bool {
        false
    }
}

/// A sequence of reversible operations
pub struct OperationChain {
    /// Operations in the chain
    operations: Vec<Box<dyn ReversibleOperation>>,
    /// Captured states
    states: Vec<Option<OperationState>>,
    /// Completed operations count
    completed: usize,
}

impl OperationChain {
    /// Create a new operation chain
    #[must_use]
    pub fn new() -> Self {
        Self {
            operations: Vec::new(),
            states: Vec::new(),
            completed: 0,
        }
    }

    /// Add an operation to the chain
    pub fn add(&mut self, operation: Box<dyn ReversibleOperation>) {
        self.operations.push(operation);
        self.states.push(None);
    }

    /// Execute all operations in sequence
    ///
    /// # Errors
    ///
    /// Returns `OperationError` if any operation fails or rollback fails
    pub async fn execute_all(&mut self) -> Result<Vec<OperationResult>, OperationError> {
        let mut results = Vec::new();

        for (i, op) in self.operations.iter().enumerate() {
            // Capture state before execution
            let state = match op.capture_state().await {
                Ok(s) => Some(s),
                Err(e) => {
                    // Rollback previous operations
                    self.rollback_to(i).await?;
                    return Err(e);
                }
            };
            self.states[i] = state;

            // Execute operation
            let result = op.execute().await?;

            if !result.success {
                // Rollback previous operations
                self.rollback_to(i).await?;
                return Err(OperationError::Failed(result.message.unwrap_or_default()));
            }

            self.completed = i + 1;
            results.push(result);
        }

        Ok(results)
    }

    /// Rollback all completed operations
    ///
    /// # Errors
    ///
    /// Returns `OperationError` if rollback of any operation fails
    pub async fn rollback_all(&mut self) -> Result<(), OperationError> {
        self.rollback_to(self.completed).await
    }

    /// Rollback to a specific index
    async fn rollback_to(&mut self, index: usize) -> Result<(), OperationError> {
        // Rollback in reverse order
        for i in (0..index).rev() {
            if let Some(op) = self.operations.get(i)
                && let Some(state) = &self.states[i]
            {
                op.rollback(state).await?;
            }
        }
        self.completed = 0;
        self.states = vec![None; self.operations.len()];
        Ok(())
    }

    /// Get the number of operations
    #[must_use]
    pub fn len(&self) -> usize {
        self.operations.len()
    }

    /// Check if the chain is empty
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.operations.is_empty()
    }
}

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

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

    struct TestOperation {
        name: String,
        should_fail: bool,
    }

    #[async_trait]
    impl ReversibleOperation for TestOperation {
        fn name(&self) -> &str {
            &self.name
        }

        async fn execute(&self) -> Result<OperationResult, OperationError> {
            if self.should_fail {
                Ok(OperationResult::failed(
                    self.name.clone(),
                    "Test failure".to_string(),
                ))
            } else {
                Ok(OperationResult::success(self.name.clone(), 10))
            }
        }

        async fn rollback(&self, _state: &OperationState) -> Result<(), OperationError> {
            Ok(())
        }

        async fn capture_state(&self) -> Result<OperationState, OperationError> {
            Ok(OperationState::new(
                self.name.clone(),
                serde_json::Value::Null,
            ))
        }
    }

    #[tokio::test]
    async fn test_operation_chain_success() {
        let mut chain = OperationChain::new();
        chain.add(Box::new(TestOperation {
            name: "op1".to_string(),
            should_fail: false,
        }));
        chain.add(Box::new(TestOperation {
            name: "op2".to_string(),
            should_fail: false,
        }));

        let results = chain.execute_all().await.unwrap();
        assert_eq!(results.len(), 2);
        assert!(results[0].success);
        assert!(results[1].success);
    }

    #[tokio::test]
    async fn test_operation_chain_failure() {
        let mut chain = OperationChain::new();
        chain.add(Box::new(TestOperation {
            name: "op1".to_string(),
            should_fail: false,
        }));
        chain.add(Box::new(TestOperation {
            name: "op2".to_string(),
            should_fail: true,
        }));

        let result = chain.execute_all().await;
        assert!(result.is_err());
    }
}