Skip to main content

gitee_cli_rs/
error.rs

1use thiserror::Error;
2
3pub type Result<T, E = GiteeError> = std::result::Result<T, E>;
4
5#[derive(Error, Debug)]
6pub enum GiteeError {
7    #[error("gitee API error ({status}): {message}")]
8    Api { status: u16, message: String },
9    #[error("authentication failed (HTTP 401): token is missing, invalid, or expired — run `gitee auth login` (or set GITEE_TOKEN)")]
10    Unauthorized,
11    #[error("not found (HTTP 404): {0}")]
12    NotFound(String),
13    #[error("http request failed: {0}")]
14    Http(#[from] reqwest::Error),
15    #[error("io error: {0}")]
16    Io(#[from] std::io::Error),
17    #[error("config error: {0}")]
18    Config(String),
19    #[error("not logged in: run `gitee auth login --token <TOKEN>` first (or set the GITEE_TOKEN env var)")]
20    NotLoggedIn,
21    #[error("could not determine repository (pass --repo owner/repo): {0}")]
22    RepoResolve(String),
23    #[error("{0}")]
24    Usage(String),
25    /// Gitee returned 429 — too many requests. Maps to exit code 5.
26    #[error("rate limited (HTTP 429): {0}")]
27    RateLimited(String),
28    /// Could not reach the Gitee host (DNS failure, connection refused, etc.).
29    /// Maps to exit code 6.
30    #[error("network error: {0}")]
31    Network(String),
32    #[error(transparent)]
33    Other(#[from] anyhow::Error),
34}
35
36/// Stable, documented exit codes (see README "Exit codes").
37///
38/// - `0` success
39/// - `1` generic failure (API error, unexpected)
40/// - `2` usage error (missing flag, bad arg, non-TTY prompt attempted)
41/// - `3` auth error (no token, invalid, expired)
42/// - `4` not found (repo/issue/PR/release)
43/// - `5` rate limited (HTTP 429)
44/// - `6` network error (host unreachable)
45impl GiteeError {
46    pub fn exit_code(&self) -> i32 {
47        use GiteeError::*;
48        match self {
49            Api { status: 429, .. } | RateLimited(_) => 5,
50            Api { status: 401, .. } | Unauthorized | NotLoggedIn => 3,
51            Api { status: 404, .. } | NotFound(_) => 4,
52            Http(e) if e.is_connect() || e.is_timeout() => 6,
53            Network(_) => 6,
54            Http(_) => 1,
55            Io(_) => 1,
56            Config(_) => 2,
57            RepoResolve(_) => 2,
58            Usage(_) => 2,
59            Api { .. } | Other(_) => 1,
60        }
61    }
62
63    /// Stable, machine-readable `code` slug for `--json` error envelopes.
64    pub fn code_slug(&self) -> &'static str {
65        use GiteeError::*;
66        match self {
67            Api { status: 429, .. } | RateLimited(_) => "rate_limited",
68            Api { status: 401, .. } | Unauthorized | NotLoggedIn => "auth",
69            Api { status: 404, .. } | NotFound(_) => "not_found",
70            Http(e) if e.is_connect() || e.is_timeout() => "network",
71            Network(_) => "network",
72            Http(_) => "http",
73            Io(_) => "io",
74            Config(_) => "config",
75            RepoResolve(_) => "repo_resolve",
76            Usage(_) => "usage",
77            Api { .. } | Other(_) => "error",
78        }
79    }
80}
81
82#[cfg(test)]
83mod exit_code_tests {
84    use super::GiteeError;
85
86    #[test]
87    fn api_429_maps_to_exit_5() {
88        assert_eq!(GiteeError::RateLimited("slow down".into()).exit_code(), 5);
89        assert_eq!(
90            GiteeError::Api { status: 429, message: "x".into() }.exit_code(),
91            5
92        );
93    }
94
95    #[test]
96    fn unauthorized_maps_to_exit_3() {
97        assert_eq!(GiteeError::Unauthorized.exit_code(), 3);
98        assert_eq!(
99            GiteeError::Api { status: 401, message: "x".into() }.exit_code(),
100            3
101        );
102        assert_eq!(GiteeError::NotLoggedIn.exit_code(), 3);
103    }
104
105    #[test]
106    fn not_found_maps_to_exit_4() {
107        assert_eq!(GiteeError::NotFound("repo".into()).exit_code(), 4);
108        assert_eq!(
109            GiteeError::Api { status: 404, message: "x".into() }.exit_code(),
110            4
111        );
112    }
113
114    #[test]
115    fn usage_maps_to_exit_2() {
116        assert_eq!(GiteeError::Usage("x".into()).exit_code(), 2);
117        assert_eq!(GiteeError::Config("x".into()).exit_code(), 2);
118        assert_eq!(
119            GiteeError::RepoResolve("x".into()).exit_code(),
120            2
121        );
122    }
123
124    #[test]
125    fn generic_api_error_maps_to_exit_1() {
126        assert_eq!(
127            GiteeError::Api { status: 500, message: "x".into() }.exit_code(),
128            1
129        );
130    }
131
132    #[test]
133    fn network_error_maps_to_exit_6() {
134        assert_eq!(GiteeError::Network("x".into()).exit_code(), 6);
135    }
136
137    #[test]
138    fn code_slugs_are_stable() {
139        assert_eq!(GiteeError::NotFound("x".into()).code_slug(), "not_found");
140        assert_eq!(GiteeError::Unauthorized.code_slug(), "auth");
141        assert_eq!(GiteeError::NotLoggedIn.code_slug(), "auth");
142        assert_eq!(GiteeError::RateLimited("x".into()).code_slug(), "rate_limited");
143        assert_eq!(GiteeError::Usage("x".into()).code_slug(), "usage");
144    }
145}