use serde::Serialize;
use std::error::Error as StdError;
use std::fmt;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, UpkeepError>;
#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
Io,
Json,
Metadata,
Http,
Rustsec,
Semver,
Utf8,
ExternalCommand,
MissingTool,
InvalidData,
TaskFailed,
Config,
Concurrency,
#[allow(dead_code)]
Internal,
}
#[derive(Debug, Error)]
pub enum UpkeepError {
#[error("{message}")]
Message { code: ErrorCode, message: String },
#[error("{message}")]
Context {
code: ErrorCode,
message: String,
#[source]
source: Box<dyn StdError + Send + Sync>,
},
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("cargo metadata error: {0}")]
Metadata(#[from] cargo_metadata::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("RustSec error: {0}")]
Rustsec(#[from] rustsec::Error),
#[error("semver error: {0}")]
Semver(#[from] semver::Error),
#[error("UTF-8 error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("tokio semaphore error: {0}")]
Acquire(#[from] tokio::sync::AcquireError),
}
impl UpkeepError {
pub fn code(&self) -> ErrorCode {
match self {
UpkeepError::Message { code, .. } => *code,
UpkeepError::Context { code, .. } => *code,
UpkeepError::Io(_) => ErrorCode::Io,
UpkeepError::Json(_) => ErrorCode::Json,
UpkeepError::Metadata(_) => ErrorCode::Metadata,
UpkeepError::Http(_) => ErrorCode::Http,
UpkeepError::Rustsec(_) => ErrorCode::Rustsec,
UpkeepError::Semver(_) => ErrorCode::Semver,
UpkeepError::Utf8(_) => ErrorCode::Utf8,
UpkeepError::Acquire(_) => ErrorCode::Concurrency,
}
}
pub fn message(code: ErrorCode, message: impl Into<String>) -> Self {
UpkeepError::Message {
code,
message: message.into(),
}
}
pub fn context<E>(code: ErrorCode, message: impl Into<String>, source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
UpkeepError::Context {
code,
message: message.into(),
source: Box::new(source),
}
}
}
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub code: ErrorCode,
pub message: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub causes: Vec<String>,
}
impl From<&UpkeepError> for ErrorResponse {
fn from(error: &UpkeepError) -> Self {
let mut causes = Vec::new();
let mut current: Option<&(dyn StdError + 'static)> = error.source();
while let Some(cause) = current {
causes.push(cause.to_string());
current = cause.source();
}
Self {
code: error.code(),
message: error.to_string(),
causes,
}
}
}
pub fn eprint_error_json(error: &UpkeepError) {
let response = ErrorResponse::from(error);
match serde_json::to_string_pretty(&response) {
Ok(payload) => eprintln!("{payload}"),
Err(err) => eprintln!(
"{}: {} (serialization error: {err})",
response.code, response.message
),
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
ErrorCode::Io => "io",
ErrorCode::Json => "json",
ErrorCode::Metadata => "metadata",
ErrorCode::Http => "http",
ErrorCode::Rustsec => "rustsec",
ErrorCode::Semver => "semver",
ErrorCode::Utf8 => "utf8",
ErrorCode::ExternalCommand => "external_command",
ErrorCode::MissingTool => "missing_tool",
ErrorCode::InvalidData => "invalid_data",
ErrorCode::TaskFailed => "task_failed",
ErrorCode::Config => "config",
ErrorCode::Concurrency => "concurrency",
ErrorCode::Internal => "internal",
};
write!(f, "{label}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_code_serializes_and_displays() {
let value = serde_json::to_value(ErrorCode::ExternalCommand).unwrap();
assert_eq!(value, serde_json::Value::String("external_command".into()));
assert_eq!(ErrorCode::ExternalCommand.to_string(), "external_command");
}
#[test]
fn message_error_preserves_code_and_message() {
let err = UpkeepError::message(ErrorCode::InvalidData, "bad input");
assert_eq!(err.code(), ErrorCode::InvalidData);
assert_eq!(err.to_string(), "bad input");
let response = ErrorResponse::from(&err);
assert_eq!(response.code, ErrorCode::InvalidData);
assert_eq!(response.message, "bad input");
assert!(response.causes.is_empty());
}
#[test]
fn context_error_includes_causes() {
let source = std::io::Error::new(std::io::ErrorKind::Other, "disk full");
let err = UpkeepError::context(ErrorCode::Io, "write failed", source);
assert_eq!(err.code(), ErrorCode::Io);
assert_eq!(err.to_string(), "write failed");
let response = ErrorResponse::from(&err);
assert_eq!(response.code, ErrorCode::Io);
assert_eq!(response.message, "write failed");
assert_eq!(response.causes, vec!["disk full".to_string()]);
}
#[test]
fn error_response_serializes_with_code_message_and_causes() {
let source = std::io::Error::new(std::io::ErrorKind::Other, "disk full");
let err = UpkeepError::context(ErrorCode::Metadata, "metadata read failed", source);
let response = ErrorResponse::from(&err);
let value = serde_json::to_value(&response).expect("serialize");
assert_eq!(value["code"], serde_json::Value::String("metadata".into()));
assert_eq!(
value["message"],
serde_json::Value::String("metadata read failed".into())
);
assert_eq!(
value["causes"],
serde_json::Value::Array(vec![serde_json::Value::String("disk full".into())])
);
}
}