transactional 0.3.1

A simple optimistic lock-free confirmation-based transactional model for Rust.
Documentation
pub type UserError = Box<dyn std::error::Error + Send + Sync + 'static>;

#[cfg(test)]
/// Converts a string slice to an `InternalFromTests` error variant for testing purposes.
///
/// This implementation allows creating a test error directly from a string slice,
/// which is useful for generating test-specific error scenarios.
///
impl From<&str> for Error {
    fn from(message: &str) -> Self {
        Error::InternalFromTests {
            message: message.to_string(),
        }
    }
}

#[derive(thiserror::Error, Debug)]
/// Represents different types of errors that can occur in the application
///
/// This enum covers scenarios related to concurrent modifications and transaction rejections,
/// with an additional test-specific error variant for internal testing purposes.
///
/// # Variants
/// - `ConcurrentModification`: Indicates an attempt to modify state while a transaction is ongoing
/// - `TransactionRejected`: Represents a modification that was rejected, with an underlying source error
/// - `InternalFromTests`: A test-specific error variant for generating errors during testing
///
/// # Examples
///
/// # Examples
///
/// Creating a `ConcurrentModification` error:
/// ```
/// use transactional::prelude::*;
///
/// let error = Error::ConcurrentModification {
///     previous: "old_value".to_string(),
///     current: "new_value".to_string(),
/// };
///
/// assert!(error.to_string().contains("attempt of modification"));
/// assert!(error.to_string().contains("old_value"));
/// assert!(error.to_string().contains("new_value"));
/// ```
///
/// Creating a `TransactionRejected` error:
/// ```
/// use std::error::Error as StdError;
/// use std::fmt;
/// use transactional::error::Error;
///
/// // A simple error type for the example
/// #[derive(Debug)]
/// struct ValidationError(String);
///
/// impl fmt::Display for ValidationError {
///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
///         write!(f, "Validation error: {}", self.0)
///     }
/// }
///
/// impl StdError for ValidationError {}
///
/// // Create the TransactionRejected error
/// let source_error = ValidationError("invalid data".to_string());
/// let error = Error::TransactionRejected {
///     previous: "old_state".to_string(),
///     current: "new_state".to_string(),
///     source: Box::new(source_error),
/// };
///
/// assert!(error.to_string().contains("rejected"));
/// assert!(error.to_string().contains("old_state"));
/// assert!(error.to_string().contains("new_state"));
/// ```
pub enum Error {
    #[error(
        "attempt of modification {previous} -> {current} while transaction is ongoing"
    )]
    ConcurrentModification { previous: String, current: String },

    #[error("modification {previous} -> {current}  rejected: {source}")]
    TransactionRejected {
        previous: String,
        current: String,
        #[source]
        source: Box<dyn std::error::Error>,
    },

    #[cfg(test)]
    #[error("Test error: {message}")]
    InternalFromTests { message: String },
}

impl Error {
    pub fn transaction_rejected<T>(
        previous: Option<&T>,
        current: Option<&T>,
        error: UserError,
    ) -> Self
    where
        T: ToString,
    {
        Error::TransactionRejected {
            previous: previous
                .map_or("**none**".to_string(), ToString::to_string)
                .to_string(),
            current: current
                .map_or("**none**".to_string(), ToString::to_string)
                .to_string(),
            source: error,
        }
    }
}

#[cfg(test)]
mod tests {
    use insta::assert_debug_snapshot;

    use super::*;

    #[test]
    fn test_from_str() {
        let message = "test message";
        let error: Error = message.into();
        assert_debug_snapshot!(error, @r#"
        InternalFromTests {
            message: "test message",
        }
        "#);
    }
}