1use std::{
2 error::Error,
3 io::{self, Write},
4 path::PathBuf,
5 result::Result as StdResult,
6};
7
8use seaplane::{
9 api::ApiErrorKind, error::SeaplaneError, rexports::container_image_ref::ImageReferenceError,
10};
11
12use crate::{
13 log::{log_level, LogLevel},
14 printer::{eprinter, Color},
15};
16
17pub type Result<T> = StdResult<T, CliError>;
18
19pub trait Context {
27 fn context<S: Into<String>>(self, msg: S) -> Self;
29
30 fn with_context<F, S>(self, f: F) -> Self
33 where
34 F: FnOnce() -> S,
35 S: Into<String>;
36
37 fn color_context<S: Into<String>>(self, color: Color, msg: S) -> Self;
43
44 fn with_color_context<F, S>(self, f: F) -> Self
51 where
52 F: FnOnce() -> (Color, S),
53 S: Into<String>;
54}
55
56impl<T> Context for StdResult<T, CliError> {
57 fn context<S: Into<String>>(self, msg: S) -> Self {
58 match self {
59 Ok(t) => Ok(t),
60 Err(cli_err) => Err(cli_err.context(msg)),
61 }
62 }
63 fn color_context<S: Into<String>>(self, color: Color, msg: S) -> Self {
64 match self {
65 Ok(t) => Ok(t),
66 Err(cli_err) => Err(cli_err.color_context(color, msg)),
67 }
68 }
69 fn with_context<F, S>(self, f: F) -> Self
70 where
71 F: FnOnce() -> S,
72 S: Into<String>,
73 {
74 match self {
75 Ok(t) => Ok(t),
76 Err(cli_err) => Err(cli_err.context(f())),
77 }
78 }
79
80 fn with_color_context<F, S>(self, f: F) -> Self
81 where
82 F: FnOnce() -> (Color, S),
83 S: Into<String>,
84 {
85 match self {
86 Ok(t) => Ok(t),
87 Err(cli_err) => {
88 let (color, msg) = f();
89 Err(cli_err.color_context(color, msg))
90 }
91 }
92 }
93}
94
95#[derive(Debug)]
96pub struct ColorString {
97 msg: String,
98 color: Option<Color>,
99}
100
101#[derive(Debug)]
102pub struct CliError {
103 kind: CliErrorKind,
104 context: Vec<ColorString>,
105 status: Option<i32>, }
107
108impl CliError {
109 pub fn bail(msg: &'static str) -> Self {
110 Self { kind: CliErrorKind::UnknownWithContext(msg), ..Default::default() }
111 }
112}
113
114impl Context for CliError {
115 fn color_context<S: Into<String>>(mut self, color: Color, msg: S) -> Self {
116 self.context
117 .push(ColorString { msg: msg.into(), color: Some(color) });
118 self
119 }
120
121 fn context<S: Into<String>>(mut self, msg: S) -> Self {
122 self.context
123 .push(ColorString { msg: msg.into(), color: None });
124 self
125 }
126
127 fn with_context<F, S>(mut self, f: F) -> Self
128 where
129 F: FnOnce() -> S,
130 S: Into<String>,
131 {
132 self.context
133 .push(ColorString { msg: f().into(), color: None });
134 self
135 }
136
137 fn with_color_context<F, S>(mut self, f: F) -> Self
138 where
139 F: FnOnce() -> (Color, S),
140 S: Into<String>,
141 {
142 let (color, msg) = f();
143 self.context
144 .push(ColorString { msg: msg.into(), color: Some(color) });
145 self
146 }
147}
148
149impl Default for CliError {
150 fn default() -> Self { Self { kind: CliErrorKind::Unknown, context: Vec::new(), status: None } }
151}
152
153impl std::fmt::Display for CliError {
157 fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 panic!("std::fmt::Display is not actually implemented for CliError by design, use CliError::print instead")
159 }
160}
161
162impl Error for CliError {}
164
165macro_rules! impl_err {
166 ($errty:ty, $variant:ident) => {
167 impl From<$errty> for CliError {
168 fn from(e: $errty) -> Self {
169 CliError { kind: CliErrorKind::$variant(e), ..Default::default() }
170 }
171 }
172 };
173}
174
175impl_err!(base64::DecodeError, Base64Decode);
178impl_err!(serde_json::Error, SerdeJson);
179impl_err!(toml::de::Error, TomlDe);
180impl_err!(toml::ser::Error, TomlSer);
181impl_err!(seaplane::error::SeaplaneError, Seaplane);
182impl_err!(seaplane::rexports::container_image_ref::ImageReferenceError, ImageReference);
183impl_err!(std::string::FromUtf8Error, InvalidUtf8);
184impl_err!(hex::FromHexError, HexDecode);
185impl_err!(std::num::ParseIntError, ParseInt);
186impl_err!(strum::ParseError, StrumParse);
187impl_err!(clap::Error, Clap);
188
189impl From<io::Error> for CliError {
190 fn from(e: io::Error) -> Self {
191 match e.kind() {
192 io::ErrorKind::NotFound => {
193 CliError { kind: CliErrorKind::MissingPath, ..Default::default() }
194 }
195 io::ErrorKind::PermissionDenied => {
196 CliError { kind: CliErrorKind::PermissionDenied, ..Default::default() }
197 }
198 _ => CliError { kind: CliErrorKind::Io(e, None), ..Default::default() },
199 }
200 }
201}
202
203impl From<tempfile::PersistError> for CliError {
204 fn from(e: tempfile::PersistError) -> Self {
205 Self {
206 kind: CliErrorKind::Io(e.error, Some(e.file.path().to_path_buf())),
207 ..Default::default()
208 }
209 }
210}
211
212impl From<tempfile::PathPersistError> for CliError {
213 fn from(e: tempfile::PathPersistError) -> Self {
214 Self { kind: CliErrorKind::Io(e.error, Some(e.path.to_path_buf())), ..Default::default() }
215 }
216}
217
218impl From<CliErrorKind> for CliError {
219 fn from(kind: CliErrorKind) -> Self { CliError { kind, ..Default::default() } }
220}
221
222#[derive(Debug)]
223pub enum CliErrorKind {
224 DuplicateName(String),
225 NoMatchingItem(String),
226 AmbiguousItem(String),
227 Io(io::Error, Option<PathBuf>),
228 SerdeJson(serde_json::Error),
229 Base64Decode(base64::DecodeError),
230 TomlDe(toml::de::Error),
231 TomlSer(toml::ser::Error),
232 HexDecode(hex::FromHexError),
233 UnknownWithContext(&'static str),
234 Seaplane(SeaplaneError),
235 ExistingValue(&'static str),
236 ImageReference(ImageReferenceError),
237 InvalidUtf8(std::string::FromUtf8Error),
238 CliArgNotUsed(&'static str),
239 InvalidCliValue(Option<&'static str>, String),
240 ConflictingArguments(String, String),
241 MissingPath,
242 Unknown,
243 PermissionDenied,
244 MissingApiKey,
245 MultipleAtStdin,
246 InlineFlightHasSpace,
247 InlineFlightMissingImage,
248 InlineFlightInvalidName(String),
249 InlineFlightUnknownItem(String),
250 InlineFlightMissingValue(String),
251 ParseInt(std::num::ParseIntError),
252 StrumParse(strum::ParseError),
253 FlightsInUse(Vec<String>),
254 EndpointInvalidFlight(String),
255 OneOff(String),
256 Clap(clap::Error),
257}
258
259impl CliErrorKind {
260 fn print(&self) {
261 use CliErrorKind::*;
262
263 match self {
264 OneOff(msg) => {
265 cli_eprintln!("{msg}");
266 }
267 FlightsInUse(flights) => {
268 cli_eprintln!("the following Flight Plans are referenced by a Formation Plan and cannot be deleted");
269 for f in flights {
270 cli_eprintln!(@Yellow, "\t{f}");
271 }
272 cli_eprintln!("");
273 cli_eprint!("(hint: override this check and force delete with '");
274 cli_eprint!(@Yellow, "--force");
275 cli_eprintln!("' which will also remove the Flight Plan from the Formation Plan)");
276 }
277 EndpointInvalidFlight(flight) => {
278 cli_eprint!("Flight Plan '");
279 cli_eprint!(@Red, "{flight}");
280 cli_eprintln!(
281 "' is referenced in an endpoint but does not exist in the local Plans"
282 );
283 }
284 ConflictingArguments(a, b) => {
285 cli_eprint!("cannot use '");
286 cli_eprint!(@Yellow, "{a}");
287 cli_eprint!("' with '");
288 cli_eprint!(@Yellow, "{b}");
289 cli_eprintln!("'");
290 cli_eprintln!(
291 "(hint: one or both arguments may have been implied from other flags)"
292 );
293 }
294 Base64Decode(e) => {
295 cli_eprintln!("base64 decode: {e}");
296 }
297 DuplicateName(name) => {
298 cli_eprint!("an item with the name '");
299 cli_eprint!(@Yellow, "{name}");
300 cli_eprintln!("' already exists");
301 }
302 NoMatchingItem(item) => {
303 cli_eprint!("the NAME or ID '");
304 cli_eprint!(@Green, "{item}");
305 cli_eprintln!("' didn't match anything");
306 }
307 AmbiguousItem(item) => {
308 cli_eprint!("the NAME or ID '");
309 cli_eprint!(@Yellow, "{item}");
310 cli_eprintln!("' is ambiguous and matches more than one item");
311 }
312 MissingPath => {
313 cli_eprintln!("missing file or directory");
314 }
315 PermissionDenied => {
316 cli_eprintln!("permission denied when accessing file or directory");
317 }
318 HexDecode(e) => {
319 cli_eprintln!("hex decode: {e}")
320 }
321 ImageReference(e) => {
322 cli_eprintln!("seaplane: {e}")
323 }
324 InvalidUtf8(e) => {
325 cli_eprintln!("invalid UTF-8: {e}")
326 }
327 StrumParse(e) => {
328 cli_eprintln!("string parse error: {e}")
329 }
330 Io(e, Some(path)) => {
331 cli_eprintln!("io: {e}");
332 cli_eprint!("\tpath: ");
333 cli_eprintln!(@Yellow, "{path:?}");
334 }
335 Io(e, None) => {
336 cli_eprintln!("io: {e}");
337 }
338 SerdeJson(e) => {
339 cli_eprintln!("json: {e}")
340 }
341 TomlDe(e) => {
342 cli_eprintln!("toml: {e}")
343 }
344 TomlSer(e) => {
345 cli_eprintln!("toml: {e}")
346 }
347 ParseInt(e) => {
348 cli_eprintln!("parse integer: {e}")
349 }
350 UnknownWithContext(e) => {
351 cli_eprintln!("unknown: {e}")
352 }
353 InvalidCliValue(a, v) => {
354 cli_eprint!("the CLI value '");
355 if let Some(val) = a {
356 cli_eprint!("--{val}=");
357 cli_eprint!(@Red, "{v}");
358 } else {
359 cli_eprint!(@Red, "{v}");
360 }
361 cli_eprintln!("' isn't valid");
362 }
363 CliArgNotUsed(a) => {
364 cli_eprint!("the CLI argument '");
365 cli_eprint!("{a}");
366 cli_eprintln!("' wasn't used but is required in this context");
367 }
368 Unknown => {
369 cli_eprintln!("unknown")
370 }
371 MissingApiKey => {
372 cli_eprintln!("no API key was found or provided")
373 }
374 MultipleAtStdin => {
375 cli_eprint!("more than one '");
376 cli_print!(@Yellow, "@-");
377 cli_println!("' values were provided and only one is allowed");
378 }
379 Seaplane(e) => match e {
380 SeaplaneError::ApiResponse(ae) => {
381 cli_eprintln!("{ae}");
382 if ae.kind == ApiErrorKind::BadRequest
383 && ae.message.contains("`force` flag was not set")
384 {
385 cli_eprint!("(hint: set the force parameter with '");
386 cli_eprint!(@Yellow, "--force");
387 cli_eprintln!("')");
388 }
389 }
390 _ => {
391 cli_eprintln!("Seaplane API: {e}")
392 }
393 },
394 ExistingValue(value) => {
395 cli_eprintln!("{value} already exists");
396 }
397 InlineFlightUnknownItem(item) => {
398 cli_eprintln!("{item} is not a valid INLINE-FLIGHT-SPEC item (valid keys are: name, image, maximum, minimum, api-permission, architecture)");
399 }
400 InlineFlightInvalidName(name) => {
401 cli_eprintln!("'{name}' is not a valid Flight Plan name");
402 }
403 InlineFlightHasSpace => {
404 cli_eprintln!("INLINE-FLIGHT-SPEC contains a space ' ' which isn't allowed.");
405 }
406 InlineFlightMissingImage => {
407 cli_eprintln!(
408 "INLINE-FLIGHT-SPEC missing image=... key and value which is required"
409 );
410 }
411 InlineFlightMissingValue(key) => {
412 cli_eprintln!("INLINE-FLIGHT-SPEC missing a value for the key {key}");
413 }
414 Clap(e) => {
415 cli_eprintln!("{e}")
416 }
417 }
418 }
419
420 pub fn into_err(self) -> CliError { CliError { kind: self, ..Default::default() } }
421
422 #[cfg(test)]
423 pub fn is_parse_int(&self) -> bool { matches!(self, Self::ParseInt(_)) }
424 #[cfg(test)]
425 pub fn is_strum_parse(&self) -> bool { matches!(self, Self::StrumParse(_)) }
426}
427
428impl PartialEq<Self> for CliErrorKind {
430 fn eq(&self, rhs: &Self) -> bool {
431 use CliErrorKind::*;
432
433 match self {
434 OneOff(_) => matches!(rhs, OneOff(_)),
435 EndpointInvalidFlight(_) => matches!(rhs, EndpointInvalidFlight(_)),
436 AmbiguousItem(_) => matches!(rhs, AmbiguousItem(_)),
437 Io(_, _) => matches!(rhs, Io(_, _)),
438 DuplicateName(_) => matches!(rhs, DuplicateName(_)),
439 MissingApiKey => matches!(rhs, MissingApiKey),
440 MissingPath => matches!(rhs, MissingPath),
441 NoMatchingItem(_) => matches!(rhs, NoMatchingItem(_)),
442 PermissionDenied => matches!(rhs, PermissionDenied),
443 MultipleAtStdin => matches!(rhs, MultipleAtStdin),
444 Seaplane(_) => matches!(rhs, Seaplane(_)),
445 SerdeJson(_) => matches!(rhs, SerdeJson(_)),
446 TomlSer(_) => matches!(rhs, TomlSer(_)),
447 TomlDe(_) => matches!(rhs, TomlDe(_)),
448 Unknown => matches!(rhs, Unknown),
449 UnknownWithContext(_) => matches!(rhs, UnknownWithContext(_)),
450 ExistingValue(_) => matches!(rhs, ExistingValue(_)),
451 ImageReference(_) => matches!(rhs, ImageReference(_)),
452 CliArgNotUsed(_) => matches!(rhs, CliArgNotUsed(_)),
453 InvalidCliValue(_, _) => matches!(rhs, InvalidCliValue(_, _)),
454 StrumParse(_) => matches!(rhs, StrumParse(_)),
455 Base64Decode(_) => matches!(rhs, Base64Decode(_)),
456 InvalidUtf8(_) => matches!(rhs, InvalidUtf8(_)),
457 HexDecode(_) => matches!(rhs, HexDecode(_)),
458 ConflictingArguments(_, _) => matches!(rhs, ConflictingArguments(_, _)),
459 InlineFlightUnknownItem(_) => matches!(rhs, InlineFlightUnknownItem(_)),
460 InlineFlightInvalidName(_) => matches!(rhs, InlineFlightInvalidName(_)),
461 InlineFlightHasSpace => matches!(rhs, InlineFlightHasSpace),
462 InlineFlightMissingImage => matches!(rhs, InlineFlightMissingImage),
463 InlineFlightMissingValue(_) => matches!(rhs, InlineFlightMissingValue(_)),
464 ParseInt(_) => matches!(rhs, ParseInt(_)),
465 FlightsInUse(_) => matches!(rhs, FlightsInUse(_)),
466 Clap(_) => matches!(rhs, Clap(_)),
467 }
468 }
469}
470
471impl CliError {
472 pub fn print(&self) {
474 if log_level() <= &LogLevel::Error {
475 {
477 let mut ptr = eprinter();
478 ptr.set_color(Color::Red);
479 let _ = write!(ptr, "error: ");
480 ptr.reset();
481 }
482
483 self.kind.print();
485
486 let mut ptr = eprinter();
488 for ColorString { color, msg } in &self.context {
489 if let Some(c) = color {
490 ptr.set_color(*c);
491 }
492 let _ = write!(ptr, "{msg}");
493 ptr.reset();
494 }
495 }
496 }
497
498 pub fn exit(&self) -> ! {
499 self.print();
500 std::process::exit(self.status.unwrap_or(1))
502 }
503
504 pub fn kind(&self) -> &CliErrorKind { &self.kind }
505}