use shep_core::exit;
use shep_core::protocol::RpcErrorCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ExitCode {
#[cfg_attr(windows, allow(dead_code))]
Success = 0,
Failure = exit::FAILURE,
Usage = exit::USAGE,
NotFound = exit::NOT_FOUND,
InvalidConfig = exit::INVALID_CONFIG,
#[cfg_attr(windows, allow(dead_code))]
DaemonUnreachable = exit::DAEMON_UNREACHABLE,
ProtocolMismatch = exit::PROTOCOL_MISMATCH,
SpawnFailed = exit::SPAWN_FAILED,
DeadlineExceeded = exit::DEADLINE_EXCEEDED,
Internal = exit::INTERNAL,
#[cfg_attr(windows, allow(dead_code))]
DaemonAlreadyRunning = 10,
FlockEmpty = 11,
VersionSkew = 12,
Unsupported = exit::UNSUPPORTED,
}
impl ExitCode {
#[must_use]
pub const fn code_str(self) -> &'static str {
match self {
Self::Success => "success",
Self::Failure => "failure",
Self::Usage => "usage",
Self::NotFound => "not_found",
Self::InvalidConfig => "invalid_config",
Self::DaemonUnreachable => "daemon_unreachable",
Self::ProtocolMismatch => "protocol_mismatch",
Self::SpawnFailed => "spawn_failed",
Self::DeadlineExceeded => "deadline_exceeded",
Self::Internal => "internal",
Self::DaemonAlreadyRunning => "daemon_already_running",
Self::FlockEmpty => "flock_empty",
Self::VersionSkew => "version_skew",
Self::Unsupported => "unsupported",
}
}
}
impl ExitCode {
const fn from_shared(code: u8) -> Self {
match code {
exit::USAGE => Self::Usage,
exit::NOT_FOUND => Self::NotFound,
exit::INVALID_CONFIG => Self::InvalidConfig,
exit::DAEMON_UNREACHABLE => Self::DaemonUnreachable,
exit::PROTOCOL_MISMATCH => Self::ProtocolMismatch,
exit::SPAWN_FAILED => Self::SpawnFailed,
exit::DEADLINE_EXCEEDED => Self::DeadlineExceeded,
exit::INTERNAL => Self::Internal,
exit::UNSUPPORTED => Self::Unsupported,
_ => Self::Failure,
}
}
}
impl From<RpcErrorCode> for ExitCode {
fn from(code: RpcErrorCode) -> Self {
Self::from_shared(code.exit_code())
}
}
impl From<&shep_client::ConnectError> for ExitCode {
fn from(err: &shep_client::ConnectError) -> Self {
Self::from_shared(err.exit_code())
}
}
impl From<&shep_client::RequestError> for ExitCode {
fn from(err: &shep_client::RequestError) -> Self {
Self::from_shared(err.exit_code())
}
}
impl From<&shep_client::LinkLost> for ExitCode {
fn from(lost: &shep_client::LinkLost) -> Self {
Self::from_shared(lost.exit_code())
}
}
impl From<&shep_client::dogs::ShepherdError> for ExitCode {
fn from(err: &shep_client::dogs::ShepherdError) -> Self {
Self::from_shared(err.exit_code())
}
}
impl From<&shep_client::spawn::SpawnError> for ExitCode {
fn from(err: &shep_client::spawn::SpawnError) -> Self {
Self::from_shared(err.exit_code())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_rpc_error_code_maps_to_a_distinct_nonzero_exit_code() {
let codes = shep_core::protocol::RpcErrorCode::ALL;
let mapped: Vec<u8> = codes.iter().map(|c| ExitCode::from(*c) as u8).collect();
assert!(
mapped.iter().all(|&c| c != 0),
"no error may map to Success"
);
let unique: std::collections::HashSet<_> = mapped.iter().collect();
assert_eq!(
unique.len(),
mapped.len(),
"distinct causes need distinct exit codes: {mapped:?}"
);
}
#[test]
fn every_shared_code_reads_back_as_the_variant_it_defines() {
let shared = [
ExitCode::Failure,
ExitCode::Usage,
ExitCode::NotFound,
ExitCode::InvalidConfig,
ExitCode::DaemonUnreachable,
ExitCode::ProtocolMismatch,
ExitCode::SpawnFailed,
ExitCode::DeadlineExceeded,
ExitCode::Internal,
ExitCode::Unsupported,
];
for code in shared {
assert_eq!(ExitCode::from_shared(code as u8), code);
}
}
#[test]
fn every_exit_code_has_its_own_machine_readable_spelling() {
let all = [
ExitCode::Success,
ExitCode::Failure,
ExitCode::Usage,
ExitCode::NotFound,
ExitCode::InvalidConfig,
ExitCode::DaemonUnreachable,
ExitCode::ProtocolMismatch,
ExitCode::SpawnFailed,
ExitCode::DeadlineExceeded,
ExitCode::Internal,
ExitCode::DaemonAlreadyRunning,
ExitCode::FlockEmpty,
ExitCode::VersionSkew,
ExitCode::Unsupported,
];
let strings: Vec<&str> = all.iter().map(|c| c.code_str()).collect();
assert!(strings.iter().all(|s| !s.is_empty()));
assert!(
strings
.iter()
.all(|s| s.chars().all(|c| c.is_ascii_lowercase() || c == '_')),
"these go on the JSON surface: {strings:?}"
);
let unique: std::collections::HashSet<_> = strings.iter().collect();
assert_eq!(
unique.len(),
strings.len(),
"duplicated spelling: {strings:?}"
);
}
#[cfg(unix)]
#[test]
fn the_already_running_exit_code_matches_the_clients_constant() {
assert_eq!(
ExitCode::DaemonAlreadyRunning as i32,
shep_client::spawn::DAEMON_ALREADY_RUNNING
);
}
}