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 #[error("rate limited (HTTP 429): {0}")]
27 RateLimited(String),
28 #[error("network error: {0}")]
31 Network(String),
32 #[error(transparent)]
33 Other(#[from] anyhow::Error),
34}
35
36impl 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 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}