use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MailNotificationHooksState {
Ready,
Process,
Pending,
Sent,
Bounce,
Exception,
Canceled,
}
impl Default for MailNotificationHooksState {
fn default() -> Self {
Self::Ready
}
}
impl std::fmt::Display for MailNotificationHooksState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ready => write!(f, "ready"),
Self::Process => write!(f, "process"),
Self::Pending => write!(f, "pending"),
Self::Sent => write!(f, "sent"),
Self::Bounce => write!(f, "bounce"),
Self::Exception => write!(f, "exception"),
Self::Canceled => write!(f, "canceled"),
}
}
}
impl FromStr for MailNotificationHooksState {
type Err = StateMachineError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"ready" => Ok(Self::Ready),
"process" => Ok(Self::Process),
"pending" => Ok(Self::Pending),
"sent" => Ok(Self::Sent),
"bounce" => Ok(Self::Bounce),
"exception" => Ok(Self::Exception),
"canceled" => Ok(Self::Canceled),
_ => Err(StateMachineError::InvalidState(s.to_string())),
}
}
}
impl MailNotificationHooksState {
pub fn is_initial(&self) -> bool {
matches!(self, Self::Ready)
}
pub fn is_final(&self) -> bool {
matches!(self, Self::Sent | Self::Bounce | Self::Canceled)
}
pub fn all() -> Vec<Self> {
vec![
Self::Ready,
Self::Process,
Self::Pending,
Self::Sent,
Self::Bounce,
Self::Exception,
Self::Canceled,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MailNotificationHooksTransition {
DispatchStarted,
SmtpAccepted,
DeliveryConfirmed,
InstantDelivered,
Bounced,
SendFailed,
Canceled,
}
impl std::fmt::Display for MailNotificationHooksTransition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DispatchStarted => write!(f, "dispatch_started"),
Self::SmtpAccepted => write!(f, "smtp_accepted"),
Self::DeliveryConfirmed => write!(f, "delivery_confirmed"),
Self::InstantDelivered => write!(f, "instant_delivered"),
Self::Bounced => write!(f, "bounced"),
Self::SendFailed => write!(f, "send_failed"),
Self::Canceled => write!(f, "canceled"),
}
}
}
impl FromStr for MailNotificationHooksTransition {
type Err = StateMachineError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"dispatch_started" => Ok(Self::DispatchStarted),
"smtp_accepted" => Ok(Self::SmtpAccepted),
"delivery_confirmed" => Ok(Self::DeliveryConfirmed),
"instant_delivered" => Ok(Self::InstantDelivered),
"bounced" => Ok(Self::Bounced),
"send_failed" => Ok(Self::SendFailed),
"canceled" => Ok(Self::Canceled),
_ => Err(StateMachineError::InvalidTransition(s.to_string())),
}
}
}
impl MailNotificationHooksTransition {
pub fn target_state(&self) -> MailNotificationHooksState {
match self {
Self::DispatchStarted => MailNotificationHooksState::Process,
Self::SmtpAccepted => MailNotificationHooksState::Pending,
Self::DeliveryConfirmed => MailNotificationHooksState::Sent,
Self::InstantDelivered => MailNotificationHooksState::Sent,
Self::Bounced => MailNotificationHooksState::Bounce,
Self::SendFailed => MailNotificationHooksState::Exception,
Self::Canceled => MailNotificationHooksState::Canceled,
}
}
pub fn all() -> Vec<Self> {
vec![
Self::DispatchStarted,
Self::SmtpAccepted,
Self::DeliveryConfirmed,
Self::InstantDelivered,
Self::Bounced,
Self::SendFailed,
Self::Canceled,
]
}
pub fn allowed_roles(&self) -> &'static [&'static str] {
match self {
Self::DispatchStarted => &[],
Self::SmtpAccepted => &[],
Self::DeliveryConfirmed => &[],
Self::InstantDelivered => &[],
Self::Bounced => &[],
Self::SendFailed => &[],
Self::Canceled => &[],
}
}
}
use super::StateMachineError;
#[derive(Debug, Clone)]
pub struct MailNotificationHooksStateMachine {
current_state: MailNotificationHooksState,
}
impl MailNotificationHooksStateMachine {
pub fn new() -> Self {
Self {
current_state: MailNotificationHooksState::default(),
}
}
pub fn from_state(state: MailNotificationHooksState) -> Self {
Self { current_state: state }
}
pub fn current_state(&self) -> MailNotificationHooksState {
self.current_state
}
pub fn can_transition(&self, transition: MailNotificationHooksTransition) -> bool {
if matches!(self.current_state, MailNotificationHooksState::Sent | MailNotificationHooksState::Bounce | MailNotificationHooksState::Canceled) {
return false;
}
match (self.current_state, transition) {
(MailNotificationHooksState::Ready, MailNotificationHooksTransition::DispatchStarted) => true,
(MailNotificationHooksState::Process, MailNotificationHooksTransition::SmtpAccepted) => true,
(MailNotificationHooksState::Exception, MailNotificationHooksTransition::SmtpAccepted) => true,
(MailNotificationHooksState::Pending, MailNotificationHooksTransition::DeliveryConfirmed) => true,
(MailNotificationHooksState::Process, MailNotificationHooksTransition::InstantDelivered) => true,
(MailNotificationHooksState::Pending, MailNotificationHooksTransition::Bounced) => true,
(MailNotificationHooksState::Process, MailNotificationHooksTransition::Bounced) => true,
(MailNotificationHooksState::Exception, MailNotificationHooksTransition::Bounced) => true,
(MailNotificationHooksState::Process, MailNotificationHooksTransition::SendFailed) => true,
(MailNotificationHooksState::Pending, MailNotificationHooksTransition::SendFailed) => true,
(MailNotificationHooksState::Ready, MailNotificationHooksTransition::Canceled) => true,
(MailNotificationHooksState::Process, MailNotificationHooksTransition::Canceled) => true,
(MailNotificationHooksState::Pending, MailNotificationHooksTransition::Canceled) => true,
_ => false,
}
}
pub fn can_transition_with_role(&self, transition: MailNotificationHooksTransition, 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: MailNotificationHooksTransition) -> Result<MailNotificationHooksState, 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: MailNotificationHooksTransition, role: &str) -> Result<MailNotificationHooksState, 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<MailNotificationHooksTransition> {
MailNotificationHooksTransition::all()
.into_iter()
.filter(|t| self.can_transition(*t))
.collect()
}
pub fn available_transitions_for_role(&self, role: &str) -> Vec<MailNotificationHooksTransition> {
MailNotificationHooksTransition::all()
.into_iter()
.filter(|t| self.can_transition_with_role(*t, role))
.collect()
}
pub fn transition_to_state(&mut self, target: MailNotificationHooksState) -> Result<MailNotificationHooksState, StateMachineError> {
let valid = MailNotificationHooksTransition::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 MailNotificationHooksStateMachine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_state() {
let sm = MailNotificationHooksStateMachine::new();
assert_eq!(sm.current_state(), MailNotificationHooksState::Ready);
assert!(sm.current_state().is_initial());
}
#[test]
fn test_valid_transition() {
let mut sm = MailNotificationHooksStateMachine::from_state(MailNotificationHooksState::Ready);
assert!(sm.can_transition(MailNotificationHooksTransition::DispatchStarted));
let result = sm.transition(MailNotificationHooksTransition::DispatchStarted);
assert!(result.is_ok());
assert_eq!(sm.current_state(), MailNotificationHooksState::Process);
}
#[test]
fn test_invalid_transition() {
let mut sm = MailNotificationHooksStateMachine::from_state(MailNotificationHooksState::Ready);
let result = sm.transition(MailNotificationHooksTransition::SmtpAccepted);
assert!(result.is_err());
}
#[test]
fn test_state_parsing() {
let state: MailNotificationHooksState = "ready".parse().unwrap();
assert_eq!(state, MailNotificationHooksState::Ready);
}
#[test]
fn test_available_transitions() {
let sm = MailNotificationHooksStateMachine::new();
let available = sm.available_transitions();
assert!(!available.is_empty() || sm.current_state().is_final());
}
}