backbone-bucket 0.4.0

Bucket Bounded Context: File Storage Module for Backbone Framework
Documentation
//! State machine for UploadSession workflow
//!
//! Generated by metaphor-schema

use serde::{Deserialize, Serialize};
use std::str::FromStr;

/// States for UploadSession workflow
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UploadSessionState {
    /// Initial state
    Initiated,
    Uploading,
    Completing,
    /// Final state
    Completed,
    /// Final state
    Failed,
    /// Final state
    Expired,
}

impl Default for UploadSessionState {
    fn default() -> Self {
        Self::Initiated
    }
}

impl std::fmt::Display for UploadSessionState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Initiated => write!(f, "initiated"),
            Self::Uploading => write!(f, "uploading"),
            Self::Completing => write!(f, "completing"),
            Self::Completed => write!(f, "completed"),
            Self::Failed => write!(f, "failed"),
            Self::Expired => write!(f, "expired"),
        }
    }
}

impl FromStr for UploadSessionState {
    type Err = StateMachineError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "initiated" => Ok(Self::Initiated),
            "uploading" => Ok(Self::Uploading),
            "completing" => Ok(Self::Completing),
            "completed" => Ok(Self::Completed),
            "failed" => Ok(Self::Failed),
            "expired" => Ok(Self::Expired),
            _ => Err(StateMachineError::InvalidState(s.to_string())),
        }
    }
}

impl UploadSessionState {
    /// Check if this is the initial state
    pub fn is_initial(&self) -> bool {
        matches!(self, Self::Initiated)
    }

    /// Check if this is a final state
    pub fn is_final(&self) -> bool {
        matches!(self, Self::Completed | Self::Failed | Self::Expired)
    }

    /// Get all possible states
    pub fn all() -> Vec<Self> {
        vec![
            Self::Initiated,
            Self::Uploading,
            Self::Completing,
            Self::Completed,
            Self::Failed,
            Self::Expired,
        ]
    }
}

/// Transitions for UploadSession workflow
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UploadSessionTransition {
    /// initiated -> uploading
    StartUpload,
    /// uploading -> uploading
    AddPart,
    /// uploading -> completing
    Complete,
    /// completing -> completed
    Finish,
    /// uploading, completing -> failed
    Fail,
    /// initiated, uploading -> failed
    Abort,
    /// initiated, uploading -> expired
    Expire,
    /// failed -> initiated
    Retry,
}

impl std::fmt::Display for UploadSessionTransition {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::StartUpload => write!(f, "start_upload"),
            Self::AddPart => write!(f, "add_part"),
            Self::Complete => write!(f, "complete"),
            Self::Finish => write!(f, "finish"),
            Self::Fail => write!(f, "fail"),
            Self::Abort => write!(f, "abort"),
            Self::Expire => write!(f, "expire"),
            Self::Retry => write!(f, "retry"),
        }
    }
}

impl FromStr for UploadSessionTransition {
    type Err = StateMachineError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "start_upload" => Ok(Self::StartUpload),
            "add_part" => Ok(Self::AddPart),
            "complete" => Ok(Self::Complete),
            "finish" => Ok(Self::Finish),
            "fail" => Ok(Self::Fail),
            "abort" => Ok(Self::Abort),
            "expire" => Ok(Self::Expire),
            "retry" => Ok(Self::Retry),
            _ => Err(StateMachineError::InvalidTransition(s.to_string())),
        }
    }
}

impl UploadSessionTransition {
    /// Get the target state of this transition
    pub fn target_state(&self) -> UploadSessionState {
        match self {
            Self::StartUpload => UploadSessionState::Uploading,
            Self::AddPart => UploadSessionState::Uploading,
            Self::Complete => UploadSessionState::Completing,
            Self::Finish => UploadSessionState::Completed,
            Self::Fail => UploadSessionState::Failed,
            Self::Abort => UploadSessionState::Failed,
            Self::Expire => UploadSessionState::Expired,
            Self::Retry => UploadSessionState::Initiated,
        }
    }

    /// Get all transitions
    pub fn all() -> Vec<Self> {
        vec![
            Self::StartUpload,
            Self::AddPart,
            Self::Complete,
            Self::Finish,
            Self::Fail,
            Self::Abort,
            Self::Expire,
            Self::Retry,
        ]
    }

    /// Get allowed roles for this transition
    pub fn allowed_roles(&self) -> &'static [&'static str] {
        match self {
            Self::StartUpload => &["system", "user"],
            Self::AddPart => &["system", "user"],
            Self::Complete => &["system", "user"],
            Self::Finish => &["system"],
            Self::Fail => &["system"],
            Self::Abort => &["user", "admin"],
            Self::Expire => &["system"],
            Self::Retry => &["user"],
        }
    }
}

use super::StateMachineError;

/// State machine for UploadSession workflow
#[derive(Debug, Clone)]
pub struct UploadSessionStateMachine {
    current_state: UploadSessionState,
}

impl UploadSessionStateMachine {
    /// Create a new state machine with initial state
    pub fn new() -> Self {
        Self {
            current_state: UploadSessionState::default(),
        }
    }

    /// Create from an existing state
    pub fn from_state(state: UploadSessionState) -> Self {
        Self { current_state: state }
    }

    /// Get the current state
    pub fn current_state(&self) -> UploadSessionState {
        self.current_state
    }

