use std::collections::VecDeque;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthState {
Unauthenticated,
Authenticating,
Authenticated,
TokenExpired,
Refreshing,
Failed,
}
impl AuthState {
#[must_use]
pub fn can_make_request(self) -> bool {
matches!(self, AuthState::Authenticated)
}
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(self, AuthState::Failed)
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Unauthenticated => "Unauthenticated",
Self::Authenticating => "Authenticating",
Self::Authenticated => "Authenticated",
Self::TokenExpired => "TokenExpired",
Self::Refreshing => "Refreshing",
Self::Failed => "Failed",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthEvent {
AuthenticateStarted,
AuthenticateCompleted,
TokenExpired,
RefreshStarted,
RefreshCompleted,
RefreshFailed,
Logout,
}
#[derive(Debug)]
pub struct QueuedRequest {
pub id: u64,
}
pub struct AuthStateMachine {
current: AuthState,
queue: VecDeque<QueuedRequest>,
next_request_id: u64,
}
impl Default for AuthStateMachine {
fn default() -> Self {
Self::new()
}
}
impl AuthStateMachine {
#[must_use]
pub fn new() -> Self {
Self {
current: AuthState::Unauthenticated,
queue: VecDeque::new(),
next_request_id: 0,
}
}
#[must_use]
pub fn current(&self) -> AuthState {
self.current
}
pub fn transition_to(&mut self, event: AuthEvent) -> Result<(), AuthStateError> {
let current = self.current;
let next = match (current, event) {
(AuthState::Unauthenticated | AuthState::Failed, AuthEvent::AuthenticateStarted) => {
AuthState::Authenticating
}
(AuthState::Unauthenticated | AuthState::TokenExpired, AuthEvent::RefreshStarted) => {
AuthState::Refreshing
}
(AuthState::Authenticating, AuthEvent::AuthenticateCompleted)
| (AuthState::Refreshing, AuthEvent::RefreshCompleted) => AuthState::Authenticated,
(
AuthState::Authenticating
| AuthState::Authenticated
| AuthState::TokenExpired
| AuthState::Refreshing
| AuthState::Failed,
AuthEvent::Logout,
) => AuthState::Unauthenticated,
(AuthState::Authenticating | AuthState::Refreshing, AuthEvent::RefreshFailed) => {
AuthState::Failed
}
(AuthState::Authenticated, AuthEvent::TokenExpired) => AuthState::TokenExpired,
(from, event) => {
return Err(AuthStateError::InvalidTransition {
from,
event: format!("{event:?}"),
});
}
};
self.current = next;
Ok(())
}
pub fn handle_event(&mut self, event: AuthEvent) -> Result<Vec<QueuedRequest>, AuthStateError> {
let was_refreshing = self.current == AuthState::Refreshing;
self.transition_to(event)?;
if was_refreshing && self.current == AuthState::Authenticated {
let drained = self.queue.drain(..).collect::<Vec<_>>();
return Ok(drained);
}
Ok(Vec::new())
}
pub fn enqueue_request(&mut self) -> u64 {
let id = self.next_request_id;
self.next_request_id += 1;
self.queue.push_back(QueuedRequest { id });
id
}
#[must_use]
pub fn queue_len(&self) -> usize {
self.queue.len()
}
#[must_use]
pub fn is_refreshing(&self) -> bool {
self.current == AuthState::Refreshing
}
pub fn reset(&mut self) {
self.current = AuthState::Unauthenticated;
self.queue.clear();
}
}
#[derive(derive_more::From, Debug)]
pub enum AuthStateError {
#[from(ignore)]
InvalidTransition { from: AuthState, event: String },
}
impl core::fmt::Display for AuthStateError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
AuthStateError::InvalidTransition { from, event } => {
write!(f, "Invalid transition from {event:?} in state {from:?}")
}
}
}
}
impl std::error::Error for AuthStateError {}