use serde::{Deserialize, Serialize};
use std::process::ExitCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "i32", try_from = "i32")]
#[repr(i32)]
pub enum ExitStatus {
Success = 0,
GeneralError = 1,
UsageError = 2,
NotFound = 3,
ValidationError = 4,
PermissionDenied = 5,
NetworkError = 10,
Timeout = 11,
RateLimited = 12,
ApiError = 13,
CacheError = 20,
CacheMiss = 21,
ConfigError = 30,
ConfigNotFound = 31,
DatabaseError = 40,
AnalyticsError = 50,
DownloadError = 60,
ImportError = 61,
PartialSuccess = 100,
DaemonAlreadyRunning = 110,
DaemonNotRunning = 111,
Interrupted = 130,
}
impl ExitStatus {
pub fn code(&self) -> i32 {
*self as i32
}
pub fn name(&self) -> &'static str {
match self {
Self::Success => "SUCCESS",
Self::GeneralError => "GENERAL_ERROR",
Self::UsageError => "USAGE_ERROR",
Self::NotFound => "NOT_FOUND",
Self::ValidationError => "VALIDATION_ERROR",
Self::PermissionDenied => "PERMISSION_DENIED",
Self::NetworkError => "NETWORK_ERROR",
Self::Timeout => "TIMEOUT",
Self::RateLimited => "RATE_LIMITED",
Self::ApiError => "API_ERROR",
Self::CacheError => "CACHE_ERROR",
Self::CacheMiss => "CACHE_MISS",
Self::ConfigError => "CONFIG_ERROR",
Self::ConfigNotFound => "CONFIG_NOT_FOUND",
Self::DatabaseError => "DATABASE_ERROR",
Self::AnalyticsError => "ANALYTICS_ERROR",
Self::DownloadError => "DOWNLOAD_ERROR",
Self::ImportError => "IMPORT_ERROR",
Self::PartialSuccess => "PARTIAL_SUCCESS",
Self::DaemonAlreadyRunning => "DAEMON_ALREADY_RUNNING",
Self::DaemonNotRunning => "DAEMON_NOT_RUNNING",
Self::Interrupted => "INTERRUPTED",
}
}
pub fn description(&self) -> &'static str {
match self {
Self::Success => "Operation completed successfully",
Self::GeneralError => "An unspecified error occurred",
Self::UsageError => "Invalid arguments or command syntax",
Self::NotFound => "Requested resource was not found",
Self::ValidationError => "Input validation failed",
Self::PermissionDenied => "Authentication or authorization failed",
Self::NetworkError => "Network connectivity issue",
Self::Timeout => "Request timed out",
Self::RateLimited => "API rate limit exceeded - wait and retry",
Self::ApiError => "GDELT API returned an error",
Self::CacheError => "Cache read/write operation failed",
Self::CacheMiss => "Cache miss when --cache-only was specified",
Self::ConfigError => "Configuration file is invalid",
Self::ConfigNotFound => "Configuration file was not found",
Self::DatabaseError => "Database operation failed",
Self::AnalyticsError => "Analytics computation failed",
Self::DownloadError => "Data download failed",
Self::ImportError => "Data import failed",
Self::PartialSuccess => "Some operations succeeded, others failed",
Self::DaemonAlreadyRunning => "Daemon is already running",
Self::DaemonNotRunning => "Daemon is not running",
Self::Interrupted => "Operation was interrupted by user",
}
}
pub fn agent_action(&self) -> &'static str {
match self {
Self::Success => "Continue with next task",
Self::GeneralError => "Retry operation or report error",
Self::UsageError => "Fix command syntax and retry",
Self::NotFound => "Handle gracefully - resource doesn't exist",
Self::ValidationError => "Fix input parameters and retry",
Self::PermissionDenied => "Check credentials or permissions",
Self::NetworkError => "Retry with exponential backoff",
Self::Timeout => "Retry with increased timeout",
Self::RateLimited => "Wait for retry-after period, then retry",
Self::ApiError => "Check GDELT API status, retry later",
Self::CacheError => "Try with --no-cache flag",
Self::CacheMiss => "Fetch fresh data (remove --cache-only)",
Self::ConfigError => "Fix configuration file",
Self::ConfigNotFound => "Create configuration file",
Self::DatabaseError => "Check database file permissions",
Self::AnalyticsError => "Check input data validity",
Self::DownloadError => "Check network, retry download",
Self::ImportError => "Check file format, retry import",
Self::PartialSuccess => "Check output for details on failures",
Self::DaemonAlreadyRunning => "Use existing daemon or stop it first",
Self::DaemonNotRunning => "Start daemon first",
Self::Interrupted => "User cancelled - no action needed",
}
}
pub fn is_success(&self) -> bool {
matches!(self, Self::Success)
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::NetworkError | Self::Timeout | Self::RateLimited | Self::ApiError
)
}
pub fn all() -> &'static [ExitStatus] {
&[
Self::Success,
Self::GeneralError,
Self::UsageError,
Self::NotFound,
Self::ValidationError,
Self::PermissionDenied,
Self::NetworkError,
Self::Timeout,
Self::RateLimited,
Self::ApiError,
Self::CacheError,
Self::CacheMiss,
Self::ConfigError,
Self::ConfigNotFound,
Self::DatabaseError,
Self::AnalyticsError,
Self::DownloadError,
Self::ImportError,
Self::PartialSuccess,
Self::DaemonAlreadyRunning,
Self::DaemonNotRunning,
Self::Interrupted,
]
}
}
impl From<ExitStatus> for i32 {
fn from(status: ExitStatus) -> Self {
status.code()
}
}
impl TryFrom<i32> for ExitStatus {
type Error = &'static str;
fn try_from(code: i32) -> Result<Self, Self::Error> {
match code {
0 => Ok(Self::Success),
1 => Ok(Self::GeneralError),
2 => Ok(Self::UsageError),
3 => Ok(Self::NotFound),
4 => Ok(Self::ValidationError),
5 => Ok(Self::PermissionDenied),
10 => Ok(Self::NetworkError),
11 => Ok(Self::Timeout),
12 => Ok(Self::RateLimited),
13 => Ok(Self::ApiError),
20 => Ok(Self::CacheError),
21 => Ok(Self::CacheMiss),
30 => Ok(Self::ConfigError),
31 => Ok(Self::ConfigNotFound),
40 => Ok(Self::DatabaseError),
50 => Ok(Self::AnalyticsError),
60 => Ok(Self::DownloadError),
61 => Ok(Self::ImportError),
100 => Ok(Self::PartialSuccess),
110 => Ok(Self::DaemonAlreadyRunning),
111 => Ok(Self::DaemonNotRunning),
130 => Ok(Self::Interrupted),
_ => Err("Unknown exit code"),
}
}
}
impl From<ExitStatus> for ExitCode {
fn from(status: ExitStatus) -> Self {
ExitCode::from(status.code() as u8)
}
}
#[derive(Debug, Serialize)]
pub struct ExitCodeInfo {
pub code: i32,
pub name: &'static str,
pub description: &'static str,
pub agent_action: &'static str,
pub retryable: bool,
}
impl From<ExitStatus> for ExitCodeInfo {
fn from(status: ExitStatus) -> Self {
Self {
code: status.code(),
name: status.name(),
description: status.description(),
agent_action: status.agent_action(),
retryable: status.is_retryable(),
}
}
}
pub fn all_exit_codes() -> Vec<ExitCodeInfo> {
ExitStatus::all().iter().map(|&s| s.into()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exit_code_roundtrip() {
for status in ExitStatus::all() {
let code = status.code();
let recovered = ExitStatus::try_from(code).unwrap();
assert_eq!(*status, recovered);
}
}
#[test]
fn test_success_is_zero() {
assert_eq!(ExitStatus::Success.code(), 0);
}
}