1use std::fmt;
4
5#[derive(Debug)]
6pub enum CliError {
7 Input(String),
9 Auth(String),
11 ReadOnly(String),
13 Conflict(String),
15 NotFound(String),
17 Api { status: u16, message: String },
19 RateLimit,
21 Http(String),
23 Other(String),
25}
26
27impl fmt::Display for CliError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 CliError::Input(m) => write!(f, "{m}"),
31 CliError::Auth(m) => write!(f, "{m}"),
32 CliError::ReadOnly(m) => write!(f, "{m}"),
33 CliError::Conflict(m) => write!(f, "{m}"),
34 CliError::NotFound(m) => write!(f, "{m}"),
35 CliError::Api { status, message } => write!(f, "Graph API {status}: {message}"),
36 CliError::RateLimit => write!(f, "rate limited by Microsoft Graph"),
37 CliError::Http(m) => write!(f, "{m}"),
38 CliError::Other(m) => write!(f, "{m}"),
39 }
40 }
41}
42
43impl std::error::Error for CliError {}
44
45impl From<reqwest::Error> for CliError {
46 fn from(err: reqwest::Error) -> Self {
47 CliError::Http(err.to_string())
48 }
49}
50
51impl From<std::io::Error> for CliError {
52 fn from(err: std::io::Error) -> Self {
53 CliError::Other(err.to_string())
54 }
55}
56
57impl From<serde_json::Error> for CliError {
58 fn from(err: serde_json::Error) -> Self {
59 CliError::Other(format!("JSON error: {err}"))
60 }
61}
62
63pub mod exit_codes {
64 pub const SUCCESS: i32 = 0;
65 pub const GENERAL: i32 = 1;
66 pub const INPUT: i32 = 2;
67 pub const AUTH: i32 = 3;
68 pub const NOT_FOUND: i32 = 4;
69 pub const API: i32 = 5;
70 pub const RATE_LIMIT: i32 = 6;
71 pub const CONFLICT: i32 = 7;
72}
73
74pub fn exit_code_for(err: &CliError) -> i32 {
75 match err {
76 CliError::Input(_) | CliError::ReadOnly(_) => exit_codes::INPUT,
77 CliError::Auth(_) => exit_codes::AUTH,
78 CliError::NotFound(_) => exit_codes::NOT_FOUND,
79 CliError::Api { .. } => exit_codes::API,
80 CliError::RateLimit => exit_codes::RATE_LIMIT,
81 CliError::Conflict(_) => exit_codes::CONFLICT,
82 CliError::Http(_) | CliError::Other(_) => exit_codes::GENERAL,
83 }
84}
85
86pub fn kind_for(err: &CliError) -> &'static str {
88 match err {
89 CliError::Input(_) => "input",
90 CliError::Auth(_) => "auth",
91 CliError::ReadOnly(_) => "read_only",
92 CliError::Conflict(_) => "conflict",
93 CliError::NotFound(_) => "not_found",
94 CliError::Api { .. } => "api",
95 CliError::RateLimit => "rate_limit",
96 CliError::Http(_) => "http",
97 CliError::Other(_) => "other",
98 }
99}
100
101pub type Result<T> = std::result::Result<T, CliError>;
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn input_error_maps_to_exit_2() {
109 assert_eq!(exit_code_for(&CliError::Input("x".into())), 2);
110 }
111
112 #[test]
113 fn read_only_error_maps_to_exit_2() {
114 assert_eq!(exit_code_for(&CliError::ReadOnly("x".into())), 2);
115 }
116
117 #[test]
118 fn auth_error_maps_to_exit_3() {
119 assert_eq!(exit_code_for(&CliError::Auth("x".into())), 3);
120 }
121
122 #[test]
123 fn not_found_maps_to_exit_4() {
124 assert_eq!(exit_code_for(&CliError::NotFound("x".into())), 4);
125 }
126
127 #[test]
128 fn api_error_maps_to_exit_5() {
129 assert_eq!(
130 exit_code_for(&CliError::Api {
131 status: 500,
132 message: "x".into()
133 }),
134 5
135 );
136 }
137
138 #[test]
139 fn rate_limit_maps_to_exit_6() {
140 assert_eq!(exit_code_for(&CliError::RateLimit), 6);
141 }
142
143 #[test]
144 fn http_error_maps_to_general() {
145 assert_eq!(exit_code_for(&CliError::Http("x".into())), 1);
146 }
147
148 #[test]
149 fn other_error_maps_to_general() {
150 assert_eq!(exit_code_for(&CliError::Other("x".into())), 1);
151 }
152
153 #[test]
154 fn conflict_maps_to_exit_7() {
155 assert_eq!(exit_code_for(&CliError::Conflict("x".into())), 7);
156 }
157
158 #[test]
159 fn display_includes_status_for_api() {
160 let e = CliError::Api {
161 status: 404,
162 message: "not found".into(),
163 };
164 assert_eq!(format!("{e}"), "Graph API 404: not found");
165 }
166
167 #[test]
168 fn kind_for_covers_all_variants() {
169 assert_eq!(kind_for(&CliError::Input("x".into())), "input");
170 assert_eq!(kind_for(&CliError::Auth("x".into())), "auth");
171 assert_eq!(kind_for(&CliError::ReadOnly("x".into())), "read_only");
172 assert_eq!(kind_for(&CliError::Conflict("x".into())), "conflict");
173 assert_eq!(kind_for(&CliError::NotFound("x".into())), "not_found");
174 assert_eq!(
175 kind_for(&CliError::Api {
176 status: 500,
177 message: "x".into()
178 }),
179 "api"
180 );
181 assert_eq!(kind_for(&CliError::RateLimit), "rate_limit");
182 assert_eq!(kind_for(&CliError::Http("x".into())), "http");
183 assert_eq!(kind_for(&CliError::Other("x".into())), "other");
184 }
185}