1use crate::ISSUE_STATUS_VALUES_NEXT_STEP;
4
5#[derive(Debug, thiserror::Error, PartialEq, Eq)]
7pub enum CliError {
8 #[error("unknown or missing command")]
10 UnknownCommand,
11 #[error("unknown command: {command}")]
13 UnknownCommandName {
14 command: String,
16 next: &'static str,
18 },
19 #[error("missing argument: {argument}")]
21 MissingArgument {
22 argument: &'static str,
24 next: &'static str,
26 },
27 #[error("missing value for {flag}")]
29 MissingFlagValue {
30 flag: &'static str,
32 next: &'static str,
34 },
35 #[error("duplicate flag: {flag}")]
37 DuplicateFlag {
38 flag: &'static str,
40 next: &'static str,
42 },
43 #[error("unexpected argument for {command}: {argument}")]
45 UnexpectedArgument {
46 argument: String,
48 command: &'static str,
50 next: &'static str,
52 },
53 #[error("unknown flag: {flag}")]
55 UnknownFlag {
56 flag: String,
58 next: &'static str,
60 },
61 #[error("unsupported flag for {command}: {flag}")]
63 UnsupportedFlag {
64 flag: String,
66 command: &'static str,
68 next: &'static str,
70 },
71 #[error("unknown resource: {resource}")]
73 UnknownResource {
74 resource: String,
76 next: &'static str,
78 },
79 #[error("unknown issue status: {0}")]
81 UnknownStatus(String),
82 #[error("unknown log level: {0}")]
84 UnknownLogLevel(String),
85 #[error("invalid limit: {0}")]
87 InvalidLimit(String),
88}
89
90#[derive(Debug, thiserror::Error)]
92pub enum RuntimeError {
93 #[error(transparent)]
95 Cli(#[from] CliError),
96 #[error(transparent)]
98 Io(#[from] std::io::Error),
99 #[error(transparent)]
101 Http(#[from] reqwest::Error),
102 #[error("not logged in: run logbrew login")]
104 MissingToken,
105 #[error("api returned status {status}: {body}")]
107 Api {
108 status: u16,
110 body: String,
112 auth_source: &'static str,
114 auth_label: &'static str,
116 },
117 #[error("LogBrew API unreachable: {message}")]
119 StatusUnavailable {
120 api_url: String,
122 status_code: Option<u16>,
124 body: Option<String>,
126 authenticated: bool,
128 auth_source: &'static str,
130 auth_label: &'static str,
132 message: String,
134 },
135 #[error("{message}")]
137 Unavailable {
138 message: &'static str,
140 next: &'static str,
142 },
143}
144
145pub fn write_cli_error<W: std::io::Write>(
151 error: &CliError,
152 json: bool,
153 output: &mut W,
154) -> Result<(), std::io::Error> {
155 if json {
156 let body = serde_json::json!({
157 "ok": false,
158 "error": cli_error_code(error),
159 "message": error.to_string(),
160 "next": cli_error_next_step(error),
161 });
162 writeln!(output, "{body}")
163 } else {
164 writeln!(output, "{error}")?;
165 writeln!(output, "Next: {}", cli_error_next_step(error))
166 }
167}
168
169pub fn write_runtime_error<W: std::io::Write>(
175 error: &RuntimeError,
176 json: bool,
177 output: &mut W,
178) -> Result<(), std::io::Error> {
179 if !json {
180 if let RuntimeError::StatusUnavailable {
181 api_url,
182 status_code,
183 body,
184 auth_label,
185 message,
186 ..
187 } = error
188 {
189 writeln!(output, "LogBrew API unreachable.")?;
190 writeln!(output, "API: {api_url}")?;
191 writeln!(output, "Auth: {auth_label}")?;
192 if let Some(status_code) = status_code {
193 writeln!(output, "Status: {status_code}")?;
194 }
195 if let Some(body) = body.as_ref().filter(|body| !body.is_empty()) {
196 writeln!(output, "Body: {body}")?;
197 } else {
198 writeln!(output, "Reason: {message}")?;
199 }
200 writeln!(output, "Next: {STATUS_UNAVAILABLE_NEXT_STEP}")?;
201 return Ok(());
202 }
203 writeln!(output, "{error}")?;
204 if let RuntimeError::Api { auth_label, .. } = error {
205 writeln!(output, "Auth: {auth_label}")?;
206 }
207 writeln!(output, "Next: {}", runtime_error_next_step(error))?;
208 return Ok(());
209 }
210
211 let body = match error {
212 RuntimeError::MissingToken
213 | RuntimeError::Unavailable { .. }
214 | RuntimeError::Io(_)
215 | RuntimeError::Http(_) => serde_json::json!({
216 "ok": false,
217 "error": runtime_error_code(error),
218 "message": error.to_string(),
219 "next": runtime_error_next_step(error),
220 }),
221 RuntimeError::Api {
222 status,
223 body,
224 auth_source,
225 ..
226 } => serde_json::json!({
227 "ok": false,
228 "error": runtime_error_code(error),
229 "message": error.to_string(),
230 "status": status,
231 "body": body,
232 "auth_source": auth_source,
233 "next": runtime_error_next_step(error),
234 }),
235 RuntimeError::StatusUnavailable {
236 api_url,
237 status_code,
238 body,
239 authenticated,
240 auth_source,
241 message,
242 ..
243 } => serde_json::json!({
244 "ok": false,
245 "error": runtime_error_code(error),
246 "status": "unreachable",
247 "status_code": status_code,
248 "body": body,
249 "api_url": api_url,
250 "authenticated": authenticated,
251 "auth_source": auth_source,
252 "message": message,
253 "next": runtime_error_next_step(error),
254 }),
255 RuntimeError::Cli(error) => serde_json::json!({
256 "ok": false,
257 "error": cli_error_code(error),
258 "message": error.to_string(),
259 "next": cli_error_next_step(error),
260 }),
261 };
262 writeln!(output, "{body}")
263}
264
265const fn cli_error_code(error: &CliError) -> &'static str {
267 match error {
268 CliError::UnknownCommand | CliError::UnknownCommandName { .. } => "unknown_command",
269 CliError::MissingArgument { .. } => "missing_argument",
270 CliError::MissingFlagValue { .. } => "missing_flag_value",
271 CliError::DuplicateFlag { .. } => "duplicate_flag",
272 CliError::UnexpectedArgument { .. } => "unexpected_argument",
273 CliError::UnknownFlag { .. } => "unknown_flag",
274 CliError::UnsupportedFlag { .. } => "unsupported_flag",
275 CliError::UnknownResource { .. } => "unknown_resource",
276 CliError::UnknownStatus(_) => "unknown_status",
277 CliError::UnknownLogLevel(_) => "unknown_log_level",
278 CliError::InvalidLimit(_) => "invalid_limit",
279 }
280}
281
282const fn cli_error_next_step(error: &CliError) -> &'static str {
284 match error {
285 CliError::InvalidLimit(_) => "use --limit with a positive whole number",
286 CliError::MissingArgument { next, .. }
287 | CliError::MissingFlagValue { next, .. }
288 | CliError::DuplicateFlag { next, .. }
289 | CliError::UnexpectedArgument { next, .. }
290 | CliError::UnsupportedFlag { next, .. }
291 | CliError::UnknownFlag { next, .. }
292 | CliError::UnknownResource { next, .. }
293 | CliError::UnknownCommandName { next, .. } => next,
294 CliError::UnknownCommand => "run logbrew --help",
295 CliError::UnknownStatus(_) => ISSUE_STATUS_VALUES_NEXT_STEP,
296 CliError::UnknownLogLevel(_) => "use one of trace, debug, info, warn, error, fatal",
297 }
298}
299
300const fn runtime_error_code(error: &RuntimeError) -> &'static str {
302 match error {
303 RuntimeError::Cli(error) => cli_error_code(error),
304 RuntimeError::Io(_) => "io_error",
305 RuntimeError::Http(_) => "http_error",
306 RuntimeError::MissingToken => "not_logged_in",
307 RuntimeError::Api { .. } => "api_error",
308 RuntimeError::StatusUnavailable { .. } => "status_unreachable",
309 RuntimeError::Unavailable { .. } => "unavailable",
310 }
311}
312
313const STATUS_UNAVAILABLE_NEXT_STEP: &str = "check LOGBREW_API_URL or network";
315
316const fn runtime_error_next_step(error: &RuntimeError) -> &'static str {
318 match error {
319 RuntimeError::Cli(error) => cli_error_next_step(error),
320 RuntimeError::MissingToken
321 | RuntimeError::Api {
322 status: 401 | 403, ..
323 } => "run logbrew login",
324 RuntimeError::Api {
325 status: 400 | 422, ..
326 } => "check command arguments or filters",
327 RuntimeError::Api { status: 404, .. } => "check the resource id or filters",
328 RuntimeError::Api { status: 429, .. } => "retry later",
329 RuntimeError::Api {
330 status: 500..=599, ..
331 } => "check LOGBREW_API_URL or retry later",
332 RuntimeError::Api { .. } => "check command arguments or retry later",
333 RuntimeError::StatusUnavailable { .. } | RuntimeError::Http(_) => {
334 STATUS_UNAVAILABLE_NEXT_STEP
335 }
336 RuntimeError::Unavailable { next, .. } => next,
337 RuntimeError::Io(_) => "check local files and permissions",
338 }
339}