1use std::io::IsTerminal;
2
3pub fn use_color() -> bool {
5 std::io::stdout().is_terminal()
6}
7
8#[derive(Clone, Copy, PartialEq, Eq)]
10pub enum OutputFormat {
11 Auto,
13 Text,
15 Json,
17}
18
19impl OutputFormat {
20 pub fn parse(s: &str) -> Option<Self> {
21 match s {
22 "auto" => Some(Self::Auto),
23 "text" => Some(Self::Text),
24 "json" => Some(Self::Json),
25 _ => None,
26 }
27 }
28}
29
30#[derive(Clone, Copy)]
35pub struct OutputConfig {
36 pub format: OutputFormat,
37 pub quiet: bool,
38}
39
40impl OutputConfig {
41 pub fn new(format: OutputFormat, quiet: bool) -> Self {
42 Self { format, quiet }
43 }
44
45 pub fn is_json(&self) -> bool {
47 match self.format {
48 OutputFormat::Json => true,
49 OutputFormat::Text => false,
50 OutputFormat::Auto => !std::io::stdout().is_terminal(),
51 }
52 }
53
54 pub fn print_data(&self, data: &str) {
56 println!("{data}");
57 }
58
59 pub fn print_message(&self, msg: &str) {
61 if !self.quiet {
62 eprintln!("{msg}");
63 }
64 }
65
66 pub fn print_result(&self, json_value: &serde_json::Value, human_message: &str) {
69 if self.is_json() {
70 println!(
71 "{}",
72 serde_json::to_string_pretty(json_value).expect("failed to serialize JSON")
73 );
74 } else {
75 self.print_message(human_message);
76 }
77 }
78}
79
80pub fn print_error_envelope(kind: &str, message: &str, hint: Option<&str>) {
83 let mut err = serde_json::json!({
84 "kind": kind,
85 "message": message,
86 });
87 if let Some(h) = hint {
88 err["hint"] = serde_json::Value::String(h.to_string());
89 }
90 eprintln!(
91 "{}",
92 serde_json::to_string(&serde_json::json!({ "error": err }))
93 .expect("failed to serialize error envelope")
94 );
95}
96
97pub mod exit_codes {
100 pub const SUCCESS: i32 = 0;
101 pub const GENERAL_ERROR: i32 = 1;
102 pub const CONFIG_ERROR: i32 = 2;
103 pub const CONFIRMATION_REQUIRED: i32 = 2;
104 pub const AUTH_ERROR: i32 = 3;
105 pub const NOT_FOUND: i32 = 4;
106 pub const API_ERROR: i32 = 5;
107}
108
109pub fn exit_code_for_error(err: &(dyn std::error::Error + 'static)) -> i32 {
111 if let Some(api_err) = err.downcast_ref::<crate::api::ApiError>() {
112 match api_err {
113 crate::api::ApiError::Auth(_) => exit_codes::AUTH_ERROR,
114 crate::api::ApiError::NotFound(_) => exit_codes::NOT_FOUND,
115 crate::api::ApiError::Api { .. } => exit_codes::API_ERROR,
116 crate::api::ApiError::Http(_) | crate::api::ApiError::Other(_) => {
117 exit_codes::GENERAL_ERROR
118 }
119 }
120 } else {
121 exit_codes::GENERAL_ERROR
122 }
123}
124
125pub fn error_kind_and_code(err: &(dyn std::error::Error + 'static)) -> (&'static str, i32) {
127 if let Some(api_err) = err.downcast_ref::<crate::api::ApiError>() {
128 match api_err {
129 crate::api::ApiError::Auth(_) => ("auth_error", exit_codes::AUTH_ERROR),
130 crate::api::ApiError::NotFound(_) => ("not_found", exit_codes::NOT_FOUND),
131 crate::api::ApiError::Api { .. } => ("api_error", exit_codes::API_ERROR),
132 crate::api::ApiError::Http(_) | crate::api::ApiError::Other(_) => {
133 ("general_error", exit_codes::GENERAL_ERROR)
134 }
135 }
136 } else {
137 ("general_error", exit_codes::GENERAL_ERROR)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::api::ApiError;
145
146 #[test]
147 fn exit_code_for_auth_error() {
148 let err = ApiError::Auth("bad key".into());
149 assert_eq!(exit_code_for_error(&err), exit_codes::AUTH_ERROR);
150 }
151
152 #[test]
153 fn exit_code_for_not_found() {
154 let err = ApiError::NotFound("Client with MAC aa:bb".into());
155 assert_eq!(exit_code_for_error(&err), exit_codes::NOT_FOUND);
156 }
157
158 #[test]
159 fn exit_code_for_api_error() {
160 let err = ApiError::Api {
161 status: 500,
162 message: "Internal Server Error".into(),
163 };
164 assert_eq!(exit_code_for_error(&err), exit_codes::API_ERROR);
165 }
166
167 #[test]
168 fn exit_code_for_other_error() {
169 let err = ApiError::Other("something".into());
170 assert_eq!(exit_code_for_error(&err), exit_codes::GENERAL_ERROR);
171 }
172
173 #[test]
174 fn exit_code_for_non_api_error() {
175 let err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
176 assert_eq!(exit_code_for_error(&err), exit_codes::GENERAL_ERROR);
177 }
178
179 #[test]
180 fn output_format_explicit_text_is_not_json() {
181 let out = OutputConfig::new(OutputFormat::Text, false);
182 assert!(!out.is_json());
183 }
184
185 #[test]
186 fn output_format_explicit_json_is_json() {
187 let out = OutputConfig::new(OutputFormat::Json, false);
188 assert!(out.is_json());
189 }
190
191 #[test]
192 fn error_kind_and_code_auth() {
193 let err = ApiError::Auth("bad".into());
194 let (kind, code) = error_kind_and_code(&err);
195 assert_eq!(kind, "auth_error");
196 assert_eq!(code, exit_codes::AUTH_ERROR);
197 }
198
199 #[test]
200 fn error_kind_and_code_not_found() {
201 let err = ApiError::NotFound("x".into());
202 let (kind, code) = error_kind_and_code(&err);
203 assert_eq!(kind, "not_found");
204 assert_eq!(code, exit_codes::NOT_FOUND);
205 }
206
207 #[test]
208 fn error_envelope_is_valid_json() {
209 let envelope = serde_json::json!({
210 "error": {
211 "kind": "auth_error",
212 "message": "Authentication error: bad key",
213 }
214 });
215 assert!(envelope["error"]["kind"].as_str().is_some());
216 assert!(envelope["error"]["message"].as_str().is_some());
217 }
218}