Skip to main content

cellos_ctl/
exit.rs

1//! Exit-code discipline — kubectl/POSIX-aligned, aligned with CellOS engineering doctrine.
2//!
3//! All non-success exits in `cellctl` MUST go through `CtlError::exit()` so the contract
4//! between the CLI and shell scripts/CI is stable:
5//!
6//! | code | meaning                                            |
7//! |------|----------------------------------------------------|
8//! | 0    | success                                            |
9//! | 1    | usage / config error (bad flag, missing file, …)   |
10//! | 2    | API error (cellos-server returned 4xx/5xx, network)|
11//! | 3    | validation error (local schema check failed)       |
12//!
13//! Errors are always written to **stderr**. Machine-readable output (JSON, names)
14//! always goes to **stdout** so it can be piped through `jq`, `xargs`, etc.
15
16use std::fmt;
17use std::process;
18
19/// Structured CLI error carrying both a message and the doctrine-mandated exit code.
20#[derive(Debug)]
21pub struct CtlError {
22    pub kind: ErrorKind,
23    pub message: String,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ErrorKind {
28    /// Usage / config error — bad CLI invocation, missing config, malformed YAML on read.
29    Usage = 1,
30    /// API error — cellos-server returned an error status, or the network call failed.
31    Api = 2,
32    /// Validation error — local schema check failed before the API call.
33    Validation = 3,
34}
35
36impl ErrorKind {
37    pub fn code(self) -> i32 {
38        self as i32
39    }
40}
41
42impl fmt::Display for ErrorKind {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Usage => f.write_str("usage"),
46            Self::Api => f.write_str("api"),
47            Self::Validation => f.write_str("validation"),
48        }
49    }
50}
51
52impl CtlError {
53    pub fn usage(msg: impl Into<String>) -> Self {
54        Self {
55            kind: ErrorKind::Usage,
56            message: msg.into(),
57        }
58    }
59    pub fn api(msg: impl Into<String>) -> Self {
60        Self {
61            kind: ErrorKind::Api,
62            message: msg.into(),
63        }
64    }
65    pub fn validation(msg: impl Into<String>) -> Self {
66        Self {
67            kind: ErrorKind::Validation,
68            message: msg.into(),
69        }
70    }
71
72    /// Emit the error to stderr and exit with the doctrine-mandated code.
73    /// Never returns.
74    pub fn exit(&self) -> ! {
75        eprintln!("cellctl: {}: {}", self.kind, self.message);
76        process::exit(self.kind.code())
77    }
78}
79
80impl fmt::Display for CtlError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(f, "{}: {}", self.kind, self.message)
83    }
84}
85
86impl std::error::Error for CtlError {}
87
88/// Convenience: convert anyhow errors into a generic API error.
89impl From<anyhow::Error> for CtlError {
90    fn from(e: anyhow::Error) -> Self {
91        Self::api(e.to_string())
92    }
93}
94
95impl From<reqwest::Error> for CtlError {
96    fn from(e: reqwest::Error) -> Self {
97        Self::api(format!("http: {e}"))
98    }
99}
100
101impl From<std::io::Error> for CtlError {
102    fn from(e: std::io::Error) -> Self {
103        Self::usage(format!("io: {e}"))
104    }
105}
106
107impl From<serde_yaml::Error> for CtlError {
108    fn from(e: serde_yaml::Error) -> Self {
109        Self::validation(format!("yaml: {e}"))
110    }
111}
112
113impl From<serde_json::Error> for CtlError {
114    fn from(e: serde_json::Error) -> Self {
115        Self::api(format!("json: {e}"))
116    }
117}
118
119pub type CtlResult<T> = Result<T, CtlError>;