use std::fmt;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum A2AError {
#[error("validation error: {0}")]
Validation(String),
#[error("invalid state transition from `{from}` to `{to}`: allowed transitions are [{allowed}]")]
InvalidTransition {
from: String,
to: String,
allowed: String,
},
#[error("{entity} not found")]
NotFound {
entity: &'static str,
},
#[error("rounding drift: total distributed {distributed} does not equal input total {expected}")]
RoundingDrift {
distributed: rust_decimal::Decimal,
expected: rust_decimal::Decimal,
},
#[error("SSRF blocked: {0}")]
SsrfBlocked(String),
#[error("HMAC verification failed")]
HmacVerificationFailed,
#[error("invalid billing interval: `{0}`. Must be one of: weekly, biweekly, monthly, quarterly, annual")]
InvalidBillingInterval(String),
#[error("release conditions not met: {unmet_descriptions:?}")]
ConditionsNotMet {
unmet_descriptions: Vec<String>,
},
#[error("recipient percentages must sum to 100, got {actual}")]
PercentageSumMismatch {
actual: rust_decimal::Decimal,
},
#[error("fixed recipient amounts must sum to {expected}, got {actual}")]
FixedSumMismatch {
expected: rust_decimal::Decimal,
actual: rust_decimal::Decimal,
},
#[error("{0}")]
Internal(String),
}
pub type A2AResult<T> = Result<T, A2AError>;
impl A2AError {
pub fn validation(msg: impl fmt::Display) -> Self {
Self::Validation(msg.to_string())
}
pub fn invalid_transition(
from: impl fmt::Display,
to: impl fmt::Display,
allowed: &[&str],
) -> Self {
Self::InvalidTransition {
from: from.to_string(),
to: to.to_string(),
allowed: allowed.join(", "),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validation_error_display() {
let err = A2AError::validation("senderAddress is required");
assert_eq!(err.to_string(), "validation error: senderAddress is required");
}
#[test]
fn invalid_transition_display() {
let err = A2AError::invalid_transition("pending", "released", &["processing"]);
assert!(err.to_string().contains("pending"));
assert!(err.to_string().contains("released"));
assert!(err.to_string().contains("processing"));
}
#[test]
fn not_found_display() {
let err = A2AError::NotFound { entity: "subscription" };
assert_eq!(err.to_string(), "subscription not found");
}
#[test]
fn ssrf_blocked_display() {
let err = A2AError::SsrfBlocked("localhost".into());
assert!(err.to_string().contains("localhost"));
}
#[test]
fn percentage_sum_mismatch_display() {
use rust_decimal_macros::dec;
let err = A2AError::PercentageSumMismatch { actual: dec!(95) };
assert!(err.to_string().contains("95"));
}
}