use serde::{Deserialize, Serialize};
use crate::error::{A2AError, A2AResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
impl Default for CircuitState {
fn default() -> Self {
Self::Closed
}
}
impl std::fmt::Display for CircuitState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Closed => write!(f, "closed"),
Self::Open => write!(f, "open"),
Self::HalfOpen => write!(f, "half_open"),
}
}
}
impl CircuitState {
#[must_use]
pub const fn allowed_transitions(self) -> &'static [Self] {
match self {
Self::Closed => &[Self::Open],
Self::Open => &[Self::HalfOpen],
Self::HalfOpen => &[Self::Closed, Self::Open],
}
}
#[must_use]
pub fn can_transition_to(self, target: Self) -> bool {
self.allowed_transitions().contains(&target)
}
#[must_use]
pub const fn is_blocking(self) -> bool {
matches!(self, Self::Open)
}
#[must_use]
pub const fn is_testing(self) -> bool {
matches!(self, Self::HalfOpen)
}
#[must_use]
pub const fn is_normal(self) -> bool {
matches!(self, Self::Closed)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Closed => "closed",
Self::Open => "open",
Self::HalfOpen => "half_open",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CircuitTransition {
pub from: CircuitState,
pub to: CircuitState,
}
impl CircuitTransition {
pub fn new(from: CircuitState, to: CircuitState) -> A2AResult<Self> {
if from.can_transition_to(to) {
Ok(Self { from, to })
} else {
let allowed: Vec<&str> =
from.allowed_transitions().iter().map(|s| s.as_str()).collect();
Err(A2AError::invalid_transition(from, to, &allowed))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn closed_can_go_to_open() {
assert!(CircuitState::Closed.can_transition_to(CircuitState::Open));
}
#[test]
fn closed_cannot_go_to_half_open() {
assert!(!CircuitState::Closed.can_transition_to(CircuitState::HalfOpen));
}
#[test]
fn open_can_go_to_half_open() {
assert!(CircuitState::Open.can_transition_to(CircuitState::HalfOpen));
}
#[test]
fn open_cannot_go_to_closed() {
assert!(!CircuitState::Open.can_transition_to(CircuitState::Closed));
}
#[test]
fn half_open_can_go_to_closed() {
assert!(CircuitState::HalfOpen.can_transition_to(CircuitState::Closed));
}
#[test]
fn half_open_can_go_to_open() {
assert!(CircuitState::HalfOpen.can_transition_to(CircuitState::Open));
}
#[test]
fn closed_is_normal() {
assert!(CircuitState::Closed.is_normal());
assert!(!CircuitState::Closed.is_blocking());
assert!(!CircuitState::Closed.is_testing());
}
#[test]
fn open_is_blocking() {
assert!(CircuitState::Open.is_blocking());
assert!(!CircuitState::Open.is_normal());
assert!(!CircuitState::Open.is_testing());
}
#[test]
fn half_open_is_testing() {
assert!(CircuitState::HalfOpen.is_testing());
assert!(!CircuitState::HalfOpen.is_blocking());
assert!(!CircuitState::HalfOpen.is_normal());
}
#[test]
fn default_is_closed() {
assert_eq!(CircuitState::default(), CircuitState::Closed);
}
#[test]
fn state_display() {
assert_eq!(CircuitState::Closed.to_string(), "closed");
assert_eq!(CircuitState::Open.to_string(), "open");
assert_eq!(CircuitState::HalfOpen.to_string(), "half_open");
}
#[test]
fn transition_valid() {
let t = CircuitTransition::new(CircuitState::Closed, CircuitState::Open).unwrap();
assert_eq!(t.from, CircuitState::Closed);
assert_eq!(t.to, CircuitState::Open);
}
#[test]
fn transition_invalid() {
let err = CircuitTransition::new(CircuitState::Closed, CircuitState::HalfOpen).unwrap_err();
assert!(matches!(err, A2AError::InvalidTransition { .. }));
}
#[test]
fn full_recovery_cycle() {
let t1 = CircuitTransition::new(CircuitState::Closed, CircuitState::Open).unwrap();
let t2 = CircuitTransition::new(t1.to, CircuitState::HalfOpen).unwrap();
let t3 = CircuitTransition::new(t2.to, CircuitState::Closed).unwrap();
assert_eq!(t3.to, CircuitState::Closed);
}
#[test]
fn half_open_failure_reopens() {
let t1 = CircuitTransition::new(CircuitState::HalfOpen, CircuitState::Open).unwrap();
assert_eq!(t1.to, CircuitState::Open);
}
}