Skip to main content

use_db_transaction/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Transaction metadata primitives for `RustUse`.
5
6use core::fmt;
7use std::error::Error;
8
9/// Transaction identifier label.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct TransactionId(String);
12
13impl TransactionId {
14    /// Creates a transaction identifier.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`TransactionError`] when the identifier is empty or contains control characters.
19    pub fn new(input: impl AsRef<str>) -> Result<Self, TransactionError> {
20        validate_text(input.as_ref()).map(|value| Self(value.to_owned()))
21    }
22
23    /// Returns the identifier label.
24    #[must_use]
25    pub fn as_str(&self) -> &str {
26        &self.0
27    }
28}
29
30/// Transaction mode metadata.
31#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub enum TransactionMode {
33    /// Read-only transaction.
34    #[default]
35    ReadOnly,
36    /// Read-write transaction.
37    ReadWrite,
38}
39
40/// Transaction state metadata.
41#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
42pub enum TransactionState {
43    /// Transaction has started.
44    #[default]
45    Started,
46    /// Transaction is active.
47    Active,
48    /// Transaction is committed.
49    Committed,
50    /// Transaction is rolled back.
51    RolledBack,
52    /// Transaction failed.
53    Failed,
54    /// State is unknown.
55    Unknown,
56}
57
58/// Common transaction isolation levels.
59#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
60pub enum TransactionIsolation {
61    /// Read uncommitted isolation.
62    ReadUncommitted,
63    /// Read committed isolation.
64    #[default]
65    ReadCommitted,
66    /// Repeatable read isolation.
67    RepeatableRead,
68    /// Serializable isolation.
69    Serializable,
70    /// Snapshot isolation.
71    Snapshot,
72}
73
74impl TransactionIsolation {
75    /// Returns a stable lowercase isolation label.
76    #[must_use]
77    pub const fn as_str(self) -> &'static str {
78        match self {
79            Self::ReadUncommitted => "read uncommitted",
80            Self::ReadCommitted => "read committed",
81            Self::RepeatableRead => "repeatable read",
82            Self::Serializable => "serializable",
83            Self::Snapshot => "snapshot",
84        }
85    }
86}
87
88/// Transaction outcome metadata.
89#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
90pub enum TransactionOutcome {
91    /// Transaction committed.
92    #[default]
93    Committed,
94    /// Transaction rolled back.
95    RolledBack,
96    /// Transaction outcome is unknown.
97    Unknown,
98}
99
100/// Transaction boundary metadata.
101#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
102pub enum TransactionBoundary {
103    /// Begin boundary.
104    #[default]
105    Begin,
106    /// Commit boundary.
107    Commit,
108    /// Rollback boundary.
109    Rollback,
110    /// Savepoint boundary.
111    Savepoint,
112}
113
114/// Error returned by transaction metadata constructors.
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub enum TransactionError {
117    /// Label was empty.
118    Empty,
119    /// Label contained a control character.
120    ControlCharacter,
121}
122
123impl fmt::Display for TransactionError {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match self {
126            Self::Empty => formatter.write_str("transaction label cannot be empty"),
127            Self::ControlCharacter => {
128                formatter.write_str("transaction label cannot contain control characters")
129            },
130        }
131    }
132}
133
134impl Error for TransactionError {}
135
136fn validate_text(input: &str) -> Result<&str, TransactionError> {
137    if input.chars().any(char::is_control) {
138        return Err(TransactionError::ControlCharacter);
139    }
140    let trimmed = input.trim();
141    if trimmed.is_empty() {
142        return Err(TransactionError::Empty);
143    }
144    Ok(trimmed)
145}
146
147#[cfg(test)]
148mod tests {
149    use super::{TransactionId, TransactionIsolation, TransactionMode, TransactionOutcome};
150
151    #[test]
152    fn stores_transaction_metadata() -> Result<(), Box<dyn std::error::Error>> {
153        let id = TransactionId::new("tx-1")?;
154
155        assert_eq!(id.as_str(), "tx-1");
156        assert_eq!(TransactionIsolation::Serializable.as_str(), "serializable");
157        assert_eq!(TransactionMode::default(), TransactionMode::ReadOnly);
158        assert_eq!(TransactionOutcome::default(), TransactionOutcome::Committed);
159        Ok(())
160    }
161}