autocache 0.5.0

automatic cache management
Documentation
use std::{error::Error as StdError, fmt, sync::Arc};

use thiserror::Error;

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Clone)]
pub struct ErrorSource(Arc<anyhow::Error>);

impl ErrorSource {
    pub fn from_anyhow(error: anyhow::Error) -> Self {
        Self(Arc::new(error))
    }

    pub fn downcast_ref<E>(&self) -> Option<&E>
    where
        E: StdError + Send + Sync + 'static,
    {
        self.0.downcast_ref()
    }
}

impl fmt::Debug for ErrorSource {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.0, formatter)
    }
}

impl fmt::Display for ErrorSource {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.0, formatter)
    }
}

impl StdError for ErrorSource {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        Some(self.0.as_ref().as_ref())
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CacheOperation {
    Read,
    Write,
    Delete,
}

impl fmt::Display for CacheOperation {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Read => "read",
            Self::Write => "write",
            Self::Delete => "delete",
        })
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum LoaderKind {
    Single,
    Batch,
}

impl fmt::Display for LoaderKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Single => "single",
            Self::Batch => "batch",
        })
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SerializationOperation {
    Encode,
    Decode,
}

impl fmt::Display for SerializationOperation {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Encode => "encode",
            Self::Decode => "decode",
        })
    }
}

#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum ConfigurationError {
    #[error("cache is required")]
    MissingCache,
    #[error("loader is required")]
    MissingLoader,
    #[error("max_batch_size must be greater than zero")]
    InvalidMaxBatchSize,
    #[error("max_concurrent_async_cache_writes must be greater than zero")]
    InvalidMaxConcurrentAsyncCacheWrites,
    #[error("async_refresh_queue_capacity must be greater than zero")]
    InvalidAsyncRefreshQueueCapacity,
    #[error("expiration duration is out of range")]
    InvalidExpirationDuration,
    #[error("expire listener is already set")]
    ExpireListenerAlreadySet,
}

#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum Error {
    #[error(transparent)]
    Configuration(#[from] ConfigurationError),

    #[error("{cache} cache {operation} failed: {source}")]
    Cache {
        operation: CacheOperation,
        cache: &'static str,
        #[source]
        source: ErrorSource,
    },

    #[error("{kind} loader failed: {source}")]
    Loader {
        kind: LoaderKind,
        #[source]
        source: ErrorSource,
    },

    #[error("failed to {operation} with {codec}: {source}")]
    Serialization {
        operation: SerializationOperation,
        codec: &'static str,
        #[source]
        source: ErrorSource,
    },

    #[error("a Tokio runtime is required for background tasks")]
    RuntimeUnavailable,

    #[error("the refresh worker is unavailable")]
    RefreshUnavailable,

    #[error("singleflight failed: {message}")]
    SingleFlight { message: Arc<str> },
}

impl Error {
    pub fn cache(
        operation: CacheOperation,
        cache: &'static str,
        source: impl Into<anyhow::Error>,
    ) -> Self {
        Self::Cache {
            operation,
            cache,
            source: ErrorSource::from_anyhow(source.into()),
        }
    }

    pub fn loader(kind: LoaderKind, source: anyhow::Error) -> Self {
        Self::Loader {
            kind,
            source: ErrorSource::from_anyhow(source),
        }
    }

    pub fn serialization(
        operation: SerializationOperation,
        codec: &'static str,
        source: impl Into<anyhow::Error>,
    ) -> Self {
        Self::Serialization {
            operation,
            codec,
            source: ErrorSource::from_anyhow(source.into()),
        }
    }

    pub(crate) fn from_cache(
        operation: CacheOperation,
        cache: &'static str,
        source: anyhow::Error,
    ) -> Self {
        match source.downcast::<Self>() {
            Ok(error) => error,
            Err(source) => Self::cache(operation, cache, source),
        }
    }
}

#[cfg(test)]
mod tests {
    use thiserror::Error;

    use super::ErrorSource;

    #[derive(Debug, Error)]
    #[error("source marker")]
    struct SourceMarker;

    #[test]
    fn error_source_preserves_downcasting() {
        let source = ErrorSource::from_anyhow(SourceMarker.into());

        assert!(source.downcast_ref::<SourceMarker>().is_some());
    }
}