use-db-transaction 0.1.0

Primitive database transaction metadata for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

//! Transaction metadata primitives for `RustUse`.

use core::fmt;
use std::error::Error;

/// Transaction identifier label.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TransactionId(String);

impl TransactionId {
    /// Creates a transaction identifier.
    ///
    /// # Errors
    ///
    /// Returns [`TransactionError`] when the identifier is empty or contains control characters.
    pub fn new(input: impl AsRef<str>) -> Result<Self, TransactionError> {
        validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
    }

    /// Returns the identifier label.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

/// Transaction mode metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TransactionMode {
    /// Read-only transaction.
    #[default]
    ReadOnly,
    /// Read-write transaction.
    ReadWrite,
}

/// Transaction state metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TransactionState {
    /// Transaction has started.
    #[default]
    Started,
    /// Transaction is active.
    Active,
    /// Transaction is committed.
    Committed,
    /// Transaction is rolled back.
    RolledBack,
    /// Transaction failed.
    Failed,
    /// State is unknown.
    Unknown,
}

/// Common transaction isolation levels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TransactionIsolation {
    /// Read uncommitted isolation.
    ReadUncommitted,
    /// Read committed isolation.
    #[default]
    ReadCommitted,
    /// Repeatable read isolation.
    RepeatableRead,
    /// Serializable isolation.
    Serializable,
    /// Snapshot isolation.
    Snapshot,
}

impl TransactionIsolation {
    /// Returns a stable lowercase isolation label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::ReadUncommitted => "read uncommitted",
            Self::ReadCommitted => "read committed",
            Self::RepeatableRead => "repeatable read",
            Self::Serializable => "serializable",
            Self::Snapshot => "snapshot",
        }
    }
}

/// Transaction outcome metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TransactionOutcome {
    /// Transaction committed.
    #[default]
    Committed,
    /// Transaction rolled back.
    RolledBack,
    /// Transaction outcome is unknown.
    Unknown,
}

/// Transaction boundary metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TransactionBoundary {
    /// Begin boundary.
    #[default]
    Begin,
    /// Commit boundary.
    Commit,
    /// Rollback boundary.
    Rollback,
    /// Savepoint boundary.
    Savepoint,
}

/// Error returned by transaction metadata constructors.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransactionError {
    /// Label was empty.
    Empty,
    /// Label contained a control character.
    ControlCharacter,
}

impl fmt::Display for TransactionError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("transaction label cannot be empty"),
            Self::ControlCharacter => {
                formatter.write_str("transaction label cannot contain control characters")
            },
        }
    }
}

impl Error for TransactionError {}

fn validate_text(input: &str) -> Result<&str, TransactionError> {
    if input.chars().any(char::is_control) {
        return Err(TransactionError::ControlCharacter);
    }
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(TransactionError::Empty);
    }
    Ok(trimmed)
}

#[cfg(test)]
mod tests {
    use super::{TransactionId, TransactionIsolation, TransactionMode, TransactionOutcome};

    #[test]
    fn stores_transaction_metadata() -> Result<(), Box<dyn std::error::Error>> {
        let id = TransactionId::new("tx-1")?;

        assert_eq!(id.as_str(), "tx-1");
        assert_eq!(TransactionIsolation::Serializable.as_str(), "serializable");
        assert_eq!(TransactionMode::default(), TransactionMode::ReadOnly);
        assert_eq!(TransactionOutcome::default(), TransactionOutcome::Committed);
        Ok(())
    }
}