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 DisputeStatus {
Filed,
EvidencePeriod,
UnderReview,
Resolved,
Escalated,
}
impl std::fmt::Display for DisputeStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Filed => write!(f, "filed"),
Self::EvidencePeriod => write!(f, "evidence_period"),
Self::UnderReview => write!(f, "under_review"),
Self::Resolved => write!(f, "resolved"),
Self::Escalated => write!(f, "escalated"),
}
}
}
impl DisputeStatus {
#[must_use]
pub const fn allowed_transitions(self) -> &'static [Self] {
match self {
Self::Filed => &[Self::EvidencePeriod],
Self::EvidencePeriod => &[Self::UnderReview],
Self::UnderReview => &[Self::Resolved, Self::Escalated],
Self::Resolved | Self::Escalated => &[],
}
}
#[must_use]
pub fn can_transition_to(self, target: Self) -> bool {
self.allowed_transitions().contains(&target)
}
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Resolved | Self::Escalated)
}
#[must_use]
pub const fn allows_evidence(self) -> bool {
matches!(self, Self::Filed | Self::EvidencePeriod)
}
#[must_use]
pub const fn allows_resolution(self) -> bool {
matches!(self, Self::Filed | Self::EvidencePeriod | Self::UnderReview)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DisputeTransition {
pub from: DisputeStatus,
pub to: DisputeStatus,
}
impl DisputeTransition {
pub fn new(from: DisputeStatus, to: DisputeStatus) -> 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))
}
}
}
impl DisputeStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Filed => "filed",
Self::EvidencePeriod => "evidence_period",
Self::UnderReview => "under_review",
Self::Resolved => "resolved",
Self::Escalated => "escalated",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filed_can_go_to_evidence_period() {
assert!(DisputeStatus::Filed.can_transition_to(DisputeStatus::EvidencePeriod));
}
#[test]
fn filed_cannot_go_to_under_review() {
assert!(!DisputeStatus::Filed.can_transition_to(DisputeStatus::UnderReview));
}
#[test]
fn filed_cannot_go_to_resolved() {
assert!(!DisputeStatus::Filed.can_transition_to(DisputeStatus::Resolved));
}
#[test]
fn evidence_period_can_go_to_under_review() {
assert!(DisputeStatus::EvidencePeriod.can_transition_to(DisputeStatus::UnderReview));
}
#[test]
fn evidence_period_cannot_go_to_resolved() {
assert!(!DisputeStatus::EvidencePeriod.can_transition_to(DisputeStatus::Resolved));
}
#[test]
fn under_review_can_go_to_resolved() {
assert!(DisputeStatus::UnderReview.can_transition_to(DisputeStatus::Resolved));
}
#[test]
fn under_review_can_go_to_escalated() {
assert!(DisputeStatus::UnderReview.can_transition_to(DisputeStatus::Escalated));
}
#[test]
fn under_review_cannot_go_back_to_filed() {
assert!(!DisputeStatus::UnderReview.can_transition_to(DisputeStatus::Filed));
}
#[test]
fn resolved_is_terminal() {
assert!(DisputeStatus::Resolved.is_terminal());
assert!(DisputeStatus::Resolved.allowed_transitions().is_empty());
}
#[test]
fn escalated_is_terminal() {
assert!(DisputeStatus::Escalated.is_terminal());
assert!(DisputeStatus::Escalated.allowed_transitions().is_empty());
}
#[test]
fn filed_is_not_terminal() {
assert!(!DisputeStatus::Filed.is_terminal());
}
#[test]
fn evidence_period_is_not_terminal() {
assert!(!DisputeStatus::EvidencePeriod.is_terminal());
}
#[test]
fn filed_allows_evidence() {
assert!(DisputeStatus::Filed.allows_evidence());
}
#[test]
fn evidence_period_allows_evidence() {
assert!(DisputeStatus::EvidencePeriod.allows_evidence());
}
#[test]
fn under_review_does_not_allow_evidence() {
assert!(!DisputeStatus::UnderReview.allows_evidence());
}
#[test]
fn resolved_does_not_allow_evidence() {
assert!(!DisputeStatus::Resolved.allows_evidence());
}
#[test]
fn filed_allows_resolution() {
assert!(DisputeStatus::Filed.allows_resolution());
}
#[test]
fn under_review_allows_resolution() {
assert!(DisputeStatus::UnderReview.allows_resolution());
}
#[test]
fn resolved_does_not_allow_resolution() {
assert!(!DisputeStatus::Resolved.allows_resolution());
}
#[test]
fn escalated_does_not_allow_resolution() {
assert!(!DisputeStatus::Escalated.allows_resolution());
}
#[test]
fn status_display() {
assert_eq!(DisputeStatus::Filed.to_string(), "filed");
assert_eq!(DisputeStatus::EvidencePeriod.to_string(), "evidence_period");
assert_eq!(DisputeStatus::UnderReview.to_string(), "under_review");
assert_eq!(DisputeStatus::Resolved.to_string(), "resolved");
assert_eq!(DisputeStatus::Escalated.to_string(), "escalated");
}
#[test]
fn transition_valid() {
let t =
DisputeTransition::new(DisputeStatus::Filed, DisputeStatus::EvidencePeriod).unwrap();
assert_eq!(t.from, DisputeStatus::Filed);
assert_eq!(t.to, DisputeStatus::EvidencePeriod);
}
#[test]
fn transition_invalid() {
let err =
DisputeTransition::new(DisputeStatus::Filed, DisputeStatus::Resolved).unwrap_err();
assert!(matches!(err, A2AError::InvalidTransition { .. }));
}
#[test]
fn transition_from_terminal_fails() {
let err =
DisputeTransition::new(DisputeStatus::Resolved, DisputeStatus::Filed).unwrap_err();
assert!(matches!(err, A2AError::InvalidTransition { .. }));
}
#[test]
fn full_lifecycle_transition() {
let t1 =
DisputeTransition::new(DisputeStatus::Filed, DisputeStatus::EvidencePeriod).unwrap();
let t2 = DisputeTransition::new(t1.to, DisputeStatus::UnderReview).unwrap();
let t3 = DisputeTransition::new(t2.to, DisputeStatus::Resolved).unwrap();
assert_eq!(t3.to, DisputeStatus::Resolved);
}
}