#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc(html_logo_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/error_label/logo.png")]
#![doc(html_favicon_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/error_label/favicon.ico")]
use std::borrow::Cow;
use std::collections::hash_set::HashSet;
use std::error::Error;
use std::fmt;
use std::io::ErrorKind;
use std::iter::successors;
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct ErrorLabel(Cow<'static, str>);
impl ErrorLabel {
#[must_use]
pub fn from_parts(parts: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
let mut parts = parts.into_iter().filter(|v| !v.as_ref().is_empty());
let mut result = match parts.next() {
Some(first) => String::from(first.as_ref()),
None => return Self::default(),
};
for part in parts {
result.push('.');
result.push_str(part.as_ref());
}
Self(Cow::Owned(result))
}
#[must_use]
pub fn from_error_chain(error: &(dyn Error + 'static), get_label: impl Fn(&(dyn Error + 'static)) -> Option<Self>) -> Self {
if error.source().is_none() {
return get_label(error).unwrap_or_default();
}
let mut seen = HashSet::new();
let chain = successors(Some(error), |e| (*e).source())
.filter_map(&get_label)
.filter(|label| seen.insert(label.clone()));
Self::from_parts(chain)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_cow(self) -> Cow<'static, str> {
self.0
}
}
impl fmt::Display for ErrorLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl From<&'static str> for ErrorLabel {
fn from(s: &'static str) -> Self {
Self(Cow::Borrowed(s))
}
}
impl From<String> for ErrorLabel {
fn from(s: String) -> Self {
Self(Cow::Owned(s))
}
}
impl From<Cow<'static, str>> for ErrorLabel {
fn from(s: Cow<'static, str>) -> Self {
Self(s)
}
}
impl From<ErrorLabel> for Cow<'static, str> {
fn from(s: ErrorLabel) -> Self {
s.0
}
}
impl PartialEq<str> for ErrorLabel {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for ErrorLabel {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl AsRef<str> for ErrorLabel {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<ErrorKind> for ErrorLabel {
fn from(kind: ErrorKind) -> Self {
match kind {
ErrorKind::NotFound => "not_found".into(),
ErrorKind::PermissionDenied => "permission_denied".into(),
ErrorKind::ConnectionRefused => "connection_refused".into(),
ErrorKind::ConnectionReset => "connection_reset".into(),
ErrorKind::HostUnreachable => "host_unreachable".into(),
ErrorKind::NetworkUnreachable => "network_unreachable".into(),
ErrorKind::ConnectionAborted => "connection_aborted".into(),
ErrorKind::NotConnected => "not_connected".into(),
ErrorKind::AddrInUse => "addr_in_use".into(),
ErrorKind::AddrNotAvailable => "addr_not_available".into(),
ErrorKind::NetworkDown => "network_down".into(),
ErrorKind::BrokenPipe => "broken_pipe".into(),
ErrorKind::AlreadyExists => "already_exists".into(),
ErrorKind::WouldBlock => "would_block".into(),
ErrorKind::NotADirectory => "not_a_directory".into(),
ErrorKind::IsADirectory => "is_a_directory".into(),
ErrorKind::DirectoryNotEmpty => "directory_not_empty".into(),
ErrorKind::ReadOnlyFilesystem => "read_only_filesystem".into(),
ErrorKind::StaleNetworkFileHandle => "stale_network_file_handle".into(),
ErrorKind::InvalidInput => "invalid_input".into(),
ErrorKind::InvalidData => "invalid_data".into(),
ErrorKind::TimedOut => "timed_out".into(),
ErrorKind::WriteZero => "write_zero".into(),
ErrorKind::StorageFull => "storage_full".into(),
ErrorKind::NotSeekable => "not_seekable".into(),
ErrorKind::QuotaExceeded => "quota_exceeded".into(),
ErrorKind::FileTooLarge => "file_too_large".into(),
ErrorKind::ResourceBusy => "resource_busy".into(),
ErrorKind::ExecutableFileBusy => "executable_file_busy".into(),
ErrorKind::Deadlock => "deadlock".into(),
ErrorKind::CrossesDevices => "crosses_devices".into(),
ErrorKind::TooManyLinks => "too_many_links".into(),
ErrorKind::InvalidFilename => "invalid_filename".into(),
ErrorKind::ArgumentListTooLong => "argument_list_too_long".into(),
ErrorKind::Interrupted => "interrupted".into(),
ErrorKind::Unsupported => "unsupported".into(),
ErrorKind::UnexpectedEof => "unexpected_eof".into(),
ErrorKind::OutOfMemory => "out_of_memory".into(),
ErrorKind::Other => "other".into(),
_ => kind.to_string().replace(' ', "_").into(),
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use testing_aids::ALL_ERROR_KINDS;
use super::*;
#[test]
fn from_static_str() {
let label = ErrorLabel::from("static_label");
assert_eq!(label, "static_label");
assert_eq!(label.as_str(), "static_label");
}
#[test]
fn from_string() {
let label = ErrorLabel::from(String::from("owned_label"));
assert_eq!(label, "owned_label");
assert_eq!(label.as_str(), "owned_label");
}
#[test]
fn from_cow() {
let cow: Cow<'static, str> = Cow::Owned(String::from("cow_label"));
let label = ErrorLabel::from(cow);
assert_eq!(label, "cow_label");
}
#[test]
fn display() {
let label = ErrorLabel::from("display_test");
assert_eq!(format!("{label}"), "display_test");
}
#[test]
fn as_ref_str() {
let label = ErrorLabel::from("as_ref_test");
let s: &str = label.as_ref();
assert_eq!(s, "as_ref_test");
}
#[test]
fn from_parts_multiple() {
let label = ErrorLabel::from_parts(["http", "client", "", "timeout"]);
assert_eq!(label, "http.client.timeout");
}
#[test]
fn from_parts_single() {
let label = ErrorLabel::from_parts(["only"]);
assert_eq!(label, "only");
}
#[test]
fn from_parts_empty() {
let label = ErrorLabel::from_parts(std::iter::empty::<&str>());
assert_eq!(label, "");
}
#[test]
fn from_parts_owned_strings() {
let parts = vec![String::from("a"), String::from("b")];
let label = ErrorLabel::from_parts(parts);
assert_eq!(label, "a.b");
}
#[test]
fn into_cow_borrowed() {
let label = ErrorLabel::from("static_value");
let cow = label.into_cow();
assert!(matches!(cow, Cow::Borrowed("static_value")));
}
#[test]
fn into_cow_owned() {
let label = ErrorLabel::from(String::from("owned_value"));
let cow = label.into_cow();
assert!(matches!(cow, Cow::Owned(_)));
assert_eq!(cow, "owned_value");
}
#[test]
fn from_error_chain_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let label = ErrorLabel::from_error_chain(&io_err, io_get_label);
assert_eq!(label, "connection_refused");
}
#[test]
fn from_error_chain_unrecognized_error() {
let err: Box<dyn Error + Send + Sync> = "unknown".into();
let label = ErrorLabel::from_error_chain(err.as_ref(), io_get_label);
assert_eq!(label, "");
}
#[cfg_attr(miri, ignore)]
#[test]
fn error_kind_all_variants() {
let kind_map: Vec<_> = ALL_ERROR_KINDS.iter().map(|v| (*v, ErrorLabel::from(*v))).collect();
insta::assert_debug_snapshot!(kind_map);
}
fn io_get_label(error: &(dyn Error + 'static)) -> Option<ErrorLabel> {
error.downcast_ref::<std::io::Error>().map(|err| err.kind().into())
}
}