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///
21/// `status` carries the HTTP status code when the error originated from a real
22/// HTTP response with a non-success status. Transport / network errors leave
23/// `status` as `None` so callers can distinguish "the server said no" from
24/// "we never reached the server". See SMOKE-TEST report Finding #1:
25/// `cellctl diff` must only treat HTTP 404 as "would create", not every
26/// `ErrorKind::Api`.
27#[derive(Debug)]
28pub struct CtlError {
29    pub kind: ErrorKind,
30    pub message: String,
31    pub status: Option<u16>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ErrorKind {
36    /// Usage / config error — bad CLI invocation, missing config, malformed YAML on read.
37    Usage = 1,
38    /// API error — cellos-server returned an error status, or the network call failed.
39    Api = 2,
40    /// Validation error — local schema check failed before the API call.
41    Validation = 3,
42}
43
44impl ErrorKind {
45    pub fn code(self) -> i32 {
46        self as i32
47    }
48}
49
50impl fmt::Display for ErrorKind {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::Usage => f.write_str("usage"),
54            Self::Api => f.write_str("api"),
55            Self::Validation => f.write_str("validation"),
56        }
57    }
58}
59
60impl CtlError {
61    pub fn usage(msg: impl Into<String>) -> Self {
62        Self {
63            kind: ErrorKind::Usage,
64            message: msg.into(),
65            status: None,
66        }
67    }
68    pub fn api(msg: impl Into<String>) -> Self {
69        Self {
70            kind: ErrorKind::Api,
71            message: msg.into(),
72            status: None,
73        }
74    }
75    pub fn validation(msg: impl Into<String>) -> Self {
76        Self {
77            kind: ErrorKind::Validation,
78            message: msg.into(),
79            status: None,
80        }
81    }
82
83    /// Attach an HTTP status code. Use this in client error paths that map
84    /// a real HTTP response into a `CtlError` so downstream callers
85    /// (e.g. `cellctl diff`'s 404→"would create" branch) can distinguish
86    /// "server returned 404" from "transport failure".
87    pub fn with_status(mut self, status: u16) -> Self {
88        self.status = Some(status);
89        self
90    }
91
92    /// Emit the error to stderr and exit with the doctrine-mandated code.
93    /// Never returns.
94    pub fn exit(&self) -> ! {
95        eprintln!("cellctl: {}: {}", self.kind, self.message);
96        process::exit(self.kind.code())
97    }
98}
99
100impl fmt::Display for CtlError {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "{}: {}", self.kind, self.message)
103    }
104}
105
106impl std::error::Error for CtlError {}
107
108/// Convenience: convert anyhow errors into a generic API error.
109impl From<anyhow::Error> for CtlError {
110    fn from(e: anyhow::Error) -> Self {
111        Self::api(e.to_string())
112    }
113}
114
115impl From<reqwest::Error> for CtlError {
116    fn from(e: reqwest::Error) -> Self {
117        Self::api(format!("http: {e}"))
118    }
119}
120
121impl From<std::io::Error> for CtlError {
122    fn from(e: std::io::Error) -> Self {
123        Self::usage(format!("io: {e}"))
124    }
125}
126
127impl From<serde_yaml::Error> for CtlError {
128    fn from(e: serde_yaml::Error) -> Self {
129        Self::validation(format!("yaml: {e}"))
130    }
131}
132
133impl From<serde_json::Error> for CtlError {
134    fn from(e: serde_json::Error) -> Self {
135        Self::api(format!("json: {e}"))
136    }
137}
138
139pub type CtlResult<T> = Result<T, CtlError>;