    /// Check if a transition is allowed from the current state
    pub fn can_transition(&self, transition: UploadSessionTransition) -> bool {
        if matches!(self.current_state, UploadSessionState::Completed | UploadSessionState::Expired) {
            return false;
        }

        match (self.current_state, transition) {
            (UploadSessionState::Initiated, UploadSessionTransition::StartUpload) => true,
            (UploadSessionState::Uploading, UploadSessionTransition::AddPart) => true,
            (UploadSessionState::Uploading, UploadSessionTransition::Complete) => true,
            (UploadSessionState::Completing, UploadSessionTransition::Finish) => true,
            (UploadSessionState::Uploading, UploadSessionTransition::Fail) => true,
            (UploadSessionState::Completing, UploadSessionTransition::Fail) => true,
            (UploadSessionState::Initiated, UploadSessionTransition::Abort) => true,
            (UploadSessionState::Uploading, UploadSessionTransition::Abort) => true,
            (UploadSessionState::Initiated, UploadSessionTransition::Expire) => true,
            (UploadSessionState::Uploading, UploadSessionTransition::Expire) => true,
            (UploadSessionState::Failed, UploadSessionTransition::Retry) => true,
            _ => false,
        }
    }

    /// Check if a transition is allowed for a given role
    pub fn can_transition_with_role(&self, transition: UploadSessionTransition, role: &str) -> bool {
        if !self.can_transition(transition) {
            return false;
        }

        let allowed_roles = transition.allowed_roles();
        if allowed_roles.is_empty() {
            return true; // No role restriction
        }

        allowed_roles.iter().any(|r| *r == role || *r == "*")
    }

    /// Apply a transition, returning the new state
    pub fn transition(&mut self, transition: UploadSessionTransition) -> Result<UploadSessionState, StateMachineError> {
        if !self.can_transition(transition) {
            return Err(StateMachineError::TransitionNotAllowed {
                transition: transition.to_string(),
                from: self.current_state.to_string(),
            });
        }

        self.current_state = transition.target_state();
        Ok(self.current_state)
    }

    /// Apply a transition with role check
    pub fn transition_with_role(&mut self, transition: UploadSessionTransition, role: &str) -> Result<UploadSessionState, StateMachineError> {
        // Check basic transition validity first
        if !self.can_transition(transition) {
            return Err(StateMachineError::TransitionNotAllowed {
                transition: transition.to_string(),
                from: self.current_state.to_string(),
            });
        }

        // Check role authorization
        if !self.can_transition_with_role(transition, role) {
            return Err(StateMachineError::RoleNotAuthorized {
                role: role.to_string(),
                transition: transition.to_string(),
            });
        }

        self.current_state = transition.target_state();
        Ok(self.current_state)
    }

    /// Get all available transitions from the current state
    pub fn available_transitions(&self) -> Vec<UploadSessionTransition> {
        UploadSessionTransition::all()
            .into_iter()
            .filter(|t| self.can_transition(*t))
            .collect()
    }

    /// Get all available transitions for a given role
    pub fn available_transitions_for_role(&self, role: &str) -> Vec<UploadSessionTransition> {
        UploadSessionTransition::all()
            .into_iter()
            .filter(|t| self.can_transition_with_role(*t, role))
            .collect()
    }

    /// Attempt to transition directly to a target state.
    ///
    /// Finds any transition that leads to the target state and applies it.
    /// Returns Err if no valid transition leads from current state to target.
    pub fn transition_to_state(&mut self, target: UploadSessionState) -> Result<UploadSessionState, StateMachineError> {
        let valid = UploadSessionTransition::all().into_iter()
            .filter(|t| self.can_transition(*t))
            .find(|t| t.target_state() == target);
        match valid {
            Some(t) => self.transition(t),
            None => Err(StateMachineError::TransitionNotAllowed {
                transition: target.to_string(),
                from: self.current_state.to_string(),
            }),
        }
    }
}

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

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

    #[test]
    fn test_initial_state() {
        let sm = UploadSessionStateMachine::new();
        assert_eq!(sm.current_state(), UploadSessionState::Initiated);
        assert!(sm.current_state().is_initial());
    }

    #[test]
    fn test_valid_transition() {
        let mut sm = UploadSessionStateMachine::from_state(UploadSessionState::Initiated);
        assert!(sm.can_transition(UploadSessionTransition::StartUpload));
        let result = sm.transition(UploadSessionTransition::StartUpload);
        assert!(result.is_ok());
        assert_eq!(sm.current_state(), UploadSessionState::Uploading);
    }

    #[test]
    fn test_invalid_transition() {
        let mut sm = UploadSessionStateMachine::from_state(UploadSessionState::Initiated);
        // AddPart is not valid from Initiated state
        let result = sm.transition(UploadSessionTransition::AddPart);
        assert!(result.is_err());
    }

    #[test]
    fn test_state_parsing() {
        let state: UploadSessionState = "initiated".parse().unwrap();
        assert_eq!(state, UploadSessionState::Initiated);
    }

    #[test]
    fn test_available_transitions() {
        let sm = UploadSessionStateMachine::new();
        let available = sm.available_transitions();
        // Should have at least some transitions available from initial state
        assert!(!available.is_empty() || sm.current_state().is_final());
    }
}