use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StoredFileState {
Uploading,
Processing,
Active,
Quarantined,
Deleted,
Purged,
}
impl Default for StoredFileState {
fn default() -> Self {
Self::Uploading
}
}
impl std::fmt::Display for StoredFileState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Uploading => write!(f, "uploading"),
Self::Processing => write!(f, "processing"),
Self::Active => write!(f, "active"),
Self::Quarantined => write!(f, "quarantined"),
Self::Deleted => write!(f, "deleted"),
Self::Purged => write!(f, "purged"),
}
}
}
impl FromStr for StoredFileState {
type Err = StateMachineError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"uploading" => Ok(Self::Uploading),
"processing" => Ok(Self::Processing),
"active" => Ok(Self::Active),
"quarantined" => Ok(Self::Quarantined),
"deleted" => Ok(Self::Deleted),
"purged" => Ok(Self::Purged),
_ => Err(StateMachineError::InvalidState(s.to_string())),
}
}
}
impl StoredFileState {
pub fn is_initial(&self) -> bool {
matches!(self, Self::Uploading)
}
pub fn is_final(&self) -> bool {
matches!(self, Self::Purged)
}
pub fn all() -> Vec<Self> {
vec![
Self::Uploading,
Self::Processing,
Self::Active,
Self::Quarantined,
Self::Deleted,
Self::Purged,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StoredFileTransition {
CompleteUpload,
MarkSafe,
Quarantine,
MarkSafeByAdmin,
SoftDelete,
Restore,
Purge,
}
impl std::fmt::Display for StoredFileTransition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CompleteUpload => write!(f, "complete_upload"),
Self::MarkSafe => write!(f, "mark_safe"),
Self::Quarantine => write!(f, "quarantine"),
Self::MarkSafeByAdmin => write!(f, "mark_safe_by_admin"),
Self::SoftDelete => write!(f, "soft_delete"),
Self::Restore => write!(f, "restore"),
Self::Purge => write!(f, "purge"),
}
}
}
impl FromStr for StoredFileTransition {
type Err = StateMachineError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"complete_upload" => Ok(Self::CompleteUpload),
"mark_safe" => Ok(Self::MarkSafe),
"quarantine" => Ok(Self::Quarantine),
"mark_safe_by_admin" => Ok(Self::MarkSafeByAdmin),
"soft_delete" => Ok(Self::SoftDelete),
"restore" => Ok(Self::Restore),
"purge" => Ok(Self::Purge),
_ => Err(StateMachineError::InvalidTransition(s.to_string())),
}
}
}
impl StoredFileTransition {
pub fn target_state(&self) -> StoredFileState {
match self {
Self::CompleteUpload => StoredFileState::Processing,
Self::MarkSafe => StoredFileState::Active,
Self::Quarantine => StoredFileState::Quarantined,
Self::MarkSafeByAdmin => StoredFileState::Active,
Self::SoftDelete => StoredFileState::Deleted,
Self::Restore => StoredFileState::Active,
Self::Purge => StoredFileState::Purged,
}
}
pub fn all() -> Vec<Self> {
vec![
Self::CompleteUpload,
Self::MarkSafe,
Self::Quarantine,
Self::MarkSafeByAdmin,
Self::SoftDelete,
Self::Restore,
Self::Purge,
]
}
pub fn allowed_roles(&self) -> &'static [&'static str] {
match self {
Self::CompleteUpload => &["system"],
Self::MarkSafe => &["system"],
Self::Quarantine => &["system"],
Self::MarkSafeByAdmin => &["admin"],
Self::SoftDelete => &["admin", "owner"],
Self::Restore => &["admin", "owner"],
Self::Purge => &["admin", "system"],
}
}
}
use super::StateMachineError;
#[derive(Debug, Clone)]
pub struct StoredFileStateMachine {
current_state: StoredFileState,
}
impl StoredFileStateMachine {
pub fn new() -> Self {
Self {
current_state: StoredFileState::default(),
}
}
pub fn from_state(state: StoredFileState) -> Self {
Self { current_state: state }
}
pub fn current_state(&self) -> StoredFileState {
self.current_state
}
pub fn can_transition(&self, transition: StoredFileTransition) -> bool {
if matches!(self.current_state, StoredFileState::Purged) {
return false;
}
match (self.current_state, transition) {
(StoredFileState::Uploading, StoredFileTransition::CompleteUpload) => true,
(StoredFileState::Processing, StoredFileTransition::MarkSafe) => true,
(StoredFileState::Processing, StoredFileTransition::Quarantine) => true,
(StoredFileState::Quarantined, StoredFileTransition::MarkSafeByAdmin) => true,
(StoredFileState::Active, StoredFileTransition::SoftDelete) => true,
(StoredFileState::Quarantined, StoredFileTransition::SoftDelete) => true,
(StoredFileState::Deleted, StoredFileTransition::Restore) => true,
(StoredFileState::Deleted, StoredFileTransition::Purge) => true,
_ => false,
}
}
pub fn can_transition_with_role(&self, transition: StoredFileTransition, role: &str) -> bool {
if !self.can_transition(transition) {
return false;
}
let allowed_roles = transition.allowed_roles();
if allowed_roles.is_empty() {
return true; }
allowed_roles.iter().any(|r| *r == role || *r == "*")
}
pub fn transition(&mut self, transition: StoredFileTransition) -> Result<StoredFileState, 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)
}
pub fn transition_with_role(&mut self, transition: StoredFileTransition, role: &str) -> Result<StoredFileState, StateMachineError> {
if !self.can_transition(transition) {
return Err(StateMachineError::TransitionNotAllowed {
transition: transition.to_string(),
from: self.current_state.to_string(),
});
}
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)
}
pub fn available_transitions(&self) -> Vec<StoredFileTransition> {
StoredFileTransition::all()
.into_iter()
.filter(|t| self.can_transition(*t))
.collect()
}
pub fn available_transitions_for_role(&self, role: &str) -> Vec<StoredFileTransition> {
StoredFileTransition::all()
.into_iter()
.filter(|t| self.can_transition_with_role(*t, role))
.collect()
}
pub fn transition_to_state(&mut self, target: StoredFileState) -> Result<StoredFileState, StateMachineError> {
let valid = StoredFileTransition::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 StoredFileStateMachine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_state() {
let sm = StoredFileStateMachine::new();
assert_eq!(sm.current_state(), StoredFileState::Uploading);
assert!(sm.current_state().is_initial());
}
#[test]
fn test_valid_transition() {
let mut sm = StoredFileStateMachine::from_state(StoredFileState::Uploading);
assert!(sm.can_transition(StoredFileTransition::CompleteUpload));
let result = sm.transition(StoredFileTransition::CompleteUpload);
assert!(result.is_ok());
assert_eq!(sm.current_state(), StoredFileState::Processing);
}
#[test]
fn test_invalid_transition() {
let mut sm = StoredFileStateMachine::from_state(StoredFileState::Uploading);
let result = sm.transition(StoredFileTransition::MarkSafe);
assert!(result.is_err());
}
#[test]
fn test_state_parsing() {
let state: StoredFileState = "uploading".parse().unwrap();
assert_eq!(state, StoredFileState::Uploading);
}
#[test]
fn test_available_transitions() {
let sm = StoredFileStateMachine::new();
let available = sm.available_transitions();
assert!(!available.is_empty() || sm.current_state().is_final());
}
}