use std::path::PathBuf;
pub type BoxedCause = Box<dyn std::error::Error + Send + Sync>;
fn path_or_local_io(path: &std::path::Path) -> String {
match path.as_os_str().is_empty() {
true => "local I/O".to_string(),
false => path.display().to_string(),
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("{what}: {detail}")]
Unaskable {
what: String,
detail: String,
},
#[error("{op} {target}")]
Bus {
op: &'static str,
target: String,
#[source]
source: BoxedCause,
},
#[error("{}", path_or_local_io(path))]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("{what}: {detail}")]
Malformed {
what: String,
detail: String,
#[source]
source: Option<BoxedCause>,
},
#[error("internal: {0} — please report this against zenkey-fleet")]
Internal(String),
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
impl Error {
pub fn is_unaskable(&self) -> bool {
matches!(self, Error::Unaskable { .. })
}
pub fn unaskable(what: impl Into<String>, detail: impl Into<String>) -> Error {
Error::Unaskable {
what: what.into(),
detail: detail.into(),
}
}
pub fn unaskable_from(what: impl Into<String>, cause: impl Into<BoxedCause>) -> Error {
Error::Unaskable {
what: what.into(),
detail: cause.into().to_string(),
}
}
pub fn bus(op: &'static str, target: impl Into<String>, cause: impl Into<BoxedCause>) -> Error {
Error::Bus {
op,
target: target.into(),
source: cause.into(),
}
}
pub fn malformed(what: impl Into<String>, detail: impl Into<String>) -> Error {
Error::Malformed {
what: what.into(),
detail: detail.into(),
source: None,
}
}
pub fn malformed_from(what: impl Into<String>, cause: impl Into<BoxedCause>) -> Error {
Error::malformed_with(what, "does not parse", cause)
}
pub fn malformed_with(
what: impl Into<String>,
detail: impl Into<String>,
cause: impl Into<BoxedCause>,
) -> Error {
Error::Malformed {
what: what.into(),
detail: detail.into(),
source: Some(cause.into()),
}
}
pub fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Error {
Error::Io {
path: path.into(),
source,
}
}
}
impl From<zenkey::slice::SliceError> for Error {
fn from(e: zenkey::slice::SliceError) -> Error {
Error::malformed_from("registry slice", e)
}
}
impl From<zenkey::KeyError> for Error {
fn from(e: zenkey::KeyError) -> Error {
Error::unaskable_from("key", e)
}
}
pub fn one_line(e: &(dyn std::error::Error + 'static)) -> String {
let mut out = e.to_string();
let mut cause = e.source();
while let Some(c) = cause {
let text = c.to_string();
if !out.contains(&text) {
out.push_str(": ");
out.push_str(&text);
}
cause = c.source();
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_a_refused_input_is_unaskable() {
assert!(Error::unaskable("v1/$*/**", "is not a key expression").is_unaskable());
assert!(Error::unaskable_from("--old-root", "bad expr").is_unaskable());
assert!(!Error::bus("subscribe", "v1/**", "no route").is_unaskable());
assert!(!Error::io("/tmp/x", std::io::Error::other("nope")).is_unaskable());
assert!(!Error::malformed(".zrec", "is not a header").is_unaskable());
assert!(!Error::Internal("a bug".into()).is_unaskable());
}
#[test]
fn no_display_is_blank() {
let cases = [
Error::io(
std::path::PathBuf::new(),
std::io::Error::other("disk full"),
),
Error::io("/tmp/x", std::io::Error::other("disk full")),
Error::bus("subscribe", "v1/**", "no route"),
Error::unaskable_from("v1/$*/**", "`*` may only follow `/`"),
Error::Internal("something".into()),
];
for e in &cases {
assert!(!e.to_string().is_empty(), "blank Display: {e:?}");
assert!(!one_line(e).starts_with(':'), "blank head: {}", one_line(e));
}
assert_eq!(
one_line(&Error::io(
std::path::PathBuf::new(),
std::io::Error::other("disk full")
)),
"local I/O: disk full"
);
}
#[test]
fn display_never_repeats_its_own_source() {
use std::error::Error as _;
let io = Error::io("/tmp/x", std::io::Error::other("no such thing"));
assert_eq!(io.to_string(), "/tmp/x");
assert_eq!(io.source().unwrap().to_string(), "no such thing");
assert_eq!(one_line(&io), "/tmp/x: no such thing");
let bus = Error::bus("subscribe", "v1/**", "no route to host");
assert_eq!(bus.to_string(), "subscribe v1/**");
assert_eq!(one_line(&bus), "subscribe v1/**: no route to host");
let refused = Error::unaskable_from("v1/$*/**", "`*` may only follow `/`");
assert!(refused.source().is_none());
assert_eq!(one_line(&refused), refused.to_string());
}
#[test]
fn a_bad_slice_is_malformed_not_unaskable() {
let e: Error = zenkey::parse_slice("this is not = = toml")
.unwrap_err()
.into();
assert!(matches!(e, Error::Malformed { .. }));
assert!(!e.is_unaskable());
}
}