Skip to main content

ready_set_sdk/
exit_code.rs

1//! Documented process exit codes.
2//!
3//! Mirrors
4//! [`docs/contracts/exit-codes.md`](https://github.com/pulsearc-ai/ready-set/blob/main/docs/contracts/exit-codes.md)
5//! exactly. Adding a variant requires a corresponding entry in that contract
6//! document.
7
8use crate::error::Error;
9
10/// Documented process exit codes returned by `ready-set` commands.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum ExitCode {
14    /// Success.
15    Ok,
16    /// The user's input was invalid.
17    UserError,
18    /// An I/O, permission, or environmental error.
19    SystemError,
20    /// A required external tool was not found on PATH.
21    DependencyMissing,
22    /// A command requiring a cargo workspace was invoked outside one.
23    NotCargoWorkspace,
24    /// A plugin violated the dispatcher↔plugin contract.
25    ContractViolation,
26    /// The dispatcher could not resolve the requested subcommand.
27    UnknownSubcommand,
28    /// A child process was terminated by signal `N`. The numeric exit
29    /// code emitted to the OS is `128 + N`, following the POSIX shell
30    /// convention. Only meaningful on Unix; Windows children always
31    /// have `ExitStatus::code() == Some(_)`.
32    Signaled(u8),
33}
34
35impl ExitCode {
36    /// Return the numeric exit code as a `u8`.
37    ///
38    /// `Signaled(n)` returns `128 + n`, saturating at `255` for
39    /// hypothetical signal numbers that would overflow.
40    #[must_use]
41    pub const fn as_u8(self) -> u8 {
42        match self {
43            Self::Ok => 0,
44            Self::UserError => 1,
45            Self::SystemError => 2,
46            Self::DependencyMissing => 3,
47            Self::NotCargoWorkspace => 4,
48            Self::ContractViolation => 5,
49            Self::UnknownSubcommand => 127,
50            Self::Signaled(n) => 128_u8.saturating_add(n),
51        }
52    }
53}
54
55impl From<ExitCode> for std::process::ExitCode {
56    fn from(value: ExitCode) -> Self {
57        Self::from(value.as_u8())
58    }
59}
60
61impl From<&Error> for ExitCode {
62    fn from(value: &Error) -> Self {
63        match value {
64            Error::TomlParse(_) | Error::JsonParse(_) => Self::UserError,
65            Error::MissingDependency { .. } => Self::DependencyMissing,
66            Error::ContractViolation(_) => Self::ContractViolation,
67            // `Error` is `#[non_exhaustive]`; the wildcard catches both the
68            // current `Io`/`Other` variants and any added in future minor
69            // releases.
70            _ => Self::SystemError,
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn numeric_values_match_contract() {
81        assert_eq!(ExitCode::Ok.as_u8(), 0);
82        assert_eq!(ExitCode::UserError.as_u8(), 1);
83        assert_eq!(ExitCode::SystemError.as_u8(), 2);
84        assert_eq!(ExitCode::DependencyMissing.as_u8(), 3);
85        assert_eq!(ExitCode::NotCargoWorkspace.as_u8(), 4);
86        assert_eq!(ExitCode::ContractViolation.as_u8(), 5);
87        assert_eq!(ExitCode::UnknownSubcommand.as_u8(), 127);
88        assert_eq!(ExitCode::Signaled(0).as_u8(), 128);
89        assert_eq!(ExitCode::Signaled(2).as_u8(), 130); // SIGINT
90        assert_eq!(ExitCode::Signaled(15).as_u8(), 143); // SIGTERM
91        assert_eq!(ExitCode::Signaled(255).as_u8(), 255); // saturates
92    }
93
94    #[test]
95    fn maps_errors_to_codes() {
96        let io = Error::Io(std::io::Error::other("nope"));
97        assert_eq!(ExitCode::from(&io), ExitCode::SystemError);
98
99        let dep = Error::MissingDependency {
100            name: "git".into(),
101            hint: None,
102        };
103        assert_eq!(ExitCode::from(&dep), ExitCode::DependencyMissing);
104
105        let contract = Error::contract("bad");
106        assert_eq!(ExitCode::from(&contract), ExitCode::ContractViolation);
107
108        let toml = Error::TomlParse("oops".into());
109        assert_eq!(ExitCode::from(&toml), ExitCode::UserError);
110    }
111}