use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
RateLimited,
Upstream,
Timeout,
InvalidInput,
Internal,
Unclassified,
}
impl ErrorCode {
#[must_use]
pub fn retryable(self) -> bool {
matches!(self, Self::RateLimited | Self::Upstream | Self::Timeout)
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::RateLimited => "rate_limited",
Self::Upstream => "upstream",
Self::Timeout => "timeout",
Self::InvalidInput => "invalid_input",
Self::Internal => "internal",
Self::Unclassified => "unclassified",
}
}
}
impl std::fmt::Display for ErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolError {
pub code: ErrorCode,
pub message: String,
pub retryable: bool,
#[serde(
rename = "retryAfterMs",
default,
skip_serializing_if = "Option::is_none"
)]
pub retry_after_ms: Option<u64>,
}
impl ToolError {
#[must_use]
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
retryable: code.retryable(),
retry_after_ms: None,
}
}
#[must_use]
pub fn rate_limited(message: impl Into<String>) -> Self {
Self::new(ErrorCode::RateLimited, message)
}
#[must_use]
pub fn upstream(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Upstream, message)
}
#[must_use]
pub fn timeout(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Timeout, message)
}
#[must_use]
pub fn invalid_input(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidInput, message)
}
#[must_use]
pub fn internal(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Internal, message)
}
#[must_use]
pub fn unclassified(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Unclassified, message)
}
#[must_use]
pub fn retry_after_ms(mut self, ms: u64) -> Self {
self.retry_after_ms = Some(ms);
self
}
#[must_use]
pub fn retryable(mut self, retryable: bool) -> Self {
self.retryable = retryable;
self
}
#[must_use]
pub fn to_json(&self) -> Value {
serde_json::to_value(self).unwrap_or_else(|_| {
json!({
"code": self.code.as_str(),
"message": self.message,
"retryable": self.retryable,
})
})
}
}
impl From<&str> for ToolError {
fn from(message: &str) -> Self {
Self::unclassified(message)
}
}
impl From<String> for ToolError {
fn from(message: String) -> Self {
Self::unclassified(message)
}
}
impl From<&String> for ToolError {
fn from(message: &String) -> Self {
Self::unclassified(message.as_str())
}
}
impl std::fmt::Display for ToolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ToolError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retryable_defaults_follow_the_code() {
assert!(ErrorCode::RateLimited.retryable());
assert!(ErrorCode::Upstream.retryable());
assert!(ErrorCode::Timeout.retryable());
assert!(!ErrorCode::InvalidInput.retryable());
assert!(!ErrorCode::Internal.retryable());
assert!(!ErrorCode::Unclassified.retryable());
}
#[test]
fn wire_shape_omits_absent_retry_after() {
let error = ToolError::rate_limited("quota exhausted").retry_after_ms(500);
let json = error.to_json();
assert_eq!(json["code"], "rate_limited");
assert_eq!(json["message"], "quota exhausted");
assert_eq!(json["retryable"], true);
assert_eq!(json["retryAfterMs"], 500);
let bare = ToolError::unclassified("boom");
let json = bare.to_json();
assert_eq!(json["code"], "unclassified");
assert!(json.get("retryAfterMs").is_none());
}
#[test]
fn as_str_matches_serde_serialization() {
for code in [
ErrorCode::RateLimited,
ErrorCode::Upstream,
ErrorCode::Timeout,
ErrorCode::InvalidInput,
ErrorCode::Internal,
ErrorCode::Unclassified,
] {
assert_eq!(serde_json::to_value(code).unwrap(), code.as_str());
}
}
#[test]
fn string_conversions_are_unclassified() {
assert_eq!(ToolError::from("boom").code, ErrorCode::Unclassified);
assert_eq!(
ToolError::from(String::from("boom")).code,
ErrorCode::Unclassified
);
let message = String::from("a missing");
let error: ToolError = (&message).into();
assert_eq!(error.message, "a missing");
}
}