#[derive(Debug)]
pub struct KiroUsageInfo {
pub limit: i64,
pub used: i64,
pub reset_in_seconds: Option<i64>,
}
impl KiroUsageInfo {
#[must_use]
pub fn is_limited(&self) -> bool {
self.used >= self.limit
}
#[must_use]
#[expect(clippy::cast_precision_loss)]
pub fn utilization(&self) -> f64 {
if self.limit > 0 {
self.used as f64 / self.limit as f64 * 100.0
} else {
100.0
}
}
pub fn parse(output: &str) -> Result<Self, Box<dyn std::error::Error>> {
let output = output.trim();
if output.is_empty() {
return Err("empty kiro output".into());
}
let mut limit: Option<i64> = None;
let mut used: Option<i64> = None;
let mut reset_in_seconds: Option<i64> = None;
for line in output.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("Usage:") {
let rest = rest.trim();
let rest = rest.strip_suffix("requests").unwrap_or(rest).trim();
let parts: Vec<&str> = rest.split('/').collect();
if parts.len() != 2 {
return Err(format!("malformed usage line: {line}").into());
}
used = Some(parts[0].parse::<i64>()?);
limit = Some(parts[1].parse::<i64>()?);
} else if let Some(rest) = line.strip_prefix("Resets in:") {
let rest = rest.trim();
let rest = rest.strip_suffix("seconds").unwrap_or(rest).trim();
reset_in_seconds = Some(rest.parse::<i64>()?);
}
}
let limit = limit.ok_or("missing usage line in kiro output")?;
let used = used.ok_or("missing usage line in kiro output")?;
Ok(Self {
limit,
used,
reset_in_seconds,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn test_is_not_limited_when_below_limit() {
let info = KiroUsageInfo {
limit: 100,
used: 42,
reset_in_seconds: Some(3600),
};
assert!(!info.is_limited());
}
#[test]
fn test_is_limited_when_used_equals_limit() {
let info = KiroUsageInfo {
limit: 100,
used: 100,
reset_in_seconds: None,
};
assert!(info.is_limited());
}
#[test]
fn test_is_limited_when_used_exceeds_limit() {
let info = KiroUsageInfo {
limit: 50,
used: 60,
reset_in_seconds: None,
};
assert!(info.is_limited());
}
#[test]
fn test_utilization_computed_correctly() {
let info = KiroUsageInfo {
limit: 200,
used: 50,
reset_in_seconds: None,
};
assert!((info.utilization() - 25.0).abs() < f64::EPSILON);
}
#[test]
fn test_utilization_returns_100_when_zero_limit() {
let info = KiroUsageInfo {
limit: 0,
used: 0,
reset_in_seconds: None,
};
assert!((info.utilization() - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_parse_full_output() -> TestResult {
let output = "Usage: 42/100 requests\nResets in: 3600 seconds\n";
let info = KiroUsageInfo::parse(output)?;
assert_eq!(info.limit, 100);
assert_eq!(info.used, 42);
assert_eq!(info.reset_in_seconds, Some(3600));
Ok(())
}
#[test]
fn test_parse_output_without_reset() -> TestResult {
let output = "Usage: 10/50 requests\n";
let info = KiroUsageInfo::parse(output)?;
assert_eq!(info.limit, 50);
assert_eq!(info.used, 10);
assert!(info.reset_in_seconds.is_none());
Ok(())
}
#[test]
fn test_parse_output_at_limit() -> TestResult {
let output = "Usage: 100/100 requests\nResets in: 0 seconds\n";
let info = KiroUsageInfo::parse(output)?;
assert!(info.is_limited());
Ok(())
}
#[test]
fn test_parse_rejects_malformed_output() {
let output = "this is not valid kiro output";
let result = KiroUsageInfo::parse(output);
assert!(result.is_err());
}
#[test]
fn test_parse_rejects_empty_output() {
let result = KiroUsageInfo::parse("");
assert!(result.is_err());
}
}