use thiserror::Error;
#[derive(Error, Debug)]
pub enum RsGuardError {
#[error("GitHub API error: {status} - {message}")]
GitHubApi {
status: u16,
message: String,
},
#[error("LLM API error ({provider}): {status} - {message}")]
LlmApi {
provider: String,
status: u16,
message: String,
},
#[error("Failed to parse verdict: {0}")]
VerdictParse(
String,
),
#[error("Configuration error: {0}")]
Config(
String,
),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error(
"Diff too large: {size_bytes} bytes ({line_count} lines). Maximum is 100KB or 1500 lines."
)]
DiffTooLarge {
size_bytes: usize,
line_count: usize,
},
#[error("No diff content found")]
EmptyDiff,
#[error("Invalid diff content: response does not appear to be a diff")]
InvalidDiffContent,
#[error("Permission denied for review state {state}: {message}")]
PermissionDenied {
state: String,
message: String,
},
}
impl RsGuardError {
pub fn is_retryable(&self) -> bool {
matches!(
self,
RsGuardError::GitHubApi {
status: 0 | 429 | 502 | 503 | 504,
..
} | RsGuardError::LlmApi {
status: 0 | 429 | 502 | 503 | 504,
..
}
)
}
pub fn is_permission_denied(&self) -> bool {
match self {
RsGuardError::GitHubApi { status: 403, .. } => true,
RsGuardError::GitHubApi {
status: 422,
message,
} => message.to_lowercase().contains("not permitted"),
RsGuardError::PermissionDenied { .. } => true,
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_retryable_github_429() {
let err = RsGuardError::GitHubApi {
status: 429,
message: "rate limited".to_string(),
};
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_github_502() {
let err = RsGuardError::GitHubApi {
status: 502,
message: "bad gateway".to_string(),
};
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_github_503() {
let err = RsGuardError::GitHubApi {
status: 503,
message: "service unavailable".to_string(),
};
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_github_504() {
let err = RsGuardError::GitHubApi {
status: 504,
message: "gateway timeout".to_string(),
};
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_github_0() {
let err = RsGuardError::GitHubApi {
status: 0,
message: "connection error".to_string(),
};
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_github_404_not_retryable() {
let err = RsGuardError::GitHubApi {
status: 404,
message: "not found".to_string(),
};
assert!(!err.is_retryable());
}
#[test]
fn test_is_retryable_github_403_not_retryable() {
let err = RsGuardError::GitHubApi {
status: 403,
message: "forbidden".to_string(),
};
assert!(!err.is_retryable());
}
#[test]
fn test_is_retryable_llm_429() {
let err = RsGuardError::LlmApi {
provider: "deepseek".to_string(),
status: 429,
message: "rate limited".to_string(),
};
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_llm_0() {
let err = RsGuardError::LlmApi {
provider: "deepseek".to_string(),
status: 0,
message: "connection error".to_string(),
};
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_config_not_retryable() {
let err = RsGuardError::Config("bad config".to_string());
assert!(!err.is_retryable());
}
#[test]
fn test_is_permission_denied_403() {
let err = RsGuardError::GitHubApi {
status: 403,
message: "forbidden".to_string(),
};
assert!(err.is_permission_denied());
}
#[test]
fn test_is_permission_denied_422_not_permitted() {
let err = RsGuardError::GitHubApi {
status: 422,
message: "Review not permitted for this user".to_string(),
};
assert!(err.is_permission_denied());
}
#[test]
fn test_is_permission_denied_422_case_insensitive() {
let err = RsGuardError::GitHubApi {
status: 422,
message: "NOT PERMITTED".to_string(),
};
assert!(err.is_permission_denied());
}
#[test]
fn test_is_permission_denied_422_other_message() {
let err = RsGuardError::GitHubApi {
status: 422,
message: "Validation failed".to_string(),
};
assert!(!err.is_permission_denied());
}
#[test]
fn test_is_permission_denied_explicit_variant() {
let err = RsGuardError::PermissionDenied {
state: "APPROVE".to_string(),
message: "not allowed".to_string(),
};
assert!(err.is_permission_denied());
}
#[test]
fn test_is_permission_denied_404_not_denied() {
let err = RsGuardError::GitHubApi {
status: 404,
message: "not found".to_string(),
};
assert!(!err.is_permission_denied());
}
}