use camino::Utf8PathBuf;
use thiserror::Error;
use crate::domain::debt::DebtError;
use crate::domain::marker::MarkerError;
#[derive(Debug, Error)]
pub enum AppError {
#[error("usage: {0}")]
Usage(String),
#[error("{count} violation(s) found")]
Violations {
count: usize,
},
#[error("missing manifest: {0}")]
ManifestMissing(Utf8PathBuf),
#[error("invalid manifest: {0}")]
ManifestInvalid(String),
#[error(transparent)]
Marker(#[from] MarkerError),
#[error(transparent)]
Debt(#[from] DebtError),
#[error("{0}")]
Refused(String),
#[error("git: {message}")]
Git {
message: String,
code: u8,
},
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl AppError {
#[must_use]
pub fn exit_code(&self) -> u8 {
match self {
Self::Violations { .. } => 1,
Self::Usage(_) => 64,
Self::ManifestInvalid(_)
| Self::Marker(_)
| Self::Debt(
DebtError::Shape(_) | DebtError::Malformed { .. } | DebtError::TwoFormats,
) => 65,
Self::ManifestMissing(_) => 66,
Self::Refused(_) | Self::Debt(_) => 73,
Self::Git { code, .. } => *code,
Self::Io(e) if e.kind() == std::io::ErrorKind::NotFound => 66,
Self::Io(e) if e.kind() == std::io::ErrorKind::PermissionDenied => 77,
Self::Io(_) => 74,
Self::Other(_) => 70,
}
}
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::Usage(_) => "Usage",
Self::Violations { .. } => "Violations",
Self::ManifestMissing(_) => "ManifestMissing",
Self::ManifestInvalid(_) => "ManifestInvalid",
Self::Marker(_) => "Marker",
Self::Debt(_) => "Debt",
Self::Refused(_) => "Refused",
Self::Git { .. } => "Git",
Self::Io(_) => "Io",
Self::Other(_) => "Other",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn io(kind: std::io::ErrorKind) -> AppError {
AppError::Io(std::io::Error::from(kind))
}
#[test]
fn violations_are_one() {
assert_eq!(AppError::Violations { count: 3 }.exit_code(), 1);
}
#[test]
fn usage_is_sixty_four() {
assert_eq!(AppError::Usage("bad target".into()).exit_code(), 64);
}
#[test]
fn invalid_manifest_is_sixty_five() {
assert_eq!(
AppError::ManifestInvalid("schema_version 3".into()).exit_code(),
65
);
}
#[test]
fn malformed_marker_is_sixty_five() {
assert_eq!(AppError::Marker(MarkerError::Malformed).exit_code(), 65);
}
#[test]
fn a_malformed_debt_file_is_sixty_five_and_a_refused_debt_verb_is_seventy_three() {
assert_eq!(
AppError::Debt(DebtError::Shape("not yaml".into())).exit_code(),
65
);
assert_eq!(AppError::Debt(DebtError::TwoFormats).exit_code(), 65);
assert_eq!(AppError::Debt(DebtError::AlreadyBaselined).exit_code(), 73);
assert_eq!(AppError::Debt(DebtError::NothingToMigrate).exit_code(), 73);
}
#[test]
fn missing_manifest_is_sixty_six() {
assert_eq!(
AppError::ManifestMissing("x/.spec-driven-docs/manifest.json".into()).exit_code(),
66
);
}
#[test]
fn refused_is_seventy_three() {
assert_eq!(
AppError::Refused("apply aborted; the target was restored".into()).exit_code(),
73
);
}
#[test]
fn not_found_io_is_sixty_six() {
assert_eq!(io(std::io::ErrorKind::NotFound).exit_code(), 66);
}
#[test]
fn permission_denied_io_is_seventy_seven() {
assert_eq!(io(std::io::ErrorKind::PermissionDenied).exit_code(), 77);
}
#[test]
fn other_io_is_seventy_four() {
assert_eq!(io(std::io::ErrorKind::BrokenPipe).exit_code(), 74);
}
#[test]
fn anyhow_is_seventy() {
assert_eq!(AppError::Other(anyhow::anyhow!("boom")).exit_code(), 70);
}
}