Skip to main content

sharepoint_cli/
error.rs

1//! Error types and exit code mapping.
2
3use std::fmt;
4
5#[derive(Debug)]
6pub enum CliError {
7    /// Bad user input or config (exit 2).
8    Input(String),
9    /// Authentication or token failure (exit 3).
10    Auth(String),
11    /// Read-only mode blocked a write (exit 2).
12    ReadOnly(String),
13    /// Resource exists with conflicting configuration (exit 7).
14    Conflict(String),
15    /// Site, library, or item not found (exit 4).
16    NotFound(String),
17    /// Graph API returned a non-2xx error (exit 5).
18    Api { status: u16, message: String },
19    /// Rate limited by Graph (exit 6).
20    RateLimit,
21    /// Underlying HTTP transport error (exit 1).
22    Http(String),
23    /// Anything else (exit 1).
24    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
86/// Stable kind string for the structured error envelope.
87pub 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}