use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RateLimitStatus {
Allowed,
AllowedWarning,
Rejected,
Unknown(String),
}
impl RateLimitStatus {
pub fn as_str(&self) -> &str {
match self {
Self::Allowed => "allowed",
Self::AllowedWarning => "allowed_warning",
Self::Rejected => "rejected",
Self::Unknown(s) => s.as_str(),
}
}
}
impl fmt::Display for RateLimitStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl From<&str> for RateLimitStatus {
fn from(s: &str) -> Self {
match s {
"allowed" => Self::Allowed,
"allowed_warning" => Self::AllowedWarning,
"rejected" => Self::Rejected,
other => Self::Unknown(other.to_string()),
}
}
}
impl Serialize for RateLimitStatus {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for RateLimitStatus {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Ok(Self::from(s.as_str()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RateLimitWindow {
FiveHour,
Hourly,
SevenDay,
Unknown(String),
}
impl RateLimitWindow {
pub fn as_str(&self) -> &str {
match self {
Self::FiveHour => "five_hour",
Self::Hourly => "hourly",
Self::SevenDay => "seven_day",
Self::Unknown(s) => s.as_str(),
}
}
}
impl fmt::Display for RateLimitWindow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl From<&str> for RateLimitWindow {
fn from(s: &str) -> Self {
match s {
"five_hour" => Self::FiveHour,
"hourly" => Self::Hourly,
"seven_day" => Self::SevenDay,
other => Self::Unknown(other.to_string()),
}
}
}
impl Serialize for RateLimitWindow {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for RateLimitWindow {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Ok(Self::from(s.as_str()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OverageStatus {
Allowed,
Rejected,
Unknown(String),
}
impl OverageStatus {
pub fn as_str(&self) -> &str {
match self {
Self::Allowed => "allowed",
Self::Rejected => "rejected",
Self::Unknown(s) => s.as_str(),
}
}
}
impl fmt::Display for OverageStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl From<&str> for OverageStatus {
fn from(s: &str) -> Self {
match s {
"allowed" => Self::Allowed,
"rejected" => Self::Rejected,
other => Self::Unknown(other.to_string()),
}
}
}
impl Serialize for OverageStatus {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for OverageStatus {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Ok(Self::from(s.as_str()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OverageDisabledReason {
OrgLevelDisabled,
OutOfCredits,
Unknown(String),
}
impl OverageDisabledReason {
pub fn as_str(&self) -> &str {
match self {
Self::OrgLevelDisabled => "org_level_disabled",
Self::OutOfCredits => "out_of_credits",
Self::Unknown(s) => s.as_str(),
}
}
}
impl fmt::Display for OverageDisabledReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl From<&str> for OverageDisabledReason {
fn from(s: &str) -> Self {
match s {
"org_level_disabled" => Self::OrgLevelDisabled,
"out_of_credits" => Self::OutOfCredits,
other => Self::Unknown(other.to_string()),
}
}
}
impl Serialize for OverageDisabledReason {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for OverageDisabledReason {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Ok(Self::from(s.as_str()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitEvent {
pub rate_limit_info: RateLimitInfo,
pub session_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitInfo {
pub status: RateLimitStatus,
#[serde(rename = "resetsAt", skip_serializing_if = "Option::is_none")]
pub resets_at: Option<u64>,
#[serde(rename = "rateLimitType", skip_serializing_if = "Option::is_none")]
pub rate_limit_type: Option<RateLimitWindow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub utilization: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", rename = "overageStatus")]
pub overage_status: Option<OverageStatus>,
#[serde(rename = "overageDisabledReason")]
pub overage_disabled_reason: Option<OverageDisabledReason>,
#[serde(rename = "isUsingOverage")]
pub is_using_overage: bool,
}
#[cfg(test)]
mod tests {
use super::{OverageDisabledReason, OverageStatus, RateLimitStatus, RateLimitWindow};
use crate::io::ClaudeOutput;
#[test]
fn test_deserialize_rate_limit_event() {
let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1771390800,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false},"uuid":"76258cfb-0dc8-4d4b-8682-77082b59c03f","session_id":"1ae0af5b-89fa-4075-8156-d5d3702f6505"}"#;
let output: ClaudeOutput = serde_json::from_str(json).unwrap();
assert!(output.is_rate_limit_event());
assert_eq!(output.message_type(), "rate_limit_event");
assert_eq!(
output.session_id(),
Some("1ae0af5b-89fa-4075-8156-d5d3702f6505")
);
let evt = output.as_rate_limit_event().unwrap();
assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
assert_eq!(evt.rate_limit_info.resets_at, Some(1771390800));
assert_eq!(
evt.rate_limit_info.rate_limit_type,
Some(RateLimitWindow::FiveHour)
);
assert_eq!(evt.rate_limit_info.utilization, None);
assert_eq!(
evt.rate_limit_info.overage_status,
Some(OverageStatus::Rejected)
);
assert_eq!(
evt.rate_limit_info.overage_disabled_reason,
Some(OverageDisabledReason::OrgLevelDisabled)
);
assert!(!evt.rate_limit_info.is_using_overage);
assert_eq!(
evt.uuid,
Some("76258cfb-0dc8-4d4b-8682-77082b59c03f".to_string())
);
}
#[test]
fn test_deserialize_rate_limit_event_minimal() {
let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":0,"rateLimitType":"hourly","overageStatus":"allowed","isUsingOverage":true},"session_id":"abc"}"#;
let output: ClaudeOutput = serde_json::from_str(json).unwrap();
let evt = output.as_rate_limit_event().unwrap();
assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
assert!(evt.rate_limit_info.is_using_overage);
assert!(evt.uuid.is_none());
}
#[test]
fn test_deserialize_rate_limit_event_allowed_warning() {
let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","resetsAt":1700000000,"rateLimitType":"five_hour","utilization":0.85,"isUsingOverage":false},"uuid":"550e8400-e29b-41d4-a716-446655440000","session_id":"test-session-id"}"#;
let output: ClaudeOutput = serde_json::from_str(json).unwrap();
let evt = output.as_rate_limit_event().unwrap();
assert_eq!(evt.rate_limit_info.status, RateLimitStatus::AllowedWarning);
assert_eq!(evt.rate_limit_info.utilization, Some(0.85));
assert_eq!(evt.rate_limit_info.overage_status, None);
assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
assert!(!evt.rate_limit_info.is_using_overage);
}
#[test]
fn test_deserialize_rate_limit_event_no_resets_at() {
let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","isUsingOverage":false},"uuid":"4269273d-3b5e-40ae-9765-cb3c12284c44","session_id":"f9626cf7-4d88-4844-9bb1-cab96909fc7b"}"#;
let output: ClaudeOutput = serde_json::from_str(json).unwrap();
let evt = output.as_rate_limit_event().unwrap();
assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
assert_eq!(evt.rate_limit_info.resets_at, None);
assert_eq!(evt.rate_limit_info.rate_limit_type, None);
assert!(!evt.rate_limit_info.is_using_overage);
}
#[test]
fn test_deserialize_rate_limit_event_rejected() {
let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"rejected","resetsAt":1700003600,"rateLimitType":"seven_day","isUsingOverage":false,"overageStatus":"rejected","overageDisabledReason":"out_of_credits"},"uuid":"660e8400-e29b-41d4-a716-446655440001","session_id":"test-session-id"}"#;
let output: ClaudeOutput = serde_json::from_str(json).unwrap();
let evt = output.as_rate_limit_event().unwrap();
assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Rejected);
assert_eq!(
evt.rate_limit_info.rate_limit_type,
Some(RateLimitWindow::SevenDay)
);
assert_eq!(
evt.rate_limit_info.overage_status,
Some(OverageStatus::Rejected)
);
assert_eq!(
evt.rate_limit_info.overage_disabled_reason,
Some(OverageDisabledReason::OutOfCredits)
);
}
}