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(_) => "invalid_input",
90 CliError::Auth(_) => "auth",
91 CliError::ReadOnly(_) => "read_only",
92 CliError::Conflict(_) => "conflict",
93 CliError::NotFound(_) => "not_found",
94 CliError::Api { .. } => "api_error",
95 CliError::RateLimit => "rate_limit",
96 CliError::Http(_) | CliError::Other(_) => "unexpected_error",
97 }
98}
99
100pub type Result<T> = std::result::Result<T, CliError>;
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn input_error_maps_to_exit_2() {
108 assert_eq!(exit_code_for(&CliError::Input("x".into())), 2);
109 }
110
111 #[test]
112 fn read_only_error_maps_to_exit_2() {
113 assert_eq!(exit_code_for(&CliError::ReadOnly("x".into())), 2);
114 }
115
116 #[test]
117 fn auth_error_maps_to_exit_3() {
118 assert_eq!(exit_code_for(&CliError::Auth("x".into())), 3);
119 }
120
121 #[test]
122 fn not_found_maps_to_exit_4() {
123 assert_eq!(exit_code_for(&CliError::NotFound("x".into())), 4);
124 }
125
126 #[test]
127 fn api_error_maps_to_exit_5() {
128 assert_eq!(
129 exit_code_for(&CliError::Api {
130 status: 500,
131 message: "x".into()
132 }),
133 5
134 );
135 }
136
137 #[test]
138 fn rate_limit_maps_to_exit_6() {
139 assert_eq!(exit_code_for(&CliError::RateLimit), 6);
140 }
141
142 #[test]
143 fn http_error_maps_to_general() {
144 assert_eq!(exit_code_for(&CliError::Http("x".into())), 1);
145 }
146
147 #[test]
148 fn other_error_maps_to_general() {
149 assert_eq!(exit_code_for(&CliError::Other("x".into())), 1);
150 }
151
152 #[test]
153 fn conflict_maps_to_exit_7() {
154 assert_eq!(exit_code_for(&CliError::Conflict("x".into())), 7);
155 }
156
157 #[test]
158 fn display_includes_status_for_api() {
159 let e = CliError::Api {
160 status: 404,
161 message: "not found".into(),
162 };
163 assert_eq!(format!("{e}"), "Graph API 404: not found");
164 }
165
166 #[test]
167 fn kind_for_covers_all_variants() {
168 assert_eq!(kind_for(&CliError::Input("x".into())), "invalid_input");
169 assert_eq!(kind_for(&CliError::Auth("x".into())), "auth");
170 assert_eq!(kind_for(&CliError::ReadOnly("x".into())), "read_only");
171 assert_eq!(kind_for(&CliError::Conflict("x".into())), "conflict");
172 assert_eq!(kind_for(&CliError::NotFound("x".into())), "not_found");
173 assert_eq!(
174 kind_for(&CliError::Api {
175 status: 500,
176 message: "x".into()
177 }),
178 "api_error"
179 );
180 assert_eq!(kind_for(&CliError::RateLimit), "rate_limit");
181 assert_eq!(kind_for(&CliError::Http("x".into())), "unexpected_error");
182 assert_eq!(kind_for(&CliError::Other("x".into())), "unexpected_error");
183 }
184}