use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BucketState {
Active,
Readonly,
Archived,
Deleted,
}
impl Default for BucketState {
fn default() -> Self {
Self::Active
}
}
impl std::fmt::Display for BucketState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Active => write!(f, "active"),
Self::Readonly => write!(f, "readonly"),
Self::Archived => write!(f, "archived"),
Self::Deleted => write!(f, "deleted"),
}
}
}
impl FromStr for BucketState {
type Err = StateMachineError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"active" => Ok(Self::Active),
"readonly" => Ok(Self::Readonly),
"archived" => Ok(Self::Archived),
"deleted" => Ok(Self::Deleted),
_ => Err(StateMachineError::InvalidState(s.to_string())),
}
}
}
impl BucketState {
pub fn is_initial(&self) -> bool {
matches!(self, Self::Active)
}
pub fn is_final(&self) -> bool {
matches!(self, Self::Deleted)
}
pub fn all() -> Vec<Self> {
vec![
Self::Active,
Self::Readonly,
Self::Archived,
Self::Deleted,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BucketTransition {
Lock,
Unlock,
Archive,
Restore,
Delete,
}
impl std::fmt::Display for BucketTransition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Lock => write!(f, "lock"),
Self::Unlock => write!(f, "unlock"),
Self::Archive => write!(f, "archive"),
Self::Restore => write!(f, "restore"),
Self::Delete => write!(f, "delete"),
}
}
}
impl FromStr for BucketTransition {
type Err = StateMachineError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"lock" => Ok(Self::Lock),
"unlock" => Ok(Self::Unlock),
"archive" => Ok(Self::Archive),
"restore" => Ok(Self::Restore),
"delete" => Ok(Self::Delete),
_ => Err(StateMachineError::InvalidTransition(s.to_string())),
}
}
}
impl BucketTransition {
pub fn target_state(&self) -> BucketState {
match self {
Self::Lock => BucketState::Readonly,
Self::Unlock => BucketState::Active,
Self::Archive => BucketState::Archived,
Self::Restore => BucketState::Active,
Self::Delete => BucketState::Deleted,
}
}
pub fn all() -> Vec<Self> {
vec![
Self::Lock,
Self::Unlock,
Self::Archive,
Self::Restore,
Self::Delete,
]
}
pub fn allowed_roles(&self) -> &'static [&'static str] {
match self {
Self::Lock => &["admin", "owner"],
Self::Unlock => &["admin", "owner"],
Self::Archive => &["admin", "owner"],
Self::Restore => &["admin", "owner"],
Self::Delete => &["admin", "owner"],
}
}
}
use super::StateMachineError;
#[derive(Debug, Clone)]
pub struct BucketStateMachine {
current_state: BucketState,
}
impl BucketStateMachine {
pub fn new() -> Self {
Self {
current_state: BucketState::default(),
}
}
pub fn from_state(state: BucketState) -> Self {
Self { current_state: state }
}
pub fn current_state(&self) -> BucketState {
self.current_state
}
pub fn can_transition(&self, transition: BucketTransition) -> bool {
if matches!(self.current_state, BucketState::Deleted) {
return false;
}
match (self.current_state, transition) {
(BucketState::Active, BucketTransition::Lock) => true,
(BucketState::Readonly, BucketTransition::Unlock) => true,
(BucketState::Active, BucketTransition::Archive) => true,
(BucketState::Readonly, BucketTransition::Archive) => true,
(BucketState::Archived, BucketTransition::Restore) => true,
(BucketState::Active, BucketTransition::Delete) => true,
(BucketState::Readonly, BucketTransition::Delete) => true,
(BucketState::Archived, BucketTransition::Delete) => true,
_ => false,
}
}
pub fn can_transition_with_role(&self, transition: BucketTransition, 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: BucketTransition) -> Result<BucketState, 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: BucketTransition, role: &str) -> Result<BucketState, 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<BucketTransition> {
BucketTransition::all()
.into_iter()
.filter(|t| self.can_transition(*t))
.collect()
}
pub fn available_transitions_for_role(&self, role: &str) -> Vec<BucketTransition> {
BucketTransition::all()
.into_iter()
.filter(|t| self.can_transition_with_role(*t, role))
.collect()
}
pub fn transition_to_state(&mut self, target: BucketState) -> Result<BucketState, StateMachineError> {
let valid = BucketTransition::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 BucketStateMachine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_state() {
let sm = BucketStateMachine::new();
assert_eq!(sm.current_state(), BucketState::Active);
assert!(sm.current_state().is_initial());
}
#[test]
fn test_valid_transition() {
let mut sm = BucketStateMachine::from_state(BucketState::Active);
assert!(sm.can_transition(BucketTransition::Lock));
let result = sm.transition(BucketTransition::Lock);
assert!(result.is_ok());
assert_eq!(sm.current_state(), BucketState::Readonly);
}
#[test]
fn test_invalid_transition() {
let mut sm = BucketStateMachine::from_state(BucketState::Active);
let result = sm.transition(BucketTransition::Unlock);
assert!(result.is_err());
}
#[test]
fn test_state_parsing() {
let state: BucketState = "active".parse().unwrap();
assert_eq!(state, BucketState::Active);
}
#[test]
fn test_available_transitions() {
let sm = BucketStateMachine::new();
let available = sm.available_transitions();
assert!(!available.is_empty() || sm.current_state().is_final());
}
}