use std::{error::Error, fmt};
pub type Result<T> = std::result::Result<T, TuffError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
Usage,
NotFound,
Refused,
Drift,
Source,
Corrupt,
Unsupported,
Io,
Internal,
}
impl ErrorKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Usage => "usage",
Self::NotFound => "not_found",
Self::Refused => "refused",
Self::Drift => "drift",
Self::Source => "source",
Self::Corrupt => "corrupt",
Self::Unsupported => "unsupported",
Self::Io => "io",
Self::Internal => "internal",
}
}
pub fn exit_code(self) -> i32 {
match self {
Self::Usage => 2,
Self::Internal => 70,
_ => 1,
}
}
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug)]
pub struct TuffError {
kind: ErrorKind,
message: String,
hint: Option<String>,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl TuffError {
pub fn of(kind: ErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
hint: None,
source: None,
}
}
pub fn new(message: impl Into<String>) -> Self {
Self::of(ErrorKind::Internal, message)
}
pub fn usage(message: impl Into<String>) -> Self {
Self::of(ErrorKind::Usage, message)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::of(ErrorKind::NotFound, message)
}
pub fn refused(message: impl Into<String>) -> Self {
Self::of(ErrorKind::Refused, message)
}
pub fn drift(message: impl Into<String>) -> Self {
Self::of(ErrorKind::Drift, message)
}
pub fn source_failed(message: impl Into<String>) -> Self {
Self::of(ErrorKind::Source, message)
}
pub fn corrupt(message: impl Into<String>) -> Self {
Self::of(ErrorKind::Corrupt, message)
}
pub fn unsupported(message: impl Into<String>) -> Self {
Self::of(ErrorKind::Unsupported, message)
}
#[must_use]
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
#[must_use]
pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
self.source = Some(Box::new(source));
self
}
pub fn kind(&self) -> ErrorKind {
self.kind
}
pub fn message(&self) -> &str {
&self.message
}
pub fn hint(&self) -> Option<&str> {
self.hint.as_deref()
}
pub fn exit_code(&self) -> i32 {
self.kind.exit_code()
}
}
impl fmt::Display for TuffError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl Error for TuffError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|source| source.as_ref() as &(dyn Error + 'static))
}
}
impl From<std::io::Error> for TuffError {
fn from(error: std::io::Error) -> Self {
Self::of(ErrorKind::Io, error.to_string()).with_source(error)
}
}
impl From<serde_json::Error> for TuffError {
fn from(error: serde_json::Error) -> Self {
Self::of(ErrorKind::Corrupt, error.to_string()).with_source(error)
}
}
impl From<toml::de::Error> for TuffError {
fn from(error: toml::de::Error) -> Self {
Self::of(
ErrorKind::Corrupt,
format!("invalid capability manifest TOML: {error}"),
)
.with_source(error)
}
}
impl From<toml::ser::Error> for TuffError {
fn from(error: toml::ser::Error) -> Self {
Self::of(
ErrorKind::Internal,
format!("could not serialize TOML: {error}"),
)
.with_source(error)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tuff_error_displays_message() {
let err = TuffError::new("test error");
assert_eq!(format!("{}", err), "test error");
}
#[test]
fn tuff_error_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err: TuffError = io_err.into();
assert!(format!("{}", err).contains("file not found"));
assert_eq!(err.kind(), ErrorKind::Io);
assert!(err.source().is_some(), "the cause chain is preserved");
}
#[test]
fn tuff_error_from_serde_json_error() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let err: TuffError = json_err.into();
assert!(!format!("{}", err).is_empty());
assert_eq!(err.kind(), ErrorKind::Corrupt);
}
#[test]
fn exit_codes_separate_the_three_audiences() {
assert_eq!(TuffError::usage("bad flag").exit_code(), 2);
assert_eq!(TuffError::new("invariant broken").exit_code(), 70);
for error in [
TuffError::not_found("x"),
TuffError::refused("x"),
TuffError::drift("x"),
TuffError::source_failed("x"),
TuffError::corrupt("x"),
TuffError::unsupported("x"),
] {
assert_eq!(error.exit_code(), 1, "{:?}", error.kind());
}
}
#[test]
fn a_hint_is_carried_separately_from_the_message() {
let error =
TuffError::drift("'x' has local changes").with_hint("use --force to replace it");
assert_eq!(format!("{error}"), "'x' has local changes");
assert_eq!(error.hint(), Some("use --force to replace it"));
assert_eq!(error.kind().as_str(), "drift");
}
}