use std::fmt;
use serde::{Deserialize, Serialize};
use crate::ids::{IdParseError, LeaseId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
#[allow(clippy::upper_case_acronyms)]
pub enum ErrorCode {
#[serde(rename = "OK")]
Ok,
#[serde(rename = "CANCELLED")]
Cancelled,
#[serde(rename = "UNKNOWN")]
Unknown,
#[serde(rename = "INVALID_ARGUMENT")]
InvalidArgument,
#[serde(rename = "DEADLINE_EXCEEDED")]
DeadlineExceeded,
#[serde(rename = "NOT_FOUND")]
NotFound,
#[serde(rename = "ALREADY_EXISTS")]
AlreadyExists,
#[serde(rename = "PERMISSION_DENIED")]
PermissionDenied,
#[serde(rename = "RESOURCE_EXHAUSTED", alias = "RATE_LIMITED")]
ResourceExhausted,
#[serde(rename = "FAILED_PRECONDITION")]
FailedPrecondition,
#[serde(rename = "ABORTED")]
Aborted,
#[serde(rename = "OUT_OF_RANGE")]
OutOfRange,
#[serde(rename = "UNIMPLEMENTED")]
Unimplemented,
#[serde(rename = "INTERNAL")]
Internal,
#[serde(rename = "UNAVAILABLE")]
Unavailable,
#[serde(rename = "DATA_LOSS")]
DataLoss,
#[serde(rename = "UNAUTHENTICATED")]
Unauthenticated,
#[serde(rename = "HEARTBEAT_LOST")]
HeartbeatLost,
#[serde(rename = "LEASE_EXPIRED")]
LeaseExpired,
#[serde(rename = "LEASE_REVOKED")]
LeaseRevoked,
#[serde(rename = "BACKPRESSURE_OVERFLOW")]
BackpressureOverflow,
#[serde(rename = "BUDGET_EXHAUSTED")]
BudgetExhausted,
#[serde(rename = "LEASE_SUBSET_VIOLATION")]
LeaseSubsetViolation,
#[serde(rename = "AGENT_VERSION_NOT_AVAILABLE")]
AgentVersionNotAvailable,
}
impl ErrorCode {
#[must_use]
pub const fn retryable(self) -> bool {
matches!(
self,
Self::ResourceExhausted
| Self::Unavailable
| Self::DeadlineExceeded
| Self::Internal
| Self::Aborted
)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ok => "OK",
Self::Cancelled => "CANCELLED",
Self::Unknown => "UNKNOWN",
Self::InvalidArgument => "INVALID_ARGUMENT",
Self::DeadlineExceeded => "DEADLINE_EXCEEDED",
Self::NotFound => "NOT_FOUND",
Self::AlreadyExists => "ALREADY_EXISTS",
Self::PermissionDenied => "PERMISSION_DENIED",
Self::ResourceExhausted => "RESOURCE_EXHAUSTED",
Self::FailedPrecondition => "FAILED_PRECONDITION",
Self::Aborted => "ABORTED",
Self::OutOfRange => "OUT_OF_RANGE",
Self::Unimplemented => "UNIMPLEMENTED",
Self::Internal => "INTERNAL",
Self::Unavailable => "UNAVAILABLE",
Self::DataLoss => "DATA_LOSS",
Self::Unauthenticated => "UNAUTHENTICATED",
Self::HeartbeatLost => "HEARTBEAT_LOST",
Self::LeaseExpired => "LEASE_EXPIRED",
Self::LeaseRevoked => "LEASE_REVOKED",
Self::BackpressureOverflow => "BACKPRESSURE_OVERFLOW",
Self::BudgetExhausted => "BUDGET_EXHAUSTED",
Self::LeaseSubsetViolation => "LEASE_SUBSET_VIOLATION",
Self::AgentVersionNotAvailable => "AGENT_VERSION_NOT_AVAILABLE",
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(clippy::upper_case_acronyms)]
pub enum ARCPError {
#[error("operation cancelled: {reason}")]
Cancelled {
reason: String,
},
#[error("invalid argument: {detail}")]
InvalidArgument {
detail: String,
},
#[error("operation timed out: {detail}")]
DeadlineExceeded {
detail: String,
},
#[error("not found: {kind} (id={id})")]
NotFound {
kind: &'static str,
id: String,
},
#[error("already exists: {kind} (id={id})")]
AlreadyExists {
kind: &'static str,
id: String,
},
#[error("permission denied: {detail}")]
PermissionDenied {
detail: String,
},
#[error("resource exhausted: {detail}")]
ResourceExhausted {
detail: String,
retry_after_seconds: Option<u64>,
},
#[error("failed precondition: {detail}")]
FailedPrecondition {
detail: String,
},
#[error("operation aborted: {detail}")]
Aborted {
detail: String,
},
#[error("argument out of range: {detail}")]
OutOfRange {
detail: String,
},
#[error("not implemented (RFC §{section}): {detail}")]
Unimplemented {
section: &'static str,
detail: String,
},
#[error("internal error: {detail}")]
Internal {
detail: String,
},
#[error("service unavailable: {detail}")]
Unavailable {
detail: String,
},
#[error("data loss: {detail}")]
DataLoss {
detail: String,
},
#[error("unauthenticated: {detail}")]
Unauthenticated {
detail: String,
},
#[error("heartbeat lost: missed_count={missed_count}")]
HeartbeatLost {
missed_count: u32,
},
#[error("lease expired: lease_id={lease_id}")]
LeaseExpired {
lease_id: LeaseId,
},
#[error("lease revoked: lease_id={lease_id} (reason={reason})")]
LeaseRevoked {
lease_id: LeaseId,
reason: String,
},
#[error("backpressure overflow: {detail}")]
BackpressureOverflow {
detail: String,
},
#[error("budget exhausted: {detail}")]
BudgetExhausted {
detail: String,
},
#[error("lease subset violation: {detail}")]
LeaseSubsetViolation {
detail: String,
},
#[error("agent version not available: {agent}@{version}")]
AgentVersionNotAvailable {
agent: String,
version: String,
},
#[error("unknown error: {detail}")]
Unknown {
detail: String,
},
#[error("serialisation error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("storage error: {detail}")]
Storage {
detail: String,
},
#[error("id parse error: {0}")]
Id(#[from] IdParseError),
}
impl ARCPError {
#[must_use]
pub const fn code(&self) -> ErrorCode {
match self {
Self::Cancelled { .. } => ErrorCode::Cancelled,
Self::InvalidArgument { .. } | Self::Id(_) => ErrorCode::InvalidArgument,
Self::DeadlineExceeded { .. } => ErrorCode::DeadlineExceeded,
Self::NotFound { .. } => ErrorCode::NotFound,
Self::AlreadyExists { .. } => ErrorCode::AlreadyExists,
Self::PermissionDenied { .. } => ErrorCode::PermissionDenied,
Self::ResourceExhausted { .. } => ErrorCode::ResourceExhausted,
Self::FailedPrecondition { .. } => ErrorCode::FailedPrecondition,
Self::Aborted { .. } => ErrorCode::Aborted,
Self::OutOfRange { .. } => ErrorCode::OutOfRange,
Self::Unimplemented { .. } => ErrorCode::Unimplemented,
Self::Internal { .. } | Self::Storage { .. } => ErrorCode::Internal,
Self::Unavailable { .. } => ErrorCode::Unavailable,
Self::DataLoss { .. } => ErrorCode::DataLoss,
Self::Unauthenticated { .. } => ErrorCode::Unauthenticated,
Self::HeartbeatLost { .. } => ErrorCode::HeartbeatLost,
Self::LeaseExpired { .. } => ErrorCode::LeaseExpired,
Self::LeaseRevoked { .. } => ErrorCode::LeaseRevoked,
Self::BackpressureOverflow { .. } => ErrorCode::BackpressureOverflow,
Self::BudgetExhausted { .. } => ErrorCode::BudgetExhausted,
Self::LeaseSubsetViolation { .. } => ErrorCode::LeaseSubsetViolation,
Self::AgentVersionNotAvailable { .. } => ErrorCode::AgentVersionNotAvailable,
Self::Unknown { .. } | Self::Serialization(_) => ErrorCode::Unknown,
}
}
#[must_use]
pub const fn retryable(&self) -> bool {
self.code().retryable()
}
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
clippy::missing_panics_doc
)]
mod tests {
use super::*;
#[test]
fn error_code_round_trips_through_serde() {
for code in [
ErrorCode::Ok,
ErrorCode::Cancelled,
ErrorCode::InvalidArgument,
ErrorCode::DeadlineExceeded,
ErrorCode::NotFound,
ErrorCode::AlreadyExists,
ErrorCode::PermissionDenied,
ErrorCode::ResourceExhausted,
ErrorCode::FailedPrecondition,
ErrorCode::Aborted,
ErrorCode::OutOfRange,
ErrorCode::Unimplemented,
ErrorCode::Internal,
ErrorCode::Unavailable,
ErrorCode::DataLoss,
ErrorCode::Unauthenticated,
ErrorCode::HeartbeatLost,
ErrorCode::LeaseExpired,
ErrorCode::LeaseRevoked,
ErrorCode::BackpressureOverflow,
ErrorCode::BudgetExhausted,
ErrorCode::LeaseSubsetViolation,
ErrorCode::AgentVersionNotAvailable,
ErrorCode::Unknown,
] {
let s = serde_json::to_string(&code).expect("serialize");
let back: ErrorCode = serde_json::from_str(&s).expect("deserialize");
assert_eq!(code, back, "round-trip for {code}");
assert_eq!(s.trim_matches('"'), code.as_str());
}
}
#[test]
fn rate_limited_alias_decodes_to_resource_exhausted() {
let code: ErrorCode = serde_json::from_str("\"RATE_LIMITED\"").expect("alias");
assert_eq!(code, ErrorCode::ResourceExhausted);
}
#[test]
fn retryability_matches_rfc_18_3() {
for c in [
ErrorCode::ResourceExhausted,
ErrorCode::Unavailable,
ErrorCode::DeadlineExceeded,
ErrorCode::Internal,
ErrorCode::Aborted,
] {
assert!(c.retryable(), "{c} should be retryable");
}
for c in [
ErrorCode::InvalidArgument,
ErrorCode::NotFound,
ErrorCode::AlreadyExists,
ErrorCode::PermissionDenied,
ErrorCode::FailedPrecondition,
ErrorCode::Unimplemented,
ErrorCode::Unauthenticated,
ErrorCode::DataLoss,
ErrorCode::LeaseSubsetViolation,
] {
assert!(!c.retryable(), "{c} should NOT be retryable");
}
}
#[test]
fn arcp_error_maps_to_canonical_code() {
let err = ARCPError::PermissionDenied {
detail: "missing lease".into(),
};
assert_eq!(err.code(), ErrorCode::PermissionDenied);
assert!(!err.retryable());
}
#[test]
fn id_parse_error_propagates_via_from() {
let parse_err: IdParseError = "junk".parse::<crate::ids::SessionId>().unwrap_err();
let err: ARCPError = parse_err.into();
assert_eq!(err.code(), ErrorCode::InvalidArgument);
}
#[test]
fn v1_1_error_codes_serialize_to_wire_strings() {
assert_eq!(ErrorCode::BudgetExhausted.as_str(), "BUDGET_EXHAUSTED");
assert_eq!(ErrorCode::LeaseExpired.as_str(), "LEASE_EXPIRED");
assert_eq!(
ErrorCode::LeaseSubsetViolation.as_str(),
"LEASE_SUBSET_VIOLATION"
);
assert_eq!(
ErrorCode::AgentVersionNotAvailable.as_str(),
"AGENT_VERSION_NOT_AVAILABLE"
);
assert_eq!(
serde_json::to_string(&ErrorCode::BudgetExhausted).expect("serialize"),
"\"BUDGET_EXHAUSTED\""
);
assert_eq!(
serde_json::to_string(&ErrorCode::AgentVersionNotAvailable).expect("serialize"),
"\"AGENT_VERSION_NOT_AVAILABLE\""
);
let budget: ErrorCode =
serde_json::from_str("\"BUDGET_EXHAUSTED\"").expect("deserialize budget");
assert_eq!(budget, ErrorCode::BudgetExhausted);
let subset: ErrorCode =
serde_json::from_str("\"LEASE_SUBSET_VIOLATION\"").expect("deserialize subset");
assert_eq!(subset, ErrorCode::LeaseSubsetViolation);
let agent_ver: ErrorCode = serde_json::from_str("\"AGENT_VERSION_NOT_AVAILABLE\"")
.expect("deserialize agent version");
assert_eq!(agent_ver, ErrorCode::AgentVersionNotAvailable);
}
#[test]
fn v1_1_arcp_errors_map_to_canonical_codes() {
let budget = ARCPError::BudgetExhausted {
detail: "cost.budget USD counter <= 0".into(),
};
assert_eq!(budget.code(), ErrorCode::BudgetExhausted);
assert!(!budget.retryable());
let subset = ARCPError::LeaseSubsetViolation {
detail: "model.use widened".into(),
};
assert_eq!(subset.code(), ErrorCode::LeaseSubsetViolation);
assert!(!subset.retryable());
let agent_ver = ARCPError::AgentVersionNotAvailable {
agent: "summarizer".into(),
version: "2.3.0".into(),
};
assert_eq!(agent_ver.code(), ErrorCode::AgentVersionNotAvailable);
assert!(!agent_ver.retryable());
}
#[test]
fn serde_error_propagates_via_from() {
let parse: Result<serde_json::Value, _> = serde_json::from_str("not-json");
let err: ARCPError = parse.unwrap_err().into();
assert_eq!(err.code(), ErrorCode::Unknown);
}
}