use std::{fmt, sync::Arc};
use thiserror::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RegistryOperation {
PrepareRegistration,
ActivateRegistration,
CloseRegistration,
PrepareSubscription,
ActivateSubscription,
CloseSubscription,
Directory,
}
impl fmt::Display for RegistryOperation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::PrepareRegistration => "prepare registration",
Self::ActivateRegistration => "activate registration",
Self::CloseRegistration => "close registration",
Self::PrepareSubscription => "prepare subscription",
Self::ActivateSubscription => "activate subscription",
Self::CloseSubscription => "close subscription",
Self::Directory => "access directory",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RegistryErrorKind {
Unavailable,
Timeout,
Unauthorized,
InvalidResource,
Conflict,
Cancelled,
CleanupAborted,
Internal,
}
impl RegistryErrorKind {
pub const fn is_retryable(self) -> bool {
matches!(self, Self::Unavailable | Self::Timeout)
}
}
impl fmt::Display for RegistryErrorKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Unavailable => "unavailable",
Self::Timeout => "timeout",
Self::Unauthorized => "unauthorized",
Self::InvalidResource => "invalid resource",
Self::Conflict => "conflict",
Self::Cancelled => "cancelled",
Self::CleanupAborted => "cleanup aborted",
Self::Internal => "internal",
})
}
}
#[derive(Error, Clone)]
#[non_exhaustive]
#[error("registry {operation} failed ({kind}): {message}")]
pub struct RegistryError {
operation: RegistryOperation,
kind: RegistryErrorKind,
message: Arc<str>,
#[source]
source: Arc<dyn std::error::Error + Send + Sync + 'static>,
}
impl RegistryError {
pub fn new<E>(operation: RegistryOperation, kind: RegistryErrorKind, source: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self {
operation,
kind,
message: Arc::from(default_message(kind)),
source: Arc::new(source),
}
}
pub fn message(
operation: RegistryOperation,
kind: RegistryErrorKind,
message: impl Into<String>,
) -> Self {
let message = message.into();
Self {
operation,
kind,
message: Arc::from(message.as_str()),
source: Arc::new(RegistryMessage(message)),
}
}
pub const fn operation(&self) -> RegistryOperation {
self.operation
}
pub const fn kind(&self) -> RegistryErrorKind {
self.kind
}
pub fn safe_message(&self) -> &str {
&self.message
}
pub const fn is_retryable(&self) -> bool {
self.kind.is_retryable()
}
pub fn source_ref(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
self.source.as_ref()
}
}
impl fmt::Debug for RegistryError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("RegistryError")
.field("operation", &self.operation)
.field("kind", &self.kind)
.field("message", &self.message)
.finish_non_exhaustive()
}
}
const fn default_message(kind: RegistryErrorKind) -> &'static str {
match kind {
RegistryErrorKind::Unavailable => "registry provider is unavailable",
RegistryErrorKind::Timeout => "registry operation timed out",
RegistryErrorKind::Unauthorized => "registry provider rejected the operation",
RegistryErrorKind::InvalidResource => "registry resource is invalid",
RegistryErrorKind::Conflict => "registry resource conflicts with existing state",
RegistryErrorKind::Cancelled => "registry operation was cancelled",
RegistryErrorKind::CleanupAborted => "registry cleanup did not complete",
RegistryErrorKind::Internal => "registry operation failed internally",
}
}
#[derive(Debug)]
struct RegistryMessage(String);
impl fmt::Display for RegistryMessage {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for RegistryMessage {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn errors_preserve_operation_kind_and_source() {
let error = RegistryError::new(
RegistryOperation::ActivateSubscription,
RegistryErrorKind::Unavailable,
std::io::Error::other("provider offline"),
);
assert_eq!(error.operation(), RegistryOperation::ActivateSubscription);
assert_eq!(error.kind(), RegistryErrorKind::Unavailable);
assert!(error.is_retryable());
assert_eq!(error.safe_message(), "registry provider is unavailable");
assert!(error.source_ref().to_string().contains("provider offline"));
assert!(
std::error::Error::source(&error)
.unwrap()
.to_string()
.contains("provider offline")
);
}
#[test]
fn formatting_shows_safe_message_without_exposing_provider_source() {
let error = RegistryError::new(
RegistryOperation::ActivateSubscription,
RegistryErrorKind::Unavailable,
std::io::Error::other("provider-token=secret"),
);
let debug = format!("{error:?}");
let display = error.to_string();
assert!(debug.contains("ActivateSubscription"));
assert!(debug.contains("Unavailable"));
assert!(debug.contains("registry provider is unavailable"));
assert!(display.contains("registry provider is unavailable"));
assert!(!debug.contains("provider-token=secret"));
assert!(!display.contains("provider-token=secret"));
}
#[test]
fn explicit_public_message_is_preserved() {
let error = RegistryError::message(
RegistryOperation::Directory,
RegistryErrorKind::InvalidResource,
"directory snapshot is invalid",
);
assert_eq!(error.safe_message(), "directory snapshot is invalid");
assert!(format!("{error:?}").contains("directory snapshot is invalid"));
assert!(error.to_string().contains("directory snapshot is invalid"));
assert_eq!(
error.source_ref().to_string(),
"directory snapshot is invalid"
);
}
}