Skip to main content

github_app_cli/
error.rs

1//! Command-level errors mapped to the workspace exit-code contract.
2
3use nils_common::cli_contract::exit;
4
5/// A command failure carrying a stable machine `code` (for the JSON error
6/// envelope) and a BSD sysexits-aligned process `exit_code`.
7#[derive(Debug, Clone)]
8pub struct CommandError {
9    pub exit_code: i32,
10    pub code: String,
11    pub message: String,
12    pub hint: Option<String>,
13}
14
15impl CommandError {
16    fn new(exit_code: i32, code: impl Into<String>, message: impl Into<String>) -> Self {
17        Self {
18            exit_code,
19            code: code.into(),
20            message: message.into(),
21            hint: None,
22        }
23    }
24
25    /// Bad CLI usage discovered after parse (exit `64`).
26    pub fn usage(code: impl Into<String>, message: impl Into<String>) -> Self {
27        Self::new(exit::USAGE, code, message)
28    }
29
30    /// Invalid or missing input data, e.g. an unreadable or malformed key
31    /// (exit `65`).
32    pub fn data(code: impl Into<String>, message: impl Into<String>) -> Self {
33        Self::new(exit::DATA, code, message)
34    }
35
36    /// A required service or resource is unavailable, e.g. a network or GitHub
37    /// API failure (exit `69`).
38    pub fn unavailable(code: impl Into<String>, message: impl Into<String>) -> Self {
39        Self::new(exit::UNAVAILABLE, code, message)
40    }
41
42    /// Attach an optional human-readable hint.
43    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
44        self.hint = Some(hint.into());
45        self
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use pretty_assertions::assert_eq;
53
54    #[test]
55    fn exit_codes_match_category() {
56        assert_eq!(CommandError::usage("c", "m").exit_code, 64);
57        assert_eq!(CommandError::data("c", "m").exit_code, 65);
58        assert_eq!(CommandError::unavailable("c", "m").exit_code, 69);
59    }
60